Merge "身份令牌验证与推荐接口"
diff --git a/LJC/personalpage/src/App.js b/LJC/personalpage/src/App.js
index ed241f4..f11e487 100644
--- a/LJC/personalpage/src/App.js
+++ b/LJC/personalpage/src/App.js
@@ -31,7 +31,7 @@
<main className="py-5">
<div className="max-w-6xl mx-auto">
<Routes>
- <Route path="/" element={<Navigate to="/user/11" replace />} />
+ {/* <Route path="/" element={<Navigate to="/user/11" replace />} /> */}
<Route path="/user/:userId" element={<UserProfileRoute />} />
</Routes>
</div>
diff --git a/LJC/personalpage/src/components/EditProfileForm.jsx b/LJC/personalpage/src/components/EditProfileForm.jsx
deleted file mode 100644
index d33cbcb..0000000
--- a/LJC/personalpage/src/components/EditProfileForm.jsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import React, { useState } from 'react';
-import { FaCamera, FaTimes } from 'react-icons/fa';
-
-const EditProfileForm = ({ user, onSave, onCancel }) => {
- const [avatar, setAvatar] = useState(user.avatar || '');
- const [bio, setBio] = useState(user.bio || '');
- const [gender, setGender] = useState('secret');
- const [birthday, setBirthday] = useState('');
- const [location, setLocation] = useState('');
-
- const handleSubmit = (e) => {
- e.preventDefault();
- onSave({ avatar, bio });
- };
-
- return (
- <form onSubmit={handleSubmit}>
- <div className="mb-6">
- <label className="block text-sm font-medium text-gray-700 mb-2">头像</label>
- <div className="flex items-center">
- <div className="relative">
- <div className="w-20 h-20 rounded-full bg-gradient-to-r from-pink-300 to-orange-300 flex items-center justify-center">
- {avatar ? (
- <img src={avatar} alt="Avatar" className="w-full h-full rounded-full" />
- ) : (
- <div className="text-white text-2xl">{user.username.charAt(0)}</div>
- )}
- </div>
- <button
- type="button"
- className="absolute bottom-0 right-0 bg-white rounded-full p-1 shadow-md"
- >
- <FaCamera className="text-gray-700 text-sm" />
- </button>
- </div>
- <div className="ml-4">
- <input
- type="text"
- value={avatar}
- onChange={(e) => setAvatar(e.target.value)}
- placeholder="输入头像URL"
- className="w-full rounded-md border-gray-300 shadow-sm text-sm"
- />
- </div>
- </div>
- </div>
-
- <div className="mb-4">
- <label className="block text-sm font-medium text-gray-700 mb-2">个人简介</label>
- <textarea
- value={bio}
- onChange={(e) => setBio(e.target.value)}
- rows="3"
- maxLength="100"
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm"
- placeholder="介绍一下自己吧~"
- />
- <div className="text-right text-xs text-gray-500 mt-1">{bio.length}/100</div>
- </div>
-
- <div className="grid grid-cols-2 gap-4 mb-4">
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">性别</label>
- <select
- value={gender}
- onChange={(e) => setGender(e.target.value)}
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm"
- >
- <option value="secret">保密</option>
- <option value="male">男</option>
- <option value="female">女</option>
- </select>
- </div>
-
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">生日</label>
- <input
- type="date"
- value={birthday}
- onChange={(e) => setBirthday(e.target.value)}
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm"
- />
- </div>
- </div>
-
- <div className="mb-6">
- <label className="block text-sm font-medium text-gray-700 mb-2">地区</label>
- <input
- type="text"
- value={location}
- onChange={(e) => setLocation(e.target.value)}
- placeholder="填写你所在的城市"
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm"
- />
- </div>
-
- <div className="flex justify-end space-x-3">
- <button
- type="button"
- onClick={onCancel}
- className="px-5 py-2 bg-gray-100 text-gray-700 rounded-full text-sm hover:bg-gray-200"
- >
- 取消
- </button>
- <button
- type="submit"
- className="px-5 py-2 bg-red-500 text-white rounded-full text-sm hover:bg-red-600"
- >
- 保存
- </button>
- </div>
- </form>
- );
-};
-
-export default EditProfileForm;
\ No newline at end of file
diff --git a/LJC/personalpage/src/components/FavoritePosts.jsx b/LJC/personalpage/src/components/FavoritePosts.jsx
deleted file mode 100644
index 4033d7b..0000000
--- a/LJC/personalpage/src/components/FavoritePosts.jsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { getFavorites } from '../services/api';
-import { FaHeart } from 'react-icons/fa';
-
-const FavoritePosts = ({ userId }) => {
- const [favorites, setFavorites] = useState([]);
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- const fetchFavorites = async () => {
- try {
- setLoading(true);
- const response = await getFavorites(userId);
- setFavorites(response.data);
- } catch (error) {
- console.error('Failed to fetch favorites:', error);
- } finally {
- setLoading(false);
- }
- };
-
- if (userId) {
- fetchFavorites();
- }
- }, [userId]);
-
- if (loading) {
- return (
- <div className="grid grid-cols-2 md:grid-cols-3 gap-4">
- {[1, 2, 3, 4, 5, 6].map(item => (
- <div key={item} className="bg-gray-100 rounded-xl aspect-square animate-pulse"></div>
- ))}
- </div>
- );
- }
-
- if (favorites.length === 0) {
- return (
- <div className="text-center py-16">
- <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-red-100 text-red-500 mb-4">
- <FaHeart className="text-2xl" />
- </div>
- <h3 className="text-lg font-medium text-gray-900">暂无收藏内容</h3>
- <p className="mt-1 text-gray-500">你还没有收藏任何笔记</p>
- </div>
- );
- }
-
- // 模拟瀑布流布局数据
- const waterfallData = favorites.map(post => ({
- ...post,
- height: Math.floor(Math.random() * 100) + 200 // 随机高度
- }));
-
- return (
- <div className="grid grid-cols-2 md:grid-cols-3 gap-4">
- {waterfallData.map(post => (
- <div
- key={post.id}
- className="bg-white rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-shadow"
- >
- <div
- className="relative bg-gray-200"
- style={{ height: `${post.height}px` }}
- >
- {/* 占位图片 */}
- <div className="absolute inset-0 bg-gradient-to-br from-pink-100 to-orange-100"></div>
-
- {/* 类型标签 */}
- <div className="absolute top-2 right-2 bg-black bg-opacity-50 text-white text-xs px-2 py-1 rounded-full">
- {post.type === 'image' ? '图文' :
- post.type === 'video' ? '视频' : '文档'}
- </div>
-
- {/* 收藏标记 */}
- <div className="absolute bottom-2 right-2 bg-red-500 rounded-full p-1">
- <FaHeart className="text-white text-xs" />
- </div>
- </div>
-
- <div className="p-3">
- <h3 className="font-medium line-clamp-2">{post.title}</h3>
- <div className="flex items-center mt-2 text-xs text-gray-500">
- <span>❤️ 2.5k</span>
- <span className="mx-2">•</span>
- <span>⭐ 156</span>
- </div>
- </div>
- </div>
- ))}
- </div>
- );
-};
-
-export default FavoritePosts;
\ No newline at end of file
diff --git a/LJC/personalpage/src/components/FollowButton.jsx b/LJC/personalpage/src/components/FollowButton.jsx
deleted file mode 100644
index 9e738d4..0000000
--- a/LJC/personalpage/src/components/FollowButton.jsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from 'react';
-import { followUser, unfollowUser } from '../services/api';
-
-const FollowButton = ({ userId, isFollowing, onFollowChange }) => {
- const handleFollow = async () => {
- try {
- if (isFollowing) {
- await unfollowUser(userId);
- onFollowChange(false);
- } else {
- await followUser(userId);
- onFollowChange(true);
- }
- } catch (error) {
- console.error('关注操作失败:', error);
- }
- };
-
- return (
- <button
- onClick={handleFollow}
- className={`px-6 py-2 rounded-full text-sm font-medium transition-all ${
- isFollowing
- ? 'bg-gray-100 text-gray-800 hover:bg-gray-200'
- : 'bg-red-500 text-white hover:bg-red-600'
- }`}
- >
- {isFollowing ? '已关注' : '关注'}
- </button>
- );
-};
-
-export default FollowButton;
\ No newline at end of file
diff --git a/Merge/back_trm/app/__pycache__/__init__.cpython-310.pyc b/Merge/back_trm/app/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..9368fce
--- /dev/null
+++ b/Merge/back_trm/app/__pycache__/__init__.cpython-310.pyc
Binary files differ
diff --git a/Merge/back_trm/app/__pycache__/routes.cpython-310.pyc b/Merge/back_trm/app/__pycache__/routes.cpython-310.pyc
new file mode 100644
index 0000000..e22e52b
--- /dev/null
+++ b/Merge/back_trm/app/__pycache__/routes.cpython-310.pyc
Binary files differ
diff --git a/Merge/back_trm/app/routes.py b/Merge/back_trm/app/routes.py
index 625ea6d..20cf99c 100644
--- a/Merge/back_trm/app/routes.py
+++ b/Merge/back_trm/app/routes.py
@@ -287,7 +287,7 @@
SessionLocal = sessionmaker(bind=engine)
session = SessionLocal()
f=Fpost(session)
- checres=f.checkid(data['userid'],'admin')
+ checres=f.checkid(data['userid'],'superadmin')
if(not checres):
f.recordlog(data['userid'],
'error',
diff --git a/Merge/back_wzy/__init__.py b/Merge/back_wzy/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Merge/back_wzy/__init__.py
diff --git a/Merge/back_wzy/__pycache__/app.cpython-310.pyc b/Merge/back_wzy/__pycache__/app.cpython-310.pyc
new file mode 100644
index 0000000..b733167
--- /dev/null
+++ b/Merge/back_wzy/__pycache__/app.cpython-310.pyc
Binary files differ
diff --git a/Merge/back_wzy/__pycache__/config.cpython-310.pyc b/Merge/back_wzy/__pycache__/config.cpython-310.pyc
index 0d915b2..325d84e 100644
--- a/Merge/back_wzy/__pycache__/config.cpython-310.pyc
+++ b/Merge/back_wzy/__pycache__/config.cpython-310.pyc
Binary files differ
diff --git a/Merge/back_wzy/app.py b/Merge/back_wzy/app.py
index a90f62f..0c371fd 100644
--- a/Merge/back_wzy/app.py
+++ b/Merge/back_wzy/app.py
@@ -1,13 +1,19 @@
# app.py
-from flask import Flask
+from flask import Flask,g,request
+import psutil
+import time
+import os
from flask_cors import CORS
from config import Config
from extensions import db, migrate
-
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+from utils.Fpost import Fpost;
+app = Flask(__name__)
+app.config.from_object(Config)
def create_app():
- app = Flask(__name__)
- app.config.from_object(Config)
+
# 启用 CORS:允许前端 http://localhost:5173 发起跨域请求
# 生产环境请根据实际域名调整 origins
@@ -24,6 +30,38 @@
return app
+proc=psutil.Process(os.getpid())
+@app.before_request
+def before_request():
+ g.start_time=time.time()
+ g.start_cpu=proc.cpu_times()
+ g.start_mem=proc.memory_info()
+
+@app.after_request
+def after_request(response):
+ end_time = time.time()
+ end_cpu = proc.cpu_times()
+ end_mem = proc.memory_info()
+
+ elapsed = end_time - g.start_time
+ cpu_user = end_cpu.user - g.start_cpu.user
+ cpu_sys = end_cpu.system - g.start_cpu.system
+ mem_rss = end_mem.rss - g.start_mem.rss
+
+ #写入性能消耗
+ engine=create_engine(Config.SQLURL)
+ SessionLocal = sessionmaker(bind=engine)
+ session = SessionLocal()
+ f=Fpost(session)
+ f.recordsyscost(
+ request.path,
+ elapsed,
+ cpu_user,
+ cpu_sys,
+ mem_rss
+ )
+ return response
+
# 只有直接用 python app.py 时,这段才会执行
if __name__ == '__main__':
app = create_app()
diff --git a/Merge/back_wzy/config.py b/Merge/back_wzy/config.py
index e5bdb32..205b03d 100644
--- a/Merge/back_wzy/config.py
+++ b/Merge/back_wzy/config.py
@@ -9,3 +9,4 @@
'SQLURL'
)
SQLALCHEMY_TRACK_MODIFICATIONS = False
+ SQLURL=os.getenv('SQLURL')
diff --git a/Merge/back_wzy/models/__pycache__/logs.cpython-310.pyc b/Merge/back_wzy/models/__pycache__/logs.cpython-310.pyc
new file mode 100644
index 0000000..3ce0df5
--- /dev/null
+++ b/Merge/back_wzy/models/__pycache__/logs.cpython-310.pyc
Binary files differ
diff --git a/Merge/back_wzy/models/__pycache__/syscost.cpython-310.pyc b/Merge/back_wzy/models/__pycache__/syscost.cpython-310.pyc
new file mode 100644
index 0000000..13585ba
--- /dev/null
+++ b/Merge/back_wzy/models/__pycache__/syscost.cpython-310.pyc
Binary files differ
diff --git a/Merge/back_wzy/models/syscost.py b/Merge/back_wzy/models/syscost.py
new file mode 100644
index 0000000..bbde029
--- /dev/null
+++ b/Merge/back_wzy/models/syscost.py
@@ -0,0 +1,15 @@
+from sqlalchemy import Column, BigInteger, DateTime, String, Float, func
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+class PerformanceData(Base):
+ __tablename__ = 'performance_data'
+
+ id = Column(BigInteger, primary_key=True, autoincrement=True)
+ record_time = Column(DateTime, nullable=False, server_default=func.now(), comment='记录时间')
+ endpoint = Column(String(255), nullable=True, comment='请求接口路径')
+ elapsed_time = Column(Float, nullable=False, comment='总耗时(秒)')
+ cpu_user = Column(Float, nullable=False, comment='用户态 CPU 时间差(秒)')
+ cpu_system = Column(Float, nullable=False, comment='系统态 CPU 时间差(秒)')
+ memory_rss = Column(BigInteger, nullable=False, comment='RSS 内存增量(字节)')
\ No newline at end of file
diff --git a/Merge/back_wzy/utils/Fpost.py b/Merge/back_wzy/utils/Fpost.py
new file mode 100644
index 0000000..c8b5f48
--- /dev/null
+++ b/Merge/back_wzy/utils/Fpost.py
@@ -0,0 +1,156 @@
+from models.user import User as users
+from models.post import Post as post
+import secrets
+import hashlib
+from datetime import datetime, timedelta
+from sqlalchemy.orm import Session
+from models.logs import Log
+from models.syscost import PerformanceData
+class Fpost:
+ def __init__(self,session:Session):
+ self.session=session
+ return
+
+
+ def getlist(self):
+ results = self.session.query(post.id, post.title,post.status)
+ return results
+
+ def getuserlist(self):
+ results= self.session.query(users.id, users.username, users.role)
+ return results
+
+ def giveadmin(self,userid):
+ res=self.session.query(users).filter(users.id==userid).first()
+ if not res:
+ return False
+ res.role='admin'
+ self.session.commit()
+ return True
+
+ def giveuser(self,userid):
+ res=self.session.query(users).filter(users.id==userid).first()
+ if not res:
+ return False
+ res.role='user'
+ self.session.commit()
+ return True
+
+ def givesuperadmin(self,userid):
+ res=self.session.query(users).filter(users.id==userid).first()
+ if not res:
+ return False
+ res.role='superadmin'
+ self.session.commit()
+ return True
+
+
+ def getpost(self,postid):
+ res=self.session.query(post).filter(post.id==postid).first()
+ return res
+ def checkid(self,userid,status=''):
+ res=self.session.query(users).filter(users.id==userid).first()
+ if(not res):
+ return False
+ if res.role !=status:
+ return False
+ return True
+
+ def review(self,postid,status):
+ print(status)
+ res=self.session.query(post).filter(post.id==postid).first()
+ if not res:
+ return False
+ res.status=status
+ self.session.commit()
+ return True
+
+ def createtoken(self, userid):
+ """
+ 根据userid创建token并插入到数据库
+ :param userid: 用户ID
+ :return: 生成的token字符串
+ """
+ # 生成随机盐值
+ salt = secrets.token_hex(16)
+
+ # 创建哈希值:userid + 当前时间戳 + 随机盐值
+ current_time = str(datetime.now().timestamp())
+ hash_input = f"{userid}_{current_time}_{salt}"
+
+ # 生成SHA256哈希值作为token
+ token = hashlib.sha256(hash_input.encode()).hexdigest()
+
+ # 设置时间
+ created_time = datetime.now()
+ expires_time = created_time + timedelta(days=1) # 一天后过期
+
+ try:
+ # 创建新的token记录
+ new_token = Token(
+ token=token,
+ expires_at=expires_time,
+ created_at=created_time
+ )
+
+ # 假设self.session是数据库会话对象
+ self.session.add(new_token)
+ self.session.commit()
+
+ return token
+
+ except Exception as e:
+ self.session.rollback()
+ raise Exception(f"创建token失败: {str(e)}")
+
+ def recordlog(self,user_id,log_type,content,ip):
+ """
+ 记录日志
+ :param user_id: 用户ID
+ :param log_type: 日志类型,'access','error','behavior','system'
+ :param content: 日志内容
+ :param ip: IP地址
+ """
+ try:
+ new_log = Log(
+ user_id=user_id,
+ type=log_type,
+ content=content,
+ ip=ip
+ )
+ self.session.add(new_log)
+ self.session.commit()
+ except Exception as e:
+ self.session.rollback()
+ raise Exception(f"记录日志失败: {str(e)}")
+
+ def getrecordlog(self):
+ res= self.session.query(Log).all()
+ return res
+
+ def recordsyscost(self, endpoint: str, elapsed_time: float, cpu_user: float, cpu_system: float, memory_rss: int):
+ """
+ 记录系统性能消耗到 performance_data 表
+ :param endpoint: 请求接口路径
+ :param elapsed_time: 总耗时(秒)
+ :param cpu_user: 用户态 CPU 时间差(秒)
+ :param cpu_system: 系统态 CPU 时间差(秒)
+ :param memory_rss: RSS 内存增量(字节)
+ """
+ try:
+ new_record = PerformanceData(
+ endpoint=endpoint,
+ elapsed_time=elapsed_time,
+ cpu_user=cpu_user,
+ cpu_system=cpu_system,
+ memory_rss=memory_rss
+ )
+ self.session.add(new_record)
+ self.session.commit()
+ except Exception as e:
+ self.session.rollback()
+ raise Exception(f"记录系统性能消耗失败: {e}")
+
+ def getsyscost(self):
+ res= self.session.query(PerformanceData).all()
+ return res
\ No newline at end of file
diff --git a/Merge/back_wzy/utils/__init__.py b/Merge/back_wzy/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Merge/back_wzy/utils/__init__.py
diff --git a/Merge/back_wzy/utils/__pycache__/Fpost.cpython-310.pyc b/Merge/back_wzy/utils/__pycache__/Fpost.cpython-310.pyc
new file mode 100644
index 0000000..92bc6b9
--- /dev/null
+++ b/Merge/back_wzy/utils/__pycache__/Fpost.cpython-310.pyc
Binary files differ
diff --git a/Merge/back_wzy/utils/__pycache__/__init__.cpython-310.pyc b/Merge/back_wzy/utils/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..2aacbf6
--- /dev/null
+++ b/Merge/back_wzy/utils/__pycache__/__init__.cpython-310.pyc
Binary files differ
diff --git a/Merge/front/package.json b/Merge/front/package.json
index 31c1d3e..54a8783 100644
--- a/Merge/front/package.json
+++ b/Merge/front/package.json
@@ -14,7 +14,8 @@
"web-vitals": "^2.1.4",
"lucide-react": "^0.468.0",
"antd": "^4.24.0",
- "crypto-js": "^4.2.0"
+ "crypto-js": "^4.2.0",
+ "recharts": "^2.1.9"
},
"scripts": {
"start": "react-scripts start",
diff --git a/Merge/front/src/App.jsx b/Merge/front/src/App.jsx
index 770bc0a..3ab9fca 100644
--- a/Merge/front/src/App.jsx
+++ b/Merge/front/src/App.jsx
@@ -1,22 +1,19 @@
-import React from 'react'
-import { BrowserRouter as Router } from 'react-router-dom'
-import Header from './components/Header'
-import Sidebar from './components/Sidebar'
-import AppRoutes from './router/App'
-import './App.css'
+import React from 'react';
+import Header from './components/Header';
+import Sidebar from './components/Sidebar';
+import AppRoutes from './router/App';
+import './App.css';
export default function App() {
return (
- <Router>
- <div className="app">
- <Header />
- <Sidebar />
- <main className="main-content">
- <div className="content-wrapper">
- <AppRoutes />
- </div>
- </main>
- </div>
- </Router>
- )
-}
+ <div className="app">
+ <Header />
+ <Sidebar />
+ <main className="main-content">
+ <div className="content-wrapper">
+ <AppRoutes />
+ </div>
+ </main>
+ </div>
+ );
+}
\ No newline at end of file
diff --git a/Merge/front/src/api/posts.js b/Merge/front/src/api/posts_trm.js
similarity index 68%
rename from Merge/front/src/api/posts.js
rename to Merge/front/src/api/posts_trm.js
index 37acf43..f28ec19 100644
--- a/Merge/front/src/api/posts.js
+++ b/Merge/front/src/api/posts_trm.js
@@ -128,4 +128,64 @@
})
if (!res.ok) throw new Error(`giveUser: ${res.status}`)
return res.json()
+}
+
+/**
+ * 获取事务日志
+ * POST /getrecordlog
+ * @param {number|string} userId 平台管理员的用户 ID
+ * @returns Promise<[ {id, user_id, type, content, ip, created_at}, … ]>
+ */
+export async function fetchRecordLog(userId) {
+ const res = await fetch(`${BASE}/getrecordlog`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ userid: userId })
+ })
+ if (!res.ok) throw new Error(`fetchRecordLog: ${res.status}`)
+ const json = await res.json()
+ console.log('fetchRecordLog response:', json)
+ if (json.status === 'error' && json.message === 'Unauthorized') {
+ throw new Error('Unauthorized')
+ }
+ let list
+ if (Array.isArray(json)) {
+ list = json
+ } else if (Array.isArray(json.data)) {
+ list = json.data
+ } else {
+ list = []
+ }
+ console.log('Normalized record log list:', list)
+ return list
+}
+
+/**
+ * 获取系统性能消耗数据
+ * POST /getsyscost
+ * @param {number|string} userId 平台管理员的用户 ID
+ * @returns Promise<[ {id, record_time, endpoint, elapsed_time, cpu_user, cpu_system, memory_rss}, … ]>
+ */
+export async function fetchSysCost(userId) {
+ const res = await fetch(`${BASE}/getsyscost`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ userid: userId })
+ })
+ if (!res.ok) throw new Error(`fetchSysCost: ${res.status}`)
+ const json = await res.json()
+ console.log('fetchSysCost response:', json)
+ if (json.status === 'error' && json.message === 'Unauthorized') {
+ throw new Error('Unauthorized')
+ }
+ let list
+ if (Array.isArray(json)) {
+ list = json
+ } else if (Array.isArray(json.data)) {
+ list = json.data
+ } else {
+ list = []
+ }
+ console.log('Normalized sys cost list:', list)
+ return list
}
\ No newline at end of file
diff --git a/Merge/front/src/components/Admin.js b/Merge/front/src/components/Admin.js
index da11100..2d97495 100644
--- a/Merge/front/src/components/Admin.js
+++ b/Merge/front/src/components/Admin.js
@@ -2,7 +2,7 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { Layout, Tabs, Input, List, Card, Button, Tag, Spin, Typography, Divider } from 'antd';
import '../style/Admin.css';
-import { fetchPosts, approvePost, rejectPost } from '../api/posts';
+import { fetchPosts, approvePost, rejectPost } from '../api/posts_trm';
export default function Admin() {
const ADMIN_USER_ID = 2;
diff --git a/Merge/front/src/components/LogsDashboard.js b/Merge/front/src/components/LogsDashboard.js
index 1bd6cb7..22047e2 100644
--- a/Merge/front/src/components/LogsDashboard.js
+++ b/Merge/front/src/components/LogsDashboard.js
@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
+import { NavLink, Outlet } from 'react-router-dom';
import '../style/Admin.css';
function LogsDashboard() {
@@ -19,25 +20,17 @@
return (
<div className="admin-container">
<h2>运行日志 & 性能 Dashboard</h2>
- <section className="dashboard-stats">
- <pre>{JSON.stringify(stats, null, 2)}</pre>
- </section>
- <section className="dashboard-logs">
- <table className="admin-table">
- <thead>
- <tr><th>时间</th><th>级别</th><th>消息</th></tr>
- </thead>
- <tbody>
- {logs.map((log, i) => (
- <tr key={i}>
- <td>{new Date(log.time).toLocaleString()}</td>
- <td>{log.level}</td>
- <td>{log.message}</td>
- </tr>
- ))}
- </tbody>
- </table>
- </section>
+ <nav className="dashboard-nav">
+ <NavLink to="transactions" className={({ isActive }) => isActive ? 'active' : ''}>
+ 事务日志
+ </NavLink>
+ <NavLink to="performance" className={({ isActive }) => isActive ? 'active' : ''}>
+ 性能日志
+ </NavLink>
+ </nav>
+
+ {/* nested routes will render here */}
+ <Outlet />
</div>
);
}
diff --git a/Merge/front/src/components/PerformanceLogs.js b/Merge/front/src/components/PerformanceLogs.js
new file mode 100644
index 0000000..be9eb99
--- /dev/null
+++ b/Merge/front/src/components/PerformanceLogs.js
@@ -0,0 +1,94 @@
+import React, { useState, useEffect } from 'react';
+import {
+ ResponsiveContainer, LineChart, Line,
+ XAxis, YAxis, Tooltip, CartesianGrid, Legend
+} from 'recharts';
+import { fetchSysCost } from '../api/posts_trm';
+
+function PerformanceLogs({ userId }) {
+ const [data, setData] = useState([]);
+
+ useEffect(() => {
+ fetchSysCost(userId)
+ .then(list => {
+ const msList = list.map(item => ({
+ ...item,
+ elapsed_time: item.elapsed_time * 1000,
+ cpu_user: item.cpu_user * 1000,
+ cpu_system: item.cpu_system * 1000,
+ memory_rss: item.memory_rss / (1024 * 1024*8), // convert bytes to MB
+ record_time_ts: new Date(item.record_time).getTime() // add numeric timestamp
+ }));
+ console.log('Converted data:', msList[0]); // debug first item
+ setData(msList);
+ })
+ .catch(err => console.error('fetchSysCost error:', err));
+ }, [userId]);
+
+ return (
+ <section className="dashboard-performance">
+ {/* 响应时间图表 */}
+ <div style={{ marginBottom: '30px' }}>
+ <h3>响应时间</h3>
+ <ResponsiveContainer width="100%" height={300}>
+ <LineChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
+ <CartesianGrid strokeDasharray="3 3" />
+ <XAxis
+ dataKey="record_time_ts"
+ type="number"
+ domain={['dataMin', 'dataMax']}
+ tickFormatter={ts => new Date(ts).toLocaleTimeString()}
+ />
+ <YAxis domain={[0, 'auto']} label={{ value: '时间 (ms)', angle: -90, position: 'insideLeft' }} />
+ <Tooltip formatter={(val) => typeof val === 'number' ? `${val.toFixed(2)} ms` : val} />
+ <Legend />
+ <Line type="monotone" dataKey="elapsed_time" stroke="#8884d8" name="响应时间 (ms)" />
+ </LineChart>
+ </ResponsiveContainer>
+ </div>
+
+ {/* CPU时间图表 */}
+ <div style={{ marginBottom: '30px' }}>
+ <h3>CPU 使用时间</h3>
+ <ResponsiveContainer width="100%" height={300}>
+ <LineChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
+ <CartesianGrid strokeDasharray="3 3" />
+ <XAxis
+ dataKey="record_time_ts"
+ type="number"
+ domain={['dataMin', 'dataMax']}
+ tickFormatter={ts => new Date(ts).toLocaleTimeString()}
+ />
+ <YAxis domain={[0, 'auto']} label={{ value: '时间 (ms)', angle: -90, position: 'insideLeft' }} />
+ <Tooltip formatter={(val) => typeof val === 'number' ? `${val.toFixed(2)} ms` : val} />
+ <Legend />
+ <Line type="monotone" dataKey="cpu_user" stroke="#82ca9d" name="CPU 用户时间 (ms)" />
+ <Line type="monotone" dataKey="cpu_system" stroke="#ffc658" name="CPU 系统时间 (ms)" />
+ </LineChart>
+ </ResponsiveContainer>
+ </div>
+
+ {/* 内存图表 */}
+ <div style={{ marginBottom: '30px' }}>
+ <h3>内存使用</h3>
+ <ResponsiveContainer width="100%" height={300}>
+ <LineChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
+ <CartesianGrid strokeDasharray="3 3" />
+ <XAxis
+ dataKey="record_time_ts"
+ type="number"
+ domain={['dataMin', 'dataMax']}
+ tickFormatter={ts => new Date(ts).toLocaleTimeString()}
+ />
+ <YAxis domain={[0, 'auto']} label={{ value: '内存 (MB)', angle: -90, position: 'insideLeft' }} />
+ <Tooltip formatter={(val) => typeof val === 'number' ? `${val.toFixed(2)} MB` : val} />
+ <Legend />
+ <Line type="monotone" dataKey="memory_rss" stroke="#ff7300" name="内存 RSS" />
+ </LineChart>
+ </ResponsiveContainer>
+ </div>
+ </section>
+ );
+}
+
+export default PerformanceLogs;
diff --git a/Merge/front/src/components/SuperAdmin.js b/Merge/front/src/components/SuperAdmin.js
index 817b708..f24e5d4 100644
--- a/Merge/front/src/components/SuperAdmin.js
+++ b/Merge/front/src/components/SuperAdmin.js
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { NavLink, Outlet } from 'react-router-dom';
import { Spin } from 'antd';
-import { fetchUserList } from '../api/posts';
+import { fetchUserList } from '../api/posts_trm';
import '../style/SuperAdmin.css';
export default function SuperAdmin() {
diff --git a/Merge/front/src/components/TransactionLogs.js b/Merge/front/src/components/TransactionLogs.js
new file mode 100644
index 0000000..9df8f67
--- /dev/null
+++ b/Merge/front/src/components/TransactionLogs.js
@@ -0,0 +1,50 @@
+import React, { useState, useEffect } from 'react';
+import { fetchRecordLog } from '../api/posts_trm';
+
+function TransactionLogs({ userId }) {
+ const [records, setRecords] = useState([]);
+
+ useEffect(() => {
+ fetchRecordLog(userId)
+ .then(data => setRecords(data))
+ .catch(err => console.error('fetchRecordLog error:', err));
+ }, [userId]);
+
+ return (
+ <section className="dashboard-logs">
+ <table className="admin-table">
+ <thead>
+ <tr>
+ <th>ID</th>
+ <th>用户ID</th>
+ <th>类型</th>
+ <th>内容</th>
+ <th>IP</th>
+ <th>创建时间</th>
+ </tr>
+ </thead>
+ <tbody>
+ {records.length > 0
+ ? records.map((r, i) => (
+ <tr key={i}>
+ <td>{r.id}</td>
+ <td>{r.user_id}</td>
+ <td>{r.type}</td>
+ <td>{r.content}</td>
+ <td>{r.ip}</td>
+ <td>{new Date(r.created_at).toLocaleString()}</td>
+ </tr>
+ ))
+ : (
+ <tr>
+ <td colSpan="6" style={{ textAlign: 'center' }}>暂无数据</td>
+ </tr>
+ )
+ }
+ </tbody>
+ </table>
+ </section>
+ );
+}
+
+export default TransactionLogs;
diff --git a/Merge/front/src/components/UserManagement.js b/Merge/front/src/components/UserManagement.js
index 4bd05c5..a48f8cf 100644
--- a/Merge/front/src/components/UserManagement.js
+++ b/Merge/front/src/components/UserManagement.js
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import '../style/Admin.css';
import { Select, message, Table } from 'antd';
-import { fetchUserList, giveUser, giveAdmin, giveSuperAdmin } from '../api/posts';
+import { fetchUserList, giveUser, giveAdmin, giveSuperAdmin } from '../api/posts_trm';
const { Option } = Select;
const ROLE_LIST = ['用户', '管理员', '超级管理员'];
diff --git a/Merge/front/src/index.js b/Merge/front/src/index.js
index 1ce450d..56520f8 100644
--- a/Merge/front/src/index.js
+++ b/Merge/front/src/index.js
@@ -8,8 +8,8 @@
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
-
- <App />
-
+ <BrowserRouter>
+ <App />
+ </BrowserRouter>
</React.StrictMode>
);
\ No newline at end of file
diff --git a/Merge/front/src/router/App.js b/Merge/front/src/router/App.js
index e1b9454..4d606f9 100644
--- a/Merge/front/src/router/App.js
+++ b/Merge/front/src/router/App.js
@@ -20,6 +20,9 @@
import ForgotPasswordPage from '../pages/ForgotPasswordPage/ForgotPasswordPage';
import TestDashboard from '../pages/TestDashboard/TestDashboard';
+import TransactionLogs from '../components/TransactionLogs';
+import PerformanceLogs from '../components/PerformanceLogs';
+
export default function AppRoutes() {
return (
<Routes>
@@ -44,9 +47,6 @@
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
<Route path="/test-dashboard" element={<TestDashboard />} />
- {/* 最后一个兜底 */}
- <Route path="*" element={<PlaceholderPage pageId="home" />} />
-
{/* 普通管理员,无 header */}
<Route path="admin" element={<AdminPage />} />
@@ -54,8 +54,17 @@
<Route path="superadmin" element={<SuperAdmin />}>
<Route index element={<Navigate to="users" replace />} />
<Route path="users" element={<UserManagement superAdminId={3} />} />
- <Route path="dashboard" element={<LogsDashboard />} />
+
+ {/* dashboard as layout */}
+ <Route path="dashboard" element={<LogsDashboard />}>
+ <Route index element={<Navigate to="transactions" replace />} />
+ <Route path="transactions" element={<TransactionLogs userId={1} />} />
+ <Route path="performance" element={<PerformanceLogs userId={1} />} />
+ </Route>
</Route>
+
+ {/* 最后一个兜底,放在最末尾 */}
+ <Route path="*" element={<PlaceholderPage pageId="home" />} />
</Routes>
);
}
\ No newline at end of file
diff --git a/Merge/front/src/style/Admin.css b/Merge/front/src/style/Admin.css
index 4a5bcb7..90e244d 100644
--- a/Merge/front/src/style/Admin.css
+++ b/Merge/front/src/style/Admin.css
@@ -386,4 +386,9 @@
.ant-layout-sider .ant-menu-item:nth-child(1),
.ant-layout-sider .ant-menu-item:nth-child(2) {
color: var(--xiaohongshu-red) !important;
+}
+
+.dashboard-nav {
+ display: flex;
+ gap: 1rem;
}
\ No newline at end of file