diff --git a/UI/ui.jsx b/UI/ui.jsx
deleted file mode 100644
index f902b068..00000000
--- a/UI/ui.jsx
+++ /dev/null
@@ -1,228 +0,0 @@
-import React from 'react';
-import { motion } from 'framer-motion';
-import { Clock, Newspaper, Megaphone, Gavel, Shield, Eye, Send, Factory, Leaf, Ship, Scale, Heart, Bus, Calendar, Brain, Wifi, Droplets } from 'lucide-react';
-
-const PublicAccountPlatform = () => {
- // 公众号数据
- const accounts = [
- {
- name: '湖南日报',
- desc: '《湖南日报》是中共湖南省委机关报,是湖南省最具权威性、发行量最大的综合性大报。',
- createTime: '2024-03-22',
- updateTime: '2025-05-13',
- icon:
- },
- {
- name: '长沙晚报',
- desc: '在这里,了解长沙!在这里,爱上长沙!',
- createTime: '2024-03-22',
- updateTime: '2025-05-12',
- icon:
- },
- {
- name: '长沙市场监管',
- desc: '长沙市市场监督管理局官方公众号平台',
- createTime: '2024-03-26',
- updateTime: '2025-05-11',
- icon:
- },
- {
- name: '微开福',
- desc: '展示区域形象,发布政务信息,提供便民服务。',
- createTime: '2025-05-09',
- updateTime: '2025-05-12',
- icon:
- },
- {
- name: '清廉长沙',
- desc: '长沙市纪委市监委官微,廉政宣传教育,接受"四风"举报。',
- createTime: '2024-03-22',
- updateTime: '2025-05-11',
- icon:
- },
- {
- name: '长沙警事',
- desc: '长沙市公安局警务信息发布平台',
- createTime: '2024-03-22',
- updateTime: '2025-05-11',
- icon:
- },
- {
- name: '文明长沙',
- desc: '传播正能量',
- createTime: '2024-04-29',
- updateTime: '2025-05-11',
- icon:
- },
- {
- name: '长沙观察',
- desc: '长沙电视台新闻频道新媒体官方平台,关注热点新闻及综合资讯。',
- createTime: '2024-03-22',
- updateTime: '2025-05-11',
- icon:
- },
- {
- name: '长沙发布',
- desc: '传递政务信息,提供服务资讯,关注百姓关切——长沙发布随时为您守候。',
- createTime: '2024-04-29',
- updateTime: '2025-05-12',
- icon:
- },
- {
- name: '新华社',
- desc: '新华通讯社官方账号。硬新闻、暖故事、好朋友。',
- createTime: '2024-03-22',
- updateTime: '2025-05-11',
- icon:
- },
- {
- name: '长沙生态环境',
- desc: '宣传环保政策法规、发布长沙环保工作动态、普及环境科学知识、聚焦环保热点问题,倡导绿色、低碳的发展和生活方式!',
- createTime: '2024-04-29',
- updateTime: '2025-05-11',
- icon:
- },
- {
- name: '雨花市场监管',
- desc: '长沙市雨花区市场监督管理局官方公众号平台',
- createTime: '2024-03-26',
- updateTime: '2025-05-11',
- icon:
- },
- {
- name: '开福市场监管',
- desc: '宣传市场监管知识、传达各级文件会议精神、解读国家相关政策',
- createTime: '2024-03-26',
- updateTime: '2025-05-11',
- icon:
- },
- {
- name: '宁乡市场监督',
- desc: '宁乡市市场监督管理局信息发布平台',
- createTime: '2024-03-26',
- updateTime: '2025-05-11',
- icon:
- },
- {
- name: '长沙县市场监管',
- desc: '了解长沙县市场监督管理局工作动态,并提供相关业务咨询服务。',
- createTime: '2024-03-26',
- updateTime: '2025-05-11',
- icon:
- },
- {
- name: '长沙城管',
- desc: '"长沙城管"秉承"城管为民"服务理念,向公众提供城市管理动态、政策法规、投诉建议等服务,助力品质长沙建设,提高市民参与度与获得感。',
- createTime: '2024-04-29',
- updateTime: '2025-05-11',
- icon:
- },
- {
- name: '长沙慈善',
- desc: '弘扬慈善文化,传递慈善正能量!',
- createTime: '2025-03-14',
- updateTime: '2025-05-12',
- icon:
- },
- {
- name: '长沙交通运输',
- desc: '服务百姓出行,传播交通声音!',
- createTime: '2024-04-29',
- updateTime: '2025-05-12',
- icon:
- },
- {
- name: '长沙会展',
- desc: '聚焦会展行业,传播会展资讯,链接会展服务,发展会展经济。',
- createTime: '2024-04-29',
- updateTime: '2025-05-11',
- icon:
- },
- {
- name: '思想长沙',
- desc: '长沙市社会科学界联合会(长沙社会科学院)官方账号',
- createTime: '2024-04-29',
- updateTime: '2025-05-11',
- icon:
- },
- {
- name: '网信长沙',
- desc: '解读网信政策法规,展示长沙网信动态,交流网信工作经验,推进网络文明建设,走好网上群众路线。',
- createTime: '2024-04-29',
- updateTime: '2025-05-12',
- icon:
- },
- {
- name: '长沙水运',
- desc: '长沙市水运事务中心(长沙市船舶检验中心)官方账号。',
- createTime: '2024-04-29',
- updateTime: '2025-05-12',
- icon:
- },
- {
- name: '天心市场监管',
- desc: '长沙市天心区市场监督管理局官方公众号平台',
- createTime: '2024-10-31',
- updateTime: '2025-05-12',
- icon:
- }
- ];
-
- return (
-
- {/* 标题区 */}
-
-
- {/* 内容区 */}
-
-
- {accounts?.map((account, index) => (
-
-
-
-
- {account?.icon}
-
-
{account?.name}
-
-
{account?.desc}
-
-
-
- 创建 {account?.createTime}
-
-
-
- 更新 {account?.updateTime}
-
-
-
-
- ))}
-
-
-
- {/* 页脚区 */}
-
-
- );
-};
-
-export default PublicAccountPlatform;
\ No newline at end of file
diff --git a/apis/article.py b/apis/article.py
new file mode 100644
index 00000000..45866ca5
--- /dev/null
+++ b/apis/article.py
@@ -0,0 +1,41 @@
+from fastapi import APIRouter, Depends, HTTPException, status, Query
+from core.auth import get_current_user
+from core.db import DB
+from .ver import API_VERSION
+
+router = APIRouter(prefix=f"{API_VERSION}/articles", tags=["文章管理"])
+@router.get("", summary="获取文章列表")
+async def get_articles(
+ offset: int = Query(0, ge=0),
+ limit: int = Query(5, ge=1, le=100),
+ current_user: dict = Depends(get_current_user)
+):
+ session = DB.get_session()
+ try:
+ articles = session.execute(
+ "SELECT * FROM articles LIMIT :limit OFFSET :offset",
+ {"limit": limit, "offset": offset}
+ ).fetchall()
+ return {"code": 0, "data": articles}
+ finally:
+ session.close()
+
+@router.get("/{article_id}", summary="获取文章详情")
+async def get_article_detail(
+ article_id: int,
+ current_user: dict = Depends(get_current_user)
+):
+ session = DB.get_session()
+ try:
+ article = session.execute(
+ "SELECT * FROM articles WHERE id = :id",
+ {"id": article_id}
+ ).fetchone()
+ if not article:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="文章不存在"
+ )
+ return {"code": 0, "data": article}
+ finally:
+ session.close()
\ No newline at end of file
diff --git a/apis/auth.py b/apis/auth.py
new file mode 100644
index 00000000..ab08f2ff
--- /dev/null
+++ b/apis/auth.py
@@ -0,0 +1,37 @@
+from fastapi import APIRouter, Depends, HTTPException, status
+from fastapi.security import OAuth2PasswordRequestForm
+from datetime import timedelta
+from core.auth import (
+ authenticate_user,
+ create_access_token,
+ get_current_user,
+ ACCESS_TOKEN_EXPIRE_MINUTES
+)
+from .ver import API_VERSION
+router = APIRouter(prefix=f"{API_VERSION}/auth", tags=["认证"])
+
+@router.post("/login", summary="用户登录")
+async def login(form_data: OAuth2PasswordRequestForm = Depends()):
+ user = authenticate_user(form_data.username, form_data.password)
+ if not user:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="用户名或密码错误",
+ )
+ access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+ access_token = create_access_token(
+ data={"sub": user.username}, expires_delta=access_token_expires
+ )
+ return {"access_token": access_token, "token_type": "bearer"}
+
+@router.post("/logout", summary="用户注销")
+async def logout(current_user: dict = Depends(get_current_user)):
+ return {"code": 0, "message": "注销成功"}
+
+@router.post("/refresh", summary="刷新Token")
+async def refresh_token(current_user: dict = Depends(get_current_user)):
+ access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+ access_token = create_access_token(
+ data={"sub": current_user["username"]}, expires_delta=access_token_expires
+ )
+ return {"access_token": access_token, "token_type": "bearer"}
\ No newline at end of file
diff --git a/apis/user.py b/apis/user.py
new file mode 100644
index 00000000..4c46f407
--- /dev/null
+++ b/apis/user.py
@@ -0,0 +1,67 @@
+from fastapi import APIRouter, Depends, HTTPException, status
+from datetime import datetime
+from core.auth import get_current_user
+from core.db import DB
+from core.models import User as DBUser
+from core.auth import pwd_context
+from .ver import API_VERSION
+
+router = APIRouter(prefix=f"{API_VERSION}/user", tags=["用户管理"])
+
+@router.get("", summary="获取用户信息")
+async def get_user_info(current_user: dict = Depends(get_current_user)):
+ session = DB.get_session()
+ try:
+ user = session.query(DBUser).filter(
+ DBUser.username == current_user["username"]
+ ).first()
+ if not user:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="用户不存在"
+ )
+ return {
+ "code": 0,
+ "data": {
+ "username": user.username,
+ "role": user.role,
+ "is_active": user.is_active,
+ "created_at": user.created_at
+ }
+ }
+ finally:
+ session.close()
+
+@router.put("", summary="修改用户资料")
+async def update_user_info(
+ update_data: dict,
+ current_user: dict = Depends(get_current_user)
+):
+ session = DB.get_session()
+ try:
+ user = session.query(DBUser).filter(
+ DBUser.username == current_user["username"]
+ ).first()
+ if not user:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="用户不存在"
+ )
+
+ if "password" in update_data:
+ user.password_hash = pwd_context.hash(update_data["password"])
+ if "is_active" in update_data:
+ user.is_active = bool(update_data["is_active"])
+
+ user.updated_at = datetime.now()
+ session.commit()
+
+ return {"code": 0, "message": "更新成功"}
+ except Exception as e:
+ session.rollback()
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=str(e)
+ )
+ finally:
+ session.close()
\ No newline at end of file
diff --git a/apis/ver.py b/apis/ver.py
new file mode 100644
index 00000000..7c29182f
--- /dev/null
+++ b/apis/ver.py
@@ -0,0 +1 @@
+API_VERSION="/api/v1/wx"
\ No newline at end of file
diff --git a/apis/wechat.py b/apis/wechat.py
new file mode 100644
index 00000000..ad41bf11
--- /dev/null
+++ b/apis/wechat.py
@@ -0,0 +1,34 @@
+from fastapi import APIRouter, Depends, HTTPException, status, Query
+from core.auth import get_current_user
+from core.db import DB
+from .ver import API_VERSION
+router = APIRouter(prefix=f"{API_VERSION}/mps", tags=["公众号管理"])
+
+@router.get("", summary="获取公众号列表")
+async def get_mps(current_user: dict = Depends(get_current_user)):
+ session = DB.get_session()
+ try:
+ mps = DB.get_all_mps()
+ return {"code": 0, "data": mps}
+ finally:
+ session.close()
+
+@router.get("/{mp_id}", summary="获取公众号详情")
+async def get_mp_detail(
+ mp_id: str,
+ current_user: dict = Depends(get_current_user)
+):
+ session = DB.get_session()
+ try:
+ mp = session.execute(
+ "SELECT * FROM mps WHERE mp_id = :id",
+ {"id": mp_id}
+ ).fetchone()
+ if not mp:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="公众号不存在"
+ )
+ return {"code": 0, "data": mp}
+ finally:
+ session.close()
\ No newline at end of file
diff --git a/app.log b/app.log
index d3ef0024..8f6de565 100644
--- a/app.log
+++ b/app.log
@@ -542,3 +542,413 @@
2025-05-14 07:33:56 - __main__ - INFO - ʼִк
2025-05-14 09:15:44 - __main__ - INFO - ʼִк
2025-05-14 11:16:17 - __main__ - INFO - ʼִк
+2025-05-14 16:53:06 - __main__ - INFO - ʼִк
+2025-05-14 16:53:35 - __main__ - INFO - ʼִк
+2025-05-14 16:59:52 - __main__ - INFO - ʼִк
+2025-05-14 17:14:48 - __main__ - INFO - ձ ½,23
+2025-05-14 17:15:43 - __main__ - INFO - ɳ ½,23
+2025-05-14 17:16:06 - __main__ - INFO - ձ ½,0
+2025-05-14 17:16:55 - __main__ - INFO - ɳ ½,0
+2025-05-14 17:17:48 - __main__ - INFO - ɳг ½,1
+2025-05-14 17:18:35 - __main__ - INFO - ½,0
+2025-05-14 17:19:36 - __main__ - INFO - ɳ ½,0
+2025-05-14 17:20:10 - __main__ - INFO - ɳ ½,0
+2025-05-14 17:20:30 - __main__ - INFO - ɳ ½,0
+2025-05-14 17:20:56 - __main__ - INFO - ɳ۲ ½,0
+2025-05-14 17:21:19 - __main__ - INFO - ɳ ½,0
+2025-05-14 17:21:59 - __main__ - INFO - » ½,1
+2025-05-14 17:22:11 - __main__ - INFO - ɳ̬ ½,1
+2025-05-14 17:22:46 - __main__ - INFO - 껨г ½,0
+2025-05-14 17:23:46 - __main__ - INFO - г ½,1
+2025-05-14 17:24:11 - __main__ - INFO - гල ½,0
+2025-05-14 17:24:25 - __main__ - INFO - ɳг ½,0
+2025-05-14 17:24:46 - __main__ - INFO - ɳǹ ½,0
+2025-05-14 17:25:47 - __main__ - INFO - ɳ ½,0
+2025-05-14 17:26:28 - __main__ - INFO - ɳͨ ½,1
+2025-05-14 17:27:04 - __main__ - INFO - ɳչ ½,0
+2025-05-14 17:27:55 - __main__ - INFO - ˼볤ɳ ½,0
+2025-05-14 17:28:40 - __main__ - INFO - ųɳ ½,1
+2025-05-14 17:29:00 - __main__ - INFO - ɳˮ ½,1
+2025-05-14 17:29:24 - __main__ - INFO - г ½,0
+2025-05-14 17:29:44 - __main__ - INFO - йںŸ,7
+2025-05-14 21:12:59 - __main__ - INFO - ձ ½,4
+2025-05-14 21:13:23 - __main__ - INFO - ɳ ½,0
+2025-05-14 21:13:59 - __main__ - INFO - ɳг ½,0
+2025-05-14 21:14:30 - __main__ - INFO - ½,1
+2025-05-14 21:15:13 - __main__ - INFO - ɳ ½,0
+2025-05-14 21:15:26 - __main__ - INFO - ɳ ½,2
+2025-05-14 21:16:06 - __main__ - INFO - ɳ ½,1
+2025-05-14 21:16:52 - __main__ - INFO - ɳ۲ ½,3
+2025-05-14 21:17:15 - __main__ - INFO - ɳ ½,1
+2025-05-14 21:18:15 - __main__ - INFO - » ½,5
+2025-05-14 21:18:28 - __main__ - INFO - ɳ̬ ½,0
+2025-05-14 21:19:11 - __main__ - INFO - 껨г ½,0
+2025-05-14 21:19:41 - __main__ - INFO - г ½,0
+2025-05-14 21:20:28 - __main__ - INFO - гල ½,1
+2025-05-14 21:20:37 - __main__ - INFO - ɳг ½,0
+2025-05-14 21:21:19 - __main__ - INFO - ɳǹ ½,1
+2025-05-14 21:21:56 - __main__ - INFO - ɳ ½,0
+2025-05-14 21:22:44 - __main__ - INFO - ɳͨ ½,0
+2025-05-14 21:23:06 - __main__ - INFO - ɳչ ½,1
+2025-05-14 21:23:55 - __main__ - INFO - ˼볤ɳ ½,1
+2025-05-14 21:24:32 - __main__ - INFO - ųɳ ½,0
+2025-05-14 21:25:19 - __main__ - INFO - ɳˮ ½,0
+2025-05-14 21:26:10 - __main__ - INFO - г ½,0
+2025-05-14 21:26:16 - __main__ - INFO - йںŸ,21
+2025-05-14 22:37:16 - __main__ - INFO - ձ ½,1
+2025-05-14 22:37:39 - __main__ - INFO - ɳ ½,0
+2025-05-14 22:38:21 - __main__ - INFO - ձ ½,0
+2025-05-14 22:39:18 - __main__ - INFO - ɳ ½,0
+2025-05-14 22:39:28 - __main__ - INFO - ɳг ½,0
+2025-05-14 22:39:55 - __main__ - INFO - ½,0
+2025-05-14 22:40:00 - __main__ - INFO - ɳ ½,0
+2025-05-14 22:40:33 - __main__ - INFO - ɳ ½,0
+2025-05-14 22:41:17 - __main__ - INFO - ɳ ½,1
+2025-05-14 22:41:58 - __main__ - INFO - ɳ۲ ½,0
+2025-05-14 22:42:38 - __main__ - INFO - ɳ ½,1
+2025-05-14 22:43:34 - __main__ - INFO - » ½,3
+2025-05-14 22:44:18 - __main__ - INFO - ɳ̬ ½,0
+2025-05-14 22:45:19 - __main__ - INFO - 껨г ½,0
+2025-05-14 22:45:24 - __main__ - INFO - г ½,0
+2025-05-14 22:45:30 - __main__ - INFO - гල ½,0
+2025-05-14 22:46:04 - __main__ - INFO - ɳг ½,0
+2025-05-14 22:46:50 - __main__ - INFO - ɳǹ ½,0
+2025-05-14 22:47:49 - __main__ - INFO - ɳ ½,0
+2025-05-14 22:48:08 - __main__ - INFO - ɳͨ ½,0
+2025-05-14 22:48:27 - __main__ - INFO - ɳչ ½,0
+2025-05-14 22:49:29 - __main__ - INFO - ˼볤ɳ ½,0
+2025-05-14 22:50:07 - __main__ - INFO - ųɳ ½,0
+2025-05-14 22:50:48 - __main__ - INFO - ɳˮ ½,0
+2025-05-14 22:51:24 - __main__ - INFO - г ½,0
+2025-05-14 22:51:56 - __main__ - INFO - йںŸ,5
+2025-05-14 23:19:15 - __main__ - INFO - ձ ½,0
+2025-05-14 23:19:15 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:19:15 - __main__ - INFO - ɳг ½,0
+2025-05-14 23:19:15 - __main__ - INFO - ½,0
+2025-05-14 23:19:15 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:19:15 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:19:15 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:19:15 - __main__ - INFO - ɳ۲ ½,0
+2025-05-14 23:19:15 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:19:15 - __main__ - INFO - » ½,0
+2025-05-14 23:19:15 - __main__ - INFO - ɳ̬ ½,0
+2025-05-14 23:19:15 - __main__ - INFO - 껨г ½,0
+2025-05-14 23:19:15 - __main__ - INFO - г ½,0
+2025-05-14 23:19:15 - __main__ - INFO - гල ½,0
+2025-05-14 23:19:15 - __main__ - INFO - ɳг ½,0
+2025-05-14 23:19:15 - __main__ - INFO - ɳǹ ½,0
+2025-05-14 23:19:15 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:19:15 - __main__ - INFO - ɳͨ ½,0
+2025-05-14 23:19:15 - __main__ - INFO - ɳչ ½,0
+2025-05-14 23:19:15 - __main__ - INFO - ˼볤ɳ ½,0
+2025-05-14 23:19:15 - __main__ - INFO - ųɳ ½,0
+2025-05-14 23:19:15 - __main__ - INFO - ɳˮ ½,0
+2025-05-14 23:19:15 - __main__ - INFO - г ½,0
+2025-05-14 23:19:15 - __main__ - INFO - йںŸ,0
+2025-05-14 23:20:43 - __main__ - INFO - ձ ½,0
+2025-05-14 23:20:43 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:20:43 - __main__ - INFO - ɳг ½,0
+2025-05-14 23:20:43 - __main__ - INFO - ½,0
+2025-05-14 23:20:43 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:20:43 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:20:43 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:20:43 - __main__ - INFO - ɳ۲ ½,0
+2025-05-14 23:20:43 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:20:43 - __main__ - INFO - » ½,0
+2025-05-14 23:20:43 - __main__ - INFO - ɳ̬ ½,0
+2025-05-14 23:20:43 - __main__ - INFO - 껨г ½,0
+2025-05-14 23:20:43 - __main__ - INFO - г ½,0
+2025-05-14 23:20:43 - __main__ - INFO - гල ½,0
+2025-05-14 23:20:43 - __main__ - INFO - ɳг ½,0
+2025-05-14 23:20:43 - __main__ - INFO - ɳǹ ½,0
+2025-05-14 23:20:43 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:20:43 - __main__ - INFO - ɳͨ ½,0
+2025-05-14 23:20:43 - __main__ - INFO - ɳչ ½,0
+2025-05-14 23:20:43 - __main__ - INFO - ˼볤ɳ ½,0
+2025-05-14 23:20:43 - __main__ - INFO - ųɳ ½,0
+2025-05-14 23:20:43 - __main__ - INFO - ɳˮ ½,0
+2025-05-14 23:20:43 - __main__ - INFO - г ½,0
+2025-05-14 23:20:43 - __main__ - INFO - йںŸ,0
+2025-05-14 23:21:05 - __main__ - INFO - ձ ½,0
+2025-05-14 23:21:05 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:21:05 - __main__ - INFO - ɳг ½,0
+2025-05-14 23:21:05 - __main__ - INFO - ½,0
+2025-05-14 23:21:05 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:21:05 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:21:05 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:21:05 - __main__ - INFO - ɳ۲ ½,0
+2025-05-14 23:21:05 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:21:05 - __main__ - INFO - » ½,0
+2025-05-14 23:21:05 - __main__ - INFO - ɳ̬ ½,0
+2025-05-14 23:21:05 - __main__ - INFO - 껨г ½,0
+2025-05-14 23:21:05 - __main__ - INFO - г ½,0
+2025-05-14 23:21:05 - __main__ - INFO - гල ½,0
+2025-05-14 23:21:05 - __main__ - INFO - ɳг ½,0
+2025-05-14 23:21:05 - __main__ - INFO - ɳǹ ½,0
+2025-05-14 23:21:05 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:21:05 - __main__ - INFO - ɳͨ ½,0
+2025-05-14 23:21:05 - __main__ - INFO - ɳչ ½,0
+2025-05-14 23:21:05 - __main__ - INFO - ˼볤ɳ ½,0
+2025-05-14 23:21:05 - __main__ - INFO - ųɳ ½,0
+2025-05-14 23:21:05 - __main__ - INFO - ɳˮ ½,0
+2025-05-14 23:21:05 - __main__ - INFO - г ½,0
+2025-05-14 23:21:05 - __main__ - INFO - йںŸ,0
+2025-05-14 23:22:26 - __main__ - INFO - ձ ½,0
+2025-05-14 23:22:26 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:22:26 - __main__ - INFO - ɳг ½,0
+2025-05-14 23:22:26 - __main__ - INFO - ½,0
+2025-05-14 23:22:26 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:22:26 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:22:26 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:22:26 - __main__ - INFO - ɳ۲ ½,0
+2025-05-14 23:22:26 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:22:26 - __main__ - INFO - » ½,0
+2025-05-14 23:22:26 - __main__ - INFO - ɳ̬ ½,0
+2025-05-14 23:22:26 - __main__ - INFO - 껨г ½,0
+2025-05-14 23:22:26 - __main__ - INFO - г ½,0
+2025-05-14 23:22:26 - __main__ - INFO - гල ½,0
+2025-05-14 23:22:26 - __main__ - INFO - ɳг ½,0
+2025-05-14 23:22:26 - __main__ - INFO - ɳǹ ½,0
+2025-05-14 23:22:26 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:22:26 - __main__ - INFO - ɳͨ ½,0
+2025-05-14 23:22:26 - __main__ - INFO - ɳչ ½,0
+2025-05-14 23:22:26 - __main__ - INFO - ˼볤ɳ ½,0
+2025-05-14 23:22:26 - __main__ - INFO - ųɳ ½,0
+2025-05-14 23:22:26 - __main__ - INFO - ɳˮ ½,0
+2025-05-14 23:22:26 - __main__ - INFO - г ½,0
+2025-05-14 23:22:26 - __main__ - INFO - йںŸ,0
+2025-05-14 23:22:48 - __main__ - INFO - ձ ½,0
+2025-05-14 23:22:48 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:22:48 - __main__ - INFO - ɳг ½,0
+2025-05-14 23:22:48 - __main__ - INFO - ½,0
+2025-05-14 23:22:48 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:22:48 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:22:48 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:22:48 - __main__ - INFO - ɳ۲ ½,0
+2025-05-14 23:22:48 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:22:48 - __main__ - INFO - » ½,0
+2025-05-14 23:22:48 - __main__ - INFO - ɳ̬ ½,0
+2025-05-14 23:22:48 - __main__ - INFO - 껨г ½,0
+2025-05-14 23:22:48 - __main__ - INFO - г ½,0
+2025-05-14 23:22:48 - __main__ - INFO - гල ½,0
+2025-05-14 23:22:48 - __main__ - INFO - ɳг ½,0
+2025-05-14 23:22:48 - __main__ - INFO - ɳǹ ½,0
+2025-05-14 23:22:48 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:22:48 - __main__ - INFO - ɳͨ ½,0
+2025-05-14 23:22:48 - __main__ - INFO - ɳչ ½,0
+2025-05-14 23:22:48 - __main__ - INFO - ˼볤ɳ ½,0
+2025-05-14 23:22:48 - __main__ - INFO - ųɳ ½,0
+2025-05-14 23:22:48 - __main__ - INFO - ɳˮ ½,0
+2025-05-14 23:22:48 - __main__ - INFO - г ½,0
+2025-05-14 23:22:48 - __main__ - INFO - йںŸ,0
+2025-05-14 23:23:40 - __main__ - INFO - ձ ½,0
+2025-05-14 23:23:40 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:23:40 - __main__ - INFO - ɳг ½,0
+2025-05-14 23:23:40 - __main__ - INFO - ½,0
+2025-05-14 23:23:40 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:23:40 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:23:40 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:23:40 - __main__ - INFO - ɳ۲ ½,0
+2025-05-14 23:23:40 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:23:40 - __main__ - INFO - » ½,0
+2025-05-14 23:23:40 - __main__ - INFO - ɳ̬ ½,0
+2025-05-14 23:23:40 - __main__ - INFO - 껨г ½,0
+2025-05-14 23:23:40 - __main__ - INFO - г ½,0
+2025-05-14 23:23:40 - __main__ - INFO - гල ½,0
+2025-05-14 23:23:40 - __main__ - INFO - ɳг ½,0
+2025-05-14 23:23:40 - __main__ - INFO - ɳǹ ½,0
+2025-05-14 23:23:40 - __main__ - INFO - ɳ ½,0
+2025-05-14 23:23:40 - __main__ - INFO - ɳͨ ½,0
+2025-05-14 23:23:40 - __main__ - INFO - ɳչ ½,0
+2025-05-14 23:23:40 - __main__ - INFO - ˼볤ɳ ½,0
+2025-05-14 23:23:40 - __main__ - INFO - ųɳ ½,0
+2025-05-14 23:23:40 - __main__ - INFO - ɳˮ ½,0
+2025-05-14 23:23:40 - __main__ - INFO - г ½,0
+2025-05-14 23:23:40 - __main__ - INFO - йںŸ,0
+2025-05-15 09:37:58 - libs.log - INFO - ձ ½,0
+2025-05-15 09:40:18 - libs.log - INFO - ձ ½,0
+2025-05-15 09:40:54 - libs.log - INFO - ɳ ½,0
+2025-05-15 09:41:13 - libs.log - INFO - ɳг ½,0
+2025-05-15 09:41:16 - libs.log - INFO - ½,0
+2025-05-15 09:42:10 - libs.log - INFO - ɳ ½,0
+2025-05-15 09:43:03 - libs.log - INFO - ɳ ½,0
+2025-05-15 09:43:40 - libs.log - INFO - ɳ ½,0
+2025-05-15 09:44:11 - libs.log - INFO - ձ ½,0
+2025-05-15 09:47:00 - libs.log - INFO - ձ ½,0
+2025-05-15 09:47:11 - libs.log - INFO - ɳ ½,0
+2025-05-15 09:47:19 - libs.log - INFO - ɳг ½,0
+2025-05-15 09:48:10 - libs.log - INFO - ½,0
+2025-05-15 09:48:22 - libs.log - INFO - ɳ ½,0
+2025-05-15 09:48:35 - libs.log - INFO - ɳ ½,0
+2025-05-15 09:49:11 - libs.log - INFO - ɳ ½,0
+2025-05-15 09:49:56 - libs.log - INFO - ɳ۲ ½,0
+2025-05-15 09:49:58 - libs.log - INFO - ɳ ½,0
+2025-05-15 09:50:18 - libs.log - INFO - » ½,0
+2025-05-15 09:50:47 - libs.log - INFO - ɳ̬ ½,0
+2025-05-15 09:51:36 - libs.log - INFO - 껨г ½,0
+2025-05-15 09:52:35 - libs.log - INFO - г ½,0
+2025-05-15 09:53:08 - libs.log - INFO - гල ½,0
+2025-05-15 09:53:37 - libs.log - INFO - ɳг ½,0
+2025-05-15 09:54:35 - libs.log - INFO - ɳǹ ½,0
+2025-05-15 09:54:59 - libs.log - INFO - ɳ ½,0
+2025-05-15 09:55:35 - libs.log - INFO - ɳͨ ½,0
+2025-05-15 09:56:05 - libs.log - INFO - ɳչ ½,0
+2025-05-15 09:56:24 - libs.log - INFO - ˼볤ɳ ½,0
+2025-05-15 09:56:32 - libs.log - INFO - ųɳ ½,0
+2025-05-15 09:56:50 - libs.log - INFO - ɳˮ ½,0
+2025-05-15 09:57:13 - libs.log - INFO - г ½,0
+2025-05-15 09:57:34 - libs.log - INFO - йںŸ,0
+2025-05-15 10:01:22 - libs.log - INFO - ձ ½,0
+2025-05-15 10:01:30 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:02:07 - libs.log - INFO - ɳг ½,0
+2025-05-15 10:02:27 - libs.log - INFO - ½,0
+2025-05-15 10:03:18 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:03:35 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:04:23 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:04:47 - libs.log - INFO - ձ ½,0
+2025-05-15 10:05:17 - libs.log - INFO - ձ ½,0
+2025-05-15 10:05:36 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:06:01 - libs.log - INFO - ձ ½,0
+2025-05-15 10:06:01 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:06:01 - libs.log - INFO - ɳг ½,0
+2025-05-15 10:06:01 - libs.log - INFO - ½,0
+2025-05-15 10:06:01 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:06:01 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:06:01 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:06:01 - libs.log - INFO - ɳ۲ ½,0
+2025-05-15 10:06:01 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:06:01 - libs.log - INFO - » ½,0
+2025-05-15 10:06:01 - libs.log - INFO - ɳ̬ ½,0
+2025-05-15 10:06:01 - libs.log - INFO - 껨г ½,0
+2025-05-15 10:06:01 - libs.log - INFO - г ½,0
+2025-05-15 10:06:01 - libs.log - INFO - гල ½,0
+2025-05-15 10:06:01 - libs.log - INFO - ɳг ½,0
+2025-05-15 10:06:01 - libs.log - INFO - ɳǹ ½,0
+2025-05-15 10:06:01 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:06:01 - libs.log - INFO - ɳͨ ½,0
+2025-05-15 10:06:01 - libs.log - INFO - ɳչ ½,0
+2025-05-15 10:06:01 - libs.log - INFO - ˼볤ɳ ½,0
+2025-05-15 10:06:01 - libs.log - INFO - ųɳ ½,0
+2025-05-15 10:06:01 - libs.log - INFO - ɳˮ ½,0
+2025-05-15 10:06:01 - libs.log - INFO - г ½,0
+2025-05-15 10:06:01 - libs.log - INFO - йںŸ,0
+2025-05-15 10:06:58 - libs.log - INFO - ձ ½,0
+2025-05-15 10:06:58 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:06:58 - libs.log - INFO - ɳг ½,0
+2025-05-15 10:06:58 - libs.log - INFO - ½,0
+2025-05-15 10:06:58 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:06:58 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:06:58 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:06:58 - libs.log - INFO - ɳ۲ ½,0
+2025-05-15 10:06:58 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:06:58 - libs.log - INFO - » ½,0
+2025-05-15 10:06:58 - libs.log - INFO - ɳ̬ ½,0
+2025-05-15 10:06:58 - libs.log - INFO - 껨г ½,0
+2025-05-15 10:06:58 - libs.log - INFO - г ½,0
+2025-05-15 10:06:58 - libs.log - INFO - гල ½,0
+2025-05-15 10:06:58 - libs.log - INFO - ɳг ½,0
+2025-05-15 10:06:58 - libs.log - INFO - ɳǹ ½,0
+2025-05-15 10:06:58 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:06:58 - libs.log - INFO - ɳͨ ½,0
+2025-05-15 10:06:58 - libs.log - INFO - ɳչ ½,0
+2025-05-15 10:06:58 - libs.log - INFO - ˼볤ɳ ½,0
+2025-05-15 10:06:58 - libs.log - INFO - ųɳ ½,0
+2025-05-15 10:06:58 - libs.log - INFO - ɳˮ ½,0
+2025-05-15 10:06:58 - libs.log - INFO - г ½,0
+2025-05-15 10:06:58 - libs.log - INFO - йںŸ,0
+2025-05-15 10:09:32 - libs.log - INFO - ձ ½,0
+2025-05-15 10:09:32 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:09:32 - libs.log - INFO - ɳг ½,0
+2025-05-15 10:09:32 - libs.log - INFO - ½,0
+2025-05-15 10:09:32 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:09:32 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:09:32 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:09:32 - libs.log - INFO - ɳ۲ ½,0
+2025-05-15 10:09:32 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:09:32 - libs.log - INFO - » ½,0
+2025-05-15 10:09:32 - libs.log - INFO - ɳ̬ ½,0
+2025-05-15 10:09:32 - libs.log - INFO - 껨г ½,0
+2025-05-15 10:09:32 - libs.log - INFO - г ½,0
+2025-05-15 10:09:32 - libs.log - INFO - гල ½,0
+2025-05-15 10:09:32 - libs.log - INFO - ɳг ½,0
+2025-05-15 10:09:32 - libs.log - INFO - ɳǹ ½,0
+2025-05-15 10:09:32 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:09:32 - libs.log - INFO - ɳͨ ½,0
+2025-05-15 10:09:32 - libs.log - INFO - ɳչ ½,0
+2025-05-15 10:09:32 - libs.log - INFO - ˼볤ɳ ½,0
+2025-05-15 10:09:32 - libs.log - INFO - ųɳ ½,0
+2025-05-15 10:09:32 - libs.log - INFO - ɳˮ ½,0
+2025-05-15 10:09:32 - libs.log - INFO - г ½,0
+2025-05-15 10:09:32 - libs.log - INFO - йںŸ,0
+2025-05-15 10:10:08 - libs.log - INFO - ձ ½,0
+2025-05-15 10:10:08 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:10:08 - libs.log - INFO - ɳг ½,0
+2025-05-15 10:10:08 - libs.log - INFO - ½,0
+2025-05-15 10:10:08 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:10:08 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:10:08 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:10:08 - libs.log - INFO - ɳ۲ ½,0
+2025-05-15 10:10:08 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:10:08 - libs.log - INFO - » ½,0
+2025-05-15 10:10:08 - libs.log - INFO - ɳ̬ ½,0
+2025-05-15 10:10:08 - libs.log - INFO - 껨г ½,0
+2025-05-15 10:10:08 - libs.log - INFO - г ½,0
+2025-05-15 10:10:08 - libs.log - INFO - гල ½,0
+2025-05-15 10:10:08 - libs.log - INFO - ɳг ½,0
+2025-05-15 10:10:08 - libs.log - INFO - ɳǹ ½,0
+2025-05-15 10:10:08 - libs.log - INFO - ɳ ½,0
+2025-05-15 10:10:08 - libs.log - INFO - ɳͨ ½,0
+2025-05-15 10:10:08 - libs.log - INFO - ɳչ ½,0
+2025-05-15 10:10:08 - libs.log - INFO - ˼볤ɳ ½,0
+2025-05-15 10:10:08 - libs.log - INFO - ųɳ ½,0
+2025-05-15 10:10:08 - libs.log - INFO - ɳˮ ½,0
+2025-05-15 10:10:08 - libs.log - INFO - г ½,0
+2025-05-15 10:10:08 - libs.log - INFO - йںŸ,0
+2025-05-15 12:00:30 - core.log - INFO - ձ ½,0
+2025-05-15 12:00:30 - core.log - INFO - ɳ ½,0
+2025-05-15 12:00:30 - core.log - INFO - ɳг ½,0
+2025-05-15 12:00:30 - core.log - INFO - ½,0
+2025-05-15 12:00:30 - core.log - INFO - ɳ ½,0
+2025-05-15 12:00:30 - core.log - INFO - ɳ ½,0
+2025-05-15 12:00:30 - core.log - INFO - ɳ ½,0
+2025-05-15 12:00:30 - core.log - INFO - ɳ۲ ½,0
+2025-05-15 12:00:30 - core.log - INFO - ɳ ½,0
+2025-05-15 12:00:30 - core.log - INFO - » ½,0
+2025-05-15 12:00:30 - core.log - INFO - ɳ̬ ½,0
+2025-05-15 12:00:30 - core.log - INFO - 껨г ½,0
+2025-05-15 12:00:30 - core.log - INFO - г ½,0
+2025-05-15 12:00:30 - core.log - INFO - гල ½,0
+2025-05-15 12:00:30 - core.log - INFO - ɳг ½,0
+2025-05-15 12:00:30 - core.log - INFO - ɳǹ ½,0
+2025-05-15 12:00:30 - core.log - INFO - ɳ ½,0
+2025-05-15 12:00:30 - core.log - INFO - ɳͨ ½,0
+2025-05-15 12:00:30 - core.log - INFO - ɳչ ½,0
+2025-05-15 12:00:30 - core.log - INFO - ˼볤ɳ ½,0
+2025-05-15 12:00:30 - core.log - INFO - ųɳ ½,0
+2025-05-15 12:00:30 - core.log - INFO - ɳˮ ½,0
+2025-05-15 12:00:30 - core.log - INFO - г ½,0
+2025-05-15 12:00:30 - core.log - INFO - йںŸ,0
+2025-05-15 15:52:05 - core.log - INFO - ձ ½,0
+2025-05-15 15:52:05 - core.log - INFO - ɳ ½,0
+2025-05-15 15:52:05 - core.log - INFO - ɳг ½,0
+2025-05-15 15:52:05 - core.log - INFO - ½,0
+2025-05-15 15:52:05 - core.log - INFO - ɳ ½,0
+2025-05-15 15:52:05 - core.log - INFO - ɳ ½,0
+2025-05-15 15:52:05 - core.log - INFO - ɳ ½,0
+2025-05-15 15:52:05 - core.log - INFO - ɳ۲ ½,0
+2025-05-15 15:52:05 - core.log - INFO - ɳ ½,0
+2025-05-15 15:52:05 - core.log - INFO - » ½,0
+2025-05-15 15:52:05 - core.log - INFO - ɳ̬ ½,0
+2025-05-15 15:52:05 - core.log - INFO - 껨г ½,0
+2025-05-15 15:52:05 - core.log - INFO - г ½,0
+2025-05-15 15:52:05 - core.log - INFO - гල ½,0
+2025-05-15 15:52:05 - core.log - INFO - ɳг ½,0
+2025-05-15 15:52:05 - core.log - INFO - ɳǹ ½,0
+2025-05-15 15:52:05 - core.log - INFO - ɳ ½,0
+2025-05-15 15:52:05 - core.log - INFO - ɳͨ ½,0
+2025-05-15 15:52:05 - core.log - INFO - ɳչ ½,0
+2025-05-15 15:52:05 - core.log - INFO - ˼볤ɳ ½,0
+2025-05-15 15:52:05 - core.log - INFO - ųɳ ½,0
+2025-05-15 15:52:05 - core.log - INFO - ɳˮ ½,0
+2025-05-15 15:52:05 - core.log - INFO - г ½,0
+2025-05-15 15:52:05 - core.log - INFO - йںŸ,0
diff --git a/core/auth.py b/core/auth.py
new file mode 100644
index 00000000..a6911bc7
--- /dev/null
+++ b/core/auth.py
@@ -0,0 +1,99 @@
+from datetime import datetime, timedelta
+import jwt
+from functools import wraps
+from fastapi import Depends, HTTPException, status
+from fastapi.security import OAuth2PasswordBearer
+from typing import Optional
+from passlib.context import CryptContext
+from core.models import User as DBUser
+from core.db import DB
+
+# 配置
+SECRET_KEY = "your-secret-key-here" # 生产环境应使用更安全的密钥
+ALGORITHM = "HS256"
+ACCESS_TOKEN_EXPIRE_MINUTES = 30
+
+# 密码哈希上下文
+pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
+
+def get_user(username: str) -> Optional[DBUser]:
+ """从数据库获取用户"""
+ session = DB.get_session()
+ try:
+ return session.query(DBUser).filter(DBUser.username == username).first()
+ finally:
+ session.close()
+
+def authenticate_user(username: str, password: str) -> Optional[DBUser]:
+ """验证用户凭据"""
+ user = get_user(username)
+ if not user or not pwd_context.verify(password, user.password_hash):
+ return None
+ return user
+
+def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
+ """创建JWT Token"""
+ to_encode = data.copy()
+ if expires_delta:
+ expire = datetime.utcnow() + expires_delta
+ else:
+ expire = datetime.utcnow() + timedelta(minutes=15)
+ to_encode.update({"exp": expire})
+ return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
+
+async def get_current_user(token: str = Depends(oauth2_scheme)):
+ """获取当前用户"""
+ credentials_exception = HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Could not validate credentials",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+ try:
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
+ username: str = payload.get("sub")
+ if username is None:
+ raise credentials_exception
+ except jwt.PyJWTError:
+ raise credentials_exception
+
+ user = get_user(username)
+ if user is None:
+ raise credentials_exception
+
+ return {
+ "username": user.username,
+ "role": user.role,
+ "permissions": user.permissions,
+ "original_user": user
+ }
+
+def requires_role(role: str):
+ """检查用户角色的装饰器"""
+ def decorator(func):
+ @wraps(func)
+ async def wrapper(*args, **kwargs):
+ current_user = kwargs.get('current_user')
+ if not current_user or current_user.get('role') != role:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Insufficient permissions"
+ )
+ return await func(*args, **kwargs)
+ return wrapper
+ return decorator
+
+def requires_permission(permission: str):
+ """检查用户权限的装饰器"""
+ def decorator(func):
+ @wraps(func)
+ async def wrapper(*args, **kwargs):
+ current_user = kwargs.get('current_user')
+ if not current_user or permission not in current_user.get('permissions', []):
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Insufficient permissions"
+ )
+ return await func(*args, **kwargs)
+ return wrapper
+ return decorator
\ No newline at end of file
diff --git a/core/config.py b/core/config.py
new file mode 100644
index 00000000..3decb2b4
--- /dev/null
+++ b/core/config.py
@@ -0,0 +1,29 @@
+import yaml
+import sys
+config = {}
+def get_config():
+ global config
+ try:
+ with open('config.yaml', 'r', encoding='utf-8') as f:
+ config = yaml.safe_load(f)
+ return config
+ except Exception as e:
+ print(f"Error loading configuration file{e}")
+ sys.exit(1)
+ return []
+def get(key,default:any=None):
+ global config
+ if key in config:
+ return config[key]
+ else:
+ print("Key {} not found in configuration".format(key))
+ if default is not None:
+ return default
+ return None
+
+config = get_config()
+DEBUG=get("debug",False)
+APP_NAME=get("app_name","we-mp-rss")
+
+
+VERSION='1.0.0'
\ No newline at end of file
diff --git a/libs/db.py b/core/db.py
similarity index 55%
rename from libs/db.py
rename to core/db.py
index fd816429..2c5628e5 100644
--- a/libs/db.py
+++ b/core/db.py
@@ -1,33 +1,8 @@
from sqlalchemy import create_engine, Column, Integer, String, DateTime
-from sqlalchemy.ext.declarative import declarative_base
+
from sqlalchemy.orm import sessionmaker
from typing import Optional, List
-
-Base = declarative_base()
-
-class Article(Base):
- __tablename__ = 'articles'
- id = Column(String(255), primary_key=True)
- mp_id = Column(String(255))
- title = Column(String(500))
- pic_url = Column(String(500))
- publish_time = Column(Integer)
- created_at = Column(DateTime)
- updated_at = Column(DateTime) # Changed from Integer to DateTime for consistency
- is_export = Column(Integer)
-
-class Feeds(Base):
- __tablename__ = 'feeds'
- id = Column(String(255), primary_key=True)
- mp_name = Column(String(255))
- mp_cover = Column(String(255))
- mp_intro = Column(String(255))
- status = Column(Integer)
- sync_time = Column(DateTime)
- update_time = Column(DateTime)
- created_at = Column(DateTime)
- updated_at = Column(DateTime)
- faker_id = Column(String(255))
+from .models import Feed,Article
class Db:
def __init__(self):
@@ -60,7 +35,7 @@ class Db:
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
- def add_article(self, article_data: dict) -> None:
+ def add_article(self, article_data: dict) -> bool:
try:
art = Article(**article_data)
self.session.add(art)
@@ -68,33 +43,43 @@ class Db:
except Exception as e:
self.session.rollback()
print(f"Failed to add article: {e}")
- return e
-
+ return False
+ return True
def get_articles(self,id:str=None,limit:int=30,offset:int=0) -> List[Article]:
try:
- # data=self.session.query(Feeds).filter_by(id=id).limit(10)
+ # data=self.session.query(Feed).filter_by(id=id).limit(10)
data=self.session.query(Article).limit(limit).offset(offset)
return data
except Exception as e:
- print(f"Failed to fetch feeds: {e}")
+ print(f"Failed to fetch Feed: {e}")
return e
- def get_all_mps(self) -> List[Feeds]:
- """Get all Feeds records"""
+ def get_all_mps(self) -> List[Feed]:
+ """Get all Feed records"""
try:
- return self.session.query(Feeds).all()
+ return self.session.query(Feed).all()
except Exception as e:
- print(f"Failed to fetch feeds: {e}")
+ print(f"Failed to fetch Feed: {e}")
return e
- def get_mps(self,mp_id:str)->Optional[Feeds]:
+ def get_mps(self,mp_id:str)->Optional[Feed]:
try:
- data=self.session.query(Feeds).filter_by(id=mp_id).first()
+ data=self.session.query(Feed).filter_by(id=mp_id).first()
# return type(data)
return data
except Exception as e:
- print(f"Failed to fetch feeds: {e}")
+ print(f"Failed to fetch Feed: {e}")
return e
def get_faker_id(self,mp_id:str):
data= self.get_mps(mp_id)
return data.faker_id
+
+ def get_session(self):
+ """获取数据库会话"""
+ if not self.session:
+ raise Exception("Database not initialized")
+ return self.session
+
+DB=Db()
+from .config import config as cfg
+DB.init(cfg.get("db"))
\ No newline at end of file
diff --git a/core/log.py b/core/log.py
new file mode 100644
index 00000000..50bcb3ae
--- /dev/null
+++ b/core/log.py
@@ -0,0 +1,22 @@
+from logging.handlers import RotatingFileHandler
+import logging
+global logger
+# 创建logger对象
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG) # 设置最低日志级别
+# 创建文件处理器,每天一个文件,保留7天备份
+handler = RotatingFileHandler('app.log', maxBytes=1024*1024, backupCount=7)
+handler.setLevel(logging.DEBUG)
+# 创建控制台处理器
+console_handler = logging.StreamHandler()
+console_handler.setLevel(logging.INFO)
+# 创建格式器并添加到处理器
+formatter = logging.Formatter(
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+ datefmt='%Y-%m-%d %H:%M:%S'
+)
+handler.setFormatter(formatter)
+console_handler.setFormatter(formatter)
+# 将处理器添加到logger
+logger.addHandler(handler)
+logger.addHandler(console_handler)
\ No newline at end of file
diff --git a/core/models/__init__.py b/core/models/__init__.py
new file mode 100644
index 00000000..40ed3ea5
--- /dev/null
+++ b/core/models/__init__.py
@@ -0,0 +1,3 @@
+from .article import Article
+from .feed import Feed
+from .user import User
\ No newline at end of file
diff --git a/core/models/article.py b/core/models/article.py
new file mode 100644
index 00000000..0390de0a
--- /dev/null
+++ b/core/models/article.py
@@ -0,0 +1,13 @@
+from .base import Base,Column,String,Integer,DateTime
+class Article(Base):
+ __tablename__ = 'articles'
+ id = Column(String(255), primary_key=True)
+ mp_id = Column(String(255))
+ title = Column(String(500))
+ pic_url = Column(String(500))
+ publish_time = Column(Integer)
+ created_at = Column(DateTime)
+ updated_at = Column(DateTime) # Changed from Integer to DateTime for consistency
+ is_export = Column(Integer)
+
+
diff --git a/core/models/base.py b/core/models/base.py
new file mode 100644
index 00000000..6ac51f66
--- /dev/null
+++ b/core/models/base.py
@@ -0,0 +1,3 @@
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy import create_engine, Column, Integer, String, DateTime,Date,ForeignKey,Boolean,Text,Enum,Table
+Base = declarative_base()
diff --git a/core/models/feed.py b/core/models/feed.py
new file mode 100644
index 00000000..90b56ee9
--- /dev/null
+++ b/core/models/feed.py
@@ -0,0 +1,13 @@
+from .base import Base,Column,String,Integer,DateTime
+class Feed(Base):
+ __tablename__ = 'feeds'
+ id = Column(String(255), primary_key=True)
+ mp_name =Column(String(255))
+ mp_cover = Column(String(255))
+ mp_intro = Column(String(255))
+ status = Column(Integer)
+ sync_time = Column(DateTime)
+ update_time = Column(DateTime)
+ created_at = Column(DateTime)
+ updated_at = Column(DateTime)
+ faker_id = Column(String(255))
\ No newline at end of file
diff --git a/core/models/user.py b/core/models/user.py
new file mode 100644
index 00000000..65761cfe
--- /dev/null
+++ b/core/models/user.py
@@ -0,0 +1,27 @@
+from .base import Base, Column, String, Integer, DateTime, Boolean
+from sqlalchemy import JSON
+
+class User(Base):
+ __tablename__ = 'users'
+ id = Column(String(255), primary_key=True)
+ username = Column(String(50), unique=True, nullable=False)
+ password_hash = Column(String(255), nullable=False)
+ is_active = Column(Boolean, default=True)
+ role = Column(String(20), default='user') # admin/editor/user
+ permissions = Column(JSON, default=list) # 权限列表
+
+ # 原有字段保持不变
+ mp_name = Column(String(255))
+ mp_cover = Column(String(255))
+ mp_intro = Column(String(255))
+ status = Column(Integer)
+ sync_time = Column(DateTime)
+ update_time = Column(DateTime)
+ created_at = Column(DateTime)
+ updated_at = Column(DateTime)
+ faker_id = Column(String(255))
+
+ def verify_password(self, password: str) -> bool:
+ """验证密码"""
+ from core.auth import pwd_context
+ return pwd_context.verify(password, self.password_hash)
\ No newline at end of file
diff --git a/core/notice.py b/core/notice.py
new file mode 100644
index 00000000..bb0de805
--- /dev/null
+++ b/core/notice.py
@@ -0,0 +1,41 @@
+import requests
+import json
+def send_dingtalk_markdown(webhook_url, title, text, is_at_all=False, at_mobiles=[]):
+ """
+ 发送Markdown格式消息
+
+ 参数:
+ - webhook_url: 机器人Webhook地址
+ - title: 消息标题
+ - text: Markdown格式内容
+ - is_at_all: 是否@所有人
+ - at_mobiles: 要@的手机号列表
+ """
+ headers = {'Content-Type': 'application/json'}
+ data = {
+ "msgtype": "markdown",
+ "markdown": {
+ "title": title,
+ "text": text
+ },
+ "at": {
+ "atMobiles": at_mobiles,
+ "isAtAll": is_at_all
+ }
+ }
+
+ response = requests.post(
+ url=webhook_url,
+ headers=headers,
+ data=json.dumps(data)
+ )
+ print(response.text)
+# 使用示例
+# markdown_text = """### 项目状态报告
+# - **项目名称**: XX系统升级
+# - **当前状态**: ✅正常运行
+# - **异常情况**: 无
+# - [查看详情](http://example.com)
+# """
+# webhook="https://oapi.dingtalk.com/robot/send?access_token=xxx"
+# send_dingtalk_markdown(webhook, "项目状态通知", markdown_text)
diff --git a/libs/rss.py b/core/rss.py
similarity index 100%
rename from libs/rss.py
rename to core/rss.py
diff --git a/libs/wx.py b/core/wx.py
similarity index 84%
rename from libs/wx.py
rename to core/wx.py
index 5db7153b..1adfcff8 100644
--- a/libs/wx.py
+++ b/core/wx.py
@@ -1,10 +1,10 @@
import requests
-import yaml
import json
import re
import datetime
-import libs.db as db
from datetime import datetime, timezone
+import core.config as cfg
+import core.db as db
def dateformat(timestamp:int):
# UTC时间对象
@@ -16,28 +16,26 @@ def dateformat(timestamp:int):
t=(local_dt.strftime("%Y-%m-%d %H:%M:%S"))
return t
-with open("config.yaml", "r") as f:
- config = yaml.safe_load(f)
def get_Articles(faker_id:str):
headers = {
- "Cookie": config["cookie"],
- "User-Agent": config["user_agent"]
+ "Cookie": cfg.get("cookie"),
+ "User-Agent": cfg.get("user_agent")
}
params = {
"sub": "list",
"sub_action": "list_ex",
"begin": 0,
- "count": config["count"],
+ "count": cfg.get("count"),
"fakeid": faker_id,
- "token": config["token"],
+ "token": cfg.get("token"),
"lang": "zh_CN",
"f": "json",
"ajax": 1
}
url = "https://mp.weixin.qq.com/cgi-bin/appmsgpublish"
headers = {
- "Cookie": config["cookie"],
- "User-Agent": config["user_agent"]
+ "Cookie": cfg.get("cookie"),
+ "User-Agent": cfg.get("user_agent")
}
data={}
try:
@@ -64,7 +62,7 @@ def get_list(faker_id:str=None,mp_id:str=None,is_add:bool=False):
try:
data=data['publish_page']['publish_list']
wx_db=db.Db()
- wx_db.init(config['db'])
+ wx_db.init(cfg.get('db'))
for i in data:
art=i['publish_info']
art=json.loads(art)
@@ -84,8 +82,8 @@ def get_list(faker_id:str=None,mp_id:str=None,is_add:bool=False):
}
articles.append(article)
if is_add:
- wx_db.add_article(article)
- print('添加成功')
+ isOk=wx_db.add_article(article)
+ print(f'添加成功{isOk}')
except Exception as e:
print(e,"出错了")
diff --git a/job.py b/job.py
index 535c4b6c..90f90f89 100644
--- a/job.py
+++ b/job.py
@@ -1,48 +1,51 @@
import schedule, time
-import libs.wx as wx
-import libs.db as db
-import logging
-from logging.handlers import RotatingFileHandler
-import yaml
-# 创建logger对象
-logger = logging.getLogger(__name__)
-logger.setLevel(logging.DEBUG) # 设置最低日志级别
+import core.wx as wx
+import core.db as db
+import core.config as cfg
+import core.notice as notice
+import random
+import core.log as log
-# 创建文件处理器,每天一个文件,保留7天备份
-handler = RotatingFileHandler('app.log', maxBytes=1024*1024, backupCount=7)
-handler.setLevel(logging.DEBUG)
-
-# 创建控制台处理器
-console_handler = logging.StreamHandler()
-console_handler.setLevel(logging.INFO)
-
-# 创建格式器并添加到处理器
-formatter = logging.Formatter(
- '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
- datefmt='%Y-%m-%d %H:%M:%S'
-)
-handler.setFormatter(formatter)
-console_handler.setFormatter(formatter)
-
-# 将处理器添加到logger
-logger.addHandler(handler)
-logger.addHandler(console_handler)
-
-with open('config.yaml', 'r') as f:
- config = yaml.safe_load(f)
+logger=log.logger
+# 初始化数据库
wx_db=db.Db()
-wx_db.init(config["db"])
+wx_db.init(cfg.get("db"))
+
+
+# 获取公众号列表
mps=wx_db.get_all_mps()
-print(mps)
def do_job():
- print("执行任务")
+ print("开始更新")
+ all_count=0
+ text=f" **时间**:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}\n"
for item in mps:
+
+ # 成功更新数量计数
+ mps_count=0
faker_id=item.faker_id
mp_id=item.id
- print(item,faker_id,mp_id)
- data=wx.get_list(faker_id,mp_id,1)
- logger.info("开始执行函数")
+ print(f'正在更新公众号:{item.mp_name}({item.id})')
+ if not cfg.DEBUG:
+ # 获取对应公众号列表
+ data=wx.get_list(faker_id,mp_id,0)
+ for art in data:
+ # 添加到数据库
+ if wx_db.add_article(art):
+ mps_count=mps_count+1
+ else:
+ print("调试模式,不进行更新")
+
+ logger.info(f"{item.mp_name} 更新结束,共更新{mps_count}")
+ text+=f"* **{item.mp_name}**({mps_count})\n"
+ all_count=all_count+mps_count
+
+ # 随机休眠 防止被封锁
+ if not cfg.DEBUG:
+ time.sleep(random.randint(2,60))
+ logger.info(f"所有公众号更新完成,共更新{all_count}条数据")
+ text+=f"\n所有公众号更新完成,共更新{all_count}条数据"
+ send_notice(text,cfg.get('app_name',default='we-mp-rss'))
def start():
schedule.every(10).seconds.do(do_job) # 每10秒执行
# schedule.every.day.at("08:00").do(job) # 每天8点执行
@@ -50,6 +53,13 @@ def start():
schedule.run_pending()
time.sleep(1)
+def send_notice(text:str="",title:str=""):
+ markdown_text = f"""### {title} 通知
+ {text}
+ """
+
+ webhook=cfg.get('notice')['dingding']
+ notice.send_dingtalk_markdown(webhook, title, markdown_text)
+
if __name__ == '__main__':
- # start()
do_job()
\ No newline at end of file
diff --git a/libs/__pycache__/db.cpython-313.pyc b/libs/__pycache__/db.cpython-313.pyc
deleted file mode 100644
index 6ffbcd32..00000000
Binary files a/libs/__pycache__/db.cpython-313.pyc and /dev/null differ
diff --git a/libs/__pycache__/db.cpython-38.pyc b/libs/__pycache__/db.cpython-38.pyc
deleted file mode 100644
index 382cb40e..00000000
Binary files a/libs/__pycache__/db.cpython-38.pyc and /dev/null differ
diff --git a/libs/__pycache__/job.cpython-313.pyc b/libs/__pycache__/job.cpython-313.pyc
deleted file mode 100644
index c618d5f4..00000000
Binary files a/libs/__pycache__/job.cpython-313.pyc and /dev/null differ
diff --git a/libs/__pycache__/rss.cpython-313.pyc b/libs/__pycache__/rss.cpython-313.pyc
deleted file mode 100644
index 1baf24e0..00000000
Binary files a/libs/__pycache__/rss.cpython-313.pyc and /dev/null differ
diff --git a/libs/__pycache__/wx.cpython-313.pyc b/libs/__pycache__/wx.cpython-313.pyc
deleted file mode 100644
index 54d9f713..00000000
Binary files a/libs/__pycache__/wx.cpython-313.pyc and /dev/null differ
diff --git a/libs/__pycache__/wx.cpython-38.pyc b/libs/__pycache__/wx.cpython-38.pyc
deleted file mode 100644
index f84e6c3c..00000000
Binary files a/libs/__pycache__/wx.cpython-38.pyc and /dev/null differ
diff --git a/main.py b/main.py
new file mode 100644
index 00000000..4fc34288
--- /dev/null
+++ b/main.py
@@ -0,0 +1,4 @@
+import uvicorn
+if __name__ == '__main__':
+ uvicorn.run("web:app", host="0.0.0.0", port=8001, reload=True,reload_excludes=['static','web_ui'])
+ pass
\ No newline at end of file
diff --git a/migrations/add_user_auth_columns.py b/migrations/add_user_auth_columns.py
new file mode 100644
index 00000000..965b8694
--- /dev/null
+++ b/migrations/add_user_auth_columns.py
@@ -0,0 +1,109 @@
+from core.db import DB
+from core.models import User
+from datetime import datetime
+from sqlalchemy import text
+
+def upgrade():
+ session = DB.get_session()
+ try:
+ # 检查表是否存在
+ table_exists = session.execute(
+ text("SHOW TABLES LIKE 'users'")
+ ).fetchone()
+
+ if not table_exists:
+ # 创建新表
+ session.execute(text("""
+ CREATE TABLE users (
+ id VARCHAR(255) PRIMARY KEY,
+ username VARCHAR(50) UNIQUE,
+ password_hash VARCHAR(255),
+ is_active BOOLEAN DEFAULT TRUE,
+ role VARCHAR(20) DEFAULT 'user',
+ permissions JSON,
+ mp_name VARCHAR(255),
+ mp_cover VARCHAR(255),
+ mp_intro VARCHAR(255),
+ status INTEGER,
+ sync_time DATETIME,
+ update_time DATETIME,
+ created_at DATETIME,
+ updated_at DATETIME,
+ faker_id VARCHAR(255)
+ )
+ """))
+ else:
+ # 检查并添加缺失的列
+ columns = session.execute(text("""
+ SELECT COLUMN_NAME
+ FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE TABLE_NAME = 'users'
+ """)).fetchall()
+ existing_columns = {c[0] for c in columns}
+
+ if 'username' not in existing_columns:
+ session.execute(text("""
+ ALTER TABLE users
+ ADD COLUMN username VARCHAR(50) UNIQUE
+ """))
+
+ if 'password_hash' not in existing_columns:
+ session.execute(text("""
+ ALTER TABLE users
+ ADD COLUMN password_hash VARCHAR(255)
+ """))
+
+ if 'is_active' not in existing_columns:
+ session.execute(text("""
+ ALTER TABLE users
+ ADD COLUMN is_active BOOLEAN DEFAULT TRUE
+ """))
+
+ if 'role' not in existing_columns:
+ session.execute(text("""
+ ALTER TABLE users
+ ADD COLUMN role VARCHAR(20) DEFAULT 'user'
+ """))
+
+ if 'permissions' not in existing_columns:
+ session.execute(text("""
+ ALTER TABLE users
+ ADD COLUMN permissions JSON
+ """))
+
+ # 创建默认管理员账户
+ from passlib.context import CryptContext
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+ from uuid import uuid4
+ admin = User(
+ id=str(uuid4()),
+ username="admin",
+ password_hash=pwd_context.hash("admin123"),
+ is_active=True,
+ role="admin",
+ permissions=["*"],
+ created_at=datetime.now(),
+ updated_at=datetime.now()
+ )
+ session.add(admin)
+ session.commit()
+ finally:
+ session.close()
+
+def downgrade():
+ session = DB.get_session()
+ try:
+ session.execute("""
+ ALTER TABLE users
+ DROP COLUMN username,
+ DROP COLUMN password_hash,
+ DROP COLUMN is_active,
+ DROP COLUMN role,
+ DROP COLUMN permissions
+ """)
+ session.commit()
+ finally:
+ session.close()
+
+if __name__ == "__main__":
+ upgrade()
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 2f5756fa..f3e0d81a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,3 +11,6 @@ schedule==1.2.2
SQLAlchemy==2.0.40
typing_extensions==4.13.2
urllib3==2.2.3
+pyjwt[crypto]==2.8.0
+passlib==1.7.4
+python-multipart==0.0.7
\ No newline at end of file
diff --git a/web.py b/web.py
index 4cf27124..e02b5a65 100644
--- a/web.py
+++ b/web.py
@@ -1,62 +1,21 @@
-from fastapi import FastAPI,Response # 导⼊FastAPI,⽤于定义API
+from fastapi import FastAPI
+from fastapi.staticfiles import StaticFiles
+from apis.auth import router as auth_router
+from apis.user import router as user_router
+from apis.article import router as article_router
+from apis.wechat import router as wechat_router
-import libs.wx as wx
-import libs.db as db
-import job as job
-import yaml
-from fastapi.responses import PlainTextResponse
-app = FastAPI() # 创建FastAPI实例
-
-# http请求方式类型:get、post、put、update、delete
-# 浏览器默认访问的是get类型,如果使用其他形式访问
+app = FastAPI(
+ title="WeRSS API",
+ description="微信公众号RSS生成服务",
+ version="1.0.0"
+)
-# 不带参数的访问形式:
-# 访问地址:http://127.0.0.1:8000
-@app.get("/")
-async def test_one():
- return {"message": "MPRSS"}
-
-@app.get("/wx/job")
-async def start_job():
- wx_db.init(config["db"])
- mps=wx_db.get_all_mps()
- wx_db.close()
-
- return {"code":"ok","data":mps }
-
-@app.get("/wx/rss/{mp_id}")
-async def wx_list(mp_id: str,update:int=0):
- wx_db.init(config["db"])
- faker_id=wx_db.get_faker_id(mp_id)
- data=wx.get_list(faker_id,mp_id,update)
- wx_db.close()
- return {"mp_id": mp_id,"data":data}
-
-
-wx_db=db.Db()
-with open("config.yaml", "r") as f:
- config = yaml.safe_load(f)
-#获取所有需要同步的公众号
-@app.get("/wx/mps")
-async def wx_mps():
- wx_db.init(config["db"])
- data=wx_db.get_all_mps()
- for i in data:
- print(i.mp_name)
- wx_db.close()
- return {"code": "ok","data":data}
-import libs.rss as rss
-@app.get("/wx/feeds")
-async def wx_mps(offset:int=0,limit:int=5):
- wx_db.init(config["db"])
- data=wx_db.get_articles(limit=limit,offset=offset)
- rss_data=[]
- for item in data:
- print(item.id)
- rss_data.append({"title":item.title,"id":item.id,"link":"https://mp.weixin.qq.com/s/"+item.id,"updated":item.created_at.isoformat()})
- wx_db.close()
- _rss_data=rss.generate_rss(rss_data,"rss.xml")
- print(rss_data)
- # return Response(content=_rss_data, media_type="application/xml")
- return Response(content=_rss_data, media_type="text/plan")
+# 集成路由
+app.include_router(auth_router)
+app.include_router(user_router)
+app.include_router(article_router)
+app.include_router(wechat_router)
+# 静态文件服务
+app.mount("/", StaticFiles(directory="static", html=True), name="static")
\ No newline at end of file
diff --git a/web_ui/.gitignore b/web_ui/.gitignore
new file mode 100644
index 00000000..76438e01
--- /dev/null
+++ b/web_ui/.gitignore
@@ -0,0 +1,2 @@
+*.log
+node_modules/
\ No newline at end of file
diff --git a/web_ui/README.md b/web_ui/README.md
new file mode 100644
index 00000000..bcaa9b14
--- /dev/null
+++ b/web_ui/README.md
@@ -0,0 +1,71 @@
+# 微信公众号订阅助手 - WebUI
+
+基于Vue3 + Vite + ArcoDesign构建的前端界面
+
+## 功能列表
+- 用户登录/登出
+- 文章列表展示
+- 添加公众号订阅
+- 修改用户信息
+- 修改密码
+
+## 手动安装依赖
+
+由于系统限制,请按以下步骤手动安装依赖:
+
+1. 打开PowerShell作为管理员
+2. 运行以下命令更改执行策略:
+```powershell
+Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
+```
+3. 安装项目依赖:
+```powershell
+npm install
+```
+4. 安装ArcoDesign Vue:
+```powershell
+npm install --save @arco-design/web-vue @arco-design/web-vue-icon @arco-design/color
+```
+5. 安装开发依赖:
+```powershell
+npm install --save-dev less less-loader
+```
+
+## 环境变量配置
+
+创建`.env`文件:
+```ini
+VITE_API_BASE_URL=http://your-api-server.com
+```
+
+## 运行项目
+
+开发模式:
+```bash
+npm run dev
+```
+
+生产构建:
+```bash
+npm run build
+```
+
+## 项目结构
+
+```
+src/
+├── api/ # API接口
+├── assets/ # 静态资源
+├── components/ # 公共组件
+├── router/ # 路由配置
+├── views/ # 页面组件
+├── App.vue # 根组件
+└── main.ts # 入口文件
+```
+
+## 技术栈
+- Vue 3
+- Vite
+- ArcoDesign Vue
+- TypeScript
+- Axios
\ No newline at end of file
diff --git a/web_ui/index.html b/web_ui/index.html
new file mode 100644
index 00000000..4a203c28
--- /dev/null
+++ b/web_ui/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ 微信公众号订阅助手
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web_ui/public/vite.svg b/web_ui/public/vite.svg
new file mode 100644
index 00000000..e7b8dfb1
--- /dev/null
+++ b/web_ui/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web_ui/src/App.vue b/web_ui/src/App.vue
new file mode 100644
index 00000000..a62e0913
--- /dev/null
+++ b/web_ui/src/App.vue
@@ -0,0 +1,213 @@
+
+
+
+
+
+
+
+
+
+
+
+ 文章列表
+
+
+
+ 添加订阅
+
+
+
+ 设置
+
+
+ 个人信息
+
+
+
+ 修改密码
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web_ui/src/api/article.ts b/web_ui/src/api/article.ts
new file mode 100644
index 00000000..3e6a9b89
--- /dev/null
+++ b/web_ui/src/api/article.ts
@@ -0,0 +1,36 @@
+import http from './http'
+
+export interface Article {
+ id: number
+ title: string
+ content: string
+ account_name: string
+ publish_time: string
+ status: string
+ link: string
+}
+
+export interface ArticleListParams {
+ page?: number
+ pageSize?: number
+ search?: string
+ status?: string
+ account_id?: number
+}
+
+export interface ArticleListResult {
+ total: number
+ data: Article[]
+}
+
+export const getArticles = (params: ArticleListParams) => {
+ return http.get('/mps', { params })
+}
+
+export const getArticleDetail = (id: number) => {
+ return http.get(`/feeds/${id}`)
+}
+
+export const deleteArticle = (id: number) => {
+ return http.delete(`/feeds/${id}`)
+}
\ No newline at end of file
diff --git a/web_ui/src/api/auth.ts b/web_ui/src/api/auth.ts
new file mode 100644
index 00000000..654a8d8c
--- /dev/null
+++ b/web_ui/src/api/auth.ts
@@ -0,0 +1,33 @@
+import http from './http'
+
+export interface LoginParams {
+ username: string
+ password: string
+}
+
+export interface LoginResult {
+ token: string
+ userInfo: {
+ username: string
+ avatar: string
+ }
+}
+
+export const login = (data: LoginParams) => {
+ const formData = new URLSearchParams()
+ formData.append('username', data.username)
+ formData.append('password', data.password)
+ return http.post('/auth/login', formData, {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ }
+ })
+}
+
+export const logout = () => {
+ return http.post('/auth/logout')
+}
+
+export const getCurrentUser = () => {
+ return http.get('/auth/user')
+}
\ No newline at end of file
diff --git a/web_ui/src/api/http.ts b/web_ui/src/api/http.ts
new file mode 100644
index 00000000..d6985647
--- /dev/null
+++ b/web_ui/src/api/http.ts
@@ -0,0 +1,40 @@
+import axios from 'axios'
+import { Message } from '@arco-design/web-vue'
+
+const service = axios.create({
+ baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
+ timeout: 10000
+})
+
+// 请求拦截器
+service.interceptors.request.use(
+ (config) => {
+ const token = localStorage.getItem('token')
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`
+ }
+ return config
+ },
+ (error) => {
+ return Promise.reject(error)
+ }
+)
+
+// 响应拦截器
+service.interceptors.response.use(
+ (response) => {
+ const res = response.data
+ console.log(res.code)
+ if (res.code !=0) {
+ Message.error(res.message || 'Error')
+ return Promise.reject(new Error(res.message || 'Error'))
+ }
+ return res.data
+ },
+ (error) => {
+ Message.error(error.message || '请求失败')
+ return Promise.reject(error)
+ }
+)
+
+export default service
\ No newline at end of file
diff --git a/web_ui/src/api/subscription.ts b/web_ui/src/api/subscription.ts
new file mode 100644
index 00000000..4078ade2
--- /dev/null
+++ b/web_ui/src/api/subscription.ts
@@ -0,0 +1,35 @@
+import http from './http'
+
+export interface Subscription {
+ id: number
+ name: string
+ accountId: string
+ rssUrl: string
+ category: string
+ description: string
+ status: string
+}
+
+export interface AddSubscriptionParams {
+ name: string
+ accountId: string
+ rssUrl: string
+ category: string
+ description?: string
+}
+
+export const getSubscriptions = () => {
+ return http.get('/wx/mps')
+}
+
+export const addSubscription = (data: AddSubscriptionParams) => {
+ return http.post('/wx/mps', data)
+}
+
+export const deleteSubscription = (id: number) => {
+ return http.delete(`/wx/mps/${id}`)
+}
+
+export const updateSubscription = (id: number, data: Partial) => {
+ return http.put(`/wx/mps/${id}`, data)
+}
\ No newline at end of file
diff --git a/web_ui/src/api/user.ts b/web_ui/src/api/user.ts
new file mode 100644
index 00000000..c5876f64
--- /dev/null
+++ b/web_ui/src/api/user.ts
@@ -0,0 +1,33 @@
+import http from './http'
+
+export interface UserInfo {
+ username: string
+ nickname: string
+ email: string
+ avatar: string
+}
+
+export interface ChangePasswordParams {
+ old_password: string
+ new_password: string
+}
+
+export const getUserInfo = () => {
+ return http.get('/user/info')
+}
+
+export const updateUserInfo = (data: Partial) => {
+ return http.put('/user/info', data)
+}
+
+export const changePassword = (data: ChangePasswordParams) => {
+ return http.put('/user/password', data)
+}
+
+export const uploadAvatar = (file: FormData) => {
+ return http.post<{ url: string }>('/user/avatar', file, {
+ headers: {
+ 'Content-Type': 'multipart/form-data'
+ }
+ })
+}
\ No newline at end of file
diff --git a/web_ui/src/assets/logo.svg b/web_ui/src/assets/logo.svg
new file mode 100644
index 00000000..429190cc
--- /dev/null
+++ b/web_ui/src/assets/logo.svg
@@ -0,0 +1,11 @@
+
diff --git a/web_ui/src/assets/vue.svg b/web_ui/src/assets/vue.svg
new file mode 100644
index 00000000..770e9d33
--- /dev/null
+++ b/web_ui/src/assets/vue.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web_ui/src/components/AddSubscriptionModal.vue b/web_ui/src/components/AddSubscriptionModal.vue
new file mode 100644
index 00000000..f532f91c
--- /dev/null
+++ b/web_ui/src/components/AddSubscriptionModal.vue
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新闻
+ 科技
+ 财经
+ 娱乐
+ 体育
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web_ui/src/components/HelloWorld.vue b/web_ui/src/components/HelloWorld.vue
new file mode 100644
index 00000000..b58e52b9
--- /dev/null
+++ b/web_ui/src/components/HelloWorld.vue
@@ -0,0 +1,41 @@
+
+
+
+ {{ msg }}
+
+
+
+
+ Edit
+ components/HelloWorld.vue to test HMR
+
+
+
+
+ Check out
+ create-vue, the official Vue + Vite starter
+
+
+ Learn more about IDE Support for Vue in the
+ Vue Docs Scaling up Guide.
+
+ Click on the Vite and Vue logos to learn more
+
+
+
diff --git a/web_ui/src/main.ts b/web_ui/src/main.ts
new file mode 100644
index 00000000..63743981
--- /dev/null
+++ b/web_ui/src/main.ts
@@ -0,0 +1,23 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+import router from './router'
+
+// 导入 ArcoDesign
+import ArcoVue from '@arco-design/web-vue'
+// 导入 ArcoDesign 图标
+// import ArcoVueIcon from '@arco-design/web-vue-icon'
+// 导入 ArcoDesign 样式
+import '@arco-design/web-vue/dist/arco.css'
+// 导入自定义样式
+import './style.css'
+
+const app = createApp(App)
+
+// 注册 ArcoDesign
+app.use(ArcoVue)
+// 注册图标组件
+// app.use(ArcoVueIcon)
+// 注册路由
+app.use(router)
+
+app.mount('#app')
\ No newline at end of file
diff --git a/web_ui/src/router/index.ts b/web_ui/src/router/index.ts
new file mode 100644
index 00000000..21183dbd
--- /dev/null
+++ b/web_ui/src/router/index.ts
@@ -0,0 +1,56 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import Login from '../views/Login.vue'
+import ArticleList from '../views/ArticleList.vue'
+import ChangePassword from '../views/ChangePassword.vue'
+import EditUser from '../views/EditUser.vue'
+import AddSubscription from '../views/AddSubscription.vue'
+
+const routes = [
+ {
+ path: '/login',
+ name: 'Login',
+ component: Login
+ },
+ {
+ path: '/',
+ name: 'ArticleList',
+ component: ArticleList,
+ meta: { requiresAuth: true }
+ },
+ {
+ path: '/change-password',
+ name: 'ChangePassword',
+ component: ChangePassword,
+ meta: { requiresAuth: true }
+ },
+ {
+ path: '/edit-user',
+ name: 'EditUser',
+ component: EditUser,
+ meta: { requiresAuth: true }
+ },
+ {
+ path: '/add-subscription',
+ name: 'AddSubscription',
+ component: AddSubscription,
+ meta: { requiresAuth: true }
+ }
+]
+
+const router = createRouter({
+ history: createWebHistory(import.meta.env.BASE_URL),
+ routes
+})
+
+// 路由守卫
+router.beforeEach((to, from, next) => {
+ const isAuthenticated = localStorage.getItem('token')
+
+ if (to.meta.requiresAuth && !isAuthenticated) {
+ next('/login')
+ } else {
+ next()
+ }
+})
+
+export default router
\ No newline at end of file
diff --git a/web_ui/src/style.css b/web_ui/src/style.css
new file mode 100644
index 00000000..9a7cf293
--- /dev/null
+++ b/web_ui/src/style.css
@@ -0,0 +1,38 @@
+/* 全局样式 */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+html, body {
+ width: 100%;
+ height: 100%;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
+ Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+#app {
+ width: 100%;
+ height: 100%;
+}
+
+/* 链接样式 */
+a {
+ color: #165DFF;
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+/* 自定义ArcoDesign主题变量 */
+:root {
+ --primary-color: #165DFF;
+ --success-color: #00B42A;
+ --warning-color: #FF7D00;
+ --danger-color: #F53F3F;
+}
\ No newline at end of file
diff --git a/web_ui/src/views/AddSubscription.vue b/web_ui/src/views/AddSubscription.vue
new file mode 100644
index 00000000..4285c9db
--- /dev/null
+++ b/web_ui/src/views/AddSubscription.vue
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新闻
+ 科技
+ 财经
+ 娱乐
+ 体育
+
+
+
+
+
+
+
+
+
+
+ 添加订阅
+
+ 重置
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web_ui/src/views/ArticleList.vue b/web_ui/src/views/ArticleList.vue
new file mode 100644
index 00000000..52f4a66a
--- /dev/null
+++ b/web_ui/src/views/ArticleList.vue
@@ -0,0 +1,195 @@
+
+
+
+
+
+
+
+ 添加订阅
+
+
+
+ 刷新
+
+
+
+
+
+
+
+
+
+
+
+ {{ statusTextMap[record.status] }}
+
+
+
+
+
+ 编辑
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web_ui/src/views/ChangePassword.vue b/web_ui/src/views/ChangePassword.vue
new file mode 100644
index 00000000..1eee9276
--- /dev/null
+++ b/web_ui/src/views/ChangePassword.vue
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 确认修改
+
+ 重置
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web_ui/src/views/EditUser.vue b/web_ui/src/views/EditUser.vue
new file mode 100644
index 00000000..6f9e89f9
--- /dev/null
+++ b/web_ui/src/views/EditUser.vue
@@ -0,0 +1,193 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存修改
+
+ 重置
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web_ui/src/views/Login.vue b/web_ui/src/views/Login.vue
new file mode 100644
index 00000000..cec1fc26
--- /dev/null
+++ b/web_ui/src/views/Login.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
+
微信公众号订阅助手
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 登录
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web_ui/src/vite-env.d.ts b/web_ui/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/web_ui/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/web_ui/vite.config.ts b/web_ui/vite.config.ts
new file mode 100644
index 00000000..3ebc51fc
--- /dev/null
+++ b/web_ui/vite.config.ts
@@ -0,0 +1,34 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import { resolve } from 'path'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [vue()],
+ resolve: {
+ alias: {
+ '@': resolve(__dirname, 'src')
+ }
+ },
+ optimizeDeps: {
+ include: ['@arco-design/web-vue', '@arco-design/web-vue-icon']
+ },
+ css: {
+ preprocessorOptions: {
+ less: {
+ javascriptEnabled: true,
+ }
+ }
+ },
+ server: {
+ port: 3000,
+ open: true,
+ proxy: {
+ '/api': {
+ target: 'http://127.0.0.1:8001',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, '/wx')
+ }
+ }
+ }
+})
\ No newline at end of file