diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5eaab30..c72f8e2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,40 +1,40 @@
-
-## 🌈 2.1.0 `2025-03-24`
-### 🚀 Features
-- `feat`: 补充剩余辅助页面 @Wesley-0808 ([#25](https://github.com/Wesley-0808/MTB-Official/pull/25))
-- `feat`: 新增`Toppic`前置图标 @Wesley-0808 ([#27](https://github.com/Wesley-0808/MTB-Official/pull/27))
-### 🐞 Bug Fixes
-- `fix`: 修复上传文件检测文件后缀名异常的问题 @Wesley-0808 ([#27](https://github.com/Wesley-0808/MTB-Official/pull/27))
-### 🚧 Others
-- `chore`: 打包后资源文件适配服务端上传文件地址 @Wesley-0808 ([#27](https://github.com/Wesley-0808/MTB-Official/pull/27))
-## 🌈 2.1.0-alpha.1 `2025-03-23`
-### 🚀 Features
-- `feat`: 补充剩余辅助页面 @Wesley-0808 ([#25](https://github.com/Wesley-0808/MTB-Official/pull/25))
-## 🌈 2.0.2 `2025-03-22`
-### 🚀 Features
-- `Server`: 支持`覆盖`设置`Header` @Wesley-0808 ([#23](https://github.com/Wesley-0808/MTB-Official/pull/23))
-- `测试路由`不支持跳转外部地址 @Wesley-0808 ([#22](https://github.com/Wesley-0808/MTB-Official/pull/22))
-### 🐞 Bug Fixes
-- 修复`Header`类型问题 @Wesley-0808 ([#22](https://github.com/Wesley-0808/MTB-Official/pull/22))
-## 🌈 2.0.1 `2025-03-19`
-### 🚀 Features
-- 新增`『取件码』`获取内容到后会延时跳转页面,优化体验 @Wesley-0808 ([#19](https://github.com/Wesley-0808/MTB-Official/pull/19))
-### 🐞 Bug Fixes
-- 修复底部页脚无法跳转本地路由的问题 @Wesley-0808 ([#19](https://github.com/Wesley-0808/MTB-Official/pull/19))
-- 修正部分页面的`router`引用 @Wesley-0808 ([#19](https://github.com/Wesley-0808/MTB-Official/pull/19))
-### 🚧 Others
-- 保留技术字段,用于区分父菜单以及适配管理页面 @Wesley-0808 ([#20](https://github.com/Wesley-0808/MTB-Official/pull/20))
-## 🌈 2.0.0 `2025-03-16`
-### 🚀 Features
-- `Component`: 适配深色模式、支持两种方式切换深色模式,可通过配置修改 @Wesley-0808 ([#7](https://github.com/Wesley-0808/MTB-Official/pull/7))
-- `Header`、`Footer`: 支持读取配置进行渲染 @Wesley-0808 ([#8](https://github.com/Wesley-0808/MTB-Official/pull/8))
-- `feat`: 共享网盘支持通过『取件码』跳转至分享文件界面 @Wesley-0808 ([#12](https://github.com/Wesley-0808/MTB-Official/pull/12))
-- `feat`: 新增测试配置页面,用于OA配置时使用 @Wesley-0808 ([#12](https://github.com/Wesley-0808/MTB-Official/pull/12))
-- `feat`: 对接api服务,配置同步 @Wesley-0808 ([#12](https://github.com/Wesley-0808/MTB-Official/pull/12))
-### 📈 Performance
-- `Header`: 重构顶部导航栏 @Wesley-0808 ([#7](https://github.com/Wesley-0808/MTB-Official/pull/7))
-- `Footer`: 重构底部页脚、优化样式 @Wesley-0808 ([#7](https://github.com/Wesley-0808/MTB-Official/pull/7))
-- `refactor`: 首页banner优化 @Wesley-0808 ([#12](https://github.com/Wesley-0808/MTB-Official/pull/12))
-- `refactor`: 重构共享网盘 @Wesley-0808 ([#12](https://github.com/Wesley-0808/MTB-Official/pull/12))
-### 🚧 Others
-- `Breaking`: `SSR(服务端渲染)`改`CSR(客户端渲染)` @Wesley-0808 ([#7](https://github.com/Wesley-0808/MTB-Official/pull/7))
+
+## 🌈 2.1.0 `2025-03-24`
+### 🚀 Features
+- `feat`: 补充剩余辅助页面 @Wesley-0808 ([#25](https://github.com/Wesley-Work/MTB-Official/pull/25))
+- `feat`: 新增`Toppic`前置图标 @Wesley-0808 ([#27](https://github.com/Wesley-Work/MTB-Official/pull/27))
+### 🐞 Bug Fixes
+- `fix`: 修复上传文件检测文件后缀名异常的问题 @Wesley-0808 ([#27](https://github.com/Wesley-Work/MTB-Official/pull/27))
+### 🚧 Others
+- `chore`: 打包后资源文件适配服务端上传文件地址 @Wesley-0808 ([#27](https://github.com/Wesley-Work/MTB-Official/pull/27))
+## 🌈 2.1.0-alpha.1 `2025-03-23`
+### 🚀 Features
+- `feat`: 补充剩余辅助页面 @Wesley-0808 ([#25](https://github.com/Wesley-Work/MTB-Official/pull/25))
+## 🌈 2.0.2 `2025-03-22`
+### 🚀 Features
+- `Server`: 支持`覆盖`设置`Header` @Wesley-0808 ([#23](https://github.com/Wesley-Work/MTB-Official/pull/23))
+- `测试路由`不支持跳转外部地址 @Wesley-0808 ([#22](https://github.com/Wesley-Work/MTB-Official/pull/22))
+### 🐞 Bug Fixes
+- 修复`Header`类型问题 @Wesley-0808 ([#22](https://github.com/Wesley-Work/MTB-Official/pull/22))
+## 🌈 2.0.1 `2025-03-19`
+### 🚀 Features
+- 新增`『取件码』`获取内容到后会延时跳转页面,优化体验 @Wesley-0808 ([#19](https://github.com/Wesley-Work/MTB-Official/pull/19))
+### 🐞 Bug Fixes
+- 修复底部页脚无法跳转本地路由的问题 @Wesley-0808 ([#19](https://github.com/Wesley-Work/MTB-Official/pull/19))
+- 修正部分页面的`router`引用 @Wesley-0808 ([#19](https://github.com/Wesley-Work/MTB-Official/pull/19))
+### 🚧 Others
+- 保留技术字段,用于区分父菜单以及适配管理页面 @Wesley-0808 ([#20](https://github.com/Wesley-Work/MTB-Official/pull/20))
+## 🌈 2.0.0 `2025-03-16`
+### 🚀 Features
+- `Component`: 适配深色模式、支持两种方式切换深色模式,可通过配置修改 @Wesley-0808 ([#7](https://github.com/Wesley-Work/MTB-Official/pull/7))
+- `Header`、`Footer`: 支持读取配置进行渲染 @Wesley-0808 ([#8](https://github.com/Wesley-Work/MTB-Official/pull/8))
+- `feat`: 共享网盘支持通过『取件码』跳转至分享文件界面 @Wesley-0808 ([#12](https://github.com/Wesley-Work/MTB-Official/pull/12))
+- `feat`: 新增测试配置页面,用于OA配置时使用 @Wesley-0808 ([#12](https://github.com/Wesley-Work/MTB-Official/pull/12))
+- `feat`: 对接api服务,配置同步 @Wesley-0808 ([#12](https://github.com/Wesley-Work/MTB-Official/pull/12))
+### 📈 Performance
+- `Header`: 重构顶部导航栏 @Wesley-0808 ([#7](https://github.com/Wesley-Work/MTB-Official/pull/7))
+- `Footer`: 重构底部页脚、优化样式 @Wesley-0808 ([#7](https://github.com/Wesley-Work/MTB-Official/pull/7))
+- `refactor`: 首页banner优化 @Wesley-0808 ([#12](https://github.com/Wesley-Work/MTB-Official/pull/12))
+- `refactor`: 重构共享网盘 @Wesley-0808 ([#12](https://github.com/Wesley-Work/MTB-Official/pull/12))
+### 🚧 Others
+- `Breaking`: `SSR(服务端渲染)`改`CSR(客户端渲染)` @Wesley-0808 ([#7](https://github.com/Wesley-Work/MTB-Official/pull/7))
diff --git a/README.md b/README.md
index deb292b..a39b985 100644
--- a/README.md
+++ b/README.md
@@ -3,16 +3,16 @@
-
-
-
+
+
+
# 🎉 About
- 媒体部官网代码仓库,使用Vue3+Vite6开发,后端为Python
-- 支持通过OA([MTB-OA](https://github.com/Wesley-0808/MTB-OA))修改页面内容
+- 支持通过OA([MTB-OA](https://github.com/Wesley-Work/MTB-OA))修改页面内容
- ShareNetdisk页面使用了『[TDesign](https://github.com/Tencent/Tdesign-vue-next)』组件库
- 顺德中专团委学生会媒体部版权所有
diff --git a/server/config/qnap.json b/service/config/qnap.json
similarity index 100%
rename from server/config/qnap.json
rename to service/config/qnap.json
diff --git a/service/officialServer.py b/service/officialServer.py
new file mode 100644
index 0000000..6499cf0
--- /dev/null
+++ b/service/officialServer.py
@@ -0,0 +1,1637 @@
+import hashlib
+import time
+import aiofiles
+import traceback
+from typing import List, Optional
+import pymysql
+import json
+import os
+import fastapi
+from fastapi.middleware.cors import CORSMiddleware
+import requests
+import httpx
+import urllib
+import uvicorn
+from pymysql.converters import escape_string
+import pymysql.cursors
+from pydantic import BaseModel, Field
+import xmltodict
+from functools import lru_cache
+import mimetypes
+
+
+# 官网业务API
+
+# 上传文件路径,需要适配前端路径
+UploadPath = "../Client/static/_upload"
+ReviewUploadPath = "./static/_upload"
+
+mimetypes.init()
+
+
+def load_mysql_config():
+ # 根据实际路径调整
+ config_path = os.path.join(os.path.dirname(__file__), "./database.config.json")
+ try:
+ with open(config_path, "r") as f:
+ cfg = json.load(f)
+ cfg["cursorclass"] = pymysql.cursors.DictCursor
+ return cfg
+ except FileNotFoundError:
+ print(f"配置文件 {config_path} 未找到!")
+ quit()
+ except json.JSONDecodeError as e:
+ print(f"配置文件解析失败: {str(e)}")
+ quit()
+
+
+mysqlConfig = load_mysql_config()
+
+
+def getValue(data, key: str):
+ """
+ 获取字典或列表中的值,如果不存在则返回None。
+ """
+ if isinstance(data, dict):
+ return data.get(key)
+ elif isinstance(data, list):
+ for item in data:
+ result = getValue(item, key)
+ if result is not None:
+ return result
+ return None
+
+
+class HeaderNode(BaseModel):
+ id: int
+ label: str
+ type: str = Field(alias="type") # 处理关键字
+ title: Optional[str] = None
+ target: Optional[str] = None
+ href: Optional[str] = None
+ isRouter: bool = False
+ onlyPC: bool = False
+ onlyMobile: bool = False
+ extraClass: Optional[str] = None
+ children: List["HeaderNode"] = []
+
+
+class FooterNode(BaseModel):
+ id: int
+ type: str = Field(alias="type") # 处理关键字
+ title: Optional[str] = None
+ label: str
+ target: Optional[str] = None
+ href: Optional[str] = None
+ isRouter: bool = False
+ onlyPC: bool = False
+ onlyMobile: bool = False
+ children: List["FooterNode"] = []
+
+
+HeaderNode.model_rebuild()
+
+FooterNode.model_rebuild()
+
+
+async def save_upload_file_chunks(
+ upload_file: fastapi.UploadFile, destination: str, chunk_size: int = 1024 * 1024
+):
+ """
+ 分块保存文件
+ """
+ try:
+ async with aiofiles.open(destination, "wb") as out_file:
+ while chunk := await upload_file.read(chunk_size):
+ await out_file.write(chunk)
+ return True
+ except Exception as e:
+ print(f"保存文件时发生错误: {str(e)}")
+ return e
+
+
+def CombineData(
+ errcode: any = 0,
+ errmsg: str = "",
+ data: any = {},
+ defaultData: str = "dict",
+) -> dict:
+ """
+ CombineData
+ :param errcode: 错误码
+ :param errmsg: 错误信息
+ :param data: 数据
+ """
+ if not data:
+ if defaultData == "dict":
+ data = {}
+ else:
+ data = []
+ return {"errcode": errcode, "errmsg": errmsg, "data": data}
+
+
+app = fastapi.FastAPI()
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins="*",
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+
+URLPREFIX = "/api"
+
+
+@app.get(f"{URLPREFIX}", description="API根")
+def index():
+ return {}
+
+
+@app.get(f"{URLPREFIX}/getToppic", description="获取置顶通知内容列表")
+def getToppic():
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ sql = "SELECT * FROM toppic"
+ cursor.execute(sql)
+ result = cursor.fetchone()
+ if not result:
+ result = []
+ return CombineData(0, "ok", result)
+ except:
+ return CombineData("gte1", traceback.format_exc())
+
+
+@app.get(f"{URLPREFIX}/getHeaderList", description="获取顶菜单内容配置")
+def getHeaderList():
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ cursor.execute("SELECT * FROM header")
+ result = cursor.fetchall()
+ if not result:
+ result = []
+ all_nodes = [dict(row) for row in result]
+
+ # 构建快速查询字典(处理无效节点)
+ node_dict = {}
+ valid_ids = set()
+ for node in all_nodes:
+ node_id = node["id"]
+ if node_id in node_dict:
+ print(f"数据重复ID: {node_id}")
+ continue
+ node_dict[node_id] = node
+ valid_ids.add(node_id)
+
+ # 递归构建树形结构(带循环检测)
+ def build_tree(node, parent_ids=None):
+ if parent_ids is None:
+ parent_ids = set()
+
+ current_id = node["id"]
+ if current_id in parent_ids:
+ print(f"检测到循环引用: {current_id}")
+ return None
+
+ parent_ids.add(current_id)
+
+ # 创建节点副本避免修改原数据
+ node = node.copy()
+
+ # 查找所有直接子节点
+ children = [
+ node_dict[child_id]
+ for child_id in valid_ids
+ if node_dict[child_id]["bindParent"] == current_id
+ ]
+
+ # 递归处理子节点
+ for child in children:
+ processed_child = build_tree(child, parent_ids.copy())
+ if processed_child:
+ if not "children" in node:
+ node["children"] = []
+ node["children"].append(processed_child)
+
+ # 清理技术字段(处理完成后统一删除)
+ del node["deep"]
+ return node
+
+ # 构建完整树形结构
+ root_nodes = [
+ node
+ for node in node_dict.values()
+ if node["bindParent"] == 0 or node["bindParent"] not in valid_ids
+ ]
+
+ final_result = []
+ for root in root_nodes:
+ tree = build_tree(root)
+ if tree:
+ final_result.append(tree)
+
+ return CombineData(0, "ok", final_result, "list")
+ except:
+ return CombineData("ghe1", traceback.format_exc())
+
+
+@app.get(f"{URLPREFIX}/getFooterList", description="获取底部页脚列表")
+def getFooterList():
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ cursor.execute("SELECT * FROM footer")
+ result = cursor.fetchall()
+ if not result:
+ result = []
+ all_nodes = [dict(row) for row in result]
+
+ # 构建快速查询字典(处理无效节点)
+ node_dict = {}
+ valid_ids = set()
+ for node in all_nodes:
+ node_id = node["id"]
+ if node_id in node_dict:
+ print(f"数据重复ID: {node_id}")
+ continue
+ node_dict[node_id] = node
+ valid_ids.add(node_id)
+
+ # 递归构建树形结构(带循环检测)
+ def build_tree(node, parent_ids=None):
+ if parent_ids is None:
+ parent_ids = set()
+
+ current_id = node["id"]
+ if current_id in parent_ids:
+ print(f"检测到循环引用: {current_id}")
+ return None
+
+ parent_ids.add(current_id)
+
+ # 创建节点副本避免修改原数据
+ node = node.copy()
+
+ # 查找所有直接子节点
+ children = [
+ node_dict[child_id]
+ for child_id in valid_ids
+ if node_dict[child_id]["bindParent"] == current_id
+ ]
+
+ # 递归处理子节点
+ for child in children:
+ processed_child = build_tree(child, parent_ids.copy())
+ if processed_child:
+ if not "children" in node:
+ node["children"] = []
+ node["children"].append(processed_child)
+
+ # 清理技术字段(处理完成后统一删除)
+ del node["deep"], node["bindParent"]
+ return node
+
+ # 构建完整树形结构
+ root_nodes = [
+ node
+ for node in node_dict.values()
+ if node["bindParent"] == 0 or node["bindParent"] not in valid_ids
+ ]
+
+ final_result = {"list": [], "links": []}
+ for root in root_nodes:
+ tree = build_tree(root)
+ if not tree["type"] in final_result:
+ final_result[tree["type"]] = []
+ if tree:
+ final_result[tree["type"]].append(tree)
+
+ return CombineData(0, "ok", final_result, "list")
+ except:
+ return CombineData("gfe1", traceback.format_exc())
+
+
+# 管理功能API
+@app.post(f"{URLPREFIX}/setToppic/del", description="设置置顶通知内容-删除")
+def delToppic(
+ id: int = fastapi.Form(),
+):
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ sql = "DELETE FROM toppic WHERE id = %s"
+ cursor.execute(sql, (id))
+ conn.commit()
+ if cursor.rowcount == 0:
+ return CombineData("dte2", f"删除失败: 找不到ID为{id}的数据")
+ return CombineData(0, "删除成功")
+ except:
+ return CombineData("dte1", f"删除失败: {traceback.format_exc()}")
+
+
+@app.post(f"{URLPREFIX}/setToppic/add", description="设置置顶通知内容-添加")
+def addToppic(
+ data: str = fastapi.Form(),
+ type: str = fastapi.Form(),
+):
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ select_sql = "SELECT * FROM toppic"
+ cursor.execute(select_sql)
+ selectResult = cursor.fetchall()
+ if selectResult:
+ return CombineData(
+ "ate2",
+ f"添加失败: 数据表有其他数据,根据规则,只允许一条置顶消息通知。",
+ )
+ # 可以添加
+ sql = "INSERT INTO toppic (data, type) VALUES (%s, %s)"
+ cursor.execute(sql, (data, type))
+ conn.commit()
+ return CombineData(0, "ok", {"id": cursor.lastrowid})
+ except:
+ return CombineData(
+ "ate1",
+ f"添加失败: {traceback.format_exc()}",
+ )
+
+
+@app.post(f"{URLPREFIX}/setToppic/edit", description="设置置顶通知内容-修改")
+def editToppic(
+ id: int = fastapi.Form(),
+ data: str = fastapi.Form(),
+ type: str = fastapi.Form(),
+):
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ sql = "UPDATE toppic SET data = %s, type = %s WHERE id = %s"
+ cursor.execute(sql, (data, type, id))
+ conn.commit()
+ if cursor.rowcount == 0:
+ return CombineData("ste2", f"更新失败: 找不到ID为{id}的数据")
+ return CombineData(0, "ok", {"id": cursor.lastrowid})
+ except:
+ return CombineData(
+ "ste1",
+ f"添加失败: {traceback.format_exc()}",
+ )
+
+
+@app.post(f"{URLPREFIX}/setHeader/del", description="设置顶菜单-删除")
+def delHeader(
+ id: List[int] = fastapi.Form(),
+):
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ ids = id if isinstance(id, list) else [id]
+ if not ids:
+ return CombineData("dhe3", "无效的ID参数")
+ placeholder = ", ".join(["%s"] * len(ids))
+ sql = f"DELETE FROM header WHERE id IN ({placeholder})"
+ cursor.execute(sql, tuple(ids))
+ conn.commit()
+ affected_rows = cursor.rowcount
+ if affected_rows == 0:
+ return CombineData("dhe2", f"删除失败:未找到ID {ids} 的数据")
+ return CombineData(
+ 0, "ok", {"deltedCount": affected_rows, "deltedID": ids}
+ )
+ except:
+ return CombineData("dhe1", f"删除失败: {traceback.format_exc()}")
+
+
+@app.post(f"{URLPREFIX}/setHeader/add", description="设置顶菜单-新增")
+def addHeader(data: str = fastapi.Form()):
+ # 解析数据
+ try:
+ data = json.loads(data)
+ except:
+ return CombineData("ahe3", "传入的数据无效")
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ conn.begin()
+
+ # 检查是否存在重复ID
+ existing_ids = set()
+ cursor.execute("SELECT id FROM header")
+ existing_ids.update(row["id"] for row in cursor.fetchall())
+
+ def insert_nodes(
+ node: List[HeaderNode] | HeaderNode,
+ parent_id: int = 0,
+ deep: int = 0,
+ ):
+ if isinstance(node, list):
+ for i in node:
+ insert_nodes(i, parent_id, deep)
+ else:
+ if not "id" in node:
+ raise ValueError("传入的数据无效,id不存在于数据中")
+ # 检查ID是否冲突
+ if node["id"] in existing_ids:
+ raise ValueError(f"ID {node['id']} 已存在")
+ sql = """
+ INSERT INTO header
+ (`id`, `title`, `label`, `href`, `target`, `isRouter`, `onlyPC`, `onlyMobile`, `type`, `extraClass`, `bindParent`, `deep`)
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
+ """
+ values = (
+ node["id"],
+ node.get("title"),
+ node.get("label"),
+ node.get("href"),
+ node.get("target"),
+ node.get("isRouter", False),
+ node.get("onlyPC", False),
+ node.get("onlyMobile", False),
+ node.get("type"),
+ node.get("extraClass"),
+ parent_id or 0,
+ deep or 0,
+ )
+ cursor.execute(sql, values)
+ existing_ids.add(node["id"])
+ # 递归
+ if "children" in node:
+ insert_nodes(node["children"], node["id"], deep + 1)
+
+ insert_nodes(data)
+ conn.commit()
+ return CombineData(
+ 0,
+ "ok",
+ {
+ "all": len(existing_ids),
+ "rootNode": len(data),
+ "rootBranch": len(existing_ids) - len(data),
+ },
+ )
+ except ValueError as e:
+
+ return CombineData("ahe2", str(e))
+ except:
+ return CombineData("ahe1", f"添加失败: {traceback.format_exc()}")
+
+
+@app.post(f"{URLPREFIX}/setHeader/coverAdd", description="设置顶菜单-强覆盖的新增")
+def addHeader(data: str = fastapi.Form()):
+ # 解析数据
+ try:
+ data = json.loads(data)
+ except:
+ return CombineData("ache3", "传入的数据无效")
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ conn.begin()
+
+ cursor.execute("DELETE FROM header")
+
+ # 检查是否存在重复ID
+ existing_ids = set()
+ cursor.execute("SELECT id FROM header")
+ existing_ids.update(row["id"] for row in cursor.fetchall())
+
+ def insert_nodes(
+ node: List[HeaderNode] | HeaderNode,
+ parent_id: int = 0,
+ deep: int = 0,
+ ):
+ if isinstance(node, list):
+ for i in node:
+ insert_nodes(i, parent_id, deep)
+ else:
+ if not "id" in node:
+ raise ValueError("传入的数据无效,id不存在于数据中")
+ # 检查ID是否冲突
+ if node["id"] in existing_ids:
+ raise ValueError(f"ID {node['id']} 已存在")
+ sql = """
+ INSERT INTO header
+ (`id`, `title`, `label`, `href`, `target`, `isRouter`, `onlyPC`, `onlyMobile`, `type`, `extraClass`, `bindParent`, `deep`)
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
+ """
+ values = (
+ node["id"],
+ node.get("title"),
+ node.get("label"),
+ node.get("href"),
+ node.get("target"),
+ node.get("isRouter", False),
+ node.get("onlyPC", False),
+ node.get("onlyMobile", False),
+ node.get("type"),
+ node.get("extraClass"),
+ parent_id or 0,
+ deep or 0,
+ )
+ cursor.execute(sql, values)
+ existing_ids.add(node["id"])
+ # 递归
+ if "children" in node:
+ insert_nodes(node["children"], node["id"], deep + 1)
+
+ insert_nodes(data)
+ conn.commit()
+ return CombineData(
+ 0,
+ "ok",
+ {
+ "all": len(existing_ids),
+ "rootNode": len(data),
+ "rootBranch": len(existing_ids) - len(data),
+ },
+ )
+ except ValueError as e:
+
+ return CombineData("ache2", str(e))
+ except:
+ return CombineData("ache1", f"添加失败: {traceback.format_exc()}")
+
+
+@app.post(f"{URLPREFIX}/setHeader/edit", description="设置顶菜单-修改")
+def editHeader(data: str = fastapi.Form()):
+ try:
+ data = json.loads(data)
+ except json.JSONDecodeError:
+ return CombineData("ehe2", "JSON格式错误")
+
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ conn.begin()
+
+ cursor.execute("DELETE FROM header")
+
+ existing_ids = set()
+
+ def insert_nodes(nodes, parent_id=0, deep=0):
+ for node in nodes:
+ sql = """
+ INSERT INTO header
+ (id, title, label, href, target, isRouter,
+ onlyPC, onlyMobile, type, extraClass, bindParent, deep)
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
+ """
+ values = (
+ node["id"],
+ node.get("title"),
+ node.get("label", ""),
+ node.get("href", ""),
+ node.get("target", "_self"),
+ node.get("isRouter", False),
+ node.get("onlyPC", False),
+ node.get("onlyMobile", False),
+ node.get("type", "parent"),
+ node.get("extraClass", ""),
+ parent_id,
+ deep,
+ )
+ # 执行插入
+ cursor.execute(sql, values)
+ existing_ids.add(node["id"])
+ # 递归
+ if "children" in node:
+ insert_nodes(node["children"], node["id"], deep + 1)
+
+ # 执行插入
+ if isinstance(data, list):
+ insert_nodes(data)
+ conn.commit()
+ return CombineData(
+ 0,
+ "ok",
+ {
+ "all": len(existing_ids),
+ "root": len(data),
+ "root-branch": len(existing_ids) - len(data),
+ },
+ )
+ else:
+
+ return CombineData("ehe3", "数据格式应为列表")
+ except:
+
+ return CombineData("ehe1", f"更新失败: {traceback.format_exc()}")
+
+
+# ManageFooter
+@app.post(f"{URLPREFIX}/setFooter/del", description="设置底部页脚-删除")
+def delFooter(
+ id: List[int] = fastapi.Form(),
+):
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ ids = id if isinstance(id, list) else [id]
+ if not ids:
+ return CombineData("dfe3", "无效的ID参数")
+ placeholder = ", ".join(["%s"] * len(ids))
+ sql = f"DELETE FROM footer WHERE id IN ({placeholder})"
+ cursor.execute(sql, tuple(ids))
+ conn.commit()
+ affected_rows = cursor.rowcount
+ if affected_rows == 0:
+ return CombineData("dfe2", f"删除失败:未找到ID {ids} 的数据")
+ return CombineData(
+ 0, "ok", {"deltedCount": affected_rows, "deltedID": ids}
+ )
+ except:
+ return CombineData("dfe1", f"删除失败: {traceback.format_exc()}")
+
+
+@app.post(f"{URLPREFIX}/setFooter/add", description="设置底部页脚-新增")
+def addFooter(data: str = fastapi.Form()):
+ # 解析数据
+ try:
+ data = json.loads(data)
+ except:
+ return CombineData("afe3", "传入的数据无效")
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ conn.begin()
+
+ # 检查是否存在重复ID
+ existing_ids = set()
+ cursor.execute("SELECT id FROM footer")
+ existing_ids.update(row["id"] for row in cursor.fetchall())
+
+ def insert_nodes(
+ node: List[FooterNode] | FooterNode,
+ types: str = None,
+ parent_id: int = 0,
+ deep: int = 0,
+ ):
+ if isinstance(node, list):
+ for i in node:
+ insert_nodes(i, types, parent_id, deep)
+ else:
+ keys = node.keys()
+ for i in keys:
+ # 根节点,拆分links和list
+ if i in data.keys():
+ insert_nodes(data[i], i, parent_id, deep)
+ continue
+ # 非根节点
+ if types:
+ if not "id" in node:
+ raise ValueError("传入的数据无效,id不存在于数据中")
+ # 检查ID是否冲突
+ if node["id"] in existing_ids:
+ raise ValueError(f"ID {node['id']} 已存在")
+ sql = """
+ INSERT INTO footer
+ (`id`, `type`, `title`, `label`, `target`, `href`, `isRouter`, `onlyPC`, `onlyMobile`, `bindParent`, `deep`)
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
+ """
+ values = (
+ node["id"],
+ types,
+ node.get("title"),
+ node.get("label"),
+ node.get("target"),
+ node.get("href"),
+ node.get("isRouter", False),
+ node.get("onlyPC", False),
+ node.get("onlyMobile", False),
+ parent_id or 0,
+ deep or 0,
+ )
+ cursor.execute(sql, values)
+ existing_ids.add(node["id"])
+ # 递归
+ if "children" in node:
+ insert_nodes(
+ node["children"], types, node["id"], deep + 1
+ )
+
+ insert_nodes(data)
+ conn.commit()
+ return CombineData(
+ 0,
+ "ok",
+ {
+ "all": len(existing_ids),
+ "root": len(data),
+ "root-branch": len(existing_ids) - len(data),
+ },
+ )
+ except ValueError as e:
+ return CombineData("afe2", str(e))
+ except:
+ return CombineData("afe1", f"添加失败: {traceback.format_exc()}")
+
+
+@app.post(f"{URLPREFIX}/setFooter/edit", description="设置底部页脚-修改")
+def editFooter(data: str = fastapi.Form()):
+ try:
+ data = json.loads(data)
+ except json.JSONDecodeError:
+ return CombineData("ehe2", "JSON格式错误")
+
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ conn.begin()
+
+ cursor.execute("DELETE FROM footer")
+
+ existing_ids = set()
+
+ # 添加部分同添加处代码,这里直接复制了
+ def insert_nodes(
+ node: List[FooterNode] | FooterNode,
+ types: str = None,
+ parent_id: int = 0,
+ deep: int = 0,
+ ):
+ if isinstance(node, list):
+ for i in node:
+ insert_nodes(i, types, parent_id, deep)
+ else:
+ keys = node.keys()
+ for i in keys:
+ if i in data.keys():
+ insert_nodes(data[i], i, parent_id, deep)
+ continue
+ if types:
+ if node["id"] in existing_ids:
+ raise ValueError(f"ID {node['id']} 已存在")
+ sql = """
+ INSERT INTO footer
+ (`id`, `type`, `title`, `label`, `target`, `href`, `isRouter`, `onlyPC`, `onlyMobile`, `bindParent`, `deep`)
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
+ """
+ values = (
+ node["id"],
+ types,
+ node.get("title"),
+ node.get("label"),
+ node.get("target"),
+ node.get("href"),
+ node.get("isRouter", False),
+ node.get("onlyPC", False),
+ node.get("onlyMobile", False),
+ parent_id or 0,
+ deep or 0,
+ )
+ cursor.execute(sql, values)
+ existing_ids.add(node["id"])
+ # 递归
+ if "children" in node:
+ insert_nodes(
+ node["children"], types, node["id"], deep + 1
+ )
+
+ insert_nodes(data)
+ conn.commit()
+ return CombineData(
+ 0,
+ "ok",
+ {
+ "all": len(existing_ids),
+ "root": len(data),
+ "root-branch": len(existing_ids) - len(data),
+ },
+ )
+ except:
+ return CombineData("efe1", f"更新失败: {traceback.format_exc()}")
+
+
+# 滑动背景管理
+@app.get(f"{URLPREFIX}/getBanner", description="获取滑动展示列表配置内容")
+def getBanner():
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ sql = "SELECT * FROM banner ORDER BY `orders`"
+ cursor.execute(sql)
+ result = cursor.fetchall()
+ if not result:
+ result = []
+ return CombineData(0, "ok", result, "list")
+ except:
+ return CombineData("gte1", traceback.format_exc())
+
+
+@app.post(f"{URLPREFIX}/setBanner", description="设置滑动展示列表内容-数据库操作")
+def setBanner(
+ id: int = fastapi.Form(),
+ mode: str = fastapi.Form(), # add or delete or edit
+ url: str = fastapi.Form(),
+ title: str = fastapi.Form(),
+ desc: str = fastapi.Form(), # subTitle
+ type: str = fastapi.Form(), # video or image
+ orders: int = fastapi.Form(),
+):
+ if mode == "add":
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ sql = "INSERT INTO banner (title, `desc`, url, type, orders) VALUES (%s, %s, %s, %s, %s)"
+ cursor.execute(sql, (title, desc, url, type, orders))
+ conn.commit()
+ return CombineData(0, "ok", {"id": cursor.lastrowid})
+ except:
+ return CombineData(
+ "abe1",
+ f"添加失败: {traceback.format_exc()}",
+ )
+ elif mode == "delete":
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ sql = "DELETE FROM banner WHERE id = %s"
+ cursor.execute(sql, (id))
+ conn.commit()
+ return CombineData(0, "ok", {"id": id})
+ except:
+ return CombineData(
+ "dee1",
+ f"删除失败: {traceback.format_exc()}",
+ )
+ elif mode == "edit":
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ sql = "UPDATE banner SET title = %s, `desc` = %s, url = %s, type = %s WHERE id = %s"
+ cursor.execute(sql, (title, desc, url, type, id))
+ conn.commit()
+ return CombineData(0, "ok", {"id": id})
+ except:
+ return CombineData(
+ "ede1",
+ f"更新失败: {traceback.format_exc()}",
+ )
+ return CombineData(-1, "null return")
+
+
+@app.post(
+ f"{URLPREFIX}/setBanner/coverAdd", description="设置滑动展示列表内容-覆盖更新"
+)
+def setBannerCoverAdd(
+ data: str = fastapi.Form(),
+):
+
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ try:
+ conn.begin()
+ cursor.execute("DELETE FROM banner")
+ jsons = json.loads(data)
+ appendIdGroup = []
+ for i in jsons:
+ title = i["title"]
+ desc = i["desc"]
+ url = i["url"]
+ type = i["type"]
+ orders = i["orders"]
+ sql = "INSERT INTO banner (title, `desc`, url, type, orders) VALUES (%s, %s, %s, %s, %s)"
+ cursor.execute(sql, (title, desc, url, type, orders))
+ appendIdGroup.append(cursor.lastrowid)
+ conn.commit()
+ return CombineData(0, "ok", {"id": appendIdGroup})
+ except:
+ conn.rollback()
+ return CombineData(
+ "abe1",
+ f"添加失败: {traceback.format_exc()}",
+ )
+ except:
+ return CombineData(
+ "abe1",
+ f"添加失败: {traceback.format_exc()}",
+ )
+ return CombineData(-1, "null return")
+
+
+@app.get(f"{URLPREFIX}/getBanner/fileList", description="获取上传的文件列表")
+def getBannerFileList():
+ try:
+ if not os.path.exists(UploadPath):
+ os.makedirs(UploadPath, exist_ok=True)
+
+ file_list = []
+ # 遍历目录并过滤隐藏文件
+ for filename in os.listdir(UploadPath):
+ file_path = os.path.join(UploadPath, filename)
+
+ # 跳过目录和隐藏文件
+ if os.path.isdir(file_path) or filename.startswith("."):
+ continue
+
+ file_stat = os.stat(file_path)
+ # 从数据库获取关联信息
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ cursor.execute(
+ "SELECT title, `desc`, type FROM banner WHERE url LIKE %s",
+ (f"%{filename}",),
+ )
+ db_info = cursor.fetchone()
+ mime_type = mimetypes.guess_type(filename)
+ if mime_type:
+ tp = mime_type[0].split("/")[0]
+ file_type = tp
+ else:
+ file_type = "unknown"
+ # 构建文件信息
+ file_info = {
+ "filename": filename,
+ "url": f"{ReviewUploadPath.lstrip('.')}/{filename}",
+ "size": file_stat.st_size,
+ "mtime": file_stat.st_mtime,
+ "title": db_info["title"] if db_info else "",
+ "description": db_info["desc"] if db_info else "",
+ "file_type": db_info["type"] if db_info else file_type,
+ "is_in_banner": bool(db_info), # 标记是否已加入轮播
+ }
+ file_list.append(file_info)
+
+ # 按修改时间倒序排序
+ file_list.sort(key=lambda x: x["mtime"], reverse=True)
+
+ return CombineData(0, "ok", file_list, "list")
+
+ except PermissionError:
+ return CombineData("gfl3", "目录访问权限不足", data=[])
+ except Exception as e:
+ return CombineData("gfl4", f"服务器内部错误: {str(e)}", data=[])
+
+
+@app.post(f"{URLPREFIX}/setBanner/upload", description="设置滑动展示列表内容-上传文件")
+async def uploadBanner(
+ file: fastapi.UploadFile = fastapi.File(...),
+ title: str = fastapi.Form(),
+ desc: str = fastapi.Form(), # subTitle
+):
+ try:
+ # 分块保存大小
+ chunk_size: Optional[int] = 1024 * 1024
+ # 黑名单扩展名
+ BLACKLIST_EXTENSIONS = [
+ ".exe",
+ ".bat",
+ ".cmd",
+ ".scr",
+ ".com",
+ ".pif",
+ ".php",
+ ".py",
+ ".rb",
+ ".pl",
+ ".sh",
+ ".asp",
+ ".aspx",
+ ".jsp",
+ ".jspx",
+ ".html",
+ ".htm",
+ ".xhtml",
+ ".js",
+ ".mht",
+ ".mhtml",
+ ".zip",
+ ".rar",
+ ".7z",
+ ".tar.gz",
+ ".tgz",
+ ".bz2",
+ ".doc",
+ ".docm",
+ ".xls",
+ ".xlsm",
+ ".ppt",
+ ".pptm",
+ ".dotm",
+ ".xltx",
+ ".xltm",
+ ".potm",
+ ".ppam",
+ ".ppsm",
+ ]
+ # 黑名单 Content-Type
+ BLACKLIST_CONTENT_TYPES = [
+ "application/x-msdownload",
+ "application/x-dosexec",
+ "application/x-httpd-php",
+ "application/x-python-code",
+ "application/x-ruby",
+ "text/plain",
+ "text/html",
+ "application/javascript",
+ "text/javascript",
+ "application/zip",
+ "application/x-rar-compressed",
+ "application/x-7z-compressed",
+ "application/msword",
+ "application/vnd.ms-excel",
+ "application/vnd.ms-powerpoint",
+ ]
+ # 文件类型
+ fileContent_type = file.content_type.split("/")[0]
+ fileType = fileContent_type
+ # file.size 是 字节 为单位的
+ # 判断黑名单
+ file_ext = os.path.splitext(file.filename)[1].lower()
+ if file_ext in BLACKLIST_EXTENSIONS or fileType in BLACKLIST_CONTENT_TYPES:
+ return CombineData("efe1", "文件类型在黑名单中,请确认文件类型是否正确!")
+ # 唯一文件名
+ fileNames = file.filename.split(".")
+ # 在第一个点前面加时间戳
+ fileName = f"{fileNames[0]}_{int(time.time()*1000)}"
+ fileNames.pop(0)
+ fileName = f"{fileName}.{'.'.join(fileNames)}"
+ fileContent = await file.read()
+ # 判断上传文件路径是否存在,不存在就创建
+ if not os.path.exists(UploadPath):
+ os.makedirs(UploadPath, exist_ok=True)
+ filePath = f"{UploadPath}/{fileName}"
+ # 储存文件
+ saveSuccess = await save_upload_file_chunks(file, filePath, chunk_size)
+ if not saveSuccess is True:
+ return CombineData("sbe_u2", saveSuccess)
+ fileUrl = f"{ReviewUploadPath}/{fileName}"
+ try:
+ append = setBanner(None, "add", fileUrl, title, desc, fileType)
+ if append["errcode"] != 0:
+ return CombineData("sbe_u4", append["errmsg"])
+ except:
+ return CombineData("sbe_u3", traceback.format_exc())
+ return CombineData(
+ 0,
+ "ok",
+ {
+ "url": fileUrl,
+ "filename": file.filename,
+ "combineFilename": fileName,
+ "content_type": file.content_type,
+ "size": len(fileContent),
+ "md5": hashlib.md5(fileContent).hexdigest(),
+ },
+ )
+ except:
+ return CombineData("sbe_u1", traceback.format_exc())
+
+
+@app.post(
+ f"{URLPREFIX}/setBanner/justUpload", description="设置滑动展示列表内容-上传文件"
+)
+async def justUploadBanner(
+ file: fastapi.UploadFile = fastapi.File(...),
+):
+ try:
+ # 分块保存大小
+ chunk_size: Optional[int] = 1024 * 1024
+ # 黑名单扩展名
+ BLACKLIST_EXTENSIONS = [
+ ".exe",
+ ".bat",
+ ".cmd",
+ ".scr",
+ ".com",
+ ".pif",
+ ".php",
+ ".py",
+ ".rb",
+ ".pl",
+ ".sh",
+ ".asp",
+ ".aspx",
+ ".jsp",
+ ".jspx",
+ ".html",
+ ".htm",
+ ".xhtml",
+ ".js",
+ ".mht",
+ ".mhtml",
+ ".zip",
+ ".rar",
+ ".7z",
+ ".tar.gz",
+ ".tgz",
+ ".bz2",
+ ".doc",
+ ".docm",
+ ".xls",
+ ".xlsm",
+ ".ppt",
+ ".pptm",
+ ".dotm",
+ ".xltx",
+ ".xltm",
+ ".potm",
+ ".ppam",
+ ".ppsm",
+ ]
+ # 黑名单 Content-Type
+ BLACKLIST_CONTENT_TYPES = [
+ "application/x-msdownload",
+ "application/x-dosexec",
+ "application/x-httpd-php",
+ "application/x-python-code",
+ "application/x-ruby",
+ "text/plain",
+ "text/html",
+ "application/javascript",
+ "text/javascript",
+ "application/zip",
+ "application/x-rar-compressed",
+ "application/x-7z-compressed",
+ "application/msword",
+ "application/vnd.ms-excel",
+ "application/vnd.ms-powerpoint",
+ ]
+ # 文件类型
+ fileContent_type = file.content_type.split("/")[0]
+ fileType = fileContent_type
+ # file.size 是 字节 为单位的
+ # 判断黑名单
+ file_ext = os.path.splitext(file.filename)[1].lower()
+ if file_ext in BLACKLIST_EXTENSIONS or fileType in BLACKLIST_CONTENT_TYPES:
+ return CombineData("efe1", "文件类型在黑名单中,请确认文件类型是否正确!")
+ # 唯一文件名
+ fileNames = file.filename.split(".")
+ # 在第一个点前面加时间戳
+ fileName = f"{fileNames[0]}_{int(time.time()*1000)}"
+ fileNames.pop(0)
+ fileName = f"{fileName}.{'.'.join(fileNames)}"
+ fileContent = await file.read()
+ # 判断上传文件路径是否存在,不存在就创建
+ if not os.path.exists(UploadPath):
+ os.makedirs(UploadPath, exist_ok=True)
+ filePath = f"{UploadPath}/{fileName}"
+ # 储存文件
+ saveSuccess = await save_upload_file_chunks(file, filePath, chunk_size)
+ if not saveSuccess is True:
+ return CombineData("sbe_u2", saveSuccess)
+ fileUrl = f"{ReviewUploadPath}/{fileName}"
+ return CombineData(
+ 0,
+ "ok",
+ {
+ "url": fileUrl,
+ "filename": file.filename,
+ "combineFilename": fileName,
+ "content_type": file.content_type,
+ "size": len(fileContent),
+ "md5": hashlib.md5(fileContent).hexdigest(),
+ },
+ )
+ except:
+ return CombineData("sbe_u1", traceback.format_exc())
+
+
+####################################################################
+####################################################################
+####################################################################
+####################################################################
+####################################################################
+####################################################################
+####################################################################
+####################################################################
+####################################################################
+####################################################################
+####################################################################
+####################################################################
+####################################################################
+####################################################################
+####################################################################
+####################################################################
+
+# 共享网盘业务
+
+
+def load_qnap_config():
+ # 根据实际路径调整
+ config_path = os.path.join(os.path.dirname(__file__), "./config/qnap.json")
+ try:
+ with open(config_path, "r") as f:
+ cfg = json.load(f)
+ return cfg
+ except FileNotFoundError:
+ print(f"配置文件 {config_path} 未找到!")
+ quit()
+ except json.JSONDecodeError as e:
+ print(f"配置文件解析失败: {str(e)}")
+ quit()
+
+
+QNAP_Config = load_qnap_config()
+SidCache = None
+
+
+def XMLToDict(xml_data):
+ try:
+ dict_data = xmltodict.parse(xml_data)
+ return dict_data
+ except Exception as err:
+ print(err)
+ return None
+
+
+@lru_cache(maxsize=2)
+def getDBSid():
+ global SidCache
+ try:
+ if QNAP_Config["useDatabaseCache"]:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ sql = "SELECT * FROM sharenetdiskcache WHERE `key`='sid'"
+ cursor.execute(sql)
+ reault = cursor.fetchall()
+ if len(reault) > 1:
+ # 删除多余的记录
+ sql = "DELETE FROM sharenetdiskcache WHERE `key`='sid'"
+ cursor.execute(sql)
+ conn.commit()
+ return None
+ if reault:
+ return reault[0]["value"]
+ else:
+ return None
+ return SidCache
+ except Exception as err:
+ print(err)
+ return None
+
+
+@lru_cache(maxsize=2)
+def getDBQtoken():
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ sql = "SELECT * FROM sharenetdiskcache WHERE `key`='qtoken'"
+ cursor.execute(sql)
+ reault = cursor.fetchall()
+ if reault:
+ return reault[0]["value"]
+ else:
+ return None
+ except Exception as err:
+ print(err)
+ return None
+
+
+# 清理sid缓存
+def update_sid_cache():
+ getDBSid.cache_clear()
+
+
+# 清理qtoken缓存
+def update_qtoken_cache():
+ getDBQtoken.cache_clear()
+
+
+def qtokenGetSid(qtoken: str):
+ global SidCache
+ try:
+ api = f"http://{QNAP_Config['ip']}:{QNAP_Config['port']}/cgi-bin/authLogin.cgi?user={QNAP_Config['username']}&qtoken={qtoken}&remme=1&duration=-1"
+ print(api)
+ requestResult = requests.get(api)
+ JsonData = XMLToDict(requestResult.content)
+ # 若解析失败
+ if not JsonData:
+ print(f"Response{requestResult.content}")
+ return None
+ # 判断应有字段是否存在
+ QDocRoot = getValue(JsonData, "QDocRoot")
+ if not QDocRoot:
+ return None
+ AuthPassed = getValue(QDocRoot, "authPassed")
+ if not AuthPassed == 1:
+ return None
+ AuthSid = getValue(QDocRoot, "authSid")
+ if not AuthPassed or not AuthSid:
+ return None
+ # 插入数据库
+ if QNAP_Config["useDatabaseCache"]:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ SidSQL = "INSERT INTO sharenetdiskcache (`key`,`value`) VALUES ('sid',%s)"
+ cursor.execute(SidSQL, (AuthSid))
+ conn.commit()
+ SidCache = AuthSid
+ return AuthSid
+ except Exception as err:
+ print(err)
+ return CombineData("NAS-LoginFail:5", "NAS设备登录失败,无法通过qtoken获取sid")
+
+
+def refreshQtoken():
+ qtoken = getDBQtoken()
+ if not qtoken:
+ sid = QNAP_Login()
+ else:
+ sid = qtokenGetSid(qtoken)
+ if not sid:
+ sid = QNAP_Login()
+ if isinstance(sid, dict):
+ print(sid)
+ return sid
+ # 插入数据库
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ removeAll = "DELETE FROM sharenetdiskcache WHERE `key`='sid'"
+ cursor.execute(removeAll)
+ SidSQL = "INSERT INTO sharenetdiskcache (`key`,`value`) VALUES ('sid',%s)"
+ cursor.execute(SidSQL, (sid))
+ conn.commit()
+
+
+def checkSid(sid: str):
+ try:
+ if not sid:
+ return False
+ api = f"http://{QNAP_Config['ip']}:{QNAP_Config['port']}/cgi-bin/filemanager/utilRequest.cgi?func=check_sid&sid={sid}"
+ requestResult = requests.get(api)
+ JsonData = requestResult.json()
+ status = getValue(JsonData, "status")
+ if status == 1:
+ return True
+ return False
+ except:
+ return False
+
+
+def QNAP_Login():
+ global SidCache
+ try:
+ api = f"http://{QNAP_Config['ip']}:{QNAP_Config['port']}/cgi-bin/authLogin.cgi?user={QNAP_Config['username']}&pwd={QNAP_Config['password']}&service=1&device=ShareNetdiskServer&duraion=-1&remme=1"
+ requestResult = requests.get(api)
+ JsonData = XMLToDict(requestResult.content)
+ # 若解析失败
+ if not JsonData:
+ print(f"Response{requestResult.content}")
+ return CombineData("NAS-LoginFail:1", "NAS设备登录失败,因为返回值解析失败")
+ # 判断应有字段是否存在
+ QDocRoot = getValue(JsonData, "QDocRoot")
+ if not QDocRoot:
+ return CombineData(
+ "NAS-LoginFail:2", "NAS设备登录失败,因为返回值缺少QDocRoot字段"
+ )
+ AuthPassed = getValue(QDocRoot, "authPassed")
+ if not AuthPassed == "1":
+ return CombineData("NAS-LoginFail:3", "NAS设备登录失败,因为设备端校验失败")
+ Qtoken = getValue(QDocRoot, "qtoken")
+ AuthSid = getValue(QDocRoot, "authSid")
+ if not AuthPassed or not Qtoken or not AuthSid:
+ return CombineData(
+ "NAS-LoginFail:4", "NAS设备登录失败,因为返回值缺少必要字段"
+ )
+ # 插入数据库
+ if QNAP_Config["useDatabaseCache"]:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ sql = "SELECT * FROM sharenetdiskcache WHERE `key`='qtoken'"
+ cursor.execute(sql)
+ reault = cursor.fetchall()
+ if len(reault) > 1:
+ sql = "DELETE FROM sharenetdiskcache WHERE `key`='qtoken'"
+ cursor.execute(sql)
+ QtokenSQL = "INSERT INTO sharenetdiskcache (`key`,`value`) VALUES ('qtoken',%s)"
+ cursor.execute(QtokenSQL, (Qtoken))
+ SidSQL = "INSERT INTO sharenetdiskcache (`key`,`value`) VALUES ('sid',%s)"
+ cursor.execute(SidSQL, (AuthSid))
+ conn.commit()
+ SidCache = AuthSid
+ return AuthSid
+ except Exception as err:
+ print(err)
+ return CombineData("NAS-LoginFail:5", "NAS设备登录失败,无法完成认证流程")
+
+
+# 获取sid,应该一步到位,先获取db,判断是否有效,无效则,产生通过qtoken获取,还是不行就重新登录
+async def getSid():
+ global SidCache
+ try:
+ # 优先使用缓存
+ sid = SidCache
+ if QNAP_Config["useDatabaseCache"]:
+ sid = getDBSid()
+ if sid and checkSid(sid):
+ return sid
+ # 是否通过qtoken刷新sid
+ if QNAP_Config["useDoubleSid"]:
+ if (qtoken := getDBQtoken()) and (newSid := qtokenGetSid(qtoken)):
+ if checkSid(newSid):
+ # 插入数据库
+ update_sid_cache()
+ return newSid
+ # 直接登录
+ loginSid = QNAP_Login()
+ if isinstance(loginSid, str):
+ update_sid_cache()
+ return loginSid
+ return CombineData("NAS-LoginFail:0", "NAS设备登录失败,无法获取sid")
+ except:
+ return CombineData("NAS-LoginFail:5", "NAS设备登录失败,无法获取会话id")
+
+
+@app.get(f"{URLPREFIX}/netdisk/getFileList")
+async def getFileList(
+ path: str = None,
+ sort: str = "filename",
+ dirs: str = "DESC",
+):
+ global SidCache
+ try:
+ sid = await getSid()
+ if isinstance(sid, dict):
+ return CombineData(sid["errcode"], sid["errmsg"])
+ # sort = "mt" # (filename/filesize/filetype/mt/privilege/owner/group)
+ # dirs = "DESC" # ASC / DESC
+ filePath = "/媒体部/@共享网盘"
+ if path:
+ filePath += path
+ params = {
+ "func": "get_list",
+ "sid": sid,
+ "sort": sort,
+ "dir": dirs,
+ "path": filePath,
+ "start": 0,
+ "limit": 9999,
+ "list_mode": "all",
+ }
+ api = f"http://{QNAP_Config['ip']}:{QNAP_Config['port']}/cgi-bin/filemanager/utilRequest.cgi?{urllib.parse.urlencode(params)}"
+ async with httpx.AsyncClient() as client:
+ response = await client.get(api)
+ JsonData = response.json()
+ # 忽略文件夹
+ ignore_folders = ["_upload"]
+ filtered = [
+ item
+ for item in JsonData.get("datas", [])
+ if isinstance(item, dict) and not item.get("filename") in ignore_folders
+ ]
+ filePath = filePath.replace("/媒体部/@共享网盘", "")
+ if filePath == "":
+ filePath = "/"
+ returnStatus = getValue(JsonData, "status")
+ if returnStatus == 5:
+ return CombineData(
+ "GetFileListFail:5",
+ "文件夹不存在",
+ {
+ "datas": [],
+ "filePath": filePath,
+ "currentPath": path,
+ },
+ )
+ elif returnStatus == 3:
+ return CombineData(
+ "GetFileListFail:3",
+ "登录凭证有误",
+ {
+ "datas": [],
+ "filePath": filePath,
+ "currentPath": path,
+ },
+ )
+ JsonData["datas"] = filtered
+ JsonData.update(
+ {
+ "filePath": filePath,
+ "currentPath": path,
+ "ignoreFolders": ignore_folders,
+ }
+ )
+ return CombineData(
+ 0,
+ "ok",
+ JsonData,
+ )
+ return CombineData(
+ "GetFileListFail:0",
+ f"获取文件列表失败,因为返回状态为{JsonData['status']}",
+ {},
+ )
+ except Exception as err:
+ print(err)
+ return CombineData(
+ "NAS-GetListFail:1", f"无法获取文件列表,{traceback.format_exc()}"
+ )
+
+
+@app.get(f"{URLPREFIX}/netdisk/getDownloadUrl")
+async def downloadFile():
+ sid = await getSid()
+ if isinstance(sid, dict):
+ return CombineData(sid["errcode"], sid["errmsg"])
+ # &source_total=4
+ # source_file=
+ path = f"/cgi-bin/filemanager/utilRequest.cgi?func=download&sid={sid}"
+ return CombineData(
+ 0,
+ "ok",
+ {
+ "protocol": "http",
+ "out_ip": QNAP_Config["out_ip"],
+ "out_port": QNAP_Config["out_port"],
+ "internal_ip": QNAP_Config["ip"],
+ "internal_port": QNAP_Config["port"],
+ "path": path,
+ "source_path": "/媒体部/@共享网盘",
+ "source_total": None,
+ "source_file": None,
+ },
+ )
+
+
+@app.get(f"{URLPREFIX}/netdisk/pick-up")
+def pickUp(
+ code: str = None,
+):
+ if not code:
+ return CombineData("GetPickUpCodeFail:2", "取件码为空")
+ # 查找取件码
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ sql = "SELECT * FROM netdisk WHERE `key`='pickUpCode' AND `value`= %s"
+ cursor.execute(sql, (code))
+ reault = cursor.fetchone()
+ return CombineData(0, "ok", reault)
+ except:
+ return CombineData(
+ "GetPickUpCodeFail:1", f"获取取件码失败,因为{traceback.format_exc()}"
+ )
+
+
+@app.get(f"{URLPREFIX}/netdisk/pick-up/getList")
+def getPickUpList():
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ sql = "SELECT * FROM netdisk WHERE `key`='pickUpCode'"
+ cursor.execute(sql)
+ reault = cursor.fetchall()
+ return CombineData(0, "ok", reault)
+ except:
+ return CombineData(
+ "GetPickUpCodeListFail:1",
+ f"获取取件码列表失败,因为{traceback.format_exc()}",
+ )
+
+
+@app.post(f"{URLPREFIX}/netdisk/pick-up/add")
+def addPickUp(
+ code: str = fastapi.Form(default=None),
+ extra: str = fastapi.Form(default=""),
+ type: str = fastapi.Form(default="redirect"),
+):
+ try:
+ if not code or not extra or not type:
+ return CombineData("AddPickUpCodeFail:1", "参数错误")
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ sql = "INSERT INTO netdisk (`key`,`value`,`extra`,`type`) VALUES ('pickUpCode',%s,%s,%s)"
+ cursor.execute(sql, (code, extra, type))
+ conn.commit()
+ id = cursor.lastrowid
+ return CombineData(
+ 0, "ok", {"id": id, "code": code, "extra": extra, "type": type}
+ )
+ except:
+ return CombineData(
+ "AddPickUpCodeFail:1", f"添加取件码失败,因为{traceback.format_exc()}"
+ )
+
+
+@app.post(f"{URLPREFIX}/netdisk/pick-up/delete")
+def deletePickUp(
+ id: int = fastapi.Form(default=None),
+):
+ try:
+ with pymysql.connect(**mysqlConfig) as conn:
+ with conn.cursor() as cursor:
+ sql = "DELETE FROM netdisk WHERE `id`=%s"
+ cursor.execute(sql, (id))
+ conn.commit()
+ return CombineData(0, "ok", {"id": id})
+ except:
+ return CombineData(
+ "DeletePickUpCodeFail:1", f"删除取件码失败,因为{traceback.format()}"
+ )
+
+
+# TODO
+# 上传默认路径是_upload,这是非
+# @app.get("/getUploadUrl")
+# def getUploadUrl():
+# http://ip:8080/cgi-bin/filemanager/utilRequest.cgi?func=upload&type=standard&sid=xxxx&dest_path=/Public&overwrite=1&progress=-Public-test.zip
+# api = f"http://{QNAP_Config['ip']}:{QNAP_Config['port']}/cgi-bin/filemanager/utilRequest.cgi?func=upload&type=standard&sid={sid}&dest_path=/媒体部/@共享网盘/_upload&overwrite=0&progress=$%7Bprogress%7D"
+
+if __name__ == "__main__":
+ uvicorn.run(app, host="0.0.0.0", port=16485)
diff --git a/server/requirements.txt b/service/requirements.txt
similarity index 100%
rename from server/requirements.txt
rename to service/requirements.txt
diff --git a/src/components/footer.tsx b/src/components/footer.tsx
index 37fbfa0..3de6323 100644
--- a/src/components/footer.tsx
+++ b/src/components/footer.tsx
@@ -94,12 +94,14 @@ export default defineComponent({
// PC端内容渲染
const renderDesktop = () => (
- {footerConfig.value?.links?.map((section, index) => (
-
- ))}
+ {footerConfig.value?.links
+ ?.sort((a, b) => (a?.orders ?? 0) - (b?.orders ?? 0))
+ .map((section, index) => (
+
+ ))}