This commit is contained in:
t@123654
2025-05-16 15:56:10 +08:00
parent f59e3fd3af
commit ca038f9f12
54 changed files with 2642 additions and 376 deletions
-228
View File
@@ -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;
+41
View File
@@ -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()
+37
View File
@@ -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"}
+67
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
API_VERSION="/api/v1/wx"
+34
View File
@@ -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()
+410
View File
@@ -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条数据
+99
View File
@@ -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
+29
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+3
View File
@@ -0,0 +1,3 @@
from .article import Article
from .feed import Feed
from .user import User
+13
View File
@@ -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)
+3
View File
@@ -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()
+13
View File
@@ -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))
+27
View File
@@ -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)
+41
View File
@@ -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)
View File
+11 -13
View File
@@ -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,"出错了")
+47 -37
View File
@@ -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.
+4
View File
@@ -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
+109
View File
@@ -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()
+3
View File
@@ -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
+18 -59
View File
@@ -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")
+2
View File
@@ -0,0 +1,2 @@
*.log
node_modules/
+71
View File
@@ -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
+13
View File
@@ -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>
+1
View File
@@ -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

+213
View File
@@ -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>
+36
View File
@@ -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}`)
}
+33
View File
@@ -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')
}
+40
View File
@@ -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
+35
View File
@@ -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)
}
+33
View File
@@ -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'
}
})
}
+11
View File
@@ -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

+1
View File
@@ -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>
+41
View File
@@ -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>
+23
View File
@@ -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')
+56
View File
@@ -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
+38
View File
@@ -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;
}
+148
View File
@@ -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>
+195
View File
@@ -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>
+142
View File
@@ -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>
+193
View File
@@ -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>
+84
View File
@@ -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>
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+34
View File
@@ -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')
}
}
}
})