mirror of
https://github.com/rachelos/we-mp-rss.git
synced 2026-09-01 15:33:05 +08:00
1.0
This commit is contained in:
@@ -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: <Newspaper size={40} />
|
||||
},
|
||||
{
|
||||
name: '长沙晚报',
|
||||
desc: '在这里,了解长沙!在这里,爱上长沙!',
|
||||
createTime: '2024-03-22',
|
||||
updateTime: '2025-05-12',
|
||||
icon: <Newspaper size={40} />
|
||||
},
|
||||
{
|
||||
name: '长沙市场监管',
|
||||
desc: '长沙市市场监督管理局官方公众号平台',
|
||||
createTime: '2024-03-26',
|
||||
updateTime: '2025-05-11',
|
||||
icon: <Scale size={40} />
|
||||
},
|
||||
{
|
||||
name: '微开福',
|
||||
desc: '展示区域形象,发布政务信息,提供便民服务。',
|
||||
createTime: '2025-05-09',
|
||||
updateTime: '2025-05-12',
|
||||
icon: <Megaphone size={40} />
|
||||
},
|
||||
{
|
||||
name: '清廉长沙',
|
||||
desc: '长沙市纪委市监委官微,廉政宣传教育,接受"四风"举报。',
|
||||
createTime: '2024-03-22',
|
||||
updateTime: '2025-05-11',
|
||||
icon: <Gavel size={40} />
|
||||
},
|
||||
{
|
||||
name: '长沙警事',
|
||||
desc: '长沙市公安局警务信息发布平台',
|
||||
createTime: '2024-03-22',
|
||||
updateTime: '2025-05-11',
|
||||
icon: <Shield size={40} />
|
||||
},
|
||||
{
|
||||
name: '文明长沙',
|
||||
desc: '传播正能量',
|
||||
createTime: '2024-04-29',
|
||||
updateTime: '2025-05-11',
|
||||
icon: <Heart size={40} />
|
||||
},
|
||||
{
|
||||
name: '长沙观察',
|
||||
desc: '长沙电视台新闻频道新媒体官方平台,关注热点新闻及综合资讯。',
|
||||
createTime: '2024-03-22',
|
||||
updateTime: '2025-05-11',
|
||||
icon: <Eye size={40} />
|
||||
},
|
||||
{
|
||||
name: '长沙发布',
|
||||
desc: '传递政务信息,提供服务资讯,关注百姓关切——长沙发布随时为您守候。',
|
||||
createTime: '2024-04-29',
|
||||
updateTime: '2025-05-12',
|
||||
icon: <Send size={40} />
|
||||
},
|
||||
{
|
||||
name: '新华社',
|
||||
desc: '新华通讯社官方账号。硬新闻、暖故事、好朋友。',
|
||||
createTime: '2024-03-22',
|
||||
updateTime: '2025-05-11',
|
||||
icon: <Newspaper size={40} />
|
||||
},
|
||||
{
|
||||
name: '长沙生态环境',
|
||||
desc: '宣传环保政策法规、发布长沙环保工作动态、普及环境科学知识、聚焦环保热点问题,倡导绿色、低碳的发展和生活方式!',
|
||||
createTime: '2024-04-29',
|
||||
updateTime: '2025-05-11',
|
||||
icon: <Leaf size={40} />
|
||||
},
|
||||
{
|
||||
name: '雨花市场监管',
|
||||
desc: '长沙市雨花区市场监督管理局官方公众号平台',
|
||||
createTime: '2024-03-26',
|
||||
updateTime: '2025-05-11',
|
||||
icon: <Scale size={40} />
|
||||
},
|
||||
{
|
||||
name: '开福市场监管',
|
||||
desc: '宣传市场监管知识、传达各级文件会议精神、解读国家相关政策',
|
||||
createTime: '2024-03-26',
|
||||
updateTime: '2025-05-11',
|
||||
icon: <Scale size={40} />
|
||||
},
|
||||
{
|
||||
name: '宁乡市场监督',
|
||||
desc: '宁乡市市场监督管理局信息发布平台',
|
||||
createTime: '2024-03-26',
|
||||
updateTime: '2025-05-11',
|
||||
icon: <Scale size={40} />
|
||||
},
|
||||
{
|
||||
name: '长沙县市场监管',
|
||||
desc: '了解长沙县市场监督管理局工作动态,并提供相关业务咨询服务。',
|
||||
createTime: '2024-03-26',
|
||||
updateTime: '2025-05-11',
|
||||
icon: <Scale size={40} />
|
||||
},
|
||||
{
|
||||
name: '长沙城管',
|
||||
desc: '"长沙城管"秉承"城管为民"服务理念,向公众提供城市管理动态、政策法规、投诉建议等服务,助力品质长沙建设,提高市民参与度与获得感。',
|
||||
createTime: '2024-04-29',
|
||||
updateTime: '2025-05-11',
|
||||
icon: <Factory size={40} />
|
||||
},
|
||||
{
|
||||
name: '长沙慈善',
|
||||
desc: '弘扬慈善文化,传递慈善正能量!',
|
||||
createTime: '2025-03-14',
|
||||
updateTime: '2025-05-12',
|
||||
icon: <Heart size={40} />
|
||||
},
|
||||
{
|
||||
name: '长沙交通运输',
|
||||
desc: '服务百姓出行,传播交通声音!',
|
||||
createTime: '2024-04-29',
|
||||
updateTime: '2025-05-12',
|
||||
icon: <Bus size={40} />
|
||||
},
|
||||
{
|
||||
name: '长沙会展',
|
||||
desc: '聚焦会展行业,传播会展资讯,链接会展服务,发展会展经济。',
|
||||
createTime: '2024-04-29',
|
||||
updateTime: '2025-05-11',
|
||||
icon: <Calendar size={40} />
|
||||
},
|
||||
{
|
||||
name: '思想长沙',
|
||||
desc: '长沙市社会科学界联合会(长沙社会科学院)官方账号',
|
||||
createTime: '2024-04-29',
|
||||
updateTime: '2025-05-11',
|
||||
icon: <Brain size={40} />
|
||||
},
|
||||
{
|
||||
name: '网信长沙',
|
||||
desc: '解读网信政策法规,展示长沙网信动态,交流网信工作经验,推进网络文明建设,走好网上群众路线。',
|
||||
createTime: '2024-04-29',
|
||||
updateTime: '2025-05-12',
|
||||
icon: <Wifi size={40} />
|
||||
},
|
||||
{
|
||||
name: '长沙水运',
|
||||
desc: '长沙市水运事务中心(长沙市船舶检验中心)官方账号。',
|
||||
createTime: '2024-04-29',
|
||||
updateTime: '2025-05-12',
|
||||
icon: <Ship size={40} />
|
||||
},
|
||||
{
|
||||
name: '天心市场监管',
|
||||
desc: '长沙市天心区市场监督管理局官方公众号平台',
|
||||
createTime: '2024-10-31',
|
||||
updateTime: '2025-05-12',
|
||||
icon: <Scale size={40} />
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F5F7FA] text-[#212121]">
|
||||
{/* 标题区 */}
|
||||
<header className="h-20 bg-white flex items-center justify-center shadow-sm">
|
||||
<div className="container mx-auto px-4">
|
||||
<h1 className="text-2xl md:text-3xl font-bold text-[#1A237E]">长沙政务公众号信息平台</h1>
|
||||
<div className="h-px bg-[#E0E0E0] mt-2"></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 内容区 */}
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{accounts?.map((account, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: index * 0.05 }}
|
||||
whileHover={{ y: -5, boxShadow: '0 4px 16px rgba(0,0,0,0.15)' }}
|
||||
className="bg-white rounded-lg shadow-md overflow-hidden cursor-pointer min-h-[180px]"
|
||||
>
|
||||
<div className="p-5">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="mr-3">
|
||||
{account?.icon}
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-[#1A237E]">{account?.name}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-[#616161] mb-4 line-clamp-2">{account?.desc}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex items-center text-xs text-[#9E9E9E] bg-[#E3F2FD] px-2 py-1 rounded">
|
||||
<Clock size={12} className="mr-1" />
|
||||
<span>创建 {account?.createTime}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-xs text-[#9E9E9E] bg-[#EEEEEE] px-2 py-1 rounded">
|
||||
<Clock size={12} className="mr-1" />
|
||||
<span>更新 {account?.updateTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* 页脚区 */}
|
||||
<footer className="bg-[#FAFAFA] py-5">
|
||||
<div className="container mx-auto px-4 text-center text-xs text-[#757575]">
|
||||
<p>created by <a href="https://space.coze.cn" className="text-[#2196F3] hover:text-[#0D47A1]">coze space</a></p>
|
||||
<p>页面内容均由 AI 生成,仅供参考</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublicAccountPlatform;
|
||||
@@ -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()
|
||||
@@ -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"}
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
API_VERSION="/api/v1/wx"
|
||||
@@ -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()
|
||||
@@ -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条数据
|
||||
|
||||
@@ -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
|
||||
@@ -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'
|
||||
+24
-39
@@ -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"))
|
||||
+22
@@ -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)
|
||||
@@ -0,0 +1,3 @@
|
||||
from .article import Article
|
||||
from .feed import Feed
|
||||
from .user import User
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
+11
-13
@@ -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,"出错了")
|
||||
|
||||
@@ -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()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -0,0 +1,2 @@
|
||||
*.log
|
||||
node_modules/
|
||||
@@ -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
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>微信公众号订阅助手</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<a-layout class="app-container">
|
||||
<!-- 头部 -->
|
||||
<a-layout-header class="app-header">
|
||||
<div class="header-left">
|
||||
<div class="logo">
|
||||
<icon-rss />
|
||||
<span>订阅助手</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<a-dropdown position="br" trigger="click">
|
||||
<div class="user-info">
|
||||
<a-avatar :size="36">
|
||||
<img v-if="userInfo.avatar" :src="userInfo.avatar" alt="avatar">
|
||||
<icon-user v-else />
|
||||
</a-avatar>
|
||||
<span class="username">{{ userInfo.username }}</span>
|
||||
</div>
|
||||
<template #content>
|
||||
<a-doption @click="goToEditUser">
|
||||
<template #icon><icon-user /></template>
|
||||
个人中心
|
||||
</a-doption>
|
||||
<a-doption @click="goToChangePassword">
|
||||
<template #icon><icon-lock /></template>
|
||||
修改密码
|
||||
</a-doption>
|
||||
<a-doption @click="handleLogout">
|
||||
<template #icon><icon-export /></template>
|
||||
退出登录
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</a-layout-header>
|
||||
|
||||
<a-layout>
|
||||
<!-- 侧边栏 -->
|
||||
<a-layout-sider
|
||||
v-if="isAuthenticated"
|
||||
collapsible
|
||||
:width="220"
|
||||
breakpoint="xl"
|
||||
@collapse="handleCollapse"
|
||||
>
|
||||
<a-menu
|
||||
:default-selected-keys="[$route.name]"
|
||||
:collapsed="collapsed"
|
||||
@menu-item-click="handleMenuClick"
|
||||
>
|
||||
<a-menu-item key="ArticleList">
|
||||
<template #icon><icon-apps /></template>
|
||||
文章列表
|
||||
</a-menu-item>
|
||||
<a-menu-item key="AddSubscription">
|
||||
<template #icon><icon-plus /></template>
|
||||
添加订阅
|
||||
</a-menu-item>
|
||||
<a-sub-menu key="Settings">
|
||||
<template #icon><icon-settings /></template>
|
||||
<template #title>设置</template>
|
||||
<a-menu-item key="EditUser">
|
||||
<template #icon><icon-user /></template>
|
||||
个人信息
|
||||
</a-menu-item>
|
||||
<a-menu-item key="ChangePassword">
|
||||
<template #icon><icon-lock /></template>
|
||||
修改密码
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<a-layout>
|
||||
<a-layout-content class="app-content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { getCurrentUser } from '@/api/auth'
|
||||
import { logout } from '@/api/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const collapsed = ref(false)
|
||||
const userInfo = ref({
|
||||
username: '',
|
||||
avatar: ''
|
||||
})
|
||||
|
||||
const isAuthenticated = computed(() => {
|
||||
return !!localStorage.getItem('token')
|
||||
})
|
||||
|
||||
const fetchUserInfo = async () => {
|
||||
try {
|
||||
const res = await getCurrentUser()
|
||||
userInfo.value = res
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCollapse = (val: boolean) => {
|
||||
collapsed.value = val
|
||||
}
|
||||
|
||||
const handleMenuClick = (key: string) => {
|
||||
router.push({ name: key })
|
||||
}
|
||||
|
||||
const goToEditUser = () => {
|
||||
router.push({ name: 'EditUser' })
|
||||
}
|
||||
|
||||
const goToChangePassword = () => {
|
||||
router.push({ name: 'ChangePassword' })
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await logout()
|
||||
localStorage.removeItem('token')
|
||||
router.push('/login')
|
||||
} catch (error) {
|
||||
Message.error('退出登录失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (isAuthenticated.value) {
|
||||
fetchUserInfo()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
height: 64px;
|
||||
background: var(--color-bg-2);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.logo svg {
|
||||
margin-right: 10px;
|
||||
font-size: 24px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.username {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
padding: 20px;
|
||||
background: var(--color-bg-1);
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -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<ArticleListResult>('/mps', { params })
|
||||
}
|
||||
|
||||
export const getArticleDetail = (id: number) => {
|
||||
return http.get<Article>(`/feeds/${id}`)
|
||||
}
|
||||
|
||||
export const deleteArticle = (id: number) => {
|
||||
return http.delete(`/feeds/${id}`)
|
||||
}
|
||||
@@ -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<LoginResult>('/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')
|
||||
}
|
||||
@@ -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
|
||||
@@ -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<Subscription[]>('/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<Subscription>) => {
|
||||
return http.put(`/wx/mps/${id}`, data)
|
||||
}
|
||||
@@ -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<UserInfo>('/user/info')
|
||||
}
|
||||
|
||||
export const updateUserInfo = (data: Partial<UserInfo>) => {
|
||||
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'
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#2563eb" />
|
||||
<stop offset="100%" stop-color="#7c3aed" />
|
||||
</linearGradient>
|
||||
<circle cx="50" cy="50" r="45" fill="url(#grad)" />
|
||||
<path d="M35,35 L65,65 M35,65 L65,35"
|
||||
stroke="white"
|
||||
stroke-width="8"
|
||||
stroke-linecap="round" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 427 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="添加订阅"
|
||||
:on-before-ok="handleSubmit"
|
||||
:on-before-cancel="handleCancel"
|
||||
@cancel="handleModalCancel"
|
||||
@close="handleModalCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="公众号名称" field="name">
|
||||
<a-input
|
||||
v-model="form.name"
|
||||
placeholder="请输入公众号名称"
|
||||
allow-clear
|
||||
>
|
||||
<template #prefix><icon-user /></template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="公众号ID" field="accountId">
|
||||
<a-input
|
||||
v-model="form.accountId"
|
||||
placeholder="请输入公众号ID"
|
||||
allow-clear
|
||||
>
|
||||
<template #prefix><icon-idcard /></template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="RSS链接" field="rssUrl">
|
||||
<a-input
|
||||
v-model="form.rssUrl"
|
||||
placeholder="请输入RSS链接"
|
||||
allow-clear
|
||||
>
|
||||
<template #prefix><icon-link /></template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="分类" field="category">
|
||||
<a-select
|
||||
v-model="form.category"
|
||||
placeholder="请选择分类"
|
||||
allow-clear
|
||||
>
|
||||
<a-option value="news">新闻</a-option>
|
||||
<a-option value="tech">科技</a-option>
|
||||
<a-option value="finance">财经</a-option>
|
||||
<a-option value="entertainment">娱乐</a-option>
|
||||
<a-option value="sports">体育</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="描述" field="description">
|
||||
<a-textarea
|
||||
v-model="form.description"
|
||||
placeholder="请输入公众号描述"
|
||||
:auto-size="{ minRows: 3, maxRows: 5 }"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { FormInstance, Message } from '@arco-design/web-vue'
|
||||
import { addSubscription } from '@/api/subscription'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const loading = ref(false)
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
accountId: '',
|
||||
rssUrl: '',
|
||||
category: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入公众号名称' }],
|
||||
accountId: [{ required: true, message: '请输入公众号ID' }],
|
||||
rssUrl: [
|
||||
{ required: true, message: '请输入RSS链接' },
|
||||
{ type: 'url', message: '请输入有效的URL' }
|
||||
],
|
||||
category: [{ required: true, message: '请选择分类' }]
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const validate = await formRef.value?.validate()
|
||||
if (validate) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
await addSubscription(form.value)
|
||||
Message.success('订阅添加成功')
|
||||
emit('update:visible', false)
|
||||
emit('success')
|
||||
return true
|
||||
} catch (error) {
|
||||
Message.error('订阅添加失败')
|
||||
return false
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
formRef.value?.resetFields()
|
||||
return true
|
||||
}
|
||||
|
||||
const handleModalCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.arco-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps<{ msg: string }>()
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
@@ -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')
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="add-subscription">
|
||||
<a-page-header
|
||||
title="添加订阅"
|
||||
subtitle="添加新的公众号订阅"
|
||||
:show-back="true"
|
||||
@back="goBack"
|
||||
/>
|
||||
|
||||
<a-card>
|
||||
<a-form
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
@submit="handleSubmit"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="公众号名称" field="name">
|
||||
<a-input
|
||||
v-model="form.name"
|
||||
placeholder="请输入公众号名称"
|
||||
allow-clear
|
||||
>
|
||||
<template #prefix><icon-user /></template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="公众号ID" field="accountId">
|
||||
<a-input
|
||||
v-model="form.accountId"
|
||||
placeholder="请输入公众号ID"
|
||||
allow-clear
|
||||
>
|
||||
<template #prefix><icon-idcard /></template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="RSS链接" field="rssUrl">
|
||||
<a-input
|
||||
v-model="form.rssUrl"
|
||||
placeholder="请输入RSS链接"
|
||||
allow-clear
|
||||
>
|
||||
<template #prefix><icon-link /></template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="分类" field="category">
|
||||
<a-select
|
||||
v-model="form.category"
|
||||
placeholder="请选择分类"
|
||||
allow-clear
|
||||
>
|
||||
<a-option value="news">新闻</a-option>
|
||||
<a-option value="tech">科技</a-option>
|
||||
<a-option value="finance">财经</a-option>
|
||||
<a-option value="entertainment">娱乐</a-option>
|
||||
<a-option value="sports">体育</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="描述" field="description">
|
||||
<a-textarea
|
||||
v-model="form.description"
|
||||
placeholder="请输入公众号描述"
|
||||
:auto-size="{ minRows: 3, maxRows: 5 }"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" html-type="submit" :loading="loading">
|
||||
添加订阅
|
||||
</a-button>
|
||||
<a-button @click="resetForm">重置</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { addSubscription } from '@/api/subscription'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
accountId: '',
|
||||
rssUrl: '',
|
||||
category: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入公众号名称' }],
|
||||
accountId: [{ required: true, message: '请输入公众号ID' }],
|
||||
rssUrl: [
|
||||
{ required: true, message: '请输入RSS链接' },
|
||||
{ type: 'url', message: '请输入有效的URL' }
|
||||
],
|
||||
category: [{ required: true, message: '请选择分类' }]
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await addSubscription(form.value)
|
||||
Message.success('订阅添加成功')
|
||||
router.push('/')
|
||||
} catch (error) {
|
||||
Message.error('订阅添加失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
form.value = {
|
||||
name: '',
|
||||
accountId: '',
|
||||
rssUrl: '',
|
||||
category: '',
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.go(-1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.add-subscription {
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.arco-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div class="article-list">
|
||||
<a-page-header
|
||||
title="文章列表"
|
||||
subtitle="管理您的公众号订阅内容"
|
||||
:show-back="false"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="showAddModal">
|
||||
<template #icon><icon-plus /></template>
|
||||
添加订阅
|
||||
</a-button>
|
||||
<a-button @click="refresh">
|
||||
<template #icon><icon-refresh /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<a-card>
|
||||
<div class="search-bar">
|
||||
<a-input-search
|
||||
v-model="searchText"
|
||||
placeholder="搜索文章标题"
|
||||
@search="handleSearch"
|
||||
allow-clear
|
||||
/>
|
||||
<a-select
|
||||
v-model="filterStatus"
|
||||
placeholder="筛选状态"
|
||||
style="width: 200px; margin-left: 12px"
|
||||
allow-clear
|
||||
>
|
||||
<a-option value="published">已发布</a-option>
|
||||
<a-option value="draft">草稿</a-option>
|
||||
<a-option value="deleted">已删除</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data="articles"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@page-change="handlePageChange"
|
||||
row-key="id"
|
||||
>
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="statusColorMap[record.status]">
|
||||
{{ statusTextMap[record.status] }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #actions="{ record }">
|
||||
<a-button type="text" @click="editArticle(record.id)">
|
||||
<template #icon><icon-edit /></template>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button type="text" status="danger" @click="deleteArticle(record.id)">
|
||||
<template #icon><icon-delete /></template>
|
||||
删除
|
||||
</a-button>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<AddSubscriptionModal
|
||||
:visible="addModalVisible"
|
||||
@update:visible="addModalVisible = $event"
|
||||
@success="handleAddSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getArticles } from '@/api/article'
|
||||
import AddSubscriptionModal from '@/components/AddSubscriptionModal.vue'
|
||||
|
||||
const articles = ref([])
|
||||
const loading = ref(false)
|
||||
const searchText = ref('')
|
||||
const filterStatus = ref('')
|
||||
const addModalVisible = ref(false)
|
||||
|
||||
const pagination = ref({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const statusTextMap = {
|
||||
published: '已发布',
|
||||
draft: '草稿',
|
||||
deleted: '已删除'
|
||||
}
|
||||
|
||||
const statusColorMap = {
|
||||
published: 'green',
|
||||
draft: 'orange',
|
||||
deleted: 'red'
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
dataIndex: 'title',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '公众号',
|
||||
dataIndex: 'account_name'
|
||||
},
|
||||
{
|
||||
title: '发布时间',
|
||||
dataIndex: 'publish_time'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
slotName: 'status'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
slotName: 'actions',
|
||||
width: 200
|
||||
}
|
||||
]
|
||||
|
||||
const fetchArticles = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getArticles({
|
||||
page: pagination.value.current,
|
||||
pageSize: pagination.value.pageSize,
|
||||
search: searchText.value,
|
||||
status: filterStatus.value
|
||||
})
|
||||
articles.value = res.data
|
||||
pagination.value.total = res.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
pagination.value.current = page
|
||||
fetchArticles()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.value.current = 1
|
||||
fetchArticles()
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
fetchArticles()
|
||||
}
|
||||
|
||||
const showAddModal = () => {
|
||||
addModalVisible.value = true
|
||||
}
|
||||
|
||||
const handleAddSuccess = () => {
|
||||
fetchArticles()
|
||||
}
|
||||
|
||||
const editArticle = (id: number) => {
|
||||
// 编辑逻辑
|
||||
}
|
||||
|
||||
const deleteArticle = (id: number) => {
|
||||
// 删除逻辑
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchArticles()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.article-list {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="change-password">
|
||||
<a-page-header
|
||||
title="修改密码"
|
||||
subtitle="定期修改密码有助于账户安全"
|
||||
:show-back="true"
|
||||
@back="goBack"
|
||||
/>
|
||||
|
||||
<a-card>
|
||||
<a-form
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
@submit="handleSubmit"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="当前密码" field="currentPassword">
|
||||
<a-input-password
|
||||
v-model="form.currentPassword"
|
||||
placeholder="请输入当前密码"
|
||||
allow-clear
|
||||
>
|
||||
<template #prefix><icon-lock /></template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="新密码" field="newPassword">
|
||||
<a-input-password
|
||||
v-model="form.newPassword"
|
||||
placeholder="请输入新密码"
|
||||
allow-clear
|
||||
>
|
||||
<template #prefix><icon-lock /></template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="确认新密码" field="confirmPassword">
|
||||
<a-input-password
|
||||
v-model="form.confirmPassword"
|
||||
placeholder="请再次输入新密码"
|
||||
allow-clear
|
||||
>
|
||||
<template #prefix><icon-lock /></template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" html-type="submit" :loading="loading">
|
||||
确认修改
|
||||
</a-button>
|
||||
<a-button @click="resetForm">重置</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { changePassword } from '@/api/user'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
|
||||
const form = ref({
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
const validatePassword = (value: string, callback: any) => {
|
||||
if (!value) {
|
||||
callback('请输入密码')
|
||||
} else if (value.length < 6) {
|
||||
callback('密码长度不能少于6位')
|
||||
} else if (value.length > 20) {
|
||||
callback('密码长度不能超过20位')
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const validateConfirmPassword = (value: string, callback: any) => {
|
||||
if (!value) {
|
||||
callback('请确认密码')
|
||||
} else if (value !== form.value.newPassword) {
|
||||
callback('两次输入的密码不一致')
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const rules = {
|
||||
currentPassword: [{ validator: validatePassword, trigger: 'blur' }],
|
||||
newPassword: [{ validator: validatePassword, trigger: 'blur' }],
|
||||
confirmPassword: [{ validator: validateConfirmPassword, trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await changePassword({
|
||||
old_password: form.value.currentPassword,
|
||||
new_password: form.value.newPassword
|
||||
})
|
||||
Message.success('密码修改成功')
|
||||
router.push('/')
|
||||
} catch (error) {
|
||||
Message.error('密码修改失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
form.value = {
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.go(-1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.change-password {
|
||||
padding: 20px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.arco-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div class="edit-user">
|
||||
<a-page-header
|
||||
title="修改个人信息"
|
||||
subtitle="更新您的账户信息"
|
||||
:show-back="true"
|
||||
@back="goBack"
|
||||
/>
|
||||
|
||||
<a-card>
|
||||
<a-form
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
@submit="handleSubmit"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="头像">
|
||||
<a-upload
|
||||
action="/api/upload"
|
||||
:file-list="fileList"
|
||||
@change="handleUploadChange"
|
||||
:show-file-list="false"
|
||||
>
|
||||
<template #upload-button>
|
||||
<div class="avatar-upload">
|
||||
<a-avatar :size="80">
|
||||
<img v-if="form.avatar" :src="form.avatar" alt="avatar">
|
||||
<icon-user v-else />
|
||||
</a-avatar>
|
||||
<div class="upload-mask">
|
||||
<icon-edit />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-upload>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="用户名" field="username">
|
||||
<a-input
|
||||
v-model="form.username"
|
||||
placeholder="请输入用户名"
|
||||
allow-clear
|
||||
>
|
||||
<template #prefix><icon-user /></template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="昵称" field="nickname">
|
||||
<a-input
|
||||
v-model="form.nickname"
|
||||
placeholder="请输入昵称"
|
||||
allow-clear
|
||||
>
|
||||
<template #prefix><icon-user /></template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="邮箱" field="email">
|
||||
<a-input
|
||||
v-model="form.email"
|
||||
placeholder="请输入邮箱"
|
||||
allow-clear
|
||||
>
|
||||
<template #prefix><icon-email /></template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" html-type="submit" :loading="loading">
|
||||
保存修改
|
||||
</a-button>
|
||||
<a-button @click="resetForm">重置</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { getUserInfo, updateUserInfo, uploadAvatar } from '@/api/user'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const fileList = ref([])
|
||||
|
||||
const form = ref({
|
||||
username: '',
|
||||
nickname: '',
|
||||
email: '',
|
||||
avatar: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
username: [{ required: true, message: '请输入用户名' }],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址' }
|
||||
]
|
||||
}
|
||||
|
||||
const handleUploadChange = async (file: any) => {
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file.file)
|
||||
const res = await uploadAvatar(formData)
|
||||
form.value.avatar = res.url
|
||||
Message.success('头像上传成功')
|
||||
} catch (error) {
|
||||
Message.error('头像上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
const fetchUserInfo = async () => {
|
||||
try {
|
||||
const res = await getUserInfo()
|
||||
form.value = {
|
||||
username: res.username,
|
||||
nickname: res.nickname,
|
||||
email: res.email,
|
||||
avatar: res.avatar
|
||||
}
|
||||
} catch (error) {
|
||||
Message.error('获取用户信息失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await updateUserInfo(form.value)
|
||||
Message.success('信息更新成功')
|
||||
} catch (error) {
|
||||
Message.error('信息更新失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
fetchUserInfo()
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchUserInfo()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.edit-user {
|
||||
padding: 20px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.avatar-upload {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.upload-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.avatar-upload:hover .upload-mask {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.arco-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<a-card class="login-card" :bordered="false">
|
||||
<template #title>
|
||||
<div class="login-title">
|
||||
<h2>微信公众号订阅助手</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a-form :model="form" @submit="handleSubmit">
|
||||
<a-form-item field="username" label="用户名">
|
||||
<a-input v-model="form.username" placeholder="请输入用户名">
|
||||
<template #prefix><icon-user /></template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="password" label="密码">
|
||||
<a-input-password v-model="form.password" placeholder="请输入密码">
|
||||
<template #prefix><icon-lock /></template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit" :loading="loading" long>
|
||||
登录
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { login } from '@/api/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const form = ref({
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const handleSubmit = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await login(form.value)
|
||||
localStorage.setItem('token', res.access_token)
|
||||
router.push('/')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
Message.error('登录失败,请检查用户名和密码')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 400px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login-title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user