From eb56519df7ce672a22dfee38b94d07dcb2c8d8a9 Mon Sep 17 00:00:00 2001
From: whm <973418690@qq.com>
Date: Tue, 17 Mar 2026 00:59:32 +0800
Subject: [PATCH] =?UTF-8?q?=E5=AE=87=E6=81=92=E4=B8=80=E5=8F=B7=E5=AE=98?=
=?UTF-8?q?=E7=BD=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.env.example | 10 +
.env.production | 13 +
.gitignore | 27 +
CONFIG.md | 50 +
MongoDB/README.md | 159 ++
MongoDB/create_collections.js | 84 +
README.md | 45 +
admin/.env.example | 5 +
admin/.env.production | 6 +
admin/Dockerfile | 12 +
admin/README.md | 12 +
admin/index.html | 12 +
admin/package-lock.json | 1694 +++++++++++++++++
admin/package.json | 22 +
admin/src/App.vue | 20 +
admin/src/api/admin.js | 73 +
admin/src/api/request.js | 34 +
admin/src/layouts/AdminLayout.vue | 112 ++
admin/src/main.js | 11 +
admin/src/router/index.js | 111 ++
admin/src/stores/auth.js | 60 +
admin/src/utils/disable-debug.js | 18 +
admin/src/views/Dashboard.vue | 88 +
admin/src/views/Login.vue | 294 +++
.../views/conversations/ConversationList.vue | 55 +
admin/src/views/settings/PaymentConfig.vue | 121 ++
admin/src/views/settings/RolePermissions.vue | 102 +
admin/src/views/settings/SMSConfig.vue | 102 +
admin/src/views/sites/HomepageEdit.vue | 217 +++
admin/src/views/sites/ModuleUpload.vue | 129 ++
admin/src/views/sites/PageList.vue | 176 ++
admin/src/views/sites/SiteList.vue | 160 ++
admin/src/views/users/UserList.vue | 213 +++
admin/src/views/workspaces/WorkspaceList.vue | 54 +
admin/vite.config.js | 17 +
deploy.sh | 56 +
docker-compose.yml | 63 +
nginx-proxy-manager.md | 30 +
nginx/nginx.conf | 35 +
pull-and-restart.sh | 24 +
push-to-gitea.sh | 29 +
restart.sh | 12 +
run-docker.sh | 24 +
scripts/kill-ports.ps1 | 11 +
scripts/ssh-tunnel.ps1 | 30 +
server/.air.toml | 45 +
server/.env.example | 9 +
server/Dockerfile | 13 +
server/README.md | 24 +
server/config/constants.go | 4 +
server/config/database.go | 56 +
server/config/db_structure.go | 78 +
server/dev.bat | 8 +
server/go.mod | 44 +
server/go.sum | 130 ++
server/handlers/auth.go | 199 ++
server/handlers/conversation.go | 67 +
server/handlers/helpers.go | 7 +
server/handlers/homepage.go | 345 ++++
server/handlers/module_upload.go | 145 ++
server/handlers/official_site.go | 50 +
server/handlers/page.go | 175 ++
server/handlers/payment_config.go | 128 ++
server/handlers/permission.go | 96 +
server/handlers/register.go | 294 +++
server/handlers/role_permission.go | 109 ++
server/handlers/site.go | 165 ++
server/handlers/sms_config.go | 94 +
server/handlers/stats.go | 34 +
server/handlers/user.go | 234 +++
server/handlers/workspace.go | 64 +
server/main.go | 218 +++
server/middleware/logger.go | 39 +
server/models/conversation.go | 12 +
server/models/payment.go | 24 +
server/models/permission.go | 38 +
server/models/site.go | 66 +
server/models/sms.go | 11 +
server/models/user.go | 77 +
server/models/workspace.go | 10 +
server/pkg/logger/logger.go | 66 +
server/pkg/schema/sync.go | 203 ++
server/scripts/inspect_db.go | 74 +
server/scripts/start-ssh-tunnel.bat | 13 +
server/uploads/.gitkeep | 1 +
server/utils/password.go | 12 +
sql/created_20260314_144115.sql | 51 +
sql/init.sql | 134 ++
sql/online_schema_20260314_144115.sql | 253 +++
stop-docker.sh | 4 +
web/.env.example | 5 +
web/.env.production | 6 +
web/Dockerfile | 12 +
web/README.md | 12 +
web/index.html | 15 +
web/package-lock.json | 1509 +++++++++++++++
web/package.json | 20 +
web/src/App.vue | 21 +
web/src/assets/landing-dynamics.css | 187 ++
web/src/config.js | 8 +
web/src/main.js | 9 +
web/src/router/index.js | 21 +
web/src/utils/disable-debug.js | 18 +
web/src/views/Home.vue | 369 ++++
web/vite.config.js | 16 +
105 files changed, 10783 insertions(+)
create mode 100644 .env.example
create mode 100644 .env.production
create mode 100644 .gitignore
create mode 100644 CONFIG.md
create mode 100644 MongoDB/README.md
create mode 100644 MongoDB/create_collections.js
create mode 100644 README.md
create mode 100644 admin/.env.example
create mode 100644 admin/.env.production
create mode 100644 admin/Dockerfile
create mode 100644 admin/README.md
create mode 100644 admin/index.html
create mode 100644 admin/package-lock.json
create mode 100644 admin/package.json
create mode 100644 admin/src/App.vue
create mode 100644 admin/src/api/admin.js
create mode 100644 admin/src/api/request.js
create mode 100644 admin/src/layouts/AdminLayout.vue
create mode 100644 admin/src/main.js
create mode 100644 admin/src/router/index.js
create mode 100644 admin/src/stores/auth.js
create mode 100644 admin/src/utils/disable-debug.js
create mode 100644 admin/src/views/Dashboard.vue
create mode 100644 admin/src/views/Login.vue
create mode 100644 admin/src/views/conversations/ConversationList.vue
create mode 100644 admin/src/views/settings/PaymentConfig.vue
create mode 100644 admin/src/views/settings/RolePermissions.vue
create mode 100644 admin/src/views/settings/SMSConfig.vue
create mode 100644 admin/src/views/sites/HomepageEdit.vue
create mode 100644 admin/src/views/sites/ModuleUpload.vue
create mode 100644 admin/src/views/sites/PageList.vue
create mode 100644 admin/src/views/sites/SiteList.vue
create mode 100644 admin/src/views/users/UserList.vue
create mode 100644 admin/src/views/workspaces/WorkspaceList.vue
create mode 100644 admin/vite.config.js
create mode 100644 deploy.sh
create mode 100644 docker-compose.yml
create mode 100644 nginx-proxy-manager.md
create mode 100644 nginx/nginx.conf
create mode 100644 pull-and-restart.sh
create mode 100644 push-to-gitea.sh
create mode 100644 restart.sh
create mode 100644 run-docker.sh
create mode 100644 scripts/kill-ports.ps1
create mode 100644 scripts/ssh-tunnel.ps1
create mode 100644 server/.air.toml
create mode 100644 server/.env.example
create mode 100644 server/Dockerfile
create mode 100644 server/README.md
create mode 100644 server/config/constants.go
create mode 100644 server/config/database.go
create mode 100644 server/config/db_structure.go
create mode 100644 server/dev.bat
create mode 100644 server/go.mod
create mode 100644 server/go.sum
create mode 100644 server/handlers/auth.go
create mode 100644 server/handlers/conversation.go
create mode 100644 server/handlers/helpers.go
create mode 100644 server/handlers/homepage.go
create mode 100644 server/handlers/module_upload.go
create mode 100644 server/handlers/official_site.go
create mode 100644 server/handlers/page.go
create mode 100644 server/handlers/payment_config.go
create mode 100644 server/handlers/permission.go
create mode 100644 server/handlers/register.go
create mode 100644 server/handlers/role_permission.go
create mode 100644 server/handlers/site.go
create mode 100644 server/handlers/sms_config.go
create mode 100644 server/handlers/stats.go
create mode 100644 server/handlers/user.go
create mode 100644 server/handlers/workspace.go
create mode 100644 server/main.go
create mode 100644 server/middleware/logger.go
create mode 100644 server/models/conversation.go
create mode 100644 server/models/payment.go
create mode 100644 server/models/permission.go
create mode 100644 server/models/site.go
create mode 100644 server/models/sms.go
create mode 100644 server/models/user.go
create mode 100644 server/models/workspace.go
create mode 100644 server/pkg/logger/logger.go
create mode 100644 server/pkg/schema/sync.go
create mode 100644 server/scripts/inspect_db.go
create mode 100644 server/scripts/start-ssh-tunnel.bat
create mode 100644 server/uploads/.gitkeep
create mode 100644 server/utils/password.go
create mode 100644 sql/created_20260314_144115.sql
create mode 100644 sql/init.sql
create mode 100644 sql/online_schema_20260314_144115.sql
create mode 100644 stop-docker.sh
create mode 100644 web/.env.example
create mode 100644 web/.env.production
create mode 100644 web/Dockerfile
create mode 100644 web/README.md
create mode 100644 web/index.html
create mode 100644 web/package-lock.json
create mode 100644 web/package.json
create mode 100644 web/src/App.vue
create mode 100644 web/src/assets/landing-dynamics.css
create mode 100644 web/src/config.js
create mode 100644 web/src/main.js
create mode 100644 web/src/router/index.js
create mode 100644 web/src/utils/disable-debug.js
create mode 100644 web/src/views/Home.vue
create mode 100644 web/vite.config.js
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..6623819
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,10 @@
+# 复制为 .env 或 .env.production 后按需修改
+# Go 服务不会自动加载 .env,需在启动前导出变量,例如:
+# Linux/Mac: export $(grep -v '^#' .env.production | xargs)
+# Windows CMD: for /f "usebackq tokens=*" %a in (".env.production") do set %a
+# Windows PowerShell: Get-Content .env.production | ForEach-Object { if ($_ -match '^([^#][^=]+)=(.*)$') { [Environment]::SetEnvironmentVariable($matches[1].Trim(), $matches[2].Trim(), 'Process') } }
+
+MONGODB_URI=mongodb://localhost:27017
+MONGODB_DB=yxd-agent-testing
+PORT=8080
+GIN_MODE=release
diff --git a/.env.production b/.env.production
new file mode 100644
index 0000000..79a3a77
--- /dev/null
+++ b/.env.production
@@ -0,0 +1,13 @@
+# 生产环境配置(本地 MongoDB,无需映射)
+# 使用方式:启动前 source .env.production 或 set -a && source .env.production && set +a
+# Windows: 在 PowerShell 中可逐行 set 这些变量,或用 dotenv 等工具加载
+
+# MongoDB(本机)
+MONGODB_URI=mongodb://localhost:27017
+MONGODB_DB=yxd-agent-testing
+
+# 服务端口
+PORT=8080
+
+# Gin 生产模式(关闭调试信息)
+GIN_MODE=release
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..eb5d082
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,27 @@
+# 依赖与构建产物
+node_modules/
+web/dist/
+admin/dist/
+server/server
+*.exe
+
+# 环境与密钥(服务器上单独配置)
+.env
+server/.env
+!.env.example
+!server/.env.example
+
+# 日志
+logs/
+*.log
+
+# 系统与编辑器
+.DS_Store
+Thumbs.db
+.idea/
+.vscode/
+*.swp
+*.swo
+
+# Docker 本地卷(不提交)
+# mongo_data 等由 compose 管理
diff --git a/CONFIG.md b/CONFIG.md
new file mode 100644
index 0000000..6cd9869
--- /dev/null
+++ b/CONFIG.md
@@ -0,0 +1,50 @@
+# 配置文件位置与加载方式
+
+**对外域名:** https://yuheng.yuxindazhineng.com(官网 /admin 管理后台 /api 接口均同域,由 Nginx Proxy Manager 反向代理)
+
+---
+
+## 一、后端(API 服务)— 启动时自动读取
+
+**配置文件:** 仅 **server/.env** 一个文件。需要改配置时直接编辑该文件即可。
+
+- 在 `server` 目录下启动时,加载 `server/.env`
+- 在项目根目录启动时,加载 `server/.env`
+
+**变量说明:** 见 `server/.env.example`(仅作参考,可不保留)。
+
+---
+
+## 二、前台(官网)— 构建时注入
+
+**配置文件位置:**
+
+| 环境 | 路径 | 说明 |
+|------|------|------|
+| 生产 | **web/.env.production** | 执行 `npm run build` 时注入 |
+| 示例 | web/.env.example | 复制为 .env 或 .env.production 后修改 |
+
+变量需以 `VITE_` 开头才会被打包进前端,如:`VITE_APP_DOMAIN`、`VITE_API_BASE`。
+
+---
+
+## 三、后台(管理端)— 构建时注入
+
+**配置文件位置:**
+
+| 环境 | 路径 | 说明 |
+|------|------|------|
+| 生产 | **admin/.env.production** | 执行 `npm run build` 时注入 |
+| 示例 | admin/.env.example | 复制为 .env 或 .env.production 后修改 |
+
+变量需以 `VITE_` 开头,如:`VITE_APP_DOMAIN`、`VITE_API_BASE`。
+
+---
+
+## 四、汇总表
+
+| 端 | 配置文件 | 自动读取时机 |
+|----|----------|--------------|
+| 后端 | **server/.env** | **进程启动时**自动加载 |
+| 前台 | **web/**(.env.production) | 执行 `npm run build` 时注入 |
+| 后台 | **admin/**(.env.production) | 执行 `npm run build` 时注入 |
diff --git a/MongoDB/README.md b/MongoDB/README.md
new file mode 100644
index 0000000..002e1bd
--- /dev/null
+++ b/MongoDB/README.md
@@ -0,0 +1,159 @@
+# MongoDB 集合说明(yh_web)
+
+默认数据库名:`yxd-agent-testing`(见 `server/config/constants.go`)。
+
+以下为功能涉及的集合及文档结构,**线上已有的集合也按此结构对照**;缺失的集合可用 `create_collections.js` 创建。
+
+---
+
+## 1. sites(站点)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| _id | ObjectID | 主键 |
+| name | string | 站点名称 |
+| domain | string | 域名 |
+| description | string | 描述(可选) |
+| created_at | string | 创建时间(如 RFC3339) |
+
+---
+
+## 2. pages(网页,属于某站点)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| _id | ObjectID | 主键 |
+| site_id | string | 站点 ID(ObjectID 十六进制字符串) |
+| slug | string | 路径标识:index, about, ... |
+| title | string | 标题 |
+| type | string | 类型:homepage / page |
+| content | string | HTML 或 JSON 字符串(首页为 HomepageData JSON) |
+| updated_at | string | 更新时间 |
+
+**索引建议**:`{ site_id: 1, slug: 1 }` 唯一,便于按站点+slug 查首页/子页。
+
+---
+
+## 3. site_assets(站点资源/上传文件)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| _id | ObjectID | 主键 |
+| site_id | string | 站点 ID |
+| name | string | 文件名/显示名 |
+| file_path | string | 相对路径 |
+| size | int64 | 字节数 |
+| content_type | string | MIME 类型 |
+| created_at | string | 创建时间 |
+
+**索引建议**:`{ site_id: 1 }`。
+
+---
+
+## 4. users(用户)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| _id | ObjectID | 主键 |
+| username | string | 用户名 |
+| mobile | string | 手机号(可选) |
+| email | string | 邮箱 |
+| password | string | 密码哈希 |
+| role | string | 角色名 |
+| role_id | int | 9527=超级管理员,1=普通用户 |
+| is_beta | bool | 是否体验用户(可选) |
+| trial_start_date | string/date | 试用开始(可选) |
+| trial_end_date | string/date | 试用结束(可选) |
+| lastLogin | string | 最后登录(可选) |
+| llm | string | LLM 配置(可选) |
+
+**索引建议**:`{ username: 1 }` 唯一,`{ mobile: 1 }` 可选。
+
+---
+
+## 5. workspaces(工作空间)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| _id | ObjectID | 主键 |
+| name | string | 名称 |
+| user_id | string | 用户 ID |
+| created_at | string | 创建时间 |
+
+**索引建议**:`{ user_id: 1 }`。
+
+---
+
+## 6. conversations(对话)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| _id | ObjectID | 主键 |
+| title | string | 标题 |
+| user_id | string | 用户 ID |
+| workspace_id | string | 工作空间 ID |
+| created_at | string | 创建时间 |
+| updated_at | string | 更新时间 |
+
+**索引建议**:`{ user_id: 1 }`,`{ workspace_id: 1 }`。
+
+---
+
+## 7. messages(消息,统计用)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| _id | ObjectID | 主键 |
+| conversation_id | string | 对话 ID |
+| role | string | user / assistant / system |
+| content | string | 内容 |
+| created_at | string | 创建时间 |
+
+**说明**:当前仅统计接口使用,若功能未实现可先建空集合。
+
+---
+
+## 8. files(文件,统计用)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| _id | ObjectID | 主键 |
+| user_id | string | 用户 ID |
+| name | string | 文件名 |
+| file_path | string | 存储路径 |
+| size | int64 | 字节数 |
+| created_at | string | 创建时间 |
+
+**说明**:当前仅统计接口使用,若功能未实现可先建空集合。
+
+---
+
+## 9. system_config(系统配置,按 _id 区分类型)
+
+单集合,`_id` 为字符串键,不同键对应不同配置结构。
+
+- ** _id: "payment"** — 支付配置
+ - wechat: { app_id, mch_id, api_key, api_key_v3, enabled }
+ - alipay: { app_id, private_key, alipay_public_key, enabled }
+
+- **_id: "sms_platform"** — 短信配置
+ - provider, access_key, secret_key, sign_name, template_id, enabled
+
+**索引**:默认 _id 唯一即可。
+
+---
+
+## 使用方式
+
+在项目根目录执行(需已安装 mongosh):
+
+```bash
+mongosh "mongodb://localhost:27017" --file MongoDB/create_collections.js
+```
+
+或进入 mongosh 后:
+
+```js
+use("yxd-agent-testing")
+// 再粘贴 create_collections.js 中的创建与索引语句
+```
diff --git a/MongoDB/create_collections.js b/MongoDB/create_collections.js
new file mode 100644
index 0000000..322af10
--- /dev/null
+++ b/MongoDB/create_collections.js
@@ -0,0 +1,84 @@
+// 创建 yh_web 所需 MongoDB 集合及索引(mongosh 执行)
+// 用法: mongosh "mongodb://localhost:27017" --file MongoDB/create_collections.js
+// 或: mongosh "mongodb://localhost:27017" MongoDB/create_collections.js
+
+const dbName = "yxd-agent-testing";
+const db = db.getSiblingDB(dbName);
+
+print("使用数据库: " + dbName);
+
+// 1. sites
+if (!db.getCollectionNames().includes("sites")) {
+ db.createCollection("sites");
+ print("已创建集合: sites");
+}
+db.sites.createIndex({ created_at: -1 }, { name: "idx_created_at", background: true });
+
+// 2. pages
+if (!db.getCollectionNames().includes("pages")) {
+ db.createCollection("pages");
+ print("已创建集合: pages");
+}
+db.pages.createIndex(
+ { site_id: 1, slug: 1 },
+ { unique: true, name: "idx_site_slug", background: true }
+);
+
+// 3. site_assets
+if (!db.getCollectionNames().includes("site_assets")) {
+ db.createCollection("site_assets");
+ print("已创建集合: site_assets");
+}
+db.site_assets.createIndex({ site_id: 1 }, { name: "idx_site_id", background: true });
+
+// 4. users
+if (!db.getCollectionNames().includes("users")) {
+ db.createCollection("users");
+ print("已创建集合: users");
+}
+db.users.createIndex({ username: 1 }, { unique: true, name: "idx_username", background: true });
+db.users.createIndex({ mobile: 1 }, { name: "idx_mobile", background: true, sparse: true });
+
+// 5. workspaces
+if (!db.getCollectionNames().includes("workspaces")) {
+ db.createCollection("workspaces");
+ print("已创建集合: workspaces");
+}
+db.workspaces.createIndex({ user_id: 1 }, { name: "idx_user_id", background: true });
+
+// 6. conversations
+if (!db.getCollectionNames().includes("conversations")) {
+ db.createCollection("conversations");
+ print("已创建集合: conversations");
+}
+db.conversations.createIndex({ user_id: 1 }, { name: "idx_user_id", background: true });
+db.conversations.createIndex({ workspace_id: 1 }, { name: "idx_workspace_id", background: true });
+
+// 7. messages(统计用,功能未实现也可先建)
+if (!db.getCollectionNames().includes("messages")) {
+ db.createCollection("messages");
+ print("已创建集合: messages");
+}
+db.messages.createIndex({ conversation_id: 1 }, { name: "idx_conversation_id", background: true });
+
+// 8. files(统计用,功能未实现也可先建)
+if (!db.getCollectionNames().includes("files")) {
+ db.createCollection("files");
+ print("已创建集合: files");
+}
+db.files.createIndex({ user_id: 1 }, { name: "idx_user_id", background: true });
+
+// 9. system_config(支付、短信等配置,_id 为字符串键)
+if (!db.getCollectionNames().includes("system_config")) {
+ db.createCollection("system_config");
+ print("已创建集合: system_config");
+}
+
+// 10. role_permissions(角色权限:role_id -> permissions 数组)
+if (!db.getCollectionNames().includes("role_permissions")) {
+ db.createCollection("role_permissions");
+ print("已创建集合: role_permissions");
+}
+db.role_permissions.createIndex({ role_id: 1 }, { unique: true, name: "idx_role_id", background: true });
+
+print("集合与索引处理完成。");
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e4848a2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,45 @@
+# 多站点管理后台
+
+前后端分离的多站点管理系统。
+
+## 项目结构
+
+```
+yh_web/
+├── admin/ # 管理后台前端 (Vue 3 + Element Plus) - 端口 3000
+├── web/ # 前台页面 (Vue 3) - 端口 3001
+├── server/ # 后端 API (Go + Gin) - 端口 8080
+└── README.md
+```
+
+## 快速启动
+
+### 1. 启动后端
+
+```bash
+cd server
+go mod tidy
+go run main.go
+```
+
+### 2. 启动管理后台
+
+```bash
+cd admin
+npm install
+npm run dev
+```
+
+### 3. 启动前台
+
+```bash
+cd web
+npm install
+npm run dev
+```
+
+## 访问地址
+
+- 管理后台: http://localhost:3000
+- 前台: http://localhost:3001
+- API: http://localhost:8080
diff --git a/admin/.env.example b/admin/.env.example
new file mode 100644
index 0000000..aa83199
--- /dev/null
+++ b/admin/.env.example
@@ -0,0 +1,5 @@
+# 复制为 .env 或 .env.production 后修改
+# 后台(管理端)环境变量 - 仅 VITE_ 前缀会在构建时注入
+
+VITE_APP_DOMAIN=https://yuheng.yuxindazhineng.com
+VITE_API_BASE=
diff --git a/admin/.env.production b/admin/.env.production
new file mode 100644
index 0000000..46a9e92
--- /dev/null
+++ b/admin/.env.production
@@ -0,0 +1,6 @@
+# 后台(管理端)生产环境 - 对外域名
+# 构建时生效:npm run build
+
+VITE_APP_DOMAIN=https://yuheng.yuxindazhineng.com
+# 与官网同域,接口走 /api,留空即可
+VITE_API_BASE=
diff --git a/admin/Dockerfile b/admin/Dockerfile
new file mode 100644
index 0000000..78b068a
--- /dev/null
+++ b/admin/Dockerfile
@@ -0,0 +1,12 @@
+FROM node:20-alpine AS builder
+WORKDIR /app
+COPY package.json package-lock.json* ./
+RUN npm ci --legacy-peer-deps 2>/dev/null || npm install --legacy-peer-deps
+COPY . .
+RUN npm run build
+
+FROM nginx:alpine
+COPY --from=builder /app/dist /usr/share/nginx/html
+RUN echo 'server { listen 80; location /admin/ { alias /usr/share/nginx/html/; try_files $uri $uri/ /admin/index.html; } }' > /etc/nginx/conf.d/default.conf
+EXPOSE 80
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/admin/README.md b/admin/README.md
new file mode 100644
index 0000000..4124a85
--- /dev/null
+++ b/admin/README.md
@@ -0,0 +1,12 @@
+# 多站点管理后台 - 前端
+
+基于 Vue 3 + Vite + Element Plus 的管理后台。
+
+## 运行
+
+```bash
+npm install
+npm run dev
+```
+
+默认端口 3000,API 代理到 8080
diff --git a/admin/index.html b/admin/index.html
new file mode 100644
index 0000000..f4d77db
--- /dev/null
+++ b/admin/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ 多站点管理后台
+
+
+
+
+
+
diff --git a/admin/package-lock.json b/admin/package-lock.json
new file mode 100644
index 0000000..74c6947
--- /dev/null
+++ b/admin/package-lock.json
@@ -0,0 +1,1694 @@
+{
+ "name": "yh-admin",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "yh-admin",
+ "version": "0.0.0",
+ "dependencies": {
+ "@element-plus/icons-vue": "^2.3.1",
+ "axios": "^1.6.2",
+ "element-plus": "^2.4.4",
+ "vue": "^3.4.0",
+ "vue-router": "^4.2.5"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^4.5.2",
+ "vite": "^5.0.10"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@ctrl/tinycolor": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz",
+ "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@element-plus/icons-vue": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
+ "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "vue": "^3.2.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
+ "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
+ "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.5",
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@popperjs/core": {
+ "name": "@sxzz/popperjs-es",
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz",
+ "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
+ "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
+ "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
+ "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
+ "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
+ "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
+ "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
+ "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
+ "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
+ "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
+ "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
+ "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
+ "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
+ "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
+ "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
+ "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
+ "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
+ "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
+ "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
+ "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
+ "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
+ "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
+ "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
+ "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/lodash": {
+ "version": "4.17.24",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
+ "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/lodash-es": {
+ "version": "4.17.12",
+ "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
+ "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/lodash": "*"
+ }
+ },
+ "node_modules/@types/web-bluetooth": {
+ "version": "0.0.20",
+ "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
+ "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
+ "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.0.0 || ^5.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz",
+ "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@vue/shared": "3.5.30",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz",
+ "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.30",
+ "@vue/shared": "3.5.30"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz",
+ "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@vue/compiler-core": "3.5.30",
+ "@vue/compiler-dom": "3.5.30",
+ "@vue/compiler-ssr": "3.5.30",
+ "@vue/shared": "3.5.30",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.8",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz",
+ "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.30",
+ "@vue/shared": "3.5.30"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz",
+ "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.30"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz",
+ "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.30",
+ "@vue/shared": "3.5.30"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz",
+ "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.30",
+ "@vue/runtime-core": "3.5.30",
+ "@vue/shared": "3.5.30",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz",
+ "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.30",
+ "@vue/shared": "3.5.30"
+ },
+ "peerDependencies": {
+ "vue": "3.5.30"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz",
+ "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==",
+ "license": "MIT"
+ },
+ "node_modules/@vueuse/core": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.0.0.tgz",
+ "integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.20",
+ "@vueuse/metadata": "12.0.0",
+ "@vueuse/shared": "12.0.0",
+ "vue": "^3.5.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/metadata": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.0.0.tgz",
+ "integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.0.0.tgz",
+ "integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==",
+ "license": "MIT",
+ "dependencies": {
+ "vue": "^3.5.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/async-validator": {
+ "version": "4.2.5",
+ "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
+ "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.13.6",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
+ "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.19",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
+ "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/element-plus": {
+ "version": "2.13.5",
+ "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.5.tgz",
+ "integrity": "sha512-dmY24fhSREfZN/PuUt0YZigMso7wWzl+B5o+YKNN15kQIn/0hzamsPU+ebj9SES0IbUqsLX1wkrzYmzU8VrVOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@ctrl/tinycolor": "^4.2.0",
+ "@element-plus/icons-vue": "^2.3.2",
+ "@floating-ui/dom": "^1.0.1",
+ "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
+ "@types/lodash": "^4.17.20",
+ "@types/lodash-es": "^4.17.12",
+ "@vueuse/core": "12.0.0",
+ "async-validator": "^4.2.5",
+ "dayjs": "^1.11.19",
+ "lodash": "^4.17.23",
+ "lodash-es": "^4.17.23",
+ "lodash-unified": "^1.0.3",
+ "memoize-one": "^6.0.0",
+ "normalize-wheel-es": "^1.2.0"
+ },
+ "peerDependencies": {
+ "vue": "^3.3.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+ "license": "MIT"
+ },
+ "node_modules/lodash-es": {
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
+ "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash-unified": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
+ "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/lodash-es": "*",
+ "lodash": "*",
+ "lodash-es": "*"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/memoize-one": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+ "license": "MIT"
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/normalize-wheel-es": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
+ "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/rollup": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
+ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.59.0",
+ "@rollup/rollup-android-arm64": "4.59.0",
+ "@rollup/rollup-darwin-arm64": "4.59.0",
+ "@rollup/rollup-darwin-x64": "4.59.0",
+ "@rollup/rollup-freebsd-arm64": "4.59.0",
+ "@rollup/rollup-freebsd-x64": "4.59.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.59.0",
+ "@rollup/rollup-linux-arm64-musl": "4.59.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.59.0",
+ "@rollup/rollup-linux-loong64-musl": "4.59.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.59.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.59.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-musl": "4.59.0",
+ "@rollup/rollup-openbsd-x64": "4.59.0",
+ "@rollup/rollup-openharmony-arm64": "4.59.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.59.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.59.0",
+ "@rollup/rollup-win32-x64-gnu": "4.59.0",
+ "@rollup/rollup-win32-x64-msvc": "4.59.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz",
+ "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.30",
+ "@vue/compiler-sfc": "3.5.30",
+ "@vue/runtime-dom": "3.5.30",
+ "@vue/server-renderer": "3.5.30",
+ "@vue/shared": "3.5.30"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-router": {
+ "version": "4.6.4",
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
+ "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ }
+ }
+}
diff --git a/admin/package.json b/admin/package.json
new file mode 100644
index 0000000..ff99822
--- /dev/null
+++ b/admin/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "yh-admin",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "vue": "^3.4.0",
+ "vue-router": "^4.2.5",
+ "element-plus": "^2.4.4",
+ "@element-plus/icons-vue": "^2.3.1",
+ "axios": "^1.6.2"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^4.5.2",
+ "vite": "^5.0.10"
+ }
+}
diff --git a/admin/src/App.vue b/admin/src/App.vue
new file mode 100644
index 0000000..cf3a8ae
--- /dev/null
+++ b/admin/src/App.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/api/admin.js b/admin/src/api/admin.js
new file mode 100644
index 0000000..1c14a98
--- /dev/null
+++ b/admin/src/api/admin.js
@@ -0,0 +1,73 @@
+import request from './request'
+
+// 登录
+export const login = (data) => request.post('/admin/login', data)
+
+// 当前用户权限(用于菜单与路由)
+export const getMyPermissions = () => request.get('/admin/my-permissions')
+
+// 角色权限管理
+export const getRolePermissionsList = () => request.get('/admin/role-permissions')
+export const updateRolePermissions = (roleId, data) => request.put(`/admin/role-permissions/${roleId}`, data)
+
+// 后台注册(手机号+验证码)
+export const sendCode = (mobile) => request.post('/admin/send-code', { mobile })
+export const register = (data) => request.post('/admin/register', data)
+// 密码找回
+export const resetPassword = (data) => request.post('/admin/reset-password', data)
+
+// 统计
+export const getStats = () => request.get('/admin/stats')
+
+// 用户
+export const getUsers = (params) => request.get('/admin/users', { params })
+export const getUserById = (id) => request.get(`/admin/users/${id}`)
+export const createUser = (data) => request.post('/admin/users', data)
+export const updateUser = (id, data) => request.put(`/admin/users/${id}`, data)
+export const deleteUser = (id) => request.delete(`/admin/users/${id}`)
+
+// 工作空间
+export const getWorkspaces = (params) => request.get('/admin/workspaces', { params })
+
+// 对话
+export const getConversations = (params) => request.get('/admin/conversations', { params })
+
+// 短信平台配置
+export const getSMSConfig = () => request.get('/admin/sms-config')
+export const updateSMSConfig = (data) => request.put('/admin/sms-config', data)
+
+// 支付配置(微信、支付宝)
+export const getPaymentConfig = () => request.get('/admin/payment-config')
+export const updatePaymentConfig = (data) => request.put('/admin/payment-config', data)
+
+// 官网站点
+export const getOfficialSite = () => request.get('/admin/system/official-site')
+export const setOfficialSite = (siteId) => request.put('/admin/system/official-site', { site_id: siteId })
+
+// 站点管理
+export const getSites = () => request.get('/admin/sites')
+export const getSiteById = (id) => request.get(`/admin/sites/${id}`)
+export const createSite = (data) => request.post('/admin/sites', data)
+export const updateSite = (id, data) => request.put(`/admin/sites/${id}`, data)
+export const deleteSite = (id) => request.delete(`/admin/sites/${id}`)
+
+// 网页管理
+export const getPages = (params) => request.get('/admin/pages', { params })
+export const getPageById = (id) => request.get(`/admin/pages/${id}`)
+export const createPage = (data) => request.post('/admin/pages', data)
+export const updatePage = (id, data) => request.put(`/admin/pages/${id}`, data)
+export const deletePage = (id) => request.delete(`/admin/pages/${id}`)
+
+// 首页编辑与下载
+export const getHomepage = (siteId) => request.get(`/admin/sites/${siteId}/homepage`)
+export const updateHomepage = (siteId, data) => request.put(`/admin/sites/${siteId}/homepage`, data)
+export const downloadHomepage = (siteId) => request.get(`/admin/sites/${siteId}/homepage/download`, { responseType: 'blob' })
+
+// 功能模块上传
+export const getSiteAssets = (siteId) => request.get(`/admin/sites/${siteId}/assets`)
+export const uploadSiteAsset = (siteId, file) => {
+ const form = new FormData()
+ form.append('file', file)
+ return request.post(`/admin/sites/${siteId}/assets`, form, { headers: { 'Content-Type': 'multipart/form-data' } })
+}
+export const deleteSiteAsset = (siteId, id) => request.delete(`/admin/sites/${siteId}/assets/${id}`)
diff --git a/admin/src/api/request.js b/admin/src/api/request.js
new file mode 100644
index 0000000..4bd78bc
--- /dev/null
+++ b/admin/src/api/request.js
@@ -0,0 +1,34 @@
+import axios from 'axios'
+import { useAuthStore } from '../stores/auth'
+
+const apiBase = (import.meta.env.VITE_API_BASE || '').replace(/\/$/, '')
+const request = axios.create({
+ baseURL: apiBase ? `${apiBase}/api` : '/api',
+ timeout: 15000
+})
+
+request.interceptors.request.use((config) => {
+ const token = useAuthStore().getToken()
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`
+ }
+ return config
+})
+
+request.interceptors.response.use(
+ (res) => res.data,
+ (err) => {
+ if (err.response?.status === 401) {
+ useAuthStore().logout()
+ window.location.pathname = '/admin/login'
+ return Promise.reject(new Error('登录已过期,请重新登录'))
+ }
+ let msg = err.response?.data?.error || err.message || '请求失败'
+ if (err.response?.data?.debug) {
+ msg += ' (' + err.response.data.debug + ')'
+ }
+ return Promise.reject(new Error(msg))
+ }
+)
+
+export default request
diff --git a/admin/src/layouts/AdminLayout.vue b/admin/src/layouts/AdminLayout.vue
new file mode 100644
index 0000000..3281855
--- /dev/null
+++ b/admin/src/layouts/AdminLayout.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/main.js b/admin/src/main.js
new file mode 100644
index 0000000..da5b638
--- /dev/null
+++ b/admin/src/main.js
@@ -0,0 +1,11 @@
+import { createApp } from 'vue'
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+import App from './App.vue'
+import router from './router'
+import './utils/disable-debug'
+
+const app = createApp(App)
+app.use(ElementPlus)
+app.use(router)
+app.mount('#app')
diff --git a/admin/src/router/index.js b/admin/src/router/index.js
new file mode 100644
index 0000000..458c93d
--- /dev/null
+++ b/admin/src/router/index.js
@@ -0,0 +1,111 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import { useAuthStore } from '../stores/auth'
+
+const routes = [
+ {
+ path: '/login',
+ name: 'Login',
+ component: () => import('../views/Login.vue'),
+ meta: { title: '登录', public: true }
+ },
+ {
+ path: '/',
+ component: () => import('../layouts/AdminLayout.vue'),
+ children: [
+ {
+ path: '',
+ name: 'Dashboard',
+ component: () => import('../views/Dashboard.vue'),
+ meta: { title: '控制台' }
+ },
+ {
+ path: 'users',
+ name: 'Users',
+ component: () => import('../views/users/UserList.vue'),
+ meta: { title: '用户管理', permission: 'user:manage' }
+ },
+ {
+ path: 'workspaces',
+ name: 'Workspaces',
+ component: () => import('../views/workspaces/WorkspaceList.vue'),
+ meta: { title: '工作空间', permission: 'workspace:manage' }
+ },
+ {
+ path: 'conversations',
+ name: 'Conversations',
+ component: () => import('../views/conversations/ConversationList.vue'),
+ meta: { title: '对话管理', permission: 'conversation:manage' }
+ },
+ {
+ path: 'sms-config',
+ name: 'SMSConfig',
+ component: () => import('../views/settings/SMSConfig.vue'),
+ meta: { title: '短信平台配置', permission: 'sms_config' }
+ },
+ {
+ path: 'payment-config',
+ name: 'PaymentConfig',
+ component: () => import('../views/settings/PaymentConfig.vue'),
+ meta: { title: '支付配置', permission: 'payment_config' }
+ },
+ {
+ path: 'sites',
+ name: 'Sites',
+ component: () => import('../views/sites/SiteList.vue'),
+ meta: { title: '站点管理', permission: 'site:manage' }
+ },
+ {
+ path: 'pages',
+ name: 'Pages',
+ component: () => import('../views/sites/PageList.vue'),
+ meta: { title: '网页管理', permission: 'page:manage' }
+ },
+ {
+ path: 'homepage-edit',
+ name: 'HomepageEdit',
+ component: () => import('../views/sites/HomepageEdit.vue'),
+ meta: { title: '首页编辑', permission: 'homepage:edit' }
+ },
+ {
+ path: 'module-upload',
+ name: 'ModuleUpload',
+ component: () => import('../views/sites/ModuleUpload.vue'),
+ meta: { title: '功能模块上传', permission: 'module:upload' }
+ },
+ {
+ path: 'role-permissions',
+ name: 'RolePermissions',
+ component: () => import('../views/settings/RolePermissions.vue'),
+ meta: { title: '角色权限管理', permission: 'role:permission' }
+ }
+ ]
+ }
+]
+
+const router = createRouter({
+ history: createWebHistory('/admin/'),
+ routes
+})
+
+router.beforeEach((to, from, next) => {
+ const auth = useAuthStore()
+ if (to.meta.public) {
+ if (auth.isLoggedIn() && to.path === '/login') {
+ next('/')
+ } else {
+ next()
+ }
+ return
+ }
+ if (!auth.isLoggedIn()) {
+ next({ path: '/login', query: { redirect: to.fullPath } })
+ return
+ }
+ if (to.meta.permission && !auth.hasPermission(to.meta.permission)) {
+ next('/')
+ return
+ }
+ next()
+})
+
+export default router
diff --git a/admin/src/stores/auth.js b/admin/src/stores/auth.js
new file mode 100644
index 0000000..daf16ce
--- /dev/null
+++ b/admin/src/stores/auth.js
@@ -0,0 +1,60 @@
+import { ref } from 'vue'
+
+const TOKEN_KEY = 'admin_token'
+const USER_KEY = 'admin_user'
+
+const permissions = ref([])
+
+export function useAuthStore() {
+ const getToken = () => localStorage.getItem(TOKEN_KEY)
+ const getUser = () => {
+ try {
+ const s = localStorage.getItem(USER_KEY)
+ return s ? JSON.parse(s) : null
+ } catch {
+ return null
+ }
+ }
+
+ const setToken = (token) => {
+ if (token) localStorage.setItem(TOKEN_KEY, token)
+ else localStorage.removeItem(TOKEN_KEY)
+ }
+
+ const setUser = (user) => {
+ if (user) localStorage.setItem(USER_KEY, JSON.stringify(user))
+ else localStorage.removeItem(USER_KEY)
+ }
+
+ const getPermissions = () => permissions.value
+ const setPermissions = (arr) => {
+ permissions.value = Array.isArray(arr) ? arr : []
+ }
+
+ const logout = () => {
+ setToken(null)
+ setUser(null)
+ setPermissions([])
+ }
+
+ const isLoggedIn = () => !!getToken()
+
+ const hasPermission = (key) => {
+ if (!key) return true
+ const user = getUser()
+ if (user && user.role_id === 9527) return true
+ return permissions.value.includes(key)
+ }
+
+ return {
+ getToken,
+ getUser,
+ setToken,
+ setUser,
+ getPermissions,
+ setPermissions,
+ hasPermission,
+ logout,
+ isLoggedIn
+ }
+}
diff --git a/admin/src/utils/disable-debug.js b/admin/src/utils/disable-debug.js
new file mode 100644
index 0000000..e599d0c
--- /dev/null
+++ b/admin/src/utils/disable-debug.js
@@ -0,0 +1,18 @@
+/**
+ * 禁止调试模式及右键 - 全局安全模块
+ * 在页面加载时立即执行
+ */
+
+// 禁止右键
+document.addEventListener('contextmenu', (e) => e.preventDefault())
+
+// 禁止 F12、Ctrl+Shift+I/J/C 等开发者工具快捷键
+document.addEventListener('keydown', (e) => {
+ if (
+ e.key === 'F12' ||
+ (e.ctrlKey && e.shiftKey && ['I', 'J', 'C'].includes(e.key.toUpperCase())) ||
+ (e.ctrlKey && e.key === 'U')
+ ) {
+ e.preventDefault()
+ }
+})
diff --git a/admin/src/views/Dashboard.vue b/admin/src/views/Dashboard.vue
new file mode 100644
index 0000000..19650ad
--- /dev/null
+++ b/admin/src/views/Dashboard.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+ {{ stats.users }}
+ 用户数
+
+
+
+
+ {{ stats.workspaces }}
+ 工作空间
+
+
+
+
+ {{ stats.conversations }}
+ 对话数
+
+
+
+
+ {{ stats.messages }}
+ 消息数
+
+
+
+
+
+
+ 快捷入口
+
+
+ 用户管理
+ 工作空间
+ 对话管理
+
+
+
+
+
+
+
+
diff --git a/admin/src/views/Login.vue b/admin/src/views/Login.vue
new file mode 100644
index 0000000..f285299
--- /dev/null
+++ b/admin/src/views/Login.vue
@@ -0,0 +1,294 @@
+
+
+
+
管理后台
+
+
+
+
+
+
+
+
+
+
+
+ 登录
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ resetCountdown > 0 ? `${resetCountdown}s` : '获取验证码' }}
+
+
+
+
+
+
+
+
+ 重置密码
+
+
+
+ 测试验证码:8888
+
+
+
+
+
+
+
+
+
+
+ {{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 注册
+
+
+
+ 测试验证码:8888
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/views/conversations/ConversationList.vue b/admin/src/views/conversations/ConversationList.vue
new file mode 100644
index 0000000..9a69577
--- /dev/null
+++ b/admin/src/views/conversations/ConversationList.vue
@@ -0,0 +1,55 @@
+
+
+
+
+ 对话管理
+
+
+
+ {{ row._id || row.id }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/views/settings/PaymentConfig.vue b/admin/src/views/settings/PaymentConfig.vue
new file mode 100644
index 0000000..c41fa1f
--- /dev/null
+++ b/admin/src/views/settings/PaymentConfig.vue
@@ -0,0 +1,121 @@
+
+
+
+
+ 支付配置
+
+
+ 微信支付
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 支付宝
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/views/settings/RolePermissions.vue b/admin/src/views/settings/RolePermissions.vue
new file mode 100644
index 0000000..be84215
--- /dev/null
+++ b/admin/src/views/settings/RolePermissions.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+ 超级管理员(9527)拥有全部权限且不可修改。为其他角色勾选其可用的后台权限。
+
+
+
+
+
+ (全部权限,不可修改)
+
+
+ {{ p.name }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/views/settings/SMSConfig.vue b/admin/src/views/settings/SMSConfig.vue
new file mode 100644
index 0000000..b211b86
--- /dev/null
+++ b/admin/src/views/settings/SMSConfig.vue
@@ -0,0 +1,102 @@
+
+
+
+
+ 短信平台配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/views/sites/HomepageEdit.vue b/admin/src/views/sites/HomepageEdit.vue
new file mode 100644
index 0000000..d946b15
--- /dev/null
+++ b/admin/src/views/sites/HomepageEdit.vue
@@ -0,0 +1,217 @@
+
+
+
+
+
+
+
+
+ 导航与标题
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 删除
+
+ + 添加链接
+
+
+ 下载按钮
+
+
+
+
+
+
+
+ 平台(轨道)
+
+
+
+
+ 删除
+
+ + 添加平台
+
+
+ 版本与徽章
+
+
+
+
+
+
+
+
+
+
+ 特性卡片
+
+
+
+
+ 删除
+
+ + 添加特性
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/views/sites/ModuleUpload.vue b/admin/src/views/sites/ModuleUpload.vue
new file mode 100644
index 0000000..953576a
--- /dev/null
+++ b/admin/src/views/sites/ModuleUpload.vue
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatSize(row.size) }}
+
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/src/views/sites/PageList.vue b/admin/src/views/sites/PageList.vue
new file mode 100644
index 0000000..8647709
--- /dev/null
+++ b/admin/src/views/sites/PageList.vue
@@ -0,0 +1,176 @@
+
+
+
+
+
+
+
+
+
+ {{ row.id }}
+
+
+
+
+
+ 首页
+ 页面
+
+
+
+
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确定
+
+
+
+
+
+
+
+
diff --git a/admin/src/views/sites/SiteList.vue b/admin/src/views/sites/SiteList.vue
new file mode 100644
index 0000000..3406a6d
--- /dev/null
+++ b/admin/src/views/sites/SiteList.vue
@@ -0,0 +1,160 @@
+
+
+
+
+
+
+
+
+
+ {{ row.id }}
+
+
+
+
+
+
+ 网页管理
+ 首页编辑
+ 功能模块
+ 官网
+ 设为官网
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确定
+
+
+
+
+
+
+
+
diff --git a/admin/src/views/users/UserList.vue b/admin/src/views/users/UserList.vue
new file mode 100644
index 0000000..ea733ef
--- /dev/null
+++ b/admin/src/views/users/UserList.vue
@@ -0,0 +1,213 @@
+
+
+
+
+
+
+
+
+
+ {{ row.id || row._id }}
+
+
+
+
+
+ 超级管理员
+ 管理员
+ 普通用户
+
+
+
+
+ 是
+ -
+
+
+
+
+
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确定
+
+
+
+
+
+
+
+
diff --git a/admin/src/views/workspaces/WorkspaceList.vue b/admin/src/views/workspaces/WorkspaceList.vue
new file mode 100644
index 0000000..489fe16
--- /dev/null
+++ b/admin/src/views/workspaces/WorkspaceList.vue
@@ -0,0 +1,54 @@
+
+
+
+
+ 工作空间管理
+
+
+
+ {{ row._id || row.id }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/vite.config.js b/admin/vite.config.js
new file mode 100644
index 0000000..c2fe782
--- /dev/null
+++ b/admin/vite.config.js
@@ -0,0 +1,17 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+export default defineConfig({
+ plugins: [vue()],
+ base: '/admin/',
+ server: {
+ port: 3000,
+ host: true,
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8080',
+ changeOrigin: true
+ }
+ }
+ }
+})
diff --git a/deploy.sh b/deploy.sh
new file mode 100644
index 0000000..4ebdbe7
--- /dev/null
+++ b/deploy.sh
@@ -0,0 +1,56 @@
+#!/usr/bin/env bash
+# 一键部署 api、web、admin 三个服务(Docker)
+# 用法:
+# 本地/当前机部署:./deploy.sh
+# 部署到远程机: DEPLOY_HOST=user@192.168.10.241 ./deploy.sh
+set -e
+ROOT="$(cd "$(dirname "$0")" && pwd)"
+cd "$ROOT"
+
+echo "=========================================="
+echo " yh_web 部署 (api + web + admin)"
+echo " 域名: https://yuheng.yuxindazhineng.com"
+echo "=========================================="
+
+run_deploy() {
+ local dir="$1"
+ cd "$dir"
+ if [ -f server/.env ]; then
+ set -a
+ source server/.env
+ set +a
+ echo "[OK] 已加载 server/.env"
+ fi
+ echo ""
+ echo "[1/3] 构建镜像 (api + web + admin)..."
+ docker compose build --no-cache 2>/dev/null || docker-compose build --no-cache
+ echo ""
+ echo "[2/3] 停止旧容器..."
+ docker compose down 2>/dev/null || docker-compose down
+ echo ""
+ echo "[3/3] 启动三个服务 + Mongo..."
+ docker compose up -d 2>/dev/null || docker-compose up -d
+ cd "$ROOT"
+ echo ""
+ echo "=========================================="
+ echo " 部署完成"
+ echo " api:9527 web:9528 admin:9529"
+ echo " 访问: https://yuheng.yuxindazhineng.com"
+ echo "=========================================="
+}
+
+if [ -n "$DEPLOY_HOST" ]; then
+ echo "远程部署到: $DEPLOY_HOST"
+ echo "同步项目目录..."
+ rsync -az --delete \
+ --exclude '.git' \
+ --exclude 'node_modules' \
+ --exclude 'web/dist' \
+ --exclude 'admin/dist' \
+ --exclude 'logs' \
+ "$ROOT/" "$DEPLOY_HOST:${DEPLOY_PATH:-yh_web}/"
+ echo "在远程执行部署..."
+ ssh "$DEPLOY_HOST" "cd ${DEPLOY_PATH:-yh_web} && chmod +x deploy.sh && ./deploy.sh"
+else
+ run_deploy "$ROOT"
+fi
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..d706947
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,63 @@
+# 域名与 HTTPS 由 Nginx Proxy Manager 统一配置: https://npm.yuxindazhineng.com/nginx/proxy
+# 本 compose 只暴露 9527(api)、9528(web)、9529(admin),由 NPM 反向代理到对外域名
+version: "3.8"
+
+services:
+ api:
+ build:
+ context: .
+ dockerfile: server/Dockerfile
+ image: yh_web-api:latest
+ container_name: yh_api
+ environment:
+ - PORT=9527
+ - MONGODB_URI=${MONGODB_URI:-mongodb://mongo:27017}
+ - MONGODB_DB=${MONGODB_DB:-yxd-agent-testing}
+ - GIN_MODE=release
+ - ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-https://yuheng.yuxindazhineng.com}
+ depends_on:
+ - mongo
+ networks:
+ - yh_net
+ ports:
+ - "9527:9527"
+
+ web:
+ build:
+ context: ./web
+ dockerfile: Dockerfile
+ image: yh_web-web:latest
+ container_name: yh_web
+ networks:
+ - yh_net
+ ports:
+ - "9528:80"
+
+ admin:
+ build:
+ context: ./admin
+ dockerfile: Dockerfile
+ image: yh_web-admin:latest
+ container_name: yh_admin
+ networks:
+ - yh_net
+ ports:
+ - "9529:80"
+
+ mongo:
+ image: mongo:7
+ container_name: yh_mongo
+ volumes:
+ - mongo_data:/data/db
+ networks:
+ - yh_net
+ # 仅内网,不暴露端口(API 容器内用 mongo:27017)
+ # ports:
+ # - "27017:27017"
+
+networks:
+ yh_net:
+ driver: bridge
+
+volumes:
+ mongo_data:
diff --git a/nginx-proxy-manager.md b/nginx-proxy-manager.md
new file mode 100644
index 0000000..13e6cb2
--- /dev/null
+++ b/nginx-proxy-manager.md
@@ -0,0 +1,30 @@
+# Nginx Proxy Manager 配置说明
+
+已在 [Nginx Proxy Manager](https://npm.yuxindazhineng.com/nginx/proxy) 配置域名和强制 HTTPS,对外统一用域名访问。
+
+## 1. 在 NPM 中编辑该域名的 Proxy Host
+
+- **Details**:域名填 **`yuheng.yuxindazhineng.com`**,**Forward Hostname / IP** 填本机 IP(如 `192.168.10.241`),**Forward Port** 填 **9528**(官网首页)。
+- **SSL**:按需开启并强制 HTTPS。
+
+## 2. Custom Locations(路径转发)
+
+在 **Custom Locations** 中保证以下三条(端口与截图不一致的请改掉):
+
+| Location | Scheme | Forward Hostname / IP | Forward Port | 说明 |
+|----------|--------|------------------------|--------------|----------|
+| `/` | `http` | `192.168.10.241` | **9528** | 官网首页 |
+| `/admin` | `http` | `192.168.10.241` | **9529** | 管理后台(端口应为 9529,不是 9289) |
+| `/api` | `http` | `192.168.10.241` | **9527** | 后端接口 |
+
+- 若当前是 `/index` 指向 9528,建议改为 **`/`** 指向 **9528**,这样首页是 `https://域名/` 而不是 `https://域名/index`。
+- `/admin` 的端口请确认为 **9529**(截图里 9289 易误,应为 9529)。
+- 务必增加 **`/api` → 9527**,否则前后台请求接口会 404。
+
+## 3. 访问方式(对外统一域名)
+
+- 官网:**https://yuheng.yuxindazhineng.com/**
+- 管理后台:**https://yuheng.yuxindazhineng.com/admin/**
+- 接口:**https://yuheng.yuxindazhineng.com/api/...**
+
+前后台与接口同域,无需再设 `VITE_API_BASE`。
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
new file mode 100644
index 0000000..42e4c70
--- /dev/null
+++ b/nginx/nginx.conf
@@ -0,0 +1,35 @@
+server {
+ listen 80;
+ server_name yuheng.yuxindazhineng.com;
+ # 若使用 HTTPS,取消下面注释并挂载证书到 /etc/nginx/ssl/
+ # listen 443 ssl;
+ # ssl_certificate /etc/nginx/ssl/fullchain.pem;
+ # ssl_certificate_key /etc/nginx/ssl/privkey.pem;
+
+ location /api/ {
+ proxy_pass http://api:9527;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ location /admin/ {
+ proxy_pass http://admin:80/admin/;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ location / {
+ proxy_pass http://web:80/;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+}
diff --git a/pull-and-restart.sh b/pull-and-restart.sh
new file mode 100644
index 0000000..11738d6
--- /dev/null
+++ b/pull-and-restart.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+# 拉取代码并重启项目(线上建议放在 /home/yxd/project,执行 ./pull-and-restart.sh)
+# 也可指定目录:PROJECT_ROOT=/home/yxd/project ./pull-and-restart.sh
+set -e
+ROOT="${PROJECT_ROOT:-$(cd "$(dirname "$0")" && pwd)}"
+cd "$ROOT"
+
+echo "=========================================="
+echo " yh_web 拉取并重启"
+echo " 路径: $ROOT"
+echo "=========================================="
+
+[ -f server/.env ] && set -a && source server/.env && set +a
+
+echo "[1/2] 拉取代码..."
+git pull
+
+echo ""
+echo "[2/2] 重新构建并启动..."
+docker compose build --no-cache 2>/dev/null || docker-compose build --no-cache
+docker compose up -d --force-recreate 2>/dev/null || docker-compose up -d --force-recreate
+
+echo ""
+echo "完成. api:9527 web:9528 admin:9529"
diff --git a/push-to-gitea.sh b/push-to-gitea.sh
new file mode 100644
index 0000000..736ff32
--- /dev/null
+++ b/push-to-gitea.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+# 推送到 Gitea: https://gitea.yuxindazhineng.com/whm/web
+set -e
+cd "$(dirname "$0")"
+GITEA_URL="https://gitea.yuxindazhineng.com/whm/web.git"
+
+if [ ! -d .git ]; then
+ echo "初始化 Git 仓库..."
+ git init
+ git branch -M main
+fi
+
+if ! git remote get-url origin 2>/dev/null; then
+ echo "添加远程: $GITEA_URL"
+ git remote add origin "$GITEA_URL"
+else
+ git remote set-url origin "$GITEA_URL"
+fi
+
+echo "添加并提交..."
+git add .
+if [ -n "$(git status --porcelain)" ]; then
+ git commit -m "chore: push to gitea (yh_web)"
+else
+ echo "无新变更"
+fi
+echo "推送到 origin..."
+git push -u origin main || git push -u origin main --force
+echo "完成: https://gitea.yuxindazhineng.com/whm/web"
diff --git a/restart.sh b/restart.sh
new file mode 100644
index 0000000..8860312
--- /dev/null
+++ b/restart.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+# 仅重启项目(不拉代码),适用于配置/环境变更后重启
+# 用法:./restart.sh 或 PROJECT_ROOT=/home/yxd/project ./restart.sh
+set -e
+ROOT="${PROJECT_ROOT:-$(cd "$(dirname "$0")" && pwd)}"
+cd "$ROOT"
+
+echo "重启 yh_web ($ROOT)..."
+[ -f server/.env ] && set -a && source server/.env && set +a
+docker compose down 2>/dev/null || docker-compose down
+docker compose up -d 2>/dev/null || docker-compose up -d
+echo "完成. api:9527 web:9528 admin:9529"
diff --git a/run-docker.sh b/run-docker.sh
new file mode 100644
index 0000000..3e40571
--- /dev/null
+++ b/run-docker.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+# 在 Docker 中运行项目,统一域名: https://yuheng.yuxindazhineng.com
+# 端口: 9527=api, 9528=index, 9529=admin(直连);对外通过 nginx 80/443
+set -e
+cd "$(dirname "$0")"
+
+echo "=== yh_web Docker 启动 ==="
+echo "对外域名与 HTTPS 由 Nginx Proxy Manager 配置,本机端口: 9527=api, 9528=web, 9529=admin"
+echo ""
+
+# 可选:从 server/.env 加载到当前 shell,供 docker-compose 使用
+if [ -f server/.env ]; then
+ set -a
+ source server/.env
+ set +a
+ echo "已加载 server/.env"
+fi
+
+docker compose build --no-cache 2>/dev/null || docker-compose build --no-cache
+docker compose up -d 2>/dev/null || docker-compose up -d
+
+echo ""
+echo "已启动。请用 NPM 配置的域名访问(如 https://yuheng.yuxindazhineng.com)"
+echo "直连: api :9527, web :9528, admin :9529"
diff --git a/scripts/kill-ports.ps1 b/scripts/kill-ports.ps1
new file mode 100644
index 0000000..5c3ff25
--- /dev/null
+++ b/scripts/kill-ports.ps1
@@ -0,0 +1,11 @@
+# Kill processes on ports 8080/3000/3001
+$ports = @(8080, 3000, 3001)
+foreach ($p in $ports) {
+ $conn = Get-NetTCPConnection -LocalPort $p -ErrorAction SilentlyContinue
+ if ($conn) {
+ $conn | ForEach-Object {
+ Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue
+ }
+ Write-Host "Port $p released"
+ }
+}
diff --git a/scripts/ssh-tunnel.ps1 b/scripts/ssh-tunnel.ps1
new file mode 100644
index 0000000..4f4405a
--- /dev/null
+++ b/scripts/ssh-tunnel.ps1
@@ -0,0 +1,30 @@
+# SSH 隧道 - 使用 Posh-SSH 自动输入密码
+# 首次运行需安装: Install-Module Posh-SSH -Scope CurrentUser
+
+$hostName = "www.yuxindazhineng.com"
+$port = 2223
+$user = "yxd"
+$password = "yuxindazn001"
+$localPort = 27017
+$remotePort = 27017
+
+if (-not (Get-Module -ListAvailable -Name Posh-SSH)) {
+ Write-Host "正在安装 Posh-SSH 模块(仅需一次)..."
+ Install-Module Posh-SSH -Scope CurrentUser -Force
+}
+
+$secPass = ConvertTo-SecureString $password -AsPlainText -Force
+$cred = New-Object System.Management.Automation.PSCredential($user, $secPass)
+
+Write-Host "正在建立 SSH 隧道..."
+$session = New-SSHSession -ComputerName $hostName -Port $port -Credential $cred -AcceptKey -Force
+if (-not $session) {
+ Write-Host "SSH 连接失败"
+ pause
+ exit 1
+}
+
+$forward = New-SSHLocalPortForward -Index 0 -LocalAddress 127.0.0.1 -LocalPort $localPort -RemoteAddress localhost -RemotePort $remotePort
+Write-Host "SSH 隧道已建立 (localhost:$localPort -> 远程:$remotePort)"
+Write-Host "保持此窗口打开,关闭即断开"
+pause
diff --git a/server/.air.toml b/server/.air.toml
new file mode 100644
index 0000000..c8e9d15
--- /dev/null
+++ b/server/.air.toml
@@ -0,0 +1,45 @@
+# Air 热加载配置(可选,当前使用 CompileDaemon)
+# 安装: go install github.com/air-verse/air@latest
+# 运行: air
+
+root = "."
+tmp_dir = "tmp"
+
+[build]
+ cmd = "go build -o ./tmp/main ."
+ bin = "./tmp/main"
+ include_ext = ["go", "tpl", "tmpl", "html"]
+ exclude_dir = ["tmp", "vendor", "node_modules"]
+ exclude_file = []
+ exclude_regex = ["_test\\.go$"]
+ exclude_unchanged = false
+ follow_symlink = false
+ full_bin = ""
+ kill_delay = "0s"
+ log = "build-errors.log"
+ poll = false
+ poll_interval = 0
+ post_cmd = []
+ pre_cmd = []
+ rerun = false
+ rerun_delay = 500
+ send_interrupt = false
+ stop_on_error = false
+
+[color]
+ app = ""
+ build = "yellow"
+ main = "magenta"
+ runner = "green"
+ watcher = "cyan"
+
+[log]
+ main_only = false
+ time = false
+
+[misc]
+ clean_on_exit = true
+
+[screen]
+ clear_on_rebuild = true
+ keep_scroll = true
diff --git a/server/.env.example b/server/.env.example
new file mode 100644
index 0000000..013bbe4
--- /dev/null
+++ b/server/.env.example
@@ -0,0 +1,9 @@
+# 复制为 .env 或 .env.production 后修改
+# Go 不会自动加载 .env,需在启动前导出变量(见项目根目录 .env.example 的说明)
+
+MONGODB_URI=mongodb://localhost:27017
+MONGODB_DB=yxd-agent-testing
+PORT=8080
+GIN_MODE=release
+SERVER_DOMAIN=https://api.example.com
+ALLOWED_ORIGINS=
diff --git a/server/Dockerfile b/server/Dockerfile
new file mode 100644
index 0000000..74b85bb
--- /dev/null
+++ b/server/Dockerfile
@@ -0,0 +1,13 @@
+# 需在项目根目录构建: docker build -f server/Dockerfile .
+FROM golang:1.21-alpine AS builder
+WORKDIR /build
+COPY server/ ./
+RUN go mod download && CGO_ENABLED=0 go build -o /app/server .
+
+FROM alpine:3.19
+WORKDIR /app
+RUN apk add --no-cache ca-certificates tzdata
+ENV TZ=Asia/Shanghai
+COPY --from=builder /app/server .
+EXPOSE 9527
+ENTRYPOINT ["./server"]
diff --git a/server/README.md b/server/README.md
new file mode 100644
index 0000000..90f364d
--- /dev/null
+++ b/server/README.md
@@ -0,0 +1,24 @@
+# 多站点管理后台 - API 服务
+
+基于 Gin 的后端 API 服务。
+
+## MongoDB 连接(SSH 穿透)
+
+MongoDB 在远程服务器,需先建立 SSH 隧道:
+
+```bash
+ssh -p 2223 -L 27017:localhost:27017 yxd@www.yuxindazhineng.com
+```
+
+或双击运行 `scripts/start-ssh-tunnel.bat`
+
+隧道建立后,`mongodb://localhost:27017` 会转发到远程 MongoDB。
+
+## 运行
+
+```bash
+go mod tidy
+go run main.go
+```
+
+默认端口 8080
diff --git a/server/config/constants.go b/server/config/constants.go
new file mode 100644
index 0000000..041e5fa
--- /dev/null
+++ b/server/config/constants.go
@@ -0,0 +1,4 @@
+package config
+
+// DBName 数据库名,可由环境变量 MONGODB_DB 覆盖
+var DBName = "yxd-agent-testing"
diff --git a/server/config/database.go b/server/config/database.go
new file mode 100644
index 0000000..8988e26
--- /dev/null
+++ b/server/config/database.go
@@ -0,0 +1,56 @@
+package config
+
+import (
+ "context"
+ "log"
+ "time"
+
+ "yh_web/server/pkg/logger"
+
+ "go.mongodb.org/mongo-driver/v2/bson"
+ "go.mongodb.org/mongo-driver/v2/mongo"
+ "go.mongodb.org/mongo-driver/v2/mongo/options"
+)
+
+var MongoClient *mongo.Client
+
+// ConnectMongoDB 连接本地 MongoDB
+func ConnectMongoDB(uri string) error {
+ clientOpts := options.Client().ApplyURI(uri)
+ client, err := mongo.Connect(clientOpts)
+ if err != nil {
+ return err
+ }
+
+ // 验证连接
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ var result bson.M
+ if err = client.Database("admin").RunCommand(ctx, bson.D{{Key: "ping", Value: 1}}).Decode(&result); err != nil {
+ return err
+ }
+
+ MongoClient = client
+ log.Println("MongoDB 连接成功")
+ logger.Log("config/database", "MongoDB 连接成功")
+ return nil
+}
+
+// GetDB 获取指定数据库;未连接 MongoDB 时返回 nil
+func GetDB(name string) *mongo.Database {
+ if MongoClient == nil {
+ return nil
+ }
+ return MongoClient.Database(name)
+}
+
+// CloseMongoDB 关闭连接
+func CloseMongoDB() {
+ if MongoClient != nil {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ _ = MongoClient.Disconnect(ctx)
+ log.Println("MongoDB 连接已关闭")
+ logger.Log("config/database", "MongoDB 连接已关闭")
+ }
+}
diff --git a/server/config/db_structure.go b/server/config/db_structure.go
new file mode 100644
index 0000000..f5a75aa
--- /dev/null
+++ b/server/config/db_structure.go
@@ -0,0 +1,78 @@
+package config
+
+import (
+ "context"
+ "time"
+
+ "go.mongodb.org/mongo-driver/v2/bson"
+)
+
+// DBStructure 数据库结构信息
+type DBStructure struct {
+ Databases []DatabaseInfo `json:"databases"`
+}
+
+// DatabaseInfo 数据库信息
+type DatabaseInfo struct {
+ Name string `json:"name"`
+ Collections []CollInfo `json:"collections"`
+}
+
+// CollInfo 集合信息
+type CollInfo struct {
+ Name string `json:"name"`
+ Count int64 `json:"count"`
+ Sample interface{} `json:"sample,omitempty"` // 采样一条文档看结构
+}
+
+// GetDBStructure 获取 MongoDB 数据结构
+func GetDBStructure() (*DBStructure, error) {
+ if MongoClient == nil {
+ return nil, nil
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ // 列出所有数据库(排除系统库)
+ dbs, err := MongoClient.ListDatabaseNames(ctx, bson.M{
+ "name": bson.M{"$nin": []string{"admin", "config", "local"}},
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ result := &DBStructure{Databases: make([]DatabaseInfo, 0, len(dbs))}
+
+ for _, dbName := range dbs {
+ db := MongoClient.Database(dbName)
+ colls, err := db.ListCollectionNames(ctx, bson.M{})
+ if err != nil {
+ continue
+ }
+
+ dbInfo := DatabaseInfo{
+ Name: dbName,
+ Collections: make([]CollInfo, 0, len(colls)),
+ }
+
+ for _, collName := range colls {
+ coll := db.Collection(collName)
+ count, _ := coll.CountDocuments(ctx, bson.M{})
+
+ // 采样一条文档
+ var sample bson.M
+ _ = coll.FindOne(ctx, bson.M{}).Decode(&sample)
+
+ dbInfo.Collections = append(dbInfo.Collections, CollInfo{
+ Name: collName,
+ Count: count,
+ Sample: sample,
+ })
+ }
+
+ result.Databases = append(result.Databases, dbInfo)
+ }
+
+ return result, nil
+}
diff --git a/server/dev.bat b/server/dev.bat
new file mode 100644
index 0000000..52c2e0d
--- /dev/null
+++ b/server/dev.bat
@@ -0,0 +1,8 @@
+@echo off
+chcp 65001 >nul
+cd /d "%~dp0"
+
+echo Starting backend with hot reload...
+if not exist tmp mkdir tmp
+
+CompileDaemon -command=tmp\main.exe -build="go build -o tmp\main.exe ."
diff --git a/server/go.mod b/server/go.mod
new file mode 100644
index 0000000..fafcd07
--- /dev/null
+++ b/server/go.mod
@@ -0,0 +1,44 @@
+module yh_web/server
+
+go 1.21
+
+require (
+ github.com/gin-gonic/gin v1.9.1
+ go.mongodb.org/mongo-driver v1.17.9
+ go.mongodb.org/mongo-driver/v2 v2.5.0
+)
+
+require (
+ github.com/bytedance/sonic v1.9.1 // indirect
+ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.2 // indirect
+ github.com/gin-contrib/sse v0.1.0 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-playground/validator/v10 v10.14.0 // indirect
+ github.com/goccy/go-json v0.10.2 // indirect
+ github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
+ github.com/joho/godotenv v1.5.1 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/compress v1.17.6 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.4 // indirect
+ github.com/leodido/go-urn v1.2.4 // indirect
+ github.com/mattn/go-isatty v0.0.19 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/pelletier/go-toml/v2 v2.0.8 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.2.11 // indirect
+ github.com/xdg-go/pbkdf2 v1.0.0 // indirect
+ github.com/xdg-go/scram v1.2.0 // indirect
+ github.com/xdg-go/stringprep v1.0.4 // indirect
+ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
+ golang.org/x/arch v0.3.0 // indirect
+ golang.org/x/crypto v0.33.0 // indirect
+ golang.org/x/net v0.21.0 // indirect
+ golang.org/x/sync v0.11.0 // indirect
+ golang.org/x/sys v0.30.0 // indirect
+ golang.org/x/text v0.22.0 // indirect
+ google.golang.org/protobuf v1.30.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/server/go.sum b/server/go.sum
new file mode 100644
index 0000000..0f12dfc
--- /dev/null
+++ b/server/go.sum
@@ -0,0 +1,130 @@
+github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
+github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
+github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
+github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
+github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
+github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
+github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
+github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
+github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
+github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
+github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
+github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
+github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
+github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
+github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
+github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
+github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
+github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
+github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
+github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
+github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
+go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
+go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
+go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
+golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
+golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
+golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
+google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/server/handlers/auth.go b/server/handlers/auth.go
new file mode 100644
index 0000000..cd213af
--- /dev/null
+++ b/server/handlers/auth.go
@@ -0,0 +1,199 @@
+package handlers
+
+import (
+ "context"
+ "errors"
+ "net/http"
+ "time"
+
+ "go.mongodb.org/mongo-driver/v2/bson"
+ "go.mongodb.org/mongo-driver/v2/mongo"
+
+ "yh_web/server/config"
+ "yh_web/server/models"
+ "yh_web/server/pkg/logger"
+ "yh_web/server/utils"
+
+ "github.com/gin-gonic/gin"
+ "github.com/golang-jwt/jwt/v5"
+)
+
+const jwtSecret = "yh_web_admin_jwt_secret_change_in_production"
+const jwtExpire = 24 * time.Hour
+
+type LoginInput struct {
+ Username string `json:"username" binding:"required"`
+ Password string `json:"password" binding:"required"`
+}
+
+type Claims struct {
+ UserID string `json:"user_id"`
+ Username string `json:"username"`
+ RoleID int `json:"role_id"`
+ Role string `json:"role"`
+ jwt.RegisteredClaims
+}
+
+// Login 后台登录,仅 role_id=9527 超级管理员可登录
+func Login(c *gin.Context) {
+ var input LoginInput
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "用户名和密码不能为空"})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("users")
+ var user models.User
+ err := coll.FindOne(ctx, bson.M{"username": input.Username}).Decode(&user)
+ if err != nil {
+ if errors.Is(err, mongo.ErrNoDocuments) {
+ // 尝试用手机号登录
+ err = coll.FindOne(ctx, bson.M{"mobile": input.Username}).Decode(&user)
+ }
+ if err != nil {
+ if errors.Is(err, mongo.ErrNoDocuments) {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
+ return
+ }
+ logger.Err("handlers/auth", "[Login] FindOne error: %v", err)
+ resp := gin.H{"error": "登录失败,请稍后重试"}
+ if gin.Mode() == gin.DebugMode {
+ resp["debug"] = err.Error()
+ }
+ c.JSON(http.StatusInternalServerError, resp)
+ return
+ }
+ }
+
+ // 超级管理员(9527)或超级用户(role_id=0, role=admin)可登录后台
+ if user.RoleID != models.RoleIDSuperAdmin && !(user.RoleID == models.RoleIDSuperUser && user.Role == "admin") {
+ c.JSON(http.StatusForbidden, gin.H{"error": "无后台访问权限"})
+ return
+ }
+
+ roleID := user.RoleID
+ if roleID == 0 && user.Role == "admin" {
+ roleID = models.RoleIDSuperAdmin
+ }
+
+ hashed := utils.HashPassword(input.Password)
+ if hashed != user.Password {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
+ return
+ }
+
+ claims := Claims{
+ UserID: user.ID.Hex(),
+ Username: user.Username,
+ RoleID: roleID,
+ Role: user.Role,
+ RegisteredClaims: jwt.RegisteredClaims{
+ ExpiresAt: jwt.NewNumericDate(time.Now().Add(jwtExpire)),
+ IssuedAt: jwt.NewNumericDate(time.Now()),
+ },
+ }
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+ tokenStr, err := token.SignedString([]byte(jwtSecret))
+ if err != nil {
+ logger.Err("handlers/auth", "JWT SignedString error: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "生成令牌失败"})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "token": tokenStr,
+ "user": gin.H{
+ "id": user.ID.Hex(),
+ "username": user.Username,
+ "role_id": roleID,
+ "role": user.Role,
+ },
+ "expires_in": int64(jwtExpire.Seconds()),
+ })
+}
+
+// AuthRequired 鉴权中间件,要求 role_id=9527
+func AuthRequired() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ tokenStr := c.GetHeader("Authorization")
+ if tokenStr == "" {
+ tokenStr = c.Query("token")
+ }
+ if len(tokenStr) > 7 && tokenStr[:7] == "Bearer " {
+ tokenStr = tokenStr[7:]
+ }
+ if tokenStr == "" {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
+ c.Abort()
+ return
+ }
+
+ var claims Claims
+ token, err := jwt.ParseWithClaims(tokenStr, &claims, func(t *jwt.Token) (interface{}, error) {
+ return []byte(jwtSecret), nil
+ })
+ if err != nil || !token.Valid {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "登录已过期,请重新登录"})
+ c.Abort()
+ return
+ }
+
+ // 仅超级管理员或超级用户(role_id=0, role=admin)可访问后台
+ if claims.RoleID != models.RoleIDSuperAdmin && !(claims.RoleID == models.RoleIDSuperUser && claims.Role == "admin") {
+ c.JSON(http.StatusForbidden, gin.H{"error": "无后台访问权限"})
+ c.Abort()
+ return
+ }
+
+ c.Set("user_id", claims.UserID)
+ c.Set("username", claims.Username)
+ c.Set("role_id", claims.RoleID)
+ c.Set("role", claims.Role)
+ c.Next()
+ }
+}
+
+// SuperUserAuthRequired 超级用户鉴权:仅 role_id=0 且 role=admin 可访问(如短信平台配置)
+// 集团超级用户 username=admin 只能配置集团信息,不能配置短信
+func SuperUserAuthRequired() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ tokenStr := c.GetHeader("Authorization")
+ if tokenStr == "" {
+ tokenStr = c.Query("token")
+ }
+ if len(tokenStr) > 7 && tokenStr[:7] == "Bearer " {
+ tokenStr = tokenStr[7:]
+ }
+ if tokenStr == "" {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
+ c.Abort()
+ return
+ }
+
+ var claims Claims
+ token, err := jwt.ParseWithClaims(tokenStr, &claims, func(t *jwt.Token) (interface{}, error) {
+ return []byte(jwtSecret), nil
+ })
+ if err != nil || !token.Valid {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "登录已过期,请重新登录"})
+ c.Abort()
+ return
+ }
+
+ // 仅 role_id=9527 且 role=admin 可配置短信平台
+ if claims.RoleID != models.RoleIDSuperAdmin || claims.Role != "admin" {
+ c.JSON(http.StatusForbidden, gin.H{"error": "仅超级管理员可配置短信平台"})
+ c.Abort()
+ return
+ }
+
+ c.Set("user_id", claims.UserID)
+ c.Set("username", claims.Username)
+ c.Set("role_id", claims.RoleID)
+ c.Set("role", claims.Role)
+ c.Next()
+ }
+}
diff --git a/server/handlers/conversation.go b/server/handlers/conversation.go
new file mode 100644
index 0000000..991542d
--- /dev/null
+++ b/server/handlers/conversation.go
@@ -0,0 +1,67 @@
+package handlers
+
+import (
+ "context"
+ "net/http"
+ "time"
+
+ "go.mongodb.org/mongo-driver/v2/bson"
+ "go.mongodb.org/mongo-driver/v2/mongo/options"
+
+ "yh_web/server/config"
+
+ "github.com/gin-gonic/gin"
+)
+
+func GetConversations(c *gin.Context) {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("conversations")
+
+ page := 1
+ pageSize := 20
+ if p := c.Query("page"); p != "" {
+ if v, err := parseInt(p); err == nil && v > 0 {
+ page = v
+ }
+ }
+ if ps := c.Query("page_size"); ps != "" {
+ if v, err := parseInt(ps); err == nil && v > 0 && v <= 100 {
+ pageSize = v
+ }
+ }
+
+ skip := (page - 1) * pageSize
+ opts := options.Find().SetSkip(int64(skip)).SetLimit(int64(pageSize)).SetSort(bson.D{{Key: "_id", Value: -1}})
+
+ filter := bson.M{}
+ if userID := c.Query("user_id"); userID != "" {
+ filter["user_id"] = userID
+ }
+ if workspaceID := c.Query("workspace_id"); workspaceID != "" {
+ filter["workspace_id"] = workspaceID
+ }
+
+ cursor, err := coll.Find(ctx, filter, opts)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ defer cursor.Close(ctx)
+
+ var list []map[string]interface{}
+ if err = cursor.All(ctx, &list); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ total, _ := coll.CountDocuments(ctx, filter)
+
+ c.JSON(http.StatusOK, gin.H{
+ "list": list,
+ "total": total,
+ "page": page,
+ "page_size": pageSize,
+ })
+}
diff --git a/server/handlers/helpers.go b/server/handlers/helpers.go
new file mode 100644
index 0000000..f37b561
--- /dev/null
+++ b/server/handlers/helpers.go
@@ -0,0 +1,7 @@
+package handlers
+
+import "strconv"
+
+func parseInt(s string) (int, error) {
+ return strconv.Atoi(s)
+}
diff --git a/server/handlers/homepage.go b/server/handlers/homepage.go
new file mode 100644
index 0000000..161bade
--- /dev/null
+++ b/server/handlers/homepage.go
@@ -0,0 +1,345 @@
+package handlers
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "strings"
+ "time"
+
+ "go.mongodb.org/mongo-driver/v2/bson"
+ "go.mongodb.org/mongo-driver/v2/mongo"
+ "go.mongodb.org/mongo-driver/v2/mongo/options"
+
+ "yh_web/server/config"
+ "yh_web/server/models"
+
+ "github.com/gin-gonic/gin"
+)
+
+const homepageSlug = "index"
+const officialSiteConfigID = "official_site_id"
+
+type officialSiteDoc struct {
+ ID string `bson:"_id"`
+ SiteID string `bson:"site_id"`
+}
+
+// getOfficialSiteID 从 system_config 读取官网站点 ID;未设置则返回第一个站点的 ID
+func getOfficialSiteID(ctx context.Context) string {
+ coll := config.GetDB(config.DBName).Collection("system_config")
+ var doc officialSiteDoc
+ err := coll.FindOne(ctx, bson.M{"_id": officialSiteConfigID}).Decode(&doc)
+ if err == nil && doc.SiteID != "" {
+ return doc.SiteID
+ }
+ sitesColl := config.GetDB(config.DBName).Collection("sites")
+ opts := options.FindOne().SetSort(bson.D{{Key: "created_at", Value: -1}})
+ var site models.Site
+ if err := sitesColl.FindOne(ctx, bson.M{}, opts).Decode(&site); err == nil {
+ return site.ID.Hex()
+ }
+ return ""
+}
+
+// GetWebHomepage 前台:获取官网站点首页数据(无需鉴权)
+func GetWebHomepage(c *gin.Context) {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ siteID := getOfficialSiteID(ctx)
+ if siteID == "" {
+ c.JSON(http.StatusOK, defaultHomepageData())
+ return
+ }
+
+ coll := config.GetDB(config.DBName).Collection("pages")
+ var page models.Page
+ err := coll.FindOne(ctx, bson.M{"site_id": siteID, "slug": homepageSlug}).Decode(&page)
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ c.JSON(http.StatusOK, defaultHomepageData())
+ return
+ }
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ var data models.HomepageData
+ if page.Content != "" {
+ if err := json.Unmarshal([]byte(page.Content), &data); err != nil {
+ c.JSON(http.StatusOK, defaultHomepageData())
+ return
+ }
+ } else {
+ data = defaultHomepageData()
+ }
+ c.JSON(http.StatusOK, data)
+}
+
+// GetHomepage 获取站点首页数据
+func GetHomepage(c *gin.Context) {
+ siteID := c.Param("site_id")
+ if siteID == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("pages")
+ var page models.Page
+ err := coll.FindOne(ctx, bson.M{"site_id": siteID, "slug": homepageSlug}).Decode(&page)
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ c.JSON(http.StatusOK, defaultHomepageData())
+ return
+ }
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ var data models.HomepageData
+ if page.Content != "" {
+ if err := json.Unmarshal([]byte(page.Content), &data); err != nil {
+ c.JSON(http.StatusOK, defaultHomepageData())
+ return
+ }
+ } else {
+ data = defaultHomepageData()
+ }
+ c.JSON(http.StatusOK, data)
+}
+
+// UpdateHomepage 更新站点首页数据
+func UpdateHomepage(c *gin.Context) {
+ siteID := c.Param("site_id")
+ if siteID == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
+ return
+ }
+
+ var data models.HomepageData
+ if err := c.ShouldBindJSON(&data); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ body, err := json.Marshal(data)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("pages")
+ now := time.Now().Format(time.RFC3339)
+ filter := bson.M{"site_id": siteID, "slug": homepageSlug}
+ update := bson.M{
+ "$set": bson.M{
+ "site_id": siteID,
+ "slug": homepageSlug,
+ "title": data.Title,
+ "type": "homepage",
+ "content": string(body),
+ "updated_at": now,
+ },
+ }
+ opts := options.UpdateOne().SetUpsert(true)
+ _, err = coll.UpdateOne(ctx, filter, update, opts)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"message": "保存成功"})
+}
+
+// DownloadHomepage 下载首页 HTML
+func DownloadHomepage(c *gin.Context) {
+ siteID := c.Param("site_id")
+ if siteID == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("pages")
+ var page models.Page
+ var data models.HomepageData
+ err := coll.FindOne(ctx, bson.M{"site_id": siteID, "slug": homepageSlug}).Decode(&page)
+ if err == nil && page.Content != "" {
+ _ = json.Unmarshal([]byte(page.Content), &data)
+ }
+ if err != nil || page.Content == "" {
+ data = defaultHomepageData()
+ }
+
+ html := renderHomepageHTML(&data)
+ c.Header("Content-Disposition", "attachment; filename=index.html")
+ c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(html))
+}
+
+func defaultHomepageData() models.HomepageData {
+ return models.HomepageData{
+ LogoText: "YUHENG ONE",
+ NavLinks: []models.NavLink{{Label: "MISSION", URL: "#"}, {Label: "DOWNLOAD", URL: "#"}, {Label: "CONTACT", URL: "#"}},
+ Title: "宇恒一号",
+ Subtitle: "INTERSTELLAR EXPLORER EDITION",
+ Description: "跨越星际的智能伙伴 · 探索无限可能
\n 引领您进入前所未有的数字宇宙",
+ DownloadText: "START EXPLORING",
+ DownloadURL: "#",
+ Platforms: []models.PlatformItem{
+ {Name: "WINDOWS", URL: "#"},
+ {Name: "MACOS", URL: "#"},
+ {Name: "LINUX", URL: "#"},
+ {Name: "IOS", URL: "#"},
+ {Name: "ANDROID", URL: "#"},
+ },
+ Version: "VERSION 3.2.1",
+ LaunchYear: "LAUNCH: 2024",
+ BadgeText: "FREE ACCESS",
+ Features: []models.FeatureItem{
+ {Title: "星际导航", Desc: "先进的AI导航系统,精准定位您的需求,引领探索之旅"},
+ {Title: "量子同步", Desc: "跨维度数据同步技术,您的数据在多宇宙中保持一致"},
+ {Title: "星际防护", Desc: "来自未来的安全加密协议,守护您的数字资产安全"},
+ },
+ FooterText: "© 2024 YUHENG ONE // STELLAR EXPLORATION INITIATIVE",
+ }
+}
+
+// renderHomepageHTML 根据数据生成首页 HTML(简化版,保留原样式与结构)
+func renderHomepageHTML(d *models.HomepageData) string {
+ if d == nil {
+ d = &models.HomepageData{}
+ }
+ titleChars := splitTitle(d.Title)
+ navHTML := ""
+ for _, l := range d.NavLinks {
+ navHTML += `` + escape(l.Label) + ``
+ }
+ platformsHTML := ""
+ for _, p := range d.Platforms {
+ platformsHTML += ``
+ }
+ featuresHTML := ""
+ for i, f := range d.Features {
+ iconPath := []string{
+ "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z",
+ "M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM19 18H6c-2.21 0-4-1.79-4-4s1.79-4 4-4h.71C7.37 7.69 9.48 6 12 6c3.04 0 5.5 2.46 5.5 5.5v.5H19c1.66 0 3 1.34 3 3s-1.34 3-3 3z",
+ "M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z",
+ }
+ if i >= len(iconPath) {
+ iconPath = append(iconPath, iconPath[0])
+ }
+ path := iconPath[i%3]
+ featuresHTML += `` + escape(f.Title) + `
` + escape(f.Desc) + `
`
+ }
+
+ sb := &strings.Builder{}
+ sb.WriteString("\n\n\n\n\n")
+ sb.WriteString(escape(d.Title))
+ sb.WriteString(" - 星际探索版\n")
+ sb.WriteString(homepageCSS)
+ sb.WriteString("\n\n\n\n\n\n\n\n")
+ for _, ch := range titleChars {
+ sb.WriteString("" + escape(ch) + "")
+ }
+ sb.WriteString("
\n\n")
+ sb.WriteString(escape(d.Subtitle))
+ sb.WriteString("
\n")
+ sb.WriteString(strings.ReplaceAll(escape(d.Description), "\n", "
\n"))
+ sb.WriteString("
\n\n")
+ sb.WriteString(platformsHTML)
+ sb.WriteString("
\n\n")
+ sb.WriteString(escape(d.Version))
+ sb.WriteString("\n🚀 ")
+ sb.WriteString(escape(d.LaunchYear))
+ sb.WriteString("\n⚡ ")
+ sb.WriteString(escape(d.BadgeText))
+ sb.WriteString("\n
\n")
+ sb.WriteString(featuresHTML)
+ sb.WriteString("
\n\n\n\n\n")
+ return sb.String()
+}
+
+func splitTitle(s string) []string {
+ var out []rune
+ for _, r := range s {
+ out = append(out, r)
+ }
+ if len(out) == 0 {
+ return []string{"宇", "恒", "一", "号"}
+ }
+ result := make([]string, len(out))
+ for i, r := range out {
+ result[i] = string(r)
+ }
+ return result
+}
+
+func escape(s string) string {
+ s = strings.ReplaceAll(s, "&", "&")
+ s = strings.ReplaceAll(s, "<", "<")
+ s = strings.ReplaceAll(s, ">", ">")
+ s = strings.ReplaceAll(s, "\"", """)
+ return s
+}
+
+const homepageCSS = `
+`
\ No newline at end of file
diff --git a/server/handlers/module_upload.go b/server/handlers/module_upload.go
new file mode 100644
index 0000000..7ff83ce
--- /dev/null
+++ b/server/handlers/module_upload.go
@@ -0,0 +1,145 @@
+package handlers
+
+import (
+ "context"
+ "net/http"
+ "os"
+ "path/filepath"
+ "time"
+
+ "go.mongodb.org/mongo-driver/v2/bson"
+ "go.mongodb.org/mongo-driver/v2/mongo/options"
+
+ "yh_web/server/config"
+ "yh_web/server/models"
+
+ "github.com/gin-gonic/gin"
+)
+
+const uploadDir = "uploads"
+
+// ListSiteAssets 站点功能模块/上传文件列表
+func ListSiteAssets(c *gin.Context) {
+ siteID := c.Param("site_id")
+ if siteID == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("site_assets")
+ opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}})
+ cursor, err := coll.Find(ctx, bson.M{"site_id": siteID}, opts)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ defer cursor.Close(ctx)
+
+ var list []models.SiteAsset
+ if err = cursor.All(ctx, &list); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ total, _ := coll.CountDocuments(ctx, bson.M{"site_id": siteID})
+ c.JSON(http.StatusOK, gin.H{"list": list, "total": total})
+}
+
+// UploadSiteAsset 上传功能模块/文件
+func UploadSiteAsset(c *gin.Context) {
+ siteID := c.Param("site_id")
+ if siteID == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
+ return
+ }
+
+ file, err := c.FormFile("file")
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "请选择要上传的文件"})
+ return
+ }
+
+ baseDir := filepath.Join(uploadDir, "sites", siteID)
+ if err := os.MkdirAll(baseDir, 0755); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "创建目录失败"})
+ return
+ }
+
+ // 避免覆盖:加时间戳
+ name := file.Filename
+ ext := filepath.Ext(name)
+ nameNoExt := name[:len(name)-len(ext)]
+ saveName := nameNoExt + "_" + time.Now().Format("20060102150405") + ext
+ relPath := filepath.Join("sites", siteID, saveName)
+ destPath := filepath.Join(uploadDir, relPath)
+
+ if err := c.SaveUploadedFile(file, destPath); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ doc := models.SiteAsset{
+ SiteID: siteID,
+ Name: file.Filename,
+ FilePath: relPath,
+ Size: file.Size,
+ ContentType: file.Header.Get("Content-Type"),
+ CreatedAt: time.Now().Format(time.RFC3339),
+ }
+ res, err := config.GetDB(config.DBName).Collection("site_assets").InsertOne(ctx, bson.M{
+ "site_id": doc.SiteID,
+ "name": doc.Name,
+ "file_path": doc.FilePath,
+ "size": doc.Size,
+ "content_type": doc.ContentType,
+ "created_at": doc.CreatedAt,
+ })
+ if err != nil {
+ os.Remove(destPath)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"id": res.InsertedID, "file_path": doc.FilePath, "message": "上传成功"})
+}
+
+// DeleteSiteAsset 删除站点资源
+func DeleteSiteAsset(c *gin.Context) {
+ siteID := c.Param("site_id")
+ idStr := c.Param("asset_id")
+ if siteID == "" || idStr == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
+ return
+ }
+
+ oid, err := bson.ObjectIDFromHex(idStr)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("site_assets")
+ var asset models.SiteAsset
+ err = coll.FindOne(ctx, bson.M{"_id": oid, "site_id": siteID}).Decode(&asset)
+ if err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "资源不存在"})
+ return
+ }
+
+ fullPath := filepath.Join(uploadDir, asset.FilePath)
+ os.Remove(fullPath)
+
+ _, err = coll.DeleteOne(ctx, bson.M{"_id": oid})
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
+}
diff --git a/server/handlers/official_site.go b/server/handlers/official_site.go
new file mode 100644
index 0000000..aaa29ce
--- /dev/null
+++ b/server/handlers/official_site.go
@@ -0,0 +1,50 @@
+package handlers
+
+import (
+ "context"
+ "net/http"
+ "time"
+
+ "go.mongodb.org/mongo-driver/v2/bson"
+ "go.mongodb.org/mongo-driver/v2/mongo/options"
+
+ "yh_web/server/config"
+
+ "github.com/gin-gonic/gin"
+)
+
+// GetOfficialSite 获取官网站点 ID
+func GetOfficialSite(c *gin.Context) {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ siteID := getOfficialSiteID(ctx)
+ c.JSON(http.StatusOK, gin.H{"site_id": siteID})
+}
+
+// SetOfficialSiteInput 设置官网站点
+type SetOfficialSiteInput struct {
+ SiteID string `json:"site_id" binding:"required"`
+}
+
+// SetOfficialSite 设置官网站点
+func SetOfficialSite(c *gin.Context) {
+ var input SetOfficialSiteInput
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("system_config")
+ doc := officialSiteDoc{ID: officialSiteConfigID, SiteID: input.SiteID}
+ opts := options.UpdateOne().SetUpsert(true)
+ _, err := coll.UpdateOne(ctx, bson.M{"_id": officialSiteConfigID}, bson.M{"$set": doc}, opts)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"message": "已设为官网站点", "site_id": input.SiteID})
+}
diff --git a/server/handlers/page.go b/server/handlers/page.go
new file mode 100644
index 0000000..07a8a10
--- /dev/null
+++ b/server/handlers/page.go
@@ -0,0 +1,175 @@
+package handlers
+
+import (
+ "context"
+ "net/http"
+ "time"
+
+ "go.mongodb.org/mongo-driver/v2/bson"
+ "go.mongodb.org/mongo-driver/v2/mongo/options"
+
+ "yh_web/server/config"
+ "yh_web/server/models"
+
+ "github.com/gin-gonic/gin"
+)
+
+// GetPages 网页列表(按站点)
+func GetPages(c *gin.Context) {
+ siteID := c.Query("site_id")
+ if siteID == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("pages")
+ opts := options.Find().SetSort(bson.D{{Key: "updated_at", Value: -1}})
+ cursor, err := coll.Find(ctx, bson.M{"site_id": siteID}, opts)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ defer cursor.Close(ctx)
+
+ var list []models.Page
+ if err = cursor.All(ctx, &list); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ total, _ := coll.CountDocuments(ctx, bson.M{"site_id": siteID})
+
+ c.JSON(http.StatusOK, gin.H{"list": list, "total": total})
+}
+
+// GetPageByID 单页
+func GetPageByID(c *gin.Context) {
+ idStr := c.Param("id")
+ oid, err := bson.ObjectIDFromHex(idStr)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无效的页面ID"})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ var page models.Page
+ err = config.GetDB(config.DBName).Collection("pages").FindOne(ctx, bson.M{"_id": oid}).Decode(&page)
+ if err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "页面不存在"})
+ return
+ }
+ c.JSON(http.StatusOK, page)
+}
+
+// CreatePageInput 创建网页
+type CreatePageInput struct {
+ SiteID string `json:"site_id" binding:"required"`
+ Slug string `json:"slug" binding:"required"`
+ Title string `json:"title" binding:"required"`
+ Type string `json:"type"` // homepage, page
+ Content string `json:"content"`
+}
+
+// CreatePage 创建网页
+func CreatePage(c *gin.Context) {
+ var input CreatePageInput
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "请填写 site_id、slug、title"})
+ return
+ }
+ if input.Type == "" {
+ input.Type = "page"
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("pages")
+ now := time.Now().Format(time.RFC3339)
+ doc := bson.M{
+ "site_id": input.SiteID,
+ "slug": input.Slug,
+ "title": input.Title,
+ "type": input.Type,
+ "content": input.Content,
+ "updated_at": now,
+ }
+ res, err := coll.InsertOne(ctx, doc)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"id": res.InsertedID, "message": "创建成功"})
+}
+
+// UpdatePageInput 更新网页
+type UpdatePageInput struct {
+ Slug *string `json:"slug"`
+ Title *string `json:"title"`
+ Type *string `json:"type"`
+ Content *string `json:"content"`
+}
+
+// UpdatePage 更新网页
+func UpdatePage(c *gin.Context) {
+ idStr := c.Param("id")
+ oid, err := bson.ObjectIDFromHex(idStr)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无效的页面ID"})
+ return
+ }
+
+ var input UpdatePageInput
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ set := bson.M{"updated_at": time.Now().Format(time.RFC3339)}
+ if input.Slug != nil {
+ set["slug"] = *input.Slug
+ }
+ if input.Title != nil {
+ set["title"] = *input.Title
+ }
+ if input.Type != nil {
+ set["type"] = *input.Type
+ }
+ if input.Content != nil {
+ set["content"] = *input.Content
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ _, err = config.GetDB(config.DBName).Collection("pages").UpdateOne(ctx, bson.M{"_id": oid}, bson.M{"$set": set})
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"message": "更新成功"})
+}
+
+// DeletePage 删除网页
+func DeletePage(c *gin.Context) {
+ idStr := c.Param("id")
+ oid, err := bson.ObjectIDFromHex(idStr)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无效的页面ID"})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ _, err = config.GetDB(config.DBName).Collection("pages").DeleteOne(ctx, bson.M{"_id": oid})
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
+}
diff --git a/server/handlers/payment_config.go b/server/handlers/payment_config.go
new file mode 100644
index 0000000..2f274d9
--- /dev/null
+++ b/server/handlers/payment_config.go
@@ -0,0 +1,128 @@
+package handlers
+
+import (
+ "context"
+ "net/http"
+ "time"
+
+ "go.mongodb.org/mongo-driver/v2/bson"
+ "go.mongodb.org/mongo-driver/v2/mongo"
+ "go.mongodb.org/mongo-driver/v2/mongo/options"
+
+ "yh_web/server/config"
+ "yh_web/server/models"
+
+ "github.com/gin-gonic/gin"
+)
+
+const paymentConfigDocID = "payment"
+
+// GetPaymentConfig 获取支付配置(仅 role_id=9527, role=admin)
+func GetPaymentConfig(c *gin.Context) {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("system_config")
+ var cfg models.PaymentConfig
+ err := coll.FindOne(ctx, bson.M{"_id": paymentConfigDocID}).Decode(&cfg)
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ c.JSON(http.StatusOK, models.PaymentConfig{})
+ return
+ }
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, cfg)
+}
+
+// PaymentConfigUpdateInput 支付配置更新
+type PaymentConfigUpdateInput struct {
+ Wechat *WechatPayUpdateInput `json:"wechat"`
+ Alipay *AlipayUpdateInput `json:"alipay"`
+}
+
+type WechatPayUpdateInput struct {
+ AppID string `json:"app_id"`
+ MchID string `json:"mch_id"`
+ APIKey string `json:"api_key"`
+ APIKeyV3 string `json:"api_key_v3"`
+ Enabled *bool `json:"enabled"`
+}
+
+type AlipayUpdateInput struct {
+ AppID string `json:"app_id"`
+ PrivateKey string `json:"private_key"`
+ AlipayPublicKey string `json:"alipay_public_key"`
+ Enabled *bool `json:"enabled"`
+}
+
+// UpdatePaymentConfig 更新支付配置
+func UpdatePaymentConfig(c *gin.Context) {
+ var input PaymentConfigUpdateInput
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("system_config")
+ set := bson.M{}
+
+ if input.Wechat != nil {
+ w := bson.M{}
+ if input.Wechat.AppID != "" {
+ w["app_id"] = input.Wechat.AppID
+ }
+ if input.Wechat.MchID != "" {
+ w["mch_id"] = input.Wechat.MchID
+ }
+ if input.Wechat.APIKey != "" {
+ w["api_key"] = input.Wechat.APIKey
+ }
+ if input.Wechat.APIKeyV3 != "" {
+ w["api_key_v3"] = input.Wechat.APIKeyV3
+ }
+ if input.Wechat.Enabled != nil {
+ w["enabled"] = *input.Wechat.Enabled
+ }
+ for k, v := range w {
+ set["wechat."+k] = v
+ }
+ }
+
+ if input.Alipay != nil {
+ a := bson.M{}
+ if input.Alipay.AppID != "" {
+ a["app_id"] = input.Alipay.AppID
+ }
+ if input.Alipay.PrivateKey != "" {
+ a["private_key"] = input.Alipay.PrivateKey
+ }
+ if input.Alipay.AlipayPublicKey != "" {
+ a["alipay_public_key"] = input.Alipay.AlipayPublicKey
+ }
+ if input.Alipay.Enabled != nil {
+ a["enabled"] = *input.Alipay.Enabled
+ }
+ for k, v := range a {
+ set["alipay."+k] = v
+ }
+ }
+
+ if len(set) == 0 {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无有效更新字段"})
+ return
+ }
+
+ opts := options.UpdateOne().SetUpsert(true)
+ _, err := coll.UpdateOne(ctx, bson.M{"_id": paymentConfigDocID}, bson.M{"$set": set}, opts)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "配置已保存"})
+}
diff --git a/server/handlers/permission.go b/server/handlers/permission.go
new file mode 100644
index 0000000..47efa9a
--- /dev/null
+++ b/server/handlers/permission.go
@@ -0,0 +1,96 @@
+package handlers
+
+import (
+ "context"
+ "net/http"
+ "time"
+
+ "go.mongodb.org/mongo-driver/v2/bson"
+ "go.mongodb.org/mongo-driver/v2/mongo"
+
+ "yh_web/server/config"
+ "yh_web/server/models"
+
+ "github.com/gin-gonic/gin"
+)
+
+// roleIDFromContext 从 context 安全取 role_id(JWT 等可能解码为 float64)
+func roleIDFromContext(c *gin.Context) (int, bool) {
+ roleIDVal, ok := c.Get("role_id")
+ if !ok {
+ return 0, false
+ }
+ switch v := roleIDVal.(type) {
+ case int:
+ return v, true
+ case float64:
+ return int(v), true
+ default:
+ return 0, false
+ }
+}
+
+// getPermissionsByRoleID 从 role_permissions 读取某角色的权限;9527 默认拥有全部
+func getPermissionsByRoleID(ctx context.Context, roleID int) []string {
+ if roleID == models.RoleIDSuperAdmin {
+ return allPermissionKeys()
+ }
+ coll := config.GetDB(config.DBName).Collection("role_permissions")
+ var doc models.RolePermissionsDoc
+ err := coll.FindOne(ctx, bson.M{"role_id": roleID}).Decode(&doc)
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ return nil
+ }
+ return nil
+ }
+ return doc.Permissions
+}
+
+func allPermissionKeys() []string {
+ keys := make([]string, 0, len(models.AllPermissions))
+ for _, p := range models.AllPermissions {
+ keys = append(keys, p.Key)
+ }
+ return keys
+}
+
+// RequirePermission 要求当前用户拥有指定权限(在 AuthRequired 之后使用)
+func RequirePermission(permission string) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ roleID, ok := roleIDFromContext(c)
+ if !ok {
+ c.JSON(http.StatusForbidden, gin.H{"error": "无权限"})
+ c.Abort()
+ return
+ }
+ if roleID == models.RoleIDSuperAdmin {
+ c.Next()
+ return
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
+ defer cancel()
+ perms := getPermissionsByRoleID(ctx, roleID)
+ for _, p := range perms {
+ if p == permission {
+ c.Next()
+ return
+ }
+ }
+ c.JSON(http.StatusForbidden, gin.H{"error": "无此操作权限"})
+ c.Abort()
+ }
+}
+
+// GetMyPermissions 返回当前用户权限列表(供前端菜单、按钮显隐)
+func GetMyPermissions(c *gin.Context) {
+ roleID, ok := roleIDFromContext(c)
+ if !ok {
+ c.JSON(http.StatusOK, gin.H{"permissions": []string{}})
+ return
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
+ defer cancel()
+ perms := getPermissionsByRoleID(ctx, roleID)
+ c.JSON(http.StatusOK, gin.H{"permissions": perms})
+}
diff --git a/server/handlers/register.go b/server/handlers/register.go
new file mode 100644
index 0000000..3baffe5
--- /dev/null
+++ b/server/handlers/register.go
@@ -0,0 +1,294 @@
+package handlers
+
+import (
+ "context"
+ "errors"
+ "log"
+ "net/http"
+ "regexp"
+ "sync"
+ "time"
+
+ "go.mongodb.org/mongo-driver/v2/bson"
+ "go.mongodb.org/mongo-driver/v2/mongo"
+
+ "yh_web/server/config"
+ "yh_web/server/models"
+ "yh_web/server/utils"
+
+ "github.com/gin-gonic/gin"
+)
+
+const (
+ testVerifyCode = "8888" // 测试验证码(未接入短信时使用)
+ codeExpire = 5 * time.Minute // 验证码有效期
+)
+
+var (
+ codeStore = make(map[string]codeEntry)
+ codeStoreMu sync.RWMutex
+)
+
+type codeEntry struct {
+ Code string
+ ExpireAt time.Time
+}
+
+// SendCodeInput 发送验证码请求
+type SendCodeInput struct {
+ Mobile string `json:"mobile" binding:"required"`
+}
+
+// RegisterInput 手机注册请求
+type RegisterInput struct {
+ Mobile string `json:"mobile" binding:"required"`
+ Code string `json:"code" binding:"required"`
+ Password string `json:"password" binding:"required"`
+ Username string `json:"username"` // 可选,默认用手机号
+ Email string `json:"email"` // 可选
+}
+
+// SendCode 发送验证码(测试阶段:任意手机号输入 8888 即可)
+func SendCode(c *gin.Context) {
+ var input SendCodeInput
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "请输入手机号"})
+ return
+ }
+
+ if !isValidMobile(input.Mobile) {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "手机号格式不正确"})
+ return
+ }
+
+ // 未接入短信平台,使用测试验证码 8888
+ codeStoreMu.Lock()
+ codeStore[input.Mobile] = codeEntry{
+ Code: testVerifyCode,
+ ExpireAt: time.Now().Add(codeExpire),
+ }
+ codeStoreMu.Unlock()
+
+ c.JSON(http.StatusOK, gin.H{
+ "message": "验证码已发送(测试环境请输入 8888)",
+ "expire": int(codeExpire.Seconds()),
+ })
+}
+
+// Register 手机号注册
+func Register(c *gin.Context) {
+ var input RegisterInput
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "请填写手机号、验证码和密码"})
+ return
+ }
+
+ if !isValidMobile(input.Mobile) {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "手机号格式不正确"})
+ return
+ }
+
+ if len(input.Password) < 6 {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "密码至少6位"})
+ return
+ }
+
+ if input.Email != "" && !isValidEmail(input.Email) {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱格式不正确"})
+ return
+ }
+
+ // 校验验证码
+ codeStoreMu.RLock()
+ entry, ok := codeStore[input.Mobile]
+ codeStoreMu.RUnlock()
+
+ if !ok || entry.Code != input.Code {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "验证码错误"})
+ return
+ }
+ if time.Now().After(entry.ExpireAt) {
+ codeStoreMu.Lock()
+ delete(codeStore, input.Mobile)
+ codeStoreMu.Unlock()
+ c.JSON(http.StatusBadRequest, gin.H{"error": "验证码已过期,请重新获取"})
+ return
+ }
+
+ // 删除已用验证码
+ codeStoreMu.Lock()
+ delete(codeStore, input.Mobile)
+ codeStoreMu.Unlock()
+
+ username := input.Username
+ if username == "" {
+ username = input.Mobile
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("users")
+
+ // 检查手机号是否已注册
+ var exist models.User
+ err := coll.FindOne(ctx, bson.M{"mobile": input.Mobile}).Decode(&exist)
+ if err == nil {
+ c.JSON(http.StatusConflict, gin.H{"error": "该手机号已注册"})
+ return
+ }
+ if !errors.Is(err, mongo.ErrNoDocuments) {
+ log.Printf("[Register] FindOne mobile error: %v", err)
+ resp := gin.H{"error": "注册失败,请稍后重试"}
+ if gin.Mode() == gin.DebugMode {
+ resp["debug"] = err.Error()
+ }
+ c.JSON(http.StatusInternalServerError, resp)
+ return
+ }
+ err = coll.FindOne(ctx, bson.M{"username": username}).Decode(&exist)
+ if err == nil {
+ c.JSON(http.StatusConflict, gin.H{"error": "用户名已存在"})
+ return
+ }
+ if !errors.Is(err, mongo.ErrNoDocuments) {
+ log.Printf("[Register] FindOne username error: %v", err)
+ resp := gin.H{"error": "注册失败,请稍后重试"}
+ if gin.Mode() == gin.DebugMode {
+ resp["debug"] = err.Error()
+ }
+ c.JSON(http.StatusInternalServerError, resp)
+ return
+ }
+
+ // 若提供了邮箱,检查是否已注册
+ if input.Email != "" {
+ err = coll.FindOne(ctx, bson.M{"email": input.Email}).Decode(&exist)
+ if err == nil {
+ c.JSON(http.StatusConflict, gin.H{"error": "该邮箱已注册"})
+ return
+ }
+ if !errors.Is(err, mongo.ErrNoDocuments) {
+ log.Printf("[Register] FindOne email error: %v", err)
+ resp := gin.H{"error": "注册失败,请稍后重试"}
+ if gin.Mode() == gin.DebugMode {
+ resp["debug"] = err.Error()
+ }
+ c.JSON(http.StatusInternalServerError, resp)
+ return
+ }
+ }
+
+ doc := bson.M{
+ "username": username,
+ "mobile": input.Mobile,
+ "password": utils.HashPassword(input.Password),
+ "role": "admin",
+ "role_id": models.RoleIDSuperAdmin,
+ }
+ if input.Email != "" {
+ doc["email"] = input.Email
+ }
+
+ _, err = coll.InsertOne(ctx, doc)
+ if err != nil {
+ log.Printf("[Register] InsertOne error: %v", err)
+ resp := gin.H{"error": "注册失败,请稍后重试"}
+ if gin.Mode() == gin.DebugMode {
+ resp["debug"] = err.Error()
+ }
+ c.JSON(http.StatusInternalServerError, resp)
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "message": "注册成功",
+ })
+}
+
+// ResetPasswordInput 密码找回请求
+type ResetPasswordInput struct {
+ Mobile string `json:"mobile" binding:"required"`
+ Code string `json:"code" binding:"required"`
+ NewPassword string `json:"new_password" binding:"required"`
+}
+
+// ResetPassword 密码找回(手机号+验证码)
+func ResetPassword(c *gin.Context) {
+ var input ResetPasswordInput
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "请填写手机号、验证码和新密码"})
+ return
+ }
+
+ if !isValidMobile(input.Mobile) {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "手机号格式不正确"})
+ return
+ }
+
+ if len(input.NewPassword) < 6 {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "新密码至少6位"})
+ return
+ }
+
+ // 校验验证码
+ codeStoreMu.RLock()
+ entry, ok := codeStore[input.Mobile]
+ codeStoreMu.RUnlock()
+
+ if !ok || entry.Code != input.Code {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "验证码错误"})
+ return
+ }
+ if time.Now().After(entry.ExpireAt) {
+ codeStoreMu.Lock()
+ delete(codeStore, input.Mobile)
+ codeStoreMu.Unlock()
+ c.JSON(http.StatusBadRequest, gin.H{"error": "验证码已过期,请重新获取"})
+ return
+ }
+
+ codeStoreMu.Lock()
+ delete(codeStore, input.Mobile)
+ codeStoreMu.Unlock()
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("users")
+ var user models.User
+ err := coll.FindOne(ctx, bson.M{"mobile": input.Mobile}).Decode(&user)
+ if err != nil {
+ if errors.Is(err, mongo.ErrNoDocuments) {
+ c.JSON(http.StatusNotFound, gin.H{"error": "该手机号未注册"})
+ return
+ }
+ log.Printf("[ResetPassword] FindOne error: %v", err)
+ resp := gin.H{"error": "操作失败"}
+ if gin.Mode() == gin.DebugMode {
+ resp["debug"] = err.Error()
+ }
+ c.JSON(http.StatusInternalServerError, resp)
+ return
+ }
+
+ _, err = coll.UpdateOne(ctx, bson.M{"_id": user.ID}, bson.M{"$set": bson.M{"password": utils.HashPassword(input.NewPassword)}})
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "重置失败"})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "密码已重置,请登录"})
+}
+
+func isValidMobile(mobile string) bool {
+ // 简单校验:11位数字,1开头
+ matched, _ := regexp.MatchString(`^1\d{10}$`, mobile)
+ return matched
+}
+
+func isValidEmail(email string) bool {
+ // 简单邮箱格式校验
+ matched, _ := regexp.MatchString(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`, email)
+ return matched
+}
diff --git a/server/handlers/role_permission.go b/server/handlers/role_permission.go
new file mode 100644
index 0000000..ad1e9ac
--- /dev/null
+++ b/server/handlers/role_permission.go
@@ -0,0 +1,109 @@
+package handlers
+
+import (
+ "context"
+ "net/http"
+ "strconv"
+ "time"
+
+ "go.mongodb.org/mongo-driver/v2/bson"
+ "go.mongodb.org/mongo-driver/v2/mongo/options"
+
+ "yh_web/server/config"
+ "yh_web/server/models"
+
+ "github.com/gin-gonic/gin"
+)
+
+// 预定义角色(与 users.role_id 对应)
+var roleMeta = []struct {
+ RoleID int `json:"role_id"`
+ RoleName string `json:"role_name"`
+}{
+ {models.RoleIDSuperAdmin, "超级管理员"},
+ {models.RoleIDSuperUser, "超级用户"},
+ {models.RoleIDUser, "普通用户"},
+}
+
+// GetRolePermissionsList 返回所有角色及其权限(用于角色权限管理页)
+func GetRolePermissionsList(c *gin.Context) {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("role_permissions")
+ cursor, err := coll.Find(ctx, bson.M{})
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ defer cursor.Close(ctx)
+
+ var docs []models.RolePermissionsDoc
+ if err = cursor.All(ctx, &docs); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ permMap := make(map[int][]string)
+ for _, d := range docs {
+ permMap[d.RoleID] = d.Permissions
+ }
+
+ list := make([]gin.H, 0, len(roleMeta))
+ for _, r := range roleMeta {
+ perms := permMap[r.RoleID]
+ if perms == nil {
+ perms = []string{}
+ }
+ if r.RoleID == models.RoleIDSuperAdmin {
+ perms = allPermissionKeys()
+ }
+ list = append(list, gin.H{
+ "role_id": r.RoleID,
+ "role_name": r.RoleName,
+ "permissions": perms,
+ })
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "list": list,
+ "all_permissions": models.AllPermissions,
+ })
+}
+
+// UpdateRolePermissionsInput 更新某角色权限
+type UpdateRolePermissionsInput struct {
+ Permissions []string `json:"permissions"`
+}
+
+// UpdateRolePermissions 更新指定角色的权限
+func UpdateRolePermissions(c *gin.Context) {
+ roleIDStr := c.Param("role_id")
+ roleID, err := strconv.Atoi(roleIDStr)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 role_id"})
+ return
+ }
+ if roleID == models.RoleIDSuperAdmin {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "超级管理员权限不可修改"})
+ return
+ }
+
+ var input UpdateRolePermissionsInput
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("role_permissions")
+ filter := bson.M{"role_id": roleID}
+ update := bson.M{"$set": bson.M{"role_id": roleID, "permissions": input.Permissions}}
+ opts := options.UpdateOne().SetUpsert(true)
+ _, err = coll.UpdateOne(ctx, filter, update, opts)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"message": "保存成功", "role_id": roleID, "permissions": input.Permissions})
+}
diff --git a/server/handlers/site.go b/server/handlers/site.go
new file mode 100644
index 0000000..5574fc4
--- /dev/null
+++ b/server/handlers/site.go
@@ -0,0 +1,165 @@
+package handlers
+
+import (
+ "context"
+ "net/http"
+ "time"
+
+ "go.mongodb.org/mongo-driver/v2/bson"
+ "go.mongodb.org/mongo-driver/v2/mongo/options"
+
+ "yh_web/server/config"
+ "yh_web/server/models"
+
+ "github.com/gin-gonic/gin"
+)
+
+// GetSites 站点列表
+func GetSites(c *gin.Context) {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("sites")
+ opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}})
+ cursor, err := coll.Find(ctx, bson.M{}, opts)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ defer cursor.Close(ctx)
+
+ var list []models.Site
+ if err = cursor.All(ctx, &list); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ total, _ := coll.CountDocuments(ctx, bson.M{})
+
+ c.JSON(http.StatusOK, gin.H{"list": list, "total": total})
+}
+
+// GetSiteByID 单个站点
+func GetSiteByID(c *gin.Context) {
+ idStr := c.Param("site_id")
+ oid, err := bson.ObjectIDFromHex(idStr)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无效的站点ID"})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ var site models.Site
+ err = config.GetDB(config.DBName).Collection("sites").FindOne(ctx, bson.M{"_id": oid}).Decode(&site)
+ if err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "站点不存在"})
+ return
+ }
+ c.JSON(http.StatusOK, site)
+}
+
+// CreateSiteInput 创建站点
+type CreateSiteInput struct {
+ Name string `json:"name" binding:"required"`
+ Domain string `json:"domain"`
+ Description string `json:"description"`
+}
+
+// CreateSite 创建站点
+func CreateSite(c *gin.Context) {
+ var input CreateSiteInput
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "请填写站点名称"})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("sites")
+ doc := bson.M{
+ "name": input.Name,
+ "domain": input.Domain,
+ "description": input.Description,
+ "created_at": time.Now().Format(time.RFC3339),
+ }
+ res, err := coll.InsertOne(ctx, doc)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"id": res.InsertedID, "message": "创建成功"})
+}
+
+// UpdateSiteInput 更新站点
+type UpdateSiteInput struct {
+ Name *string `json:"name"`
+ Domain *string `json:"domain"`
+ Description *string `json:"description"`
+}
+
+// UpdateSite 更新站点
+func UpdateSite(c *gin.Context) {
+ idStr := c.Param("site_id")
+ oid, err := bson.ObjectIDFromHex(idStr)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无效的站点ID"})
+ return
+ }
+
+ var input UpdateSiteInput
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ set := bson.M{}
+ if input.Name != nil {
+ set["name"] = *input.Name
+ }
+ if input.Domain != nil {
+ set["domain"] = *input.Domain
+ }
+ if input.Description != nil {
+ set["description"] = *input.Description
+ }
+ if len(set) == 0 {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无有效更新字段"})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ _, err = config.GetDB(config.DBName).Collection("sites").UpdateOne(ctx, bson.M{"_id": oid}, bson.M{"$set": set})
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"message": "更新成功"})
+}
+
+// DeleteSite 删除站点
+func DeleteSite(c *gin.Context) {
+ idStr := c.Param("site_id")
+ oid, err := bson.ObjectIDFromHex(idStr)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无效的站点ID"})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ db := config.GetDB(config.DBName)
+ _, err = db.Collection("sites").DeleteOne(ctx, bson.M{"_id": oid})
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ db.Collection("pages").DeleteMany(ctx, bson.M{"site_id": idStr})
+ db.Collection("site_assets").DeleteMany(ctx, bson.M{"site_id": idStr})
+
+ c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
+}
diff --git a/server/handlers/sms_config.go b/server/handlers/sms_config.go
new file mode 100644
index 0000000..725afbb
--- /dev/null
+++ b/server/handlers/sms_config.go
@@ -0,0 +1,94 @@
+package handlers
+
+import (
+ "context"
+ "net/http"
+ "time"
+
+ "go.mongodb.org/mongo-driver/v2/bson"
+ "go.mongodb.org/mongo-driver/v2/mongo"
+ "go.mongodb.org/mongo-driver/v2/mongo/options"
+
+ "yh_web/server/config"
+ "yh_web/server/models"
+
+ "github.com/gin-gonic/gin"
+)
+
+const smsConfigDocID = "sms_platform"
+
+// GetSMSConfig 获取短信平台配置(仅超级用户 role_id=0, role=admin)
+func GetSMSConfig(c *gin.Context) {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("system_config")
+ var cfg models.SMSConfig
+ err := coll.FindOne(ctx, bson.M{"_id": smsConfigDocID}).Decode(&cfg)
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ c.JSON(http.StatusOK, models.SMSConfig{})
+ return
+ }
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, cfg)
+}
+
+// SMSConfigUpdateInput 短信配置更新
+type SMSConfigUpdateInput struct {
+ Provider string `json:"provider"`
+ AccessKey string `json:"access_key"`
+ SecretKey string `json:"secret_key"`
+ SignName string `json:"sign_name"`
+ TemplateID string `json:"template_id"`
+ Enabled *bool `json:"enabled"`
+}
+
+// UpdateSMSConfig 更新短信平台配置(仅超级用户)
+func UpdateSMSConfig(c *gin.Context) {
+ var input SMSConfigUpdateInput
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("system_config")
+ update := bson.M{}
+ if input.Provider != "" {
+ update["provider"] = input.Provider
+ }
+ if input.AccessKey != "" {
+ update["access_key"] = input.AccessKey
+ }
+ if input.SecretKey != "" {
+ update["secret_key"] = input.SecretKey
+ }
+ if input.SignName != "" {
+ update["sign_name"] = input.SignName
+ }
+ if input.TemplateID != "" {
+ update["template_id"] = input.TemplateID
+ }
+ if input.Enabled != nil {
+ update["enabled"] = *input.Enabled
+ }
+
+ if len(update) == 0 {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无有效更新字段"})
+ return
+ }
+
+ opts := options.UpdateOne().SetUpsert(true)
+ _, err := coll.UpdateOne(ctx, bson.M{"_id": smsConfigDocID}, bson.M{"$set": update}, opts)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "配置已保存"})
+}
diff --git a/server/handlers/stats.go b/server/handlers/stats.go
new file mode 100644
index 0000000..7a611c0
--- /dev/null
+++ b/server/handlers/stats.go
@@ -0,0 +1,34 @@
+package handlers
+
+import (
+ "context"
+ "net/http"
+ "time"
+
+ "go.mongodb.org/mongo-driver/v2/bson"
+
+ "yh_web/server/config"
+
+ "github.com/gin-gonic/gin"
+)
+
+func GetStats(c *gin.Context) {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ db := config.GetDB(config.DBName)
+
+ users, _ := db.Collection("users").CountDocuments(ctx, bson.M{})
+ workspaces, _ := db.Collection("workspaces").CountDocuments(ctx, bson.M{})
+ conversations, _ := db.Collection("conversations").CountDocuments(ctx, bson.M{})
+ messages, _ := db.Collection("messages").CountDocuments(ctx, bson.M{})
+ files, _ := db.Collection("files").CountDocuments(ctx, bson.M{})
+
+ c.JSON(http.StatusOK, gin.H{
+ "users": users,
+ "workspaces": workspaces,
+ "conversations": conversations,
+ "messages": messages,
+ "files": files,
+ })
+}
diff --git a/server/handlers/user.go b/server/handlers/user.go
new file mode 100644
index 0000000..2f2881b
--- /dev/null
+++ b/server/handlers/user.go
@@ -0,0 +1,234 @@
+package handlers
+
+import (
+ "context"
+ "errors"
+ "net/http"
+ "time"
+
+ "go.mongodb.org/mongo-driver/v2/bson"
+ "go.mongodb.org/mongo-driver/v2/mongo"
+ "go.mongodb.org/mongo-driver/v2/mongo/options"
+
+ "yh_web/server/config"
+ "yh_web/server/models"
+ "yh_web/server/utils"
+
+ "github.com/gin-gonic/gin"
+)
+
+func GetUsers(c *gin.Context) {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("users")
+
+ page := 1
+ pageSize := 20
+ if p := c.Query("page"); p != "" {
+ if v, err := parseInt(p); err == nil && v > 0 {
+ page = v
+ }
+ }
+ if ps := c.Query("page_size"); ps != "" {
+ if v, err := parseInt(ps); err == nil && v > 0 && v <= 100 {
+ pageSize = v
+ }
+ }
+
+ skip := (page - 1) * pageSize
+ opts := options.Find().SetSkip(int64(skip)).SetLimit(int64(pageSize)).SetSort(bson.D{{Key: "_id", Value: -1}})
+
+ cursor, err := coll.Find(ctx, bson.M{}, opts)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ defer cursor.Close(ctx)
+
+ var users []models.User
+ if err = cursor.All(ctx, &users); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ total, _ := coll.CountDocuments(ctx, bson.M{})
+
+ c.JSON(http.StatusOK, gin.H{
+ "list": users,
+ "total": total,
+ "page": page,
+ "page_size": pageSize,
+ })
+}
+
+func GetUserByID(c *gin.Context) {
+ id := c.Param("id")
+ oid, err := bson.ObjectIDFromHex(id)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ var user models.User
+ err = config.GetDB(config.DBName).Collection("users").FindOne(ctx, bson.M{"_id": oid}).Decode(&user)
+ if err != nil {
+ if errors.Is(err, mongo.ErrNoDocuments) {
+ c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
+ return
+ }
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, user)
+}
+
+func CreateUser(c *gin.Context) {
+ var input models.UserCreateInput
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("users")
+
+ // 检查用户名是否已存在
+ var exist models.User
+ if err := coll.FindOne(ctx, bson.M{"username": input.Username}).Decode(&exist); err == nil {
+ c.JSON(http.StatusConflict, gin.H{"error": "用户名已存在"})
+ return
+ }
+ if err := coll.FindOne(ctx, bson.M{"email": input.Email}).Decode(&exist); err == nil {
+ c.JSON(http.StatusConflict, gin.H{"error": "邮箱已存在"})
+ return
+ }
+
+ if input.Role == "" {
+ input.Role = "user"
+ }
+ roleID := input.RoleID
+ if roleID == 0 {
+ roleID = models.RoleIDUser
+ }
+
+ doc := bson.M{
+ "username": input.Username,
+ "email": input.Email,
+ "password": utils.HashPassword(input.Password),
+ "role": input.Role,
+ "role_id": roleID,
+ "is_beta": input.IsBeta,
+ }
+ if input.TrialStartDate != "" {
+ doc["trial_start_date"] = input.TrialStartDate
+ }
+ if input.TrialEndDate != "" {
+ doc["trial_end_date"] = input.TrialEndDate
+ }
+ if input.LLM != "" {
+ doc["llm"] = input.LLM
+ }
+
+ res, err := coll.InsertOne(ctx, doc)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusCreated, gin.H{"id": res.InsertedID, "message": "创建成功"})
+}
+
+func UpdateUser(c *gin.Context) {
+ id := c.Param("id")
+ oid, err := bson.ObjectIDFromHex(id)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
+ return
+ }
+
+ var input models.UserUpdateInput
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ update := bson.M{}
+ if input.Username != nil {
+ update["username"] = *input.Username
+ }
+ if input.Email != nil {
+ update["email"] = *input.Email
+ }
+ if input.Password != nil && *input.Password != "" {
+ update["password"] = utils.HashPassword(*input.Password)
+ }
+ if input.Role != nil {
+ update["role"] = *input.Role
+ }
+ if input.IsBeta != nil {
+ update["is_beta"] = *input.IsBeta
+ }
+ if input.TrialStartDate != nil {
+ update["trial_start_date"] = *input.TrialStartDate
+ }
+ if input.TrialEndDate != nil {
+ update["trial_end_date"] = *input.TrialEndDate
+ }
+ if input.LLM != nil {
+ update["llm"] = *input.LLM
+ }
+ if input.RoleID != nil {
+ update["role_id"] = *input.RoleID
+ }
+
+ if len(update) == 0 {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无有效更新字段"})
+ return
+ }
+
+ res, err := config.GetDB(config.DBName).Collection("users").UpdateOne(ctx, bson.M{"_id": oid}, bson.M{"$set": update})
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ if res.MatchedCount == 0 {
+ c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "更新成功"})
+}
+
+func DeleteUser(c *gin.Context) {
+ id := c.Param("id")
+ oid, err := bson.ObjectIDFromHex(id)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ res, err := config.GetDB(config.DBName).Collection("users").DeleteOne(ctx, bson.M{"_id": oid})
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ if res.DeletedCount == 0 {
+ c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
+}
diff --git a/server/handlers/workspace.go b/server/handlers/workspace.go
new file mode 100644
index 0000000..3a887f5
--- /dev/null
+++ b/server/handlers/workspace.go
@@ -0,0 +1,64 @@
+package handlers
+
+import (
+ "context"
+ "net/http"
+ "time"
+
+ "go.mongodb.org/mongo-driver/v2/bson"
+ "go.mongodb.org/mongo-driver/v2/mongo/options"
+
+ "yh_web/server/config"
+
+ "github.com/gin-gonic/gin"
+)
+
+func GetWorkspaces(c *gin.Context) {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ coll := config.GetDB(config.DBName).Collection("workspaces")
+
+ page := 1
+ pageSize := 20
+ if p := c.Query("page"); p != "" {
+ if v, err := parseInt(p); err == nil && v > 0 {
+ page = v
+ }
+ }
+ if ps := c.Query("page_size"); ps != "" {
+ if v, err := parseInt(ps); err == nil && v > 0 && v <= 100 {
+ pageSize = v
+ }
+ }
+
+ skip := (page - 1) * pageSize
+ opts := options.Find().SetSkip(int64(skip)).SetLimit(int64(pageSize)).SetSort(bson.D{{Key: "_id", Value: -1}})
+
+ filter := bson.M{}
+ if userID := c.Query("user_id"); userID != "" {
+ filter["user_id"] = userID
+ }
+
+ cursor, err := coll.Find(ctx, filter, opts)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ defer cursor.Close(ctx)
+
+ var list []map[string]interface{}
+ if err = cursor.All(ctx, &list); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ total, _ := coll.CountDocuments(ctx, filter)
+
+ c.JSON(http.StatusOK, gin.H{
+ "list": list,
+ "total": total,
+ "page": page,
+ "page_size": pageSize,
+ })
+}
diff --git a/server/main.go b/server/main.go
new file mode 100644
index 0000000..72109d9
--- /dev/null
+++ b/server/main.go
@@ -0,0 +1,218 @@
+package main
+
+import (
+ "context"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "yh_web/server/config"
+ "yh_web/server/handlers"
+ "yh_web/server/middleware"
+ "yh_web/server/models"
+ "yh_web/server/pkg/logger"
+ "yh_web/server/pkg/schema"
+
+ "github.com/gin-gonic/gin"
+ "github.com/joho/godotenv"
+)
+
+// loadEnv 启动时自动加载 .env(在 server 目录或项目根/server 下),不覆盖已存在的环境变量
+func loadEnv() {
+ wd, _ := os.Getwd()
+ serverDir := wd
+ if !strings.HasSuffix(filepath.Clean(wd), "server") {
+ serverDir = filepath.Join(wd, "server")
+ }
+ envPath := filepath.Clean(filepath.Join(serverDir, ".env"))
+ if _, err := os.Stat(envPath); err == nil {
+ if err := godotenv.Load(envPath); err == nil {
+ log.Printf("已加载配置: %s", envPath)
+ }
+ }
+}
+
+func main() {
+ loadEnv()
+
+ // 初始化根目录 logs/server(支持从项目根或 server 目录启动)
+ wd, _ := os.Getwd()
+ baseDir := filepath.Join(wd, "logs", "server")
+ if strings.HasSuffix(filepath.Clean(wd), "server") {
+ baseDir = filepath.Join(wd, "..", "logs", "server")
+ }
+ logger.Init(filepath.Clean(baseDir))
+
+ // 连接 MongoDB;URI 从环境变量 MONGODB_URI 读取,默认 mongodb://localhost:27017;SKIP_MONGODB=1 时可跳过
+ if os.Getenv("SKIP_MONGODB") != "1" {
+ mongoURI := os.Getenv("MONGODB_URI")
+ if mongoURI == "" {
+ mongoURI = "mongodb://localhost:27017"
+ }
+ if dbName := os.Getenv("MONGODB_DB"); dbName != "" {
+ config.DBName = dbName
+ }
+ if err := config.ConnectMongoDB(mongoURI); err != nil {
+ logger.Err("main", "MongoDB 连接失败: %v(若仅需健康检查可设置环境变量 SKIP_MONGODB=1 后启动)", err)
+ log.Fatalf("MongoDB 连接失败: %v(若仅需健康检查可设置环境变量 SKIP_MONGODB=1 后启动)", err)
+ }
+ defer config.CloseMongoDB()
+
+ // 启动时获取线上表结构并生成 sql;缺失的集合在线上创建并生成 created_*.sql
+ projectRoot := wd
+ if strings.HasSuffix(filepath.Clean(wd), "server") {
+ projectRoot = filepath.Join(wd, "..")
+ }
+ projectRoot = filepath.Clean(projectRoot)
+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+ if err := schema.Sync(ctx, projectRoot); err != nil {
+ logger.Err("main", "启动时同步表结构失败: %v", err)
+ log.Printf("警告: 启动时同步表结构失败: %v", err)
+ }
+ cancel()
+ } else {
+ logger.Log("main", "已跳过 MongoDB 连接(SKIP_MONGODB=1),仅 /api/health 等不依赖数据库的接口可用")
+ log.Println("已跳过 MongoDB 连接(SKIP_MONGODB=1),仅 /api/health 等不依赖数据库的接口可用")
+ }
+
+ r := gin.Default()
+ r.Use(middleware.ErrorLogger())
+
+ // CORS(ALLOWED_ORIGINS 为空则允许所有来源;否则仅允许配置的域名)
+ allowedOriginsEnv := os.Getenv("ALLOWED_ORIGINS")
+ r.Use(func(c *gin.Context) {
+ origin := c.GetHeader("Origin")
+ if allowedOriginsEnv != "" {
+ for _, o := range strings.Split(allowedOriginsEnv, ",") {
+ if strings.TrimSpace(o) == origin {
+ c.Header("Access-Control-Allow-Origin", origin)
+ break
+ }
+ }
+ } else {
+ c.Header("Access-Control-Allow-Origin", "*")
+ }
+ c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
+ c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
+ if c.Request.Method == "OPTIONS" {
+ c.AbortWithStatus(http.StatusNoContent)
+ return
+ }
+ c.Next()
+ })
+
+ // 未连接 MongoDB 时,/api/admin 下所有接口返回 503(健康检查不受影响)
+ r.Use(func(c *gin.Context) {
+ if strings.HasPrefix(c.Request.URL.Path, "/api/admin") && config.MongoClient == nil {
+ c.JSON(http.StatusServiceUnavailable, gin.H{"error": "服务暂不可用,数据库未连接"})
+ c.Abort()
+ return
+ }
+ c.Next()
+ })
+
+ // 健康检查
+ r.GET("/api/health", func(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H{"status": "ok"})
+ })
+
+ // 登录、注册(无需鉴权)
+ r.POST("/api/admin/login", handlers.Login)
+ r.POST("/api/admin/send-code", handlers.SendCode)
+ r.POST("/api/admin/register", handlers.Register)
+ r.POST("/api/admin/reset-password", handlers.ResetPassword)
+
+ // 后台 API 路由组(需鉴权)
+ admin := r.Group("/api/admin")
+ admin.Use(handlers.AuthRequired())
+ {
+ admin.GET("/info", func(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H{"message": "admin api"})
+ })
+ admin.GET("/my-permissions", handlers.GetMyPermissions)
+ admin.GET("/db-structure", func(c *gin.Context) {
+ structure, err := config.GetDBStructure()
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, structure)
+ })
+ admin.GET("/stats", handlers.GetStats)
+
+ // 用户管理
+ admin.GET("/users", handlers.RequirePermission(models.PermUserManage), handlers.GetUsers)
+ admin.GET("/users/:id", handlers.RequirePermission(models.PermUserManage), handlers.GetUserByID)
+ admin.POST("/users", handlers.RequirePermission(models.PermUserManage), handlers.CreateUser)
+ admin.PUT("/users/:id", handlers.RequirePermission(models.PermUserManage), handlers.UpdateUser)
+ admin.DELETE("/users/:id", handlers.RequirePermission(models.PermUserManage), handlers.DeleteUser)
+
+ // 工作空间
+ admin.GET("/workspaces", handlers.RequirePermission(models.PermWorkspaceManage), handlers.GetWorkspaces)
+
+ // 对话
+ admin.GET("/conversations", handlers.RequirePermission(models.PermConversationManage), handlers.GetConversations)
+
+ // 站点管理(带子路径的路由放前面;与 :site_id 统一,避免 Gin 路由冲突)
+ admin.GET("/sites/:site_id/homepage/download", handlers.RequirePermission(models.PermHomepageEdit), handlers.DownloadHomepage)
+ admin.GET("/sites/:site_id/homepage", handlers.RequirePermission(models.PermHomepageEdit), handlers.GetHomepage)
+ admin.PUT("/sites/:site_id/homepage", handlers.RequirePermission(models.PermHomepageEdit), handlers.UpdateHomepage)
+ admin.GET("/sites/:site_id/assets", handlers.RequirePermission(models.PermSiteManage), handlers.ListSiteAssets)
+ admin.POST("/sites/:site_id/assets", handlers.RequirePermission(models.PermModuleUpload), handlers.UploadSiteAsset)
+ admin.DELETE("/sites/:site_id/assets/:asset_id", handlers.RequirePermission(models.PermSiteManage), handlers.DeleteSiteAsset)
+ admin.GET("/sites", handlers.RequirePermission(models.PermSiteManage), handlers.GetSites)
+ admin.GET("/sites/:site_id", handlers.RequirePermission(models.PermSiteManage), handlers.GetSiteByID)
+ admin.POST("/sites", handlers.RequirePermission(models.PermSiteManage), handlers.CreateSite)
+ admin.PUT("/sites/:site_id", handlers.RequirePermission(models.PermSiteManage), handlers.UpdateSite)
+ admin.DELETE("/sites/:site_id", handlers.RequirePermission(models.PermSiteManage), handlers.DeleteSite)
+ admin.GET("/system/official-site", handlers.RequirePermission(models.PermSiteManage), handlers.GetOfficialSite)
+ admin.PUT("/system/official-site", handlers.RequirePermission(models.PermSiteManage), handlers.SetOfficialSite)
+
+ // 角色权限管理
+ admin.GET("/role-permissions", handlers.RequirePermission(models.PermRolePermission), handlers.GetRolePermissionsList)
+ admin.PUT("/role-permissions/:role_id", handlers.RequirePermission(models.PermRolePermission), handlers.UpdateRolePermissions)
+
+ // 网页管理(按站点)
+ admin.GET("/pages", handlers.RequirePermission(models.PermPageManage), handlers.GetPages)
+ admin.GET("/pages/:id", handlers.RequirePermission(models.PermPageManage), handlers.GetPageByID)
+ admin.POST("/pages", handlers.RequirePermission(models.PermPageManage), handlers.CreatePage)
+ admin.PUT("/pages/:id", handlers.RequirePermission(models.PermPageManage), handlers.UpdatePage)
+ admin.DELETE("/pages/:id", handlers.RequirePermission(models.PermPageManage), handlers.DeletePage)
+
+ // 短信平台配置
+ smsConfig := admin.Group("/sms-config")
+ smsConfig.Use(handlers.RequirePermission(models.PermSMSConfig))
+ {
+ smsConfig.GET("", handlers.GetSMSConfig)
+ smsConfig.PUT("", handlers.UpdateSMSConfig)
+ }
+
+ // 支付配置(微信、支付宝)
+ paymentConfig := admin.Group("/payment-config")
+ paymentConfig.Use(handlers.RequirePermission(models.PermPaymentConfig))
+ {
+ paymentConfig.GET("", handlers.GetPaymentConfig)
+ paymentConfig.PUT("", handlers.UpdatePaymentConfig)
+ }
+ }
+
+ // 官网站点首页(前台,无需鉴权)
+ r.GET("/api/web/homepage", handlers.GetWebHomepage)
+
+ // 前台 API 路由组
+ web := r.Group("/api/web")
+ {
+ web.GET("/info", func(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H{"message": "web api"})
+ })
+ }
+
+ port := os.Getenv("PORT")
+ if port == "" {
+ port = "8080"
+ }
+ r.Run(":" + port)
+}
diff --git a/server/middleware/logger.go b/server/middleware/logger.go
new file mode 100644
index 0000000..f0d5e79
--- /dev/null
+++ b/server/middleware/logger.go
@@ -0,0 +1,39 @@
+package middleware
+
+import (
+ "bytes"
+
+ "yh_web/server/pkg/logger"
+
+ "github.com/gin-gonic/gin"
+)
+
+// responseBodyWriter 包装 ResponseWriter 以捕获响应体
+type responseBodyWriter struct {
+ gin.ResponseWriter
+ body *bytes.Buffer
+}
+
+func (w responseBodyWriter) Write(b []byte) (int, error) {
+ w.body.Write(b)
+ return w.ResponseWriter.Write(b)
+}
+
+// ErrorLogger 在 4xx/5xx 时记录响应体中的错误信息
+func ErrorLogger() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ w := &responseBodyWriter{
+ ResponseWriter: c.Writer,
+ body: &bytes.Buffer{},
+ }
+ c.Writer = w
+
+ c.Next()
+
+ if w.Status() >= 400 {
+ if body := w.body.String(); body != "" {
+ logger.Err("middleware/logger", "[%d] %s %s | body: %s", w.Status(), c.Request.Method, c.Request.URL.Path, body)
+ }
+ }
+ }
+}
diff --git a/server/models/conversation.go b/server/models/conversation.go
new file mode 100644
index 0000000..615d066
--- /dev/null
+++ b/server/models/conversation.go
@@ -0,0 +1,12 @@
+package models
+
+import "go.mongodb.org/mongo-driver/v2/bson"
+
+type Conversation struct {
+ ID bson.ObjectID `bson:"_id,omitempty" json:"id"`
+ Title string `bson:"title" json:"title"`
+ UserID string `bson:"user_id" json:"user_id"`
+ WorkspaceID string `bson:"workspace_id" json:"workspace_id"`
+ CreatedAt string `bson:"created_at" json:"created_at"`
+ UpdatedAt string `bson:"updated_at" json:"updated_at"`
+}
diff --git a/server/models/payment.go b/server/models/payment.go
new file mode 100644
index 0000000..d76ccc4
--- /dev/null
+++ b/server/models/payment.go
@@ -0,0 +1,24 @@
+package models
+
+// WechatPayConfig 微信支付配置
+type WechatPayConfig struct {
+ AppID string `bson:"app_id" json:"app_id"`
+ MchID string `bson:"mch_id" json:"mch_id"`
+ APIKey string `bson:"api_key" json:"api_key"`
+ APIKeyV3 string `bson:"api_key_v3" json:"api_key_v3"`
+ Enabled bool `bson:"enabled" json:"enabled"`
+}
+
+// AlipayConfig 支付宝配置
+type AlipayConfig struct {
+ AppID string `bson:"app_id" json:"app_id"`
+ PrivateKey string `bson:"private_key" json:"private_key"`
+ AlipayPublicKey string `bson:"alipay_public_key" json:"alipay_public_key"`
+ Enabled bool `bson:"enabled" json:"enabled"`
+}
+
+// PaymentConfig 支付配置(微信+支付宝)
+type PaymentConfig struct {
+ Wechat *WechatPayConfig `bson:"wechat,omitempty" json:"wechat,omitempty"`
+ Alipay *AlipayConfig `bson:"alipay,omitempty" json:"alipay,omitempty"`
+}
diff --git a/server/models/permission.go b/server/models/permission.go
new file mode 100644
index 0000000..7543e4c
--- /dev/null
+++ b/server/models/permission.go
@@ -0,0 +1,38 @@
+package models
+
+// 权限码(与前端、路由 meta.permission 一致)
+const (
+ PermSiteManage = "site:manage"
+ PermHomepageEdit = "homepage:edit"
+ PermPageManage = "page:manage"
+ PermModuleUpload = "module:upload"
+ PermUserManage = "user:manage"
+ PermWorkspaceManage = "workspace:manage"
+ PermConversationManage = "conversation:manage"
+ PermSMSConfig = "sms_config"
+ PermPaymentConfig = "payment_config"
+ PermRolePermission = "role:permission" // 角色权限管理
+)
+
+// AllPermissions 所有可配置权限(用于角色权限管理页)
+var AllPermissions = []struct {
+ Key string
+ Name string
+}{
+ {PermSiteManage, "站点管理"},
+ {PermHomepageEdit, "首页编辑"},
+ {PermPageManage, "网页管理"},
+ {PermModuleUpload, "功能模块上传"},
+ {PermUserManage, "用户管理"},
+ {PermWorkspaceManage, "工作空间"},
+ {PermConversationManage, "对话管理"},
+ {PermSMSConfig, "短信配置"},
+ {PermPaymentConfig, "支付配置"},
+ {PermRolePermission, "角色权限管理"},
+}
+
+// RolePermissionsDoc MongoDB 文档:角色 ID -> 权限列表
+type RolePermissionsDoc struct {
+ RoleID int `bson:"role_id" json:"role_id"`
+ Permissions []string `bson:"permissions" json:"permissions"`
+}
diff --git a/server/models/site.go b/server/models/site.go
new file mode 100644
index 0000000..d66e28a
--- /dev/null
+++ b/server/models/site.go
@@ -0,0 +1,66 @@
+package models
+
+import "go.mongodb.org/mongo-driver/v2/bson"
+
+// Site 站点
+type Site struct {
+ ID bson.ObjectID `bson:"_id,omitempty" json:"id"`
+ Name string `bson:"name" json:"name"`
+ Domain string `bson:"domain" json:"domain"`
+ Description string `bson:"description,omitempty" json:"description,omitempty"`
+ CreatedAt string `bson:"created_at" json:"created_at"`
+}
+
+// Page 网页(属于某站点)
+type Page struct {
+ ID bson.ObjectID `bson:"_id,omitempty" json:"id"`
+ SiteID string `bson:"site_id" json:"site_id"`
+ Slug string `bson:"slug" json:"slug"` // index, about, ...
+ Title string `bson:"title" json:"title"`
+ Type string `bson:"type" json:"type"` // homepage, page
+ Content string `bson:"content" json:"content"` // HTML 或 JSON 字符串
+ UpdatedAt string `bson:"updated_at" json:"updated_at"`
+}
+
+// HomepageData 首页可编辑数据(与 yuheng-download-space 对应)
+type HomepageData struct {
+ LogoText string `json:"logo_text"` // YUHENG ONE
+ NavLinks []NavLink `json:"nav_links"` // MISSION, DOWNLOAD, CONTACT
+ Title string `json:"title"` // 宇恒一号
+ Subtitle string `json:"subtitle"` // INTERSTELLAR EXPLORER EDITION
+ Description string `json:"description"` // 跨越星际的智能伙伴...
+ DownloadText string `json:"download_text"` // START EXPLORING
+ DownloadURL string `json:"download_url"` // #
+ Platforms []PlatformItem `json:"platforms"` // Windows, macOS, ...
+ Version string `json:"version"` // VERSION 3.2.1
+ LaunchYear string `json:"launch_year"` // LAUNCH: 2024
+ BadgeText string `json:"badge_text"` // FREE ACCESS
+ Features []FeatureItem `json:"features"` // 星际导航等
+ FooterText string `json:"footer_text"` // © 2024 YUHENG ONE
+}
+
+type NavLink struct {
+ Label string `json:"label"`
+ URL string `json:"url"`
+}
+
+type PlatformItem struct {
+ Name string `json:"name"` // WINDOWS, MACOS, ...
+ URL string `json:"url"`
+}
+
+type FeatureItem struct {
+ Title string `json:"title"`
+ Desc string `json:"desc"`
+}
+
+// SiteAsset 站点功能模块/上传文件
+type SiteAsset struct {
+ ID bson.ObjectID `bson:"_id,omitempty" json:"id"`
+ SiteID string `bson:"site_id" json:"site_id"`
+ Name string `bson:"name" json:"name"`
+ FilePath string `bson:"file_path" json:"file_path"` // 相对路径
+ Size int64 `bson:"size" json:"size"`
+ ContentType string `bson:"content_type" json:"content_type"`
+ CreatedAt string `bson:"created_at" json:"created_at"`
+}
diff --git a/server/models/sms.go b/server/models/sms.go
new file mode 100644
index 0000000..cd4a446
--- /dev/null
+++ b/server/models/sms.go
@@ -0,0 +1,11 @@
+package models
+
+// SMSConfig 短信平台配置(仅超级用户 role_id=0, role=admin 可配置)
+type SMSConfig struct {
+ Provider string `bson:"provider" json:"provider"` // 服务商:aliyun/tencent/...
+ AccessKey string `bson:"access_key" json:"access_key"` // AccessKey
+ SecretKey string `bson:"secret_key" json:"secret_key"` // SecretKey
+ SignName string `bson:"sign_name" json:"sign_name"` // 签名
+ TemplateID string `bson:"template_id" json:"template_id"` // 模板ID
+ Enabled bool `bson:"enabled" json:"enabled"` // 是否已启用
+}
diff --git a/server/models/user.go b/server/models/user.go
new file mode 100644
index 0000000..a83e9f7
--- /dev/null
+++ b/server/models/user.go
@@ -0,0 +1,77 @@
+package models
+
+import (
+ "fmt"
+ "time"
+
+ "go.mongodb.org/mongo-driver/v2/bson"
+)
+
+const (
+ RoleIDSuperAdmin = 9527 // 超级管理员(后台登录)
+ RoleIDSuperUser = 0 // 超级用户(role_id=0 且 role=admin,可配置短信平台)
+ RoleIDUser = 1 // 普通用户
+)
+
+// FlexDate 可解码 BSON DateTime 或 string,统一以 string 输出(兼容库中既有日期类型又有字符串的情况)
+type FlexDate string
+
+func (d *FlexDate) UnmarshalBSONValue(typ byte, data []byte) error {
+ rv := bson.RawValue{Type: bson.Type(typ), Value: data}
+ switch bson.Type(typ) {
+ case bson.TypeDateTime:
+ if dt, ok := rv.DateTimeOK(); ok {
+ *d = FlexDate(time.UnixMilli(dt).Format("2006-01-02"))
+ }
+ return nil
+ case bson.TypeString:
+ if s, ok := rv.StringValueOK(); ok {
+ *d = FlexDate(s)
+ }
+ return nil
+ case bson.TypeNull, bson.TypeUndefined:
+ *d = ""
+ return nil
+ default:
+ return fmt.Errorf("FlexDate: cannot decode type %v", typ)
+ }
+}
+
+type User struct {
+ ID bson.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
+ Username string `bson:"username" json:"username"`
+ Mobile string `bson:"mobile,omitempty" json:"mobile,omitempty"`
+ Email string `bson:"email" json:"email"`
+ Password string `bson:"password" json:"-"` // 不返回给前端
+ Role string `bson:"role" json:"role"`
+ RoleID int `bson:"role_id,omitempty" json:"role_id"` // 9527=超级管理员 1=普通用户
+ IsBeta bool `bson:"is_beta,omitempty" json:"is_beta"`
+ TrialStartDate FlexDate `bson:"trial_start_date,omitempty" json:"trial_start_date,omitempty"`
+ TrialEndDate FlexDate `bson:"trial_end_date,omitempty" json:"trial_end_date,omitempty"`
+ LastLogin string `bson:"lastLogin,omitempty" json:"last_login,omitempty"`
+ LLM string `bson:"llm,omitempty" json:"llm,omitempty"`
+}
+
+type UserCreateInput struct {
+ Username string `json:"username" binding:"required"`
+ Email string `json:"email" binding:"required"`
+ Password string `json:"password" binding:"required"`
+ Role string `json:"role"`
+ RoleID int `json:"role_id"` // 9527=超级管理员 1=普通用户
+ IsBeta bool `json:"is_beta"`
+ TrialStartDate string `json:"trial_start_date"`
+ TrialEndDate string `json:"trial_end_date"`
+ LLM string `json:"llm"`
+}
+
+type UserUpdateInput struct {
+ Username *string `json:"username"`
+ Email *string `json:"email"`
+ Password *string `json:"password"` // 若提供则更新
+ Role *string `json:"role"`
+ RoleID *int `json:"role_id"`
+ IsBeta *bool `json:"is_beta"`
+ TrialStartDate *string `json:"trial_start_date"`
+ TrialEndDate *string `json:"trial_end_date"`
+ LLM *string `json:"llm"`
+}
diff --git a/server/models/workspace.go b/server/models/workspace.go
new file mode 100644
index 0000000..16cc4fd
--- /dev/null
+++ b/server/models/workspace.go
@@ -0,0 +1,10 @@
+package models
+
+import "go.mongodb.org/mongo-driver/v2/bson"
+
+type Workspace struct {
+ ID bson.ObjectID `bson:"_id,omitempty" json:"id"`
+ Name string `bson:"name" json:"name"`
+ UserID string `bson:"user_id" json:"user_id"`
+ CreatedAt string `bson:"created_at" json:"created_at"`
+}
diff --git a/server/pkg/logger/logger.go b/server/pkg/logger/logger.go
new file mode 100644
index 0000000..6bc38c0
--- /dev/null
+++ b/server/pkg/logger/logger.go
@@ -0,0 +1,66 @@
+package logger
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "sync"
+ "time"
+)
+
+var (
+ baseDir string
+ mu sync.Mutex
+)
+
+// Init 初始化日志根目录(例如 logs/server),应在 main 中调用
+func Init(dir string) {
+ baseDir = filepath.Clean(dir)
+ _ = os.MkdirAll(baseDir, 0755)
+}
+
+// pathFile 返回 path 对应的 .log 或 .err 的完整路径;path 如 "main" 或 "handlers/auth"
+func pathFile(path, ext string) string {
+ // 安全化:只保留路径分隔符和字母数字
+ clean := filepath.Clean(path)
+ if clean == "" || clean == "." {
+ clean = "main"
+ }
+ full := filepath.Join(baseDir, clean+ext)
+ dir := filepath.Dir(full)
+ _ = os.MkdirAll(dir, 0755)
+ return full
+}
+
+func appendLine(fpath, line string) {
+ if baseDir == "" || fpath == "" {
+ return
+ }
+ mu.Lock()
+ defer mu.Unlock()
+ f, err := os.OpenFile(fpath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
+ if err != nil {
+ return
+ }
+ defer f.Close()
+ _, _ = f.WriteString(line)
+}
+
+func line(level, format string, a ...interface{}) string {
+ return fmt.Sprintf("%s [%s] %s\n", time.Now().Format("2006-01-02 15:04:05"), level, fmt.Sprintf(format, a...))
+}
+
+// Log 写普通日志:写入 log.log(简单总日志)和 path 对应的 xxx.log(按路径的详细日志)
+func Log(path, format string, a ...interface{}) {
+ msg := line("INFO", format, a...)
+ appendLine(filepath.Join(baseDir, "log.log"), msg)
+ appendLine(pathFile(path, ".log"), msg)
+}
+
+// Err 写报错:写入 path 对应的 xxx.err(仅报错)、xxx.log 和 log.log
+func Err(path, format string, a ...interface{}) {
+ msg := line("ERROR", format, a...)
+ appendLine(pathFile(path, ".err"), msg)
+ appendLine(pathFile(path, ".log"), msg)
+ appendLine(filepath.Join(baseDir, "log.log"), msg)
+}
\ No newline at end of file
diff --git a/server/pkg/schema/sync.go b/server/pkg/schema/sync.go
new file mode 100644
index 0000000..071a714
--- /dev/null
+++ b/server/pkg/schema/sync.go
@@ -0,0 +1,203 @@
+package schema
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ "yh_web/server/config"
+
+ "go.mongodb.org/mongo-driver/v2/bson"
+ "go.mongodb.org/mongo-driver/v2/mongo"
+ "go.mongodb.org/mongo-driver/v2/mongo/options"
+)
+
+// 应用需要的集合及索引(与 MongoDB/create_collections.js、sql/init.sql 一致)
+var requiredCollections = map[string][]indexSpec{
+ "sites": {{Keys: bson.D{{Key: "created_at", Value: -1}}, Name: "idx_created_at"}},
+ "pages": {{Keys: bson.D{{Key: "site_id", Value: 1}, {Key: "slug", Value: 1}}, Name: "idx_site_slug", Unique: true}},
+ "site_assets": {{Keys: bson.D{{Key: "site_id", Value: 1}}, Name: "idx_site_id"}},
+ "users": {{Keys: bson.D{{Key: "username", Value: 1}}, Name: "idx_username", Unique: true}, {Keys: bson.D{{Key: "mobile", Value: 1}}, Name: "idx_mobile", Sparse: true}},
+ "workspaces": {{Keys: bson.D{{Key: "user_id", Value: 1}}, Name: "idx_user_id"}},
+ "conversations": {{Keys: bson.D{{Key: "user_id", Value: 1}}, Name: "idx_user_id"}, {Keys: bson.D{{Key: "workspace_id", Value: 1}}, Name: "idx_workspace_id"}},
+ "messages": {{Keys: bson.D{{Key: "conversation_id", Value: 1}}, Name: "idx_conversation_id"}},
+ "files": {{Keys: bson.D{{Key: "user_id", Value: 1}}, Name: "idx_user_id"}},
+ "system_config": {},
+ "role_permissions": {{Keys: bson.D{{Key: "role_id", Value: 1}}, Name: "idx_role_id", Unique: true}},
+}
+
+type indexSpec struct {
+ Keys bson.D
+ Name string
+ Unique bool
+ Sparse bool
+}
+
+// 每个集合对应的单表 SQL DDL(仅 CREATE TABLE 段落,用于 created_*.sql);反引号用 \x60 表示
+var tableDDL = map[string]string{
+ "sites": "CREATE TABLE IF NOT EXISTS \x60sites\x60 (\n \x60id\x60 VARCHAR(24) NOT NULL COMMENT '主键,与 MongoDB ObjectID 字符串一致',\n \x60name\x60 VARCHAR(255) NOT NULL DEFAULT '' COMMENT '站点名称',\n \x60domain\x60 VARCHAR(255) NOT NULL DEFAULT '' COMMENT '域名',\n \x60description\x60 TEXT COMMENT '描述',\n \x60created_at\x60 VARCHAR(32) NOT NULL DEFAULT '' COMMENT '创建时间',\n PRIMARY KEY (\x60id\x60),\n KEY \x60idx_created_at\x60 (\x60created_at\x60)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='站点表';",
+ "pages": "CREATE TABLE IF NOT EXISTS \x60pages\x60 (\n \x60id\x60 VARCHAR(24) NOT NULL COMMENT '主键',\n \x60site_id\x60 VARCHAR(24) NOT NULL DEFAULT '' COMMENT '站点ID',\n \x60slug\x60 VARCHAR(128) NOT NULL DEFAULT '' COMMENT '路径标识 index, about, ...',\n \x60title\x60 VARCHAR(255) NOT NULL DEFAULT '' COMMENT '标题',\n \x60type\x60 VARCHAR(32) NOT NULL DEFAULT 'page' COMMENT '类型 homepage, page',\n \x60content\x60 LONGTEXT COMMENT 'HTML 或 JSON 字符串',\n \x60updated_at\x60 VARCHAR(32) NOT NULL DEFAULT '' COMMENT '更新时间',\n PRIMARY KEY (\x60id\x60),\n KEY \x60idx_site_slug\x60 (\x60site_id\x60, \x60slug\x60)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='网页表';",
+ "site_assets": "CREATE TABLE IF NOT EXISTS \x60site_assets\x60 (\n \x60id\x60 VARCHAR(24) NOT NULL COMMENT '主键',\n \x60site_id\x60 VARCHAR(24) NOT NULL DEFAULT '' COMMENT '站点ID',\n \x60name\x60 VARCHAR(255) NOT NULL DEFAULT '' COMMENT '文件名/显示名',\n \x60file_path\x60 VARCHAR(512) NOT NULL DEFAULT '' COMMENT '相对路径',\n \x60size\x60 BIGINT NOT NULL DEFAULT 0 COMMENT '字节数',\n \x60content_type\x60 VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'MIME 类型',\n \x60created_at\x60 VARCHAR(32) NOT NULL DEFAULT '' COMMENT '创建时间',\n PRIMARY KEY (\x60id\x60),\n KEY \x60idx_site_id\x60 (\x60site_id\x60)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='站点资源表';",
+ "users": "CREATE TABLE IF NOT EXISTS \x60users\x60 (\n \x60id\x60 VARCHAR(24) NOT NULL COMMENT '主键',\n \x60username\x60 VARCHAR(64) NOT NULL DEFAULT '' COMMENT '用户名',\n \x60mobile\x60 VARCHAR(32) NOT NULL DEFAULT '' COMMENT '手机号',\n \x60email\x60 VARCHAR(128) NOT NULL DEFAULT '' COMMENT '邮箱',\n \x60password\x60 VARCHAR(255) NOT NULL DEFAULT '' COMMENT '密码哈希',\n \x60role\x60 VARCHAR(32) NOT NULL DEFAULT '' COMMENT '角色名',\n \x60role_id\x60 INT NOT NULL DEFAULT 1 COMMENT '9527=超级管理员 1=普通用户',\n \x60is_beta\x60 TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否体验用户',\n \x60trial_start_date\x60 VARCHAR(16) NOT NULL DEFAULT '' COMMENT '试用开始日期',\n \x60trial_end_date\x60 VARCHAR(16) NOT NULL DEFAULT '' COMMENT '试用结束日期',\n \x60last_login\x60 VARCHAR(32) NOT NULL DEFAULT '' COMMENT '最后登录时间',\n \x60llm\x60 VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'LLM 配置',\n PRIMARY KEY (\x60id\x60),\n UNIQUE KEY \x60uk_username\x60 (\x60username\x60),\n KEY \x60idx_mobile\x60 (\x60mobile\x60),\n KEY \x60idx_email\x60 (\x60email\x60),\n KEY \x60idx_role_id\x60 (\x60role_id\x60)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';",
+ "workspaces": "CREATE TABLE IF NOT EXISTS \x60workspaces\x60 (\n \x60id\x60 VARCHAR(24) NOT NULL COMMENT '主键',\n \x60name\x60 VARCHAR(255) NOT NULL DEFAULT '' COMMENT '名称',\n \x60user_id\x60 VARCHAR(24) NOT NULL DEFAULT '' COMMENT '用户ID',\n \x60created_at\x60 VARCHAR(32) NOT NULL DEFAULT '' COMMENT '创建时间',\n PRIMARY KEY (\x60id\x60),\n KEY \x60idx_user_id\x60 (\x60user_id\x60)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作空间表';",
+ "conversations": "CREATE TABLE IF NOT EXISTS \x60conversations\x60 (\n \x60id\x60 VARCHAR(24) NOT NULL COMMENT '主键',\n \x60title\x60 VARCHAR(255) NOT NULL DEFAULT '' COMMENT '标题',\n \x60user_id\x60 VARCHAR(24) NOT NULL DEFAULT '' COMMENT '用户ID',\n \x60workspace_id\x60 VARCHAR(24) NOT NULL DEFAULT '' COMMENT '工作空间ID',\n \x60created_at\x60 VARCHAR(32) NOT NULL DEFAULT '' COMMENT '创建时间',\n \x60updated_at\x60 VARCHAR(32) NOT NULL DEFAULT '' COMMENT '更新时间',\n PRIMARY KEY (\x60id\x60),\n KEY \x60idx_user_id\x60 (\x60user_id\x60),\n KEY \x60idx_workspace_id\x60 (\x60workspace_id\x60)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='对话表';",
+ "messages": "CREATE TABLE IF NOT EXISTS \x60messages\x60 (\n \x60id\x60 VARCHAR(24) NOT NULL COMMENT '主键',\n \x60conversation_id\x60 VARCHAR(24) NOT NULL DEFAULT '' COMMENT '对话ID',\n \x60role\x60 VARCHAR(16) NOT NULL DEFAULT '' COMMENT 'role: user/assistant/system',\n \x60content\x60 LONGTEXT COMMENT '内容',\n \x60created_at\x60 VARCHAR(32) NOT NULL DEFAULT '' COMMENT '创建时间',\n PRIMARY KEY (\x60id\x60),\n KEY \x60idx_conversation_id\x60 (\x60conversation_id\x60)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='消息表';",
+ "files": "CREATE TABLE IF NOT EXISTS \x60files\x60 (\n \x60id\x60 VARCHAR(24) NOT NULL COMMENT '主键',\n \x60user_id\x60 VARCHAR(24) NOT NULL DEFAULT '' COMMENT '用户ID',\n \x60name\x60 VARCHAR(255) NOT NULL DEFAULT '' COMMENT '文件名',\n \x60file_path\x60 VARCHAR(512) NOT NULL DEFAULT '' COMMENT '存储路径',\n \x60size\x60 BIGINT NOT NULL DEFAULT 0 COMMENT '字节数',\n \x60created_at\x60 VARCHAR(32) NOT NULL DEFAULT '' COMMENT '创建时间',\n PRIMARY KEY (\x60id\x60),\n KEY \x60idx_user_id\x60 (\x60user_id\x60)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文件表';",
+ "system_config": "CREATE TABLE IF NOT EXISTS \x60system_config\x60 (\n \x60id\x60 VARCHAR(64) NOT NULL COMMENT '配置键 payment, sms_platform 等',\n \x60payload\x60 JSON COMMENT '配置内容(支付/短信等)',\n \x60updated_at\x60 VARCHAR(32) NOT NULL DEFAULT '' COMMENT '更新时间',\n PRIMARY KEY (\x60id\x60)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统配置表';",
+}
+
+// CollectionInfo 线上集合信息(名称、文档数、索引)
+type CollectionInfo struct {
+ Name string `json:"name"`
+ Count int64 `json:"count"`
+ Indexes []string `json:"indexes"` // 索引名或 key 描述
+}
+
+// Sync 连接 MongoDB 后调用:获取线上表结构并生成 sql;缺失的集合在线上创建,并生成本次创建的 sql
+// projectRoot 为项目根目录(sql/ 所在目录)
+func Sync(ctx context.Context, projectRoot string) error {
+ db := config.GetDB(config.DBName)
+ if db == nil {
+ return nil
+ }
+
+ sqlDir := filepath.Join(projectRoot, "sql")
+ _ = os.MkdirAll(sqlDir, 0755)
+
+ // 1. 获取当前已有集合
+ existing, err := db.ListCollectionNames(ctx, bson.M{})
+ if err != nil {
+ return fmt.Errorf("列出集合失败: %w", err)
+ }
+ existingSet := make(map[string]bool)
+ for _, n := range existing {
+ existingSet[n] = true
+ }
+
+ // 2. 缺失的集合在线上创建(集合 + 索引)
+ var created []string
+ for collName, idxSpecs := range requiredCollections {
+ if !existingSet[collName] {
+ if err := db.CreateCollection(ctx, collName); err != nil {
+ return fmt.Errorf("创建集合 %s 失败: %w", collName, err)
+ }
+ created = append(created, collName)
+ coll := db.Collection(collName)
+ for _, spec := range idxSpecs {
+ opts := options.Index().SetName(spec.Name)
+ if spec.Unique {
+ opts.SetUnique(true)
+ }
+ if spec.Sparse {
+ opts.SetSparse(true)
+ }
+ _, _ = coll.Indexes().CreateOne(ctx, mongo.IndexModel{Keys: spec.Keys, Options: opts})
+ }
+ existingSet[collName] = true
+ }
+ }
+ sort.Strings(created)
+
+ // 3. 拉取线上结构:每个集合的名称、文档数、索引列表
+ allNames := make([]string, 0, len(existingSet))
+ for n := range existingSet {
+ allNames = append(allNames, n)
+ }
+ sort.Strings(allNames)
+
+ infos := make([]CollectionInfo, 0, len(allNames))
+ for _, name := range allNames {
+ coll := db.Collection(name)
+ count, _ := coll.CountDocuments(ctx, bson.M{})
+ indexLines := listIndexes(ctx, coll)
+ infos = append(infos, CollectionInfo{Name: name, Count: count, Indexes: indexLines})
+ }
+
+ ts := time.Now().Format("20060102_150405")
+
+ // 4. 生成「线上表结构」的 sql 文件
+ if err := writeOnlineSchemaSQL(sqlDir, config.DBName, ts, infos); err != nil {
+ return err
+ }
+
+ // 5. 若有本次创建的集合,生成「需要创建的 sql」并在本地写入(线上已通过上面 CreateCollection 创建)
+ if len(created) > 0 {
+ if err := writeCreatedSQL(sqlDir, ts, created); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func listIndexes(ctx context.Context, coll *mongo.Collection) []string {
+ cursor, err := coll.Indexes().List(ctx)
+ if err != nil {
+ return nil
+ }
+ defer cursor.Close(ctx)
+
+ var lines []string
+ for cursor.Next(ctx) {
+ var spec bson.M
+ if err := cursor.Decode(&spec); err != nil {
+ continue
+ }
+ name, _ := spec["name"].(string)
+ key, _ := spec["key"].(bson.M)
+ if name != "" {
+ lines = append(lines, fmt.Sprintf("%s: %v", name, key))
+ }
+ }
+ return lines
+}
+
+func writeOnlineSchemaSQL(sqlDir, dbName, ts string, infos []CollectionInfo) error {
+ var b strings.Builder
+ b.WriteString("-- 线上 MongoDB 表结构快照(对应数据库: " + dbName + ")\n")
+ b.WriteString("-- 生成时间: " + time.Now().Format("2006-01-02 15:04:05") + "\n")
+ b.WriteString("-- 以下为集合与索引说明;等效 MySQL 建表见 sql/init.sql\n\n")
+ b.WriteString("SET NAMES utf8mb4;\n\n")
+
+ for _, info := range infos {
+ b.WriteString("-- -------------------------------\n")
+ b.WriteString("-- 集合: " + info.Name + " (文档数: " + fmt.Sprintf("%d", info.Count) + ")\n")
+ b.WriteString("-- -------------------------------\n")
+ for _, idx := range info.Indexes {
+ b.WriteString("-- 索引: " + idx + "\n")
+ }
+ if len(info.Indexes) == 0 {
+ b.WriteString("-- 索引: _id\n")
+ }
+ b.WriteString("\n")
+ // 附上等效 CREATE TABLE(便于对照)
+ if ddl, ok := tableDDL[info.Name]; ok {
+ b.WriteString(ddl + "\n\n")
+ }
+ }
+
+ fpath := filepath.Join(sqlDir, "online_schema_"+ts+".sql")
+ return os.WriteFile(fpath, []byte(b.String()), 0644)
+}
+
+func writeCreatedSQL(sqlDir, ts string, created []string) error {
+ var b strings.Builder
+ b.WriteString("-- 本次启动时在线上缺失并已创建的集合,对应 SQL 建表(MySQL 等效)\n")
+ b.WriteString("-- 生成时间: " + time.Now().Format("2006-01-02 15:04:05") + "\n")
+ b.WriteString("-- 线上 MongoDB 已通过 CreateCollection 创建;本文件供留档与 SQL 环境对照。\n\n")
+ b.WriteString("SET NAMES utf8mb4;\n\n")
+
+ for _, name := range created {
+ if ddl, ok := tableDDL[name]; ok {
+ b.WriteString("-- " + name + "\n")
+ b.WriteString(ddl + "\n\n")
+ }
+ }
+
+ fpath := filepath.Join(sqlDir, "created_"+ts+".sql")
+ return os.WriteFile(fpath, []byte(b.String()), 0644)
+}
diff --git a/server/scripts/inspect_db.go b/server/scripts/inspect_db.go
new file mode 100644
index 0000000..22fdd8e
--- /dev/null
+++ b/server/scripts/inspect_db.go
@@ -0,0 +1,74 @@
+// 独立运行:go run scripts/inspect_db.go
+// 查看 MongoDB 数据结构(需先建立 SSH 隧道)
+
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log"
+ "time"
+
+ "go.mongodb.org/mongo-driver/v2/bson"
+ "go.mongodb.org/mongo-driver/v2/mongo"
+ "go.mongodb.org/mongo-driver/v2/mongo/options"
+)
+
+func main() {
+ uri := "mongodb://localhost:27017"
+ client, err := mongo.Connect(options.Client().ApplyURI(uri))
+ if err != nil {
+ log.Fatalf("连接失败: %v", err)
+ }
+ defer client.Disconnect(context.TODO())
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ // 验证连接
+ var result bson.M
+ if err = client.Database("admin").RunCommand(ctx, bson.D{{Key: "ping", Value: 1}}).Decode(&result); err != nil {
+ log.Fatalf("Ping 失败: %v", err)
+ }
+ fmt.Println("MongoDB 连接成功\n")
+
+ // 列出数据库(排除系统库)
+ dbs, err := client.ListDatabaseNames(ctx, bson.M{
+ "name": bson.M{"$nin": []string{"admin", "config", "local"}},
+ })
+ if err != nil {
+ log.Fatalf("列出数据库失败: %v", err)
+ }
+
+ output := map[string]interface{}{"databases": []interface{}{}}
+
+ for _, dbName := range dbs {
+ db := client.Database(dbName)
+ colls, _ := db.ListCollectionNames(ctx, bson.M{})
+
+ dbInfo := map[string]interface{}{
+ "name": dbName,
+ "collections": []interface{}{},
+ }
+
+ for _, collName := range colls {
+ coll := db.Collection(collName)
+ count, _ := coll.CountDocuments(ctx, bson.M{})
+
+ var sample bson.M
+ _ = coll.FindOne(ctx, bson.M{}).Decode(&sample)
+
+ dbInfo["collections"] = append(dbInfo["collections"].([]interface{}), map[string]interface{}{
+ "name": collName,
+ "count": count,
+ "sample": sample,
+ })
+ }
+
+ output["databases"] = append(output["databases"].([]interface{}), dbInfo)
+ }
+
+ jsonBytes, _ := json.MarshalIndent(output, "", " ")
+ fmt.Println(string(jsonBytes))
+}
diff --git a/server/scripts/start-ssh-tunnel.bat b/server/scripts/start-ssh-tunnel.bat
new file mode 100644
index 0000000..d01d5e4
--- /dev/null
+++ b/server/scripts/start-ssh-tunnel.bat
@@ -0,0 +1,13 @@
+@echo off
+REM MongoDB SSH 穿透 - 将远程 27017 映射到本地 27017
+REM 运行此脚本后再启动 server,即可连接远程 MongoDB
+
+echo 正在建立 SSH 隧道...
+echo 远程: www.yuxindazhineng.com:27017 -^> 本地: localhost:27017
+echo.
+echo 保持此窗口打开,隧道有效。关闭窗口即断开。
+echo.
+
+ssh -p 2223 -L 27017:localhost:27017 yxd@www.yuxindazhineng.com
+
+pause
diff --git a/server/uploads/.gitkeep b/server/uploads/.gitkeep
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/server/uploads/.gitkeep
@@ -0,0 +1 @@
+
diff --git a/server/utils/password.go b/server/utils/password.go
new file mode 100644
index 0000000..11372fd
--- /dev/null
+++ b/server/utils/password.go
@@ -0,0 +1,12 @@
+package utils
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+)
+
+// HashPassword 使用 SHA-256 哈希密码(与 Python 实现一致)
+func HashPassword(password string) string {
+ h := sha256.Sum256([]byte(password))
+ return hex.EncodeToString(h[:])
+}
diff --git a/sql/created_20260314_144115.sql b/sql/created_20260314_144115.sql
new file mode 100644
index 0000000..215828d
--- /dev/null
+++ b/sql/created_20260314_144115.sql
@@ -0,0 +1,51 @@
+-- 本次启动时在线上缺失并已创建的集合,对应 SQL 建表(MySQL 等效)
+-- 生成时间: 2026-03-14 14:41:15
+-- 线上 MongoDB 已通过 CreateCollection 创建;本文件供留档与 SQL 环境对照。
+
+SET NAMES utf8mb4;
+
+-- pages
+CREATE TABLE IF NOT EXISTS `pages` (
+ `id` VARCHAR(24) NOT NULL COMMENT '主键',
+ `site_id` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '站点ID',
+ `slug` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '路径标识 index, about, ...',
+ `title` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '标题',
+ `type` VARCHAR(32) NOT NULL DEFAULT 'page' COMMENT '类型 homepage, page',
+ `content` LONGTEXT COMMENT 'HTML 或 JSON 字符串',
+ `updated_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '更新时间',
+ PRIMARY KEY (`id`),
+ KEY `idx_site_slug` (`site_id`, `slug`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='网页表';
+
+-- site_assets
+CREATE TABLE IF NOT EXISTS `site_assets` (
+ `id` VARCHAR(24) NOT NULL COMMENT '主键',
+ `site_id` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '站点ID',
+ `name` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '文件名/显示名',
+ `file_path` VARCHAR(512) NOT NULL DEFAULT '' COMMENT '相对路径',
+ `size` BIGINT NOT NULL DEFAULT 0 COMMENT '字节数',
+ `content_type` VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'MIME 类型',
+ `created_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '创建时间',
+ PRIMARY KEY (`id`),
+ KEY `idx_site_id` (`site_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='站点资源表';
+
+-- sites
+CREATE TABLE IF NOT EXISTS `sites` (
+ `id` VARCHAR(24) NOT NULL COMMENT '主键,与 MongoDB ObjectID 字符串一致',
+ `name` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '站点名称',
+ `domain` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '域名',
+ `description` TEXT COMMENT '描述',
+ `created_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '创建时间',
+ PRIMARY KEY (`id`),
+ KEY `idx_created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='站点表';
+
+-- system_config
+CREATE TABLE IF NOT EXISTS `system_config` (
+ `id` VARCHAR(64) NOT NULL COMMENT '配置键 payment, sms_platform 等',
+ `payload` JSON COMMENT '配置内容(支付/短信等)',
+ `updated_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '更新时间',
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统配置表';
+
diff --git a/sql/init.sql b/sql/init.sql
new file mode 100644
index 0000000..9088489
--- /dev/null
+++ b/sql/init.sql
@@ -0,0 +1,134 @@
+-- yh_web 建表语句(MySQL 5.7+ / MariaDB 10.2+)
+-- 与当前功能及线上已有结构对应,便于迁移或对照。主键使用 VARCHAR(24) 与 MongoDB ObjectID 字符串形式兼容。
+
+SET NAMES utf8mb4;
+
+-- -------------------------------
+-- 站点
+-- -------------------------------
+CREATE TABLE IF NOT EXISTS `sites` (
+ `id` VARCHAR(24) NOT NULL COMMENT '主键,与 MongoDB ObjectID 字符串一致',
+ `name` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '站点名称',
+ `domain` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '域名',
+ `description` TEXT COMMENT '描述',
+ `created_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '创建时间',
+ PRIMARY KEY (`id`),
+ KEY `idx_created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='站点表';
+
+-- -------------------------------
+-- 网页(属于某站点)
+-- -------------------------------
+CREATE TABLE IF NOT EXISTS `pages` (
+ `id` VARCHAR(24) NOT NULL COMMENT '主键',
+ `site_id` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '站点ID',
+ `slug` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '路径标识 index, about, ...',
+ `title` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '标题',
+ `type` VARCHAR(32) NOT NULL DEFAULT 'page' COMMENT '类型 homepage, page',
+ `content` LONGTEXT COMMENT 'HTML 或 JSON 字符串',
+ `updated_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '更新时间',
+ PRIMARY KEY (`id`),
+ KEY `idx_site_slug` (`site_id`, `slug`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='网页表';
+
+-- -------------------------------
+-- 站点资源/上传文件
+-- -------------------------------
+CREATE TABLE IF NOT EXISTS `site_assets` (
+ `id` VARCHAR(24) NOT NULL COMMENT '主键',
+ `site_id` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '站点ID',
+ `name` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '文件名/显示名',
+ `file_path` VARCHAR(512) NOT NULL DEFAULT '' COMMENT '相对路径',
+ `size` BIGINT NOT NULL DEFAULT 0 COMMENT '字节数',
+ `content_type` VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'MIME 类型',
+ `created_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '创建时间',
+ PRIMARY KEY (`id`),
+ KEY `idx_site_id` (`site_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='站点资源表';
+
+-- -------------------------------
+-- 用户
+-- -------------------------------
+CREATE TABLE IF NOT EXISTS `users` (
+ `id` VARCHAR(24) NOT NULL COMMENT '主键',
+ `username` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '用户名',
+ `mobile` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '手机号',
+ `email` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '邮箱',
+ `password` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '密码哈希',
+ `role` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '角色名',
+ `role_id` INT NOT NULL DEFAULT 1 COMMENT '9527=超级管理员 1=普通用户',
+ `is_beta` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否体验用户',
+ `trial_start_date` VARCHAR(16) NOT NULL DEFAULT '' COMMENT '试用开始日期',
+ `trial_end_date` VARCHAR(16) NOT NULL DEFAULT '' COMMENT '试用结束日期',
+ `last_login` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '最后登录时间',
+ `llm` VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'LLM 配置',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_username` (`username`),
+ KEY `idx_mobile` (`mobile`),
+ KEY `idx_email` (`email`),
+ KEY `idx_role_id` (`role_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
+
+-- -------------------------------
+-- 工作空间
+-- -------------------------------
+CREATE TABLE IF NOT EXISTS `workspaces` (
+ `id` VARCHAR(24) NOT NULL COMMENT '主键',
+ `name` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '名称',
+ `user_id` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '用户ID',
+ `created_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '创建时间',
+ PRIMARY KEY (`id`),
+ KEY `idx_user_id` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作空间表';
+
+-- -------------------------------
+-- 对话
+-- -------------------------------
+CREATE TABLE IF NOT EXISTS `conversations` (
+ `id` VARCHAR(24) NOT NULL COMMENT '主键',
+ `title` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '标题',
+ `user_id` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '用户ID',
+ `workspace_id` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '工作空间ID',
+ `created_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '创建时间',
+ `updated_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '更新时间',
+ PRIMARY KEY (`id`),
+ KEY `idx_user_id` (`user_id`),
+ KEY `idx_workspace_id` (`workspace_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='对话表';
+
+-- -------------------------------
+-- 消息(统计用,功能上若未实现可先建表)
+-- -------------------------------
+CREATE TABLE IF NOT EXISTS `messages` (
+ `id` VARCHAR(24) NOT NULL COMMENT '主键',
+ `conversation_id` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '对话ID',
+ `role` VARCHAR(16) NOT NULL DEFAULT '' COMMENT 'role: user/assistant/system',
+ `content` LONGTEXT COMMENT '内容',
+ `created_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '创建时间',
+ PRIMARY KEY (`id`),
+ KEY `idx_conversation_id` (`conversation_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='消息表';
+
+-- -------------------------------
+-- 文件(统计用,功能上若未实现可先建表)
+-- -------------------------------
+CREATE TABLE IF NOT EXISTS `files` (
+ `id` VARCHAR(24) NOT NULL COMMENT '主键',
+ `user_id` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '用户ID',
+ `name` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '文件名',
+ `file_path` VARCHAR(512) NOT NULL DEFAULT '' COMMENT '存储路径',
+ `size` BIGINT NOT NULL DEFAULT 0 COMMENT '字节数',
+ `created_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '创建时间',
+ PRIMARY KEY (`id`),
+ KEY `idx_user_id` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文件表';
+
+-- -------------------------------
+-- 系统配置(键值,_id 为配置键:payment / sms_platform)
+-- -------------------------------
+CREATE TABLE IF NOT EXISTS `system_config` (
+ `id` VARCHAR(64) NOT NULL COMMENT '配置键 payment, sms_platform 等',
+ `payload` JSON COMMENT '配置内容(支付/短信等)',
+ `updated_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '更新时间',
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统配置表';
diff --git a/sql/online_schema_20260314_144115.sql b/sql/online_schema_20260314_144115.sql
new file mode 100644
index 0000000..560864e
--- /dev/null
+++ b/sql/online_schema_20260314_144115.sql
@@ -0,0 +1,253 @@
+-- 线上 MongoDB 表结构快照(对应数据库: yxd-agent-testing)
+-- 生成时间: 2026-03-14 14:41:15
+-- 以下为集合与索引说明;等效 MySQL 建表见 sql/init.sql
+
+SET NAMES utf8mb4;
+
+-- -------------------------------
+-- 集合: app_domains (文档数: 1)
+-- -------------------------------
+-- 索引: _id_: {}
+-- 索引: user_id_1: {}
+-- 索引: domain_prefix_1: {}
+-- 索引: token_1: {}
+
+-- -------------------------------
+-- 集合: conversations (文档数: 61)
+-- -------------------------------
+-- 索引: _id_: {}
+
+CREATE TABLE IF NOT EXISTS `conversations` (
+ `id` VARCHAR(24) NOT NULL COMMENT '主键',
+ `title` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '标题',
+ `user_id` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '用户ID',
+ `workspace_id` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '工作空间ID',
+ `created_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '创建时间',
+ `updated_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '更新时间',
+ PRIMARY KEY (`id`),
+ KEY `idx_user_id` (`user_id`),
+ KEY `idx_workspace_id` (`workspace_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='对话表';
+
+-- -------------------------------
+-- 集合: file_logs (文档数: 0)
+-- -------------------------------
+-- 索引: _id_: {}
+-- 索引: file_id_1_user_id_1_created_at_1: {}
+
+-- -------------------------------
+-- 集合: file_permissions (文档数: 8)
+-- -------------------------------
+-- 索引: _id_: {}
+-- 索引: file_id_1_user_id_1_team_id_1: {}
+
+-- -------------------------------
+-- 集合: files (文档数: 34)
+-- -------------------------------
+-- 索引: _id_: {}
+-- 索引: owner_id_1_full_path_1: {}
+
+CREATE TABLE IF NOT EXISTS `files` (
+ `id` VARCHAR(24) NOT NULL COMMENT '主键',
+ `user_id` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '用户ID',
+ `name` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '文件名',
+ `file_path` VARCHAR(512) NOT NULL DEFAULT '' COMMENT '存储路径',
+ `size` BIGINT NOT NULL DEFAULT 0 COMMENT '字节数',
+ `created_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '创建时间',
+ PRIMARY KEY (`id`),
+ KEY `idx_user_id` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文件表';
+
+-- -------------------------------
+-- 集合: knowledge_bases (文档数: 0)
+-- -------------------------------
+-- 索引: _id_: {}
+-- 索引: user_id_1_name_1: {}
+-- 索引: read_users_1: {}
+-- 索引: write_users_1: {}
+
+-- -------------------------------
+-- 集合: messages (文档数: 1469)
+-- -------------------------------
+-- 索引: _id_: {}
+
+CREATE TABLE IF NOT EXISTS `messages` (
+ `id` VARCHAR(24) NOT NULL COMMENT '主键',
+ `conversation_id` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '对话ID',
+ `role` VARCHAR(16) NOT NULL DEFAULT '' COMMENT 'role: user/assistant/system',
+ `content` LONGTEXT COMMENT '内容',
+ `created_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '创建时间',
+ PRIMARY KEY (`id`),
+ KEY `idx_conversation_id` (`conversation_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='消息表';
+
+-- -------------------------------
+-- 集合: orders (文档数: 0)
+-- -------------------------------
+-- 索引: _id_: {}
+-- 索引: user_id_1_payment_id_1: {}
+
+-- -------------------------------
+-- 集合: overload (文档数: 2)
+-- -------------------------------
+-- 索引: _id_: {}
+
+-- -------------------------------
+-- 集合: pages (文档数: 0)
+-- -------------------------------
+-- 索引: _id_: {}
+-- 索引: idx_site_slug: {}
+
+CREATE TABLE IF NOT EXISTS `pages` (
+ `id` VARCHAR(24) NOT NULL COMMENT '主键',
+ `site_id` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '站点ID',
+ `slug` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '路径标识 index, about, ...',
+ `title` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '标题',
+ `type` VARCHAR(32) NOT NULL DEFAULT 'page' COMMENT '类型 homepage, page',
+ `content` LONGTEXT COMMENT 'HTML 或 JSON 字符串',
+ `updated_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '更新时间',
+ PRIMARY KEY (`id`),
+ KEY `idx_site_slug` (`site_id`, `slug`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='网页表';
+
+-- -------------------------------
+-- 集合: permanent (文档数: 5)
+-- -------------------------------
+-- 索引: _id_: {}
+-- 索引: user_id_1_database_id_1_team_id_1: {}
+
+-- -------------------------------
+-- 集合: permanent_databases (文档数: 6)
+-- -------------------------------
+-- 索引: _id_: {}
+-- 索引: user_id_1: {}
+
+-- -------------------------------
+-- 集合: public_database (文档数: 5)
+-- -------------------------------
+-- 索引: _id_: {}
+-- 索引: database_name_1_user_id_1_team_id_1: {}
+
+-- -------------------------------
+-- 集合: railway_accounts (文档数: 0)
+-- -------------------------------
+-- 索引: _id_: {}
+-- 索引: user_id_1_account_name_1: {}
+
+-- -------------------------------
+-- 集合: railway_accounts_new (文档数: 25)
+-- -------------------------------
+-- 索引: _id_: {}
+-- 索引: account_1: {}
+
+-- -------------------------------
+-- 集合: scheduler_tasks (文档数: 2)
+-- -------------------------------
+-- 索引: _id_: {}
+
+-- -------------------------------
+-- 集合: site_assets (文档数: 0)
+-- -------------------------------
+-- 索引: _id_: {}
+-- 索引: idx_site_id: {}
+
+CREATE TABLE IF NOT EXISTS `site_assets` (
+ `id` VARCHAR(24) NOT NULL COMMENT '主键',
+ `site_id` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '站点ID',
+ `name` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '文件名/显示名',
+ `file_path` VARCHAR(512) NOT NULL DEFAULT '' COMMENT '相对路径',
+ `size` BIGINT NOT NULL DEFAULT 0 COMMENT '字节数',
+ `content_type` VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'MIME 类型',
+ `created_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '创建时间',
+ PRIMARY KEY (`id`),
+ KEY `idx_site_id` (`site_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='站点资源表';
+
+-- -------------------------------
+-- 集合: sites (文档数: 0)
+-- -------------------------------
+-- 索引: _id_: {}
+-- 索引: idx_created_at: {}
+
+CREATE TABLE IF NOT EXISTS `sites` (
+ `id` VARCHAR(24) NOT NULL COMMENT '主键,与 MongoDB ObjectID 字符串一致',
+ `name` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '站点名称',
+ `domain` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '域名',
+ `description` TEXT COMMENT '描述',
+ `created_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '创建时间',
+ PRIMARY KEY (`id`),
+ KEY `idx_created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='站点表';
+
+-- -------------------------------
+-- 集合: system_config (文档数: 0)
+-- -------------------------------
+-- 索引: _id_: {}
+
+CREATE TABLE IF NOT EXISTS `system_config` (
+ `id` VARCHAR(64) NOT NULL COMMENT '配置键 payment, sms_platform 等',
+ `payload` JSON COMMENT '配置内容(支付/短信等)',
+ `updated_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '更新时间',
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统配置表';
+
+-- -------------------------------
+-- 集合: team_membership (文档数: 47)
+-- -------------------------------
+-- 索引: _id_: {}
+-- 索引: team_id_1_user_id_1: {}
+
+-- -------------------------------
+-- 集合: teams (文档数: 3)
+-- -------------------------------
+-- 索引: _id_: {}
+-- 索引: name_1: {}
+
+-- -------------------------------
+-- 集合: users (文档数: 45)
+-- -------------------------------
+-- 索引: _id_: {}
+-- 索引: username_1: {}
+-- 索引: email_1: {}
+
+CREATE TABLE IF NOT EXISTS `users` (
+ `id` VARCHAR(24) NOT NULL COMMENT '主键',
+ `username` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '用户名',
+ `mobile` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '手机号',
+ `email` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '邮箱',
+ `password` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '密码哈希',
+ `role` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '角色名',
+ `role_id` INT NOT NULL DEFAULT 1 COMMENT '9527=超级管理员 1=普通用户',
+ `is_beta` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否体验用户',
+ `trial_start_date` VARCHAR(16) NOT NULL DEFAULT '' COMMENT '试用开始日期',
+ `trial_end_date` VARCHAR(16) NOT NULL DEFAULT '' COMMENT '试用结束日期',
+ `last_login` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '最后登录时间',
+ `llm` VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'LLM 配置',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_username` (`username`),
+ KEY `idx_mobile` (`mobile`),
+ KEY `idx_email` (`email`),
+ KEY `idx_role_id` (`role_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
+
+-- -------------------------------
+-- 集合: word_config (文档数: 0)
+-- -------------------------------
+-- 索引: _id_: {}
+-- 索引: user_id_1: {}
+
+-- -------------------------------
+-- 集合: workspaces (文档数: 73)
+-- -------------------------------
+-- 索引: _id_: {}
+-- 索引: user_id_1_name_1: {}
+
+CREATE TABLE IF NOT EXISTS `workspaces` (
+ `id` VARCHAR(24) NOT NULL COMMENT '主键',
+ `name` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '名称',
+ `user_id` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '用户ID',
+ `created_at` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '创建时间',
+ PRIMARY KEY (`id`),
+ KEY `idx_user_id` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工作空间表';
+
diff --git a/stop-docker.sh b/stop-docker.sh
new file mode 100644
index 0000000..1ca15a4
--- /dev/null
+++ b/stop-docker.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+cd "$(dirname "$0")"
+docker compose down 2>/dev/null || docker-compose down
+echo "已停止所有容器"
diff --git a/web/.env.example b/web/.env.example
new file mode 100644
index 0000000..9df518b
--- /dev/null
+++ b/web/.env.example
@@ -0,0 +1,5 @@
+# 复制为 .env 或 .env.production 后修改
+# 前台(官网)环境变量 - 仅 VITE_ 前缀会在构建时注入
+
+VITE_APP_DOMAIN=https://yuheng.yuxindazhineng.com
+VITE_API_BASE=
diff --git a/web/.env.production b/web/.env.production
new file mode 100644
index 0000000..353bfe1
--- /dev/null
+++ b/web/.env.production
@@ -0,0 +1,6 @@
+# 前台(官网)生产环境 - 对外域名
+# 构建时生效:npm run build
+
+VITE_APP_DOMAIN=https://yuheng.yuxindazhineng.com
+# 与官网同域,接口走 /api,留空即可
+VITE_API_BASE=
diff --git a/web/Dockerfile b/web/Dockerfile
new file mode 100644
index 0000000..f0c1b5e
--- /dev/null
+++ b/web/Dockerfile
@@ -0,0 +1,12 @@
+FROM node:20-alpine AS builder
+WORKDIR /app
+COPY package.json package-lock.json* ./
+RUN npm ci --legacy-peer-deps 2>/dev/null || npm install --legacy-peer-deps
+COPY . .
+RUN npm run build
+
+FROM nginx:alpine
+COPY --from=builder /app/dist /usr/share/nginx/html
+RUN echo 'server { listen 80; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } }' > /etc/nginx/conf.d/default.conf
+EXPOSE 80
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/web/README.md b/web/README.md
new file mode 100644
index 0000000..6c43a8e
--- /dev/null
+++ b/web/README.md
@@ -0,0 +1,12 @@
+# 多站点管理 - 前台
+
+基于 Vue 3 + Vite 的前台展示页面。
+
+## 运行
+
+```bash
+npm install
+npm run dev
+```
+
+默认端口 3001,API 代理到 8080
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..50b7d28
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ 宇恒一号 - 星际探索版
+
+
+
+
+
+
+
+
+
diff --git a/web/package-lock.json b/web/package-lock.json
new file mode 100644
index 0000000..16af6ee
--- /dev/null
+++ b/web/package-lock.json
@@ -0,0 +1,1509 @@
+{
+ "name": "yh-web",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "yh-web",
+ "version": "0.0.0",
+ "dependencies": {
+ "axios": "^1.6.2",
+ "vue": "^3.4.0",
+ "vue-router": "^4.2.5"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^4.5.2",
+ "vite": "^5.0.10"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
+ "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
+ "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
+ "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
+ "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
+ "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
+ "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
+ "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
+ "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
+ "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
+ "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
+ "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
+ "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
+ "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
+ "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
+ "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
+ "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
+ "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
+ "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
+ "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
+ "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
+ "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
+ "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
+ "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
+ "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.0.0 || ^5.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz",
+ "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@vue/shared": "3.5.30",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz",
+ "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.30",
+ "@vue/shared": "3.5.30"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz",
+ "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@vue/compiler-core": "3.5.30",
+ "@vue/compiler-dom": "3.5.30",
+ "@vue/compiler-ssr": "3.5.30",
+ "@vue/shared": "3.5.30",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.8",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz",
+ "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.30",
+ "@vue/shared": "3.5.30"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz",
+ "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.30"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz",
+ "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.30",
+ "@vue/shared": "3.5.30"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz",
+ "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.30",
+ "@vue/runtime-core": "3.5.30",
+ "@vue/shared": "3.5.30",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz",
+ "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.30",
+ "@vue/shared": "3.5.30"
+ },
+ "peerDependencies": {
+ "vue": "3.5.30"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz",
+ "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==",
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.13.6",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
+ "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/rollup": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
+ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.59.0",
+ "@rollup/rollup-android-arm64": "4.59.0",
+ "@rollup/rollup-darwin-arm64": "4.59.0",
+ "@rollup/rollup-darwin-x64": "4.59.0",
+ "@rollup/rollup-freebsd-arm64": "4.59.0",
+ "@rollup/rollup-freebsd-x64": "4.59.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.59.0",
+ "@rollup/rollup-linux-arm64-musl": "4.59.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.59.0",
+ "@rollup/rollup-linux-loong64-musl": "4.59.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.59.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.59.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-musl": "4.59.0",
+ "@rollup/rollup-openbsd-x64": "4.59.0",
+ "@rollup/rollup-openharmony-arm64": "4.59.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.59.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.59.0",
+ "@rollup/rollup-win32-x64-gnu": "4.59.0",
+ "@rollup/rollup-win32-x64-msvc": "4.59.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz",
+ "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.30",
+ "@vue/compiler-sfc": "3.5.30",
+ "@vue/runtime-dom": "3.5.30",
+ "@vue/server-renderer": "3.5.30",
+ "@vue/shared": "3.5.30"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-router": {
+ "version": "4.6.4",
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
+ "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ }
+ }
+}
diff --git a/web/package.json b/web/package.json
new file mode 100644
index 0000000..6ab2936
--- /dev/null
+++ b/web/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "yh-web",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "vue": "^3.4.0",
+ "vue-router": "^4.2.5",
+ "axios": "^1.6.2"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^4.5.2",
+ "vite": "^5.0.10"
+ }
+}
diff --git a/web/src/App.vue b/web/src/App.vue
new file mode 100644
index 0000000..7f27591
--- /dev/null
+++ b/web/src/App.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/web/src/assets/landing-dynamics.css b/web/src/assets/landing-dynamics.css
new file mode 100644
index 0000000..b4983a8
--- /dev/null
+++ b/web/src/assets/landing-dynamics.css
@@ -0,0 +1,187 @@
+/* 首页动效:星星、流星、标题、按钮、平台图标 - 全局生效 */
+
+/* 星星闪烁 */
+.landing .stars .star {
+ position: absolute;
+ background: #fff;
+ border-radius: 50%;
+ animation: landing-twinkle var(--duration, 2.5s) ease-in-out infinite;
+ will-change: opacity, transform;
+}
+@keyframes landing-twinkle {
+ 0%, 100% { opacity: var(--min-opacity, 0.3); transform: scale(1); }
+ 50% { opacity: 1; transform: scale(1.2); }
+}
+
+/* 流星 */
+.landing ~ .comet,
+body .comet {
+ position: fixed !important;
+ width: 120px;
+ height: 3px;
+ z-index: 99;
+ pointer-events: none;
+ background: linear-gradient(90deg, rgba(255,255,255,0.9) 0%, transparent 100%);
+ border-radius: 2px;
+ box-shadow: 0 0 10px rgba(255,255,255,0.6);
+}
+
+/* 标题浮动 + 发光 */
+.landing .title-3d {
+ animation: landing-float-title 4s ease-in-out infinite;
+}
+@keyframes landing-float-title {
+ 0%, 100% { transform: translateY(0) rotateX(0deg); }
+ 50% { transform: translateY(-15px) rotateX(5deg); }
+}
+.landing .title-3d span {
+ animation: landing-glow-text 3s ease-in-out infinite;
+}
+@keyframes landing-glow-text {
+ 0%, 100% { text-shadow: 0 0 10px rgba(0,212,255,0.5); }
+ 50% { text-shadow: 0 0 30px rgba(255,45,149,0.8); }
+}
+
+/* 副标题脉冲 */
+.landing .subtitle-space {
+ animation: landing-pulse-glow 2s ease-in-out infinite;
+}
+@keyframes landing-pulse-glow {
+ 0%, 100% { opacity: 0.7; text-shadow: 0 0 10px #00d4ff; }
+ 50% { opacity: 1; text-shadow: 0 0 30px #00d4ff; }
+}
+
+/* 按钮波纹 */
+.landing .warp-effect {
+ animation: landing-warp-drive 1.5s ease-out infinite;
+}
+@keyframes landing-warp-drive {
+ 0% { width: 0; height: 0; opacity: 1; }
+ 100% { width: 300px; height: 300px; opacity: 0; }
+}
+
+/* 平台图标:立体水波纹(双层环+立体光晕)+ 波纹间碰撞效果 */
+.landing .orbit-platform {
+ position: relative;
+ z-index: 1;
+ transition: all 0.4s ease;
+ overflow: visible;
+}
+/* 立体水波纹 - 内环(先动) */
+.landing .orbit-platform::before {
+ content: '';
+ position: absolute;
+ top: -8px;
+ left: -8px;
+ right: -8px;
+ bottom: -8px;
+ border-radius: 50%;
+ border: 2px solid rgba(0,212,255,0.5);
+ box-shadow:
+ 0 0 0 1px rgba(0,212,255,0.3),
+ inset 0 0 15px rgba(0,212,255,0.08),
+ 0 0 20px rgba(0,212,255,0.15);
+ animation: orbit-ripple-inner 2s ease-out infinite;
+ pointer-events: none;
+}
+/* 立体水波纹 - 外环(主波,带厚度与景深) */
+.landing .orbit-platform::after {
+ content: '';
+ position: absolute;
+ top: -5px;
+ left: -5px;
+ right: -5px;
+ bottom: -5px;
+ border: 1px solid rgba(0,212,255,0.9);
+ border-radius: 50%;
+ box-shadow:
+ 0 0 0 2px rgba(0,212,255,0.25),
+ 0 0 0 4px rgba(0,212,255,0.1),
+ 0 0 15px rgba(0,212,255,0.35),
+ 0 0 30px rgba(255,45,149,0.15),
+ inset 0 0 20px rgba(0,212,255,0.06);
+ animation: orbit-pulse-ripple 2s ease-out infinite;
+ pointer-events: none;
+}
+@keyframes orbit-ripple-inner {
+ 0% { transform: scale(1); opacity: 0.7; }
+ 100% { transform: scale(1.45); opacity: 0; }
+}
+@keyframes orbit-pulse-ripple {
+ 0% { transform: scale(1); opacity: 0.9; }
+ 100% { transform: scale(1.5); opacity: 0; }
+}
+/* 中心碰撞光点(波纹交汇瞬间) */
+.landing .orbit-platforms {
+ position: relative;
+}
+.landing .orbit-platforms::after {
+ content: '';
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ width: 24px;
+ height: 24px;
+ margin-left: -12px;
+ margin-top: -12px;
+ border-radius: 50%;
+ background: radial-gradient(circle, rgba(0,212,255,0.95) 0%, rgba(255,45,149,0.5) 35%, transparent 65%);
+ box-shadow: 0 0 40px rgba(0,212,255,0.7), 0 0 80px rgba(255,45,149,0.4);
+ pointer-events: none;
+ z-index: 0;
+ animation: orbit-collision 2s ease-out infinite;
+}
+@keyframes orbit-collision {
+ 0%, 48% { transform: scale(0.2); opacity: 0; }
+ 58% { transform: scale(0.6); opacity: 0.5; }
+ 72% { transform: scale(1); opacity: 1; }
+ 85% { transform: scale(1.3); opacity: 0.4; }
+ 100% { transform: scale(1.6); opacity: 0; }
+}
+/* 碰撞扩散环:波纹相撞后产生的同心圆扩散(立体环) */
+.landing .orbit-collision-ring {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ width: 60px;
+ height: 60px;
+ margin-left: -30px;
+ margin-top: -30px;
+ border-radius: 50%;
+ border: 2px solid transparent;
+ box-shadow:
+ 0 0 0 1px rgba(0,212,255,0.6),
+ 0 0 0 3px rgba(0,212,255,0.2),
+ 0 0 25px rgba(0,212,255,0.4),
+ inset 0 0 15px rgba(0,212,255,0.1);
+ pointer-events: none;
+ z-index: 0;
+ animation: orbit-collision-ring 2s ease-out infinite;
+}
+@keyframes orbit-collision-ring {
+ 0%, 52% { transform: scale(0.3); opacity: 0; border-color: rgba(0,212,255,0); }
+ 62% { transform: scale(0.6); opacity: 0.8; border-color: rgba(0,212,255,0.8); }
+ 75% { transform: scale(1); opacity: 1; border-color: rgba(255,45,149,0.4); }
+ 90% { transform: scale(1.5); opacity: 0.3; border-color: rgba(0,212,255,0.1); }
+ 100% { transform: scale(1.8); opacity: 0; border-color: rgba(0,212,255,0); }
+}
+.landing .orbit-platform:hover {
+ background: rgba(0,212,255,0.25);
+ border-color: #00d4ff;
+ transform: translateY(-12px) scale(1.08);
+ box-shadow: 0 12px 35px rgba(0,212,255,0.4);
+}
+.landing .orbit-platform:hover::before,
+.landing .orbit-platform:hover::after {
+ animation-duration: 1.2s;
+}
+
+/* 底部卡片 hover */
+.landing .feature-space {
+ transition: all 0.5s ease;
+}
+.landing .feature-space:hover {
+ transform: translateY(-15px);
+ border-color: rgba(0,212,255,0.4);
+ box-shadow: 0 25px 50px rgba(0,0,0,0.3);
+}
diff --git a/web/src/config.js b/web/src/config.js
new file mode 100644
index 0000000..302ad97
--- /dev/null
+++ b/web/src/config.js
@@ -0,0 +1,8 @@
+/**
+ * 前台(官网)运行时常量,来自构建时注入的环境变量
+ * 开发时未设置则为空,请求走同源 /api(依赖 vite proxy)
+ */
+const appDomain = import.meta.env.VITE_APP_DOMAIN || ''
+const apiBase = (import.meta.env.VITE_API_BASE || '').replace(/\/$/, '')
+
+export { appDomain, apiBase }
diff --git a/web/src/main.js b/web/src/main.js
new file mode 100644
index 0000000..9a551e6
--- /dev/null
+++ b/web/src/main.js
@@ -0,0 +1,9 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+import router from './router'
+import './utils/disable-debug'
+import './assets/landing-dynamics.css'
+
+const app = createApp(App)
+app.use(router)
+app.mount('#app')
diff --git a/web/src/router/index.js b/web/src/router/index.js
new file mode 100644
index 0000000..501f90b
--- /dev/null
+++ b/web/src/router/index.js
@@ -0,0 +1,21 @@
+import { createRouter, createWebHistory } from 'vue-router'
+
+const routes = [
+ {
+ path: '/',
+ name: 'Home',
+ component: () => import('../views/Home.vue'),
+ meta: { title: '首页' }
+ },
+ {
+ path: '/index',
+ redirect: '/'
+ }
+]
+
+const router = createRouter({
+ history: createWebHistory(),
+ routes
+})
+
+export default router
diff --git a/web/src/utils/disable-debug.js b/web/src/utils/disable-debug.js
new file mode 100644
index 0000000..e599d0c
--- /dev/null
+++ b/web/src/utils/disable-debug.js
@@ -0,0 +1,18 @@
+/**
+ * 禁止调试模式及右键 - 全局安全模块
+ * 在页面加载时立即执行
+ */
+
+// 禁止右键
+document.addEventListener('contextmenu', (e) => e.preventDefault())
+
+// 禁止 F12、Ctrl+Shift+I/J/C 等开发者工具快捷键
+document.addEventListener('keydown', (e) => {
+ if (
+ e.key === 'F12' ||
+ (e.ctrlKey && e.shiftKey && ['I', 'J', 'C'].includes(e.key.toUpperCase())) ||
+ (e.ctrlKey && e.key === 'U')
+ ) {
+ e.preventDefault()
+ }
+})
diff --git a/web/src/views/Home.vue b/web/src/views/Home.vue
new file mode 100644
index 0000000..cdfb9f9
--- /dev/null
+++ b/web/src/views/Home.vue
@@ -0,0 +1,369 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ ch }}
+
+
+ {{ data.subtitle || 'INTERSTELLAR EXPLORER EDITION' }}
+
+
+
+
+ {{ data.version || 'VERSION 3.2.1' }}
+ 🚀 {{ data.launch_year || 'LAUNCH: 2024' }}
+ ⚡ {{ data.badge_text || 'FREE ACCESS' }}
+
+
+
+
+
{{ f.title }}
+
{{ f.desc }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/vite.config.js b/web/vite.config.js
new file mode 100644
index 0000000..3315208
--- /dev/null
+++ b/web/vite.config.js
@@ -0,0 +1,16 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+export default defineConfig({
+ plugins: [vue()],
+ server: {
+ port: 3001,
+ host: true,
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8080',
+ changeOrigin: true
+ }
+ }
+ }
+})