上传和查看帖子
Change-Id: I6a7987a94d35481c52e610a760816b60b773a378
diff --git a/API/API-TRM/WZY/xhs_front/README.md b/API/API-TRM/WZY/xhs_front/README.md
new file mode 100644
index 0000000..6f35930
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_front/README.md
@@ -0,0 +1,185 @@
+# 小红书内容创作平台
+
+这是一个基于 React + Vite 构建的小红书内容创作平台界面,完全复制了小红书官方创作服务平台的设计和功能。
+
+## 功能特性 ✨
+
+### 🎨 界面设计
+- **完全还原小红书官方设计**:精确复制了小红书创作服务平台的视觉风格
+- **响应式布局**:支持桌面端和移动端适配
+- **现代化UI**:使用 Lucide React 图标库,提供清晰美观的界面
+
+### 📤 上传功能
+- **双模式上传**:支持图片上传和视频上传两种模式
+- **拖拽上传**:支持文件拖拽到上传区域
+- **点击上传**:点击按钮选择文件上传
+- **文件验证**:
+ - 图片:支持 JPEG、JPG、PNG、WebP 格式,最大 32MB
+ - 视频:支持 MP4、MOV、AVI 格式,最大 2GB
+- **实时进度显示**:带有动画效果的上传进度条
+
+### 🖼️ 文件管理
+- **文件预览**:上传后实时显示文件缩略图
+- **文件信息**:显示文件名、大小等详细信息
+- **批量管理**:支持单个删除和批量清除
+- **文件计数**:实时显示已上传文件数量
+
+### 🎯 交互体验
+- **拖拽反馈**:拖拽时提供视觉反馈效果
+- **加载状态**:上传过程中的加载动画
+- **操作提示**:完成上传后的成功提示
+- **悬停效果**:按钮和文件项的悬停交互
+
+### 🗂️ 导航系统
+- **侧边栏导航**:完整的功能菜单
+- **可展开子菜单**:数据看板等功能的子选项
+- **活跃状态**:当前选中页面的高亮显示
+- **固定布局**:头部和侧边栏固定定位
+
+## 技术栈 🛠️
+
+- **前端框架**:React 19.1.0
+- **构建工具**:Vite 6.0.5
+- **图标库**:Lucide React 0.468.0
+- **样式**:纯 CSS(无预处理器)
+- **开发语言**:JavaScript + JSX
+
+## 安装运行 🚀
+
+1. **安装依赖**
+ ```bash
+ npm install
+ ```
+
+2. **启动开发服务器**
+ ```bash
+ npm run dev
+ ```
+
+3. **打开浏览器**
+ ```
+ http://localhost:5173
+ ```
+
+4. **构建生产版本**
+ ```bash
+ npm run build
+ ```
+
+## 项目结构 📁
+
+```
+发布页面/
+├── public/ # 静态资源
+│ └── vite.svg # Vite 图标
+├── src/
+│ ├── App.jsx # 主应用组件
+│ ├── App.css # 主样式文件
+│ ├── index.css # 全局样式
+│ └── main.jsx # 应用入口
+├── index.html # HTML 入口文件
+├── package.json # 项目配置
+├── vite.config.js # Vite 配置
+└── README.md # 项目说明
+```
+
+## 核心功能实现 💡
+
+### 文件上传处理
+```javascript
+const handleFileUpload = () => {
+ const input = document.createElement('input')
+ input.type = 'file'
+ input.accept = activeTab === 'video' ? 'video/*' : 'image/*'
+ input.multiple = activeTab === 'image'
+
+ input.onchange = (e) => {
+ const files = Array.from(e.target.files)
+ if (files.length > 0 && validateFiles(files)) {
+ simulateUpload(files)
+ }
+ }
+
+ input.click()
+}
+```
+
+### 拖拽上传实现
+```javascript
+const handleDrop = (e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ setIsDragOver(false)
+
+ const files = Array.from(e.dataTransfer.files)
+ if (files.length > 0 && validateFiles(files)) {
+ simulateUpload(files)
+ }
+}
+```
+
+### 文件验证机制
+```javascript
+const validateFiles = (files) => {
+ const validImageTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
+ const validVideoTypes = ['video/mp4', 'video/mov', 'video/avi']
+
+ const validTypes = activeTab === 'video' ? validVideoTypes : validImageTypes
+ const maxSize = activeTab === 'video' ? 2 * 1024 * 1024 * 1024 : 32 * 1024 * 1024
+
+ return files.every(file =>
+ validTypes.includes(file.type) && file.size <= maxSize
+ )
+}
+```
+
+## 样式特色 🎨
+
+### 响应式设计
+- 桌面端:固定侧边栏布局
+- 移动端:隐藏侧边栏,堆叠布局
+- 自适应文件网格:根据屏幕大小调整列数
+
+### 动画效果
+- 拖拽时的放大效果
+- 进度条的流光动画
+- 文件项的悬停过渡
+- 页面切换的淡入效果
+
+### 色彩方案
+- 主色调:#ff4757(小红书红)
+- 背景色:#f5f7fa(浅灰蓝)
+- 文字色:#333(深灰)
+- 边框色:#e8eaed(浅灰)
+
+## 待扩展功能 🔮
+
+- **后端集成**:连接真实的文件上传 API
+- **用户认证**:登录注册功能
+- **内容编辑**:笔记内容编辑器
+- **数据统计**:真实的数据看板功能
+- **社交功能**:评论、点赞等互动功能
+
+## 开发说明 📝
+
+这个项目完全基于前端实现,所有的上传功能都是模拟的。文件预览使用了 `URL.createObjectURL()` 来生成本地预览链接。在实际部署时,需要:
+
+1. 集成后端文件上传 API
+2. 实现用户认证系统
+3. 添加数据持久化
+4. 优化性能和安全性
+
+## 浏览器兼容性 🌐
+
+- Chrome 90+
+- Firefox 88+
+- Safari 14+
+- Edge 90+
+
+## 许可证 📄
+
+MIT License
+
+---
+
+**注意**:本项目仅用于学习和演示目的,请遵守相关法律法规和平台使用条款。
diff --git a/API/API-TRM/WZY/xhs_front/index.html b/API/API-TRM/WZY/xhs_front/index.html
new file mode 100644
index 0000000..b919940
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_front/index.html
@@ -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="root"></div>
+ <script type="module" src="/src/main.jsx"></script>
+ </body>
+</html>
diff --git a/API/API-TRM/WZY/xhs_front/package.json b/API/API-TRM/WZY/xhs_front/package.json
new file mode 100644
index 0000000..b1d8b25
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_front/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "xiaohongshu-creator-platform",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "lucide-react": "^0.468.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^7.6.2"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^4.3.4",
+ "vite": "^6.0.5"
+ }
+}
diff --git a/API/API-TRM/WZY/xhs_front/public/vite.svg b/API/API-TRM/WZY/xhs_front/public/vite.svg
new file mode 100644
index 0000000..ee9fada
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_front/public/vite.svg
@@ -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>
diff --git a/API/API-TRM/WZY/xhs_front/src/App.css b/API/API-TRM/WZY/xhs_front/src/App.css
new file mode 100644
index 0000000..8fdd8d7
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_front/src/App.css
@@ -0,0 +1,585 @@
+.app {
+ display: flex;
+ min-height: 100vh;
+ background-color: #f5f7fa;
+}
+
+/* Header */
+.header {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 60px;
+ background: #fff;
+ border-bottom: 1px solid #e8eaed;
+ display: flex;
+ align-items: center;
+ padding: 0 20px;
+ z-index: 1000;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.header-left {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.logo {
+ background: #ff4757;
+ color: white;
+ padding: 6px 12px;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: bold;
+}
+
+.header-title {
+ font-size: 18px;
+ font-weight: 500;
+ color: #333;
+}
+
+.header-right {
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.user-info {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: #666;
+ font-size: 14px;
+}
+
+/* Sidebar */
+.sidebar {
+ position: fixed;
+ left: 0;
+ top: 60px;
+ width: 200px;
+ height: calc(100vh - 60px);
+ background: #fff;
+ border-right: 1px solid #e8eaed;
+ overflow-y: auto;
+ z-index: 999;
+}
+
+.publish-btn {
+ margin: 16px;
+ background: #ff4757;
+ color: white;
+ padding: 10px 16px;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: 500;
+ text-align: center;
+ transition: background 0.2s;
+}
+
+.publish-btn:hover {
+ background: #ff3742;
+}
+
+.nav-menu {
+ padding: 0;
+ list-style: none;
+}
+
+.nav-item {
+ border-bottom: 1px solid #f0f0f0;
+}
+
+.nav-link {
+ display: flex;
+ align-items: center;
+ padding: 12px 20px;
+ color: #333;
+ font-size: 14px;
+ transition: all 0.2s;
+ gap: 8px;
+}
+
+.nav-link:hover {
+ background: #f8f9fa;
+ color: #ff4757;
+}
+
+.nav-link.active {
+ background: linear-gradient(135deg, #ff4757, #ff6b7a);
+ color: white;
+ font-weight: 500;
+}
+
+.nav-link.active .lucide {
+ color: white;
+}
+
+.nav-submenu {
+ padding-left: 20px;
+ background: #fafafa;
+}
+
+.nav-submenu .nav-link {
+ padding: 8px 20px;
+ font-size: 13px;
+ color: #666;
+}
+
+.nav-submenu .nav-link:hover {
+ color: #ff4757;
+}
+
+/* Main Content */
+.main-content {
+ margin-left: 200px;
+ padding-top: 60px;
+ flex: 1;
+ min-height: 100vh;
+}
+
+.content-wrapper {
+ padding: 20px;
+ /* 原来是 max-width:1200px; */
+ max-width: none; /* 或者直接注释掉这一行 */
+ width: auto; /* 确保它能撑满父级 */
+ margin: 0; /* 取消水平 auto 居中 */
+}
+
+/* Upload Area */
+.upload-tabs {
+ display: flex;
+ gap: 20px;
+ margin-bottom: 30px;
+ border-bottom: 1px solid #e8eaed;
+}
+
+.upload-tab {
+ padding: 12px 0;
+ font-size: 16px;
+ color: #666;
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ transition: all 0.2s;
+}
+
+.upload-tab.active {
+ color: #ff4757;
+ border-bottom-color: #ff4757;
+ font-weight: 500;
+}
+
+.upload-area {
+ background: #fff;
+ border-radius: 8px;
+ padding: 80px 40px;
+ text-align: center;
+ border: 2px dashed #ddd;
+ margin-bottom: 40px;
+ transition: all 0.2s;
+ min-height: 300px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ position: relative;
+}
+
+.upload-area:hover {
+ border-color: #ff4757;
+ background: #fff8f8;
+}
+
+.upload-area.drag-over {
+ border-color: #ff4757;
+ background: #fff0f0;
+ transform: scale(1.02);
+}
+
+.upload-icon {
+ width: 100px;
+ height: 100px;
+ margin: 0 auto 30px;
+ background: #f8f9fa;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 40px;
+ color: #ccc;
+ transition: all 0.3s ease;
+}
+
+.upload-area:hover .upload-icon {
+ background: #ff475710;
+ color: #ff4757;
+ transform: scale(1.1);
+}
+
+.upload-area.drag-over .upload-icon {
+ background: #ff475720;
+ color: #ff4757;
+ transform: scale(1.2);
+}
+
+.upload-title {
+ font-size: 20px;
+ color: #333;
+ margin-bottom: 12px;
+ font-weight: 500;
+}
+
+.upload-subtitle {
+ font-size: 14px;
+ color: #999;
+ margin-bottom: 30px;
+}
+
+.upload-btn {
+ background: #ff4757;
+ color: white;
+ padding: 14px 28px;
+ border-radius: 6px;
+ font-size: 16px;
+ font-weight: 500;
+ transition: background 0.2s;
+ min-width: 120px;
+}
+
+.upload-btn:hover:not(:disabled) {
+ background: #ff3742;
+}
+
+.upload-btn:disabled {
+ background: #ccc;
+ cursor: not-allowed;
+}
+
+.upload-btn.uploading {
+ background: #ff4757;
+ opacity: 0.8;
+}
+
+/* File Preview */
+.file-preview-area {
+ background: #fff;
+ border-radius: 8px;
+ padding: 20px;
+ margin-bottom: 40px;
+ border: 1px solid #e8eaed;
+}
+
+/* Preview Header */
+.preview-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+}
+
+.preview-title {
+ font-size: 16px;
+ color: #333;
+ margin-bottom: 16px;
+ font-weight: 500;
+}
+
+.clear-files-btn {
+ background: #ff4757;
+ color: white;
+ padding: 6px 12px;
+ border-radius: 4px;
+ font-size: 12px;
+ transition: background 0.2s;
+}
+
+.clear-files-btn:hover {
+ background: #ff3742;
+}
+
+.file-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+ gap: 16px;
+}
+
+.file-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 12px;
+ border: 1px solid #f0f0f0;
+ border-radius: 6px;
+ transition: all 0.2s ease;
+ position: relative;
+}
+
+.file-item:hover {
+ border-color: #ff4757;
+ box-shadow: 0 2px 8px rgba(255, 71, 87, 0.1);
+}
+
+.file-item:hover .remove-file-btn {
+ opacity: 1;
+}
+
+.remove-file-btn {
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ background: rgba(255, 71, 87, 0.8);
+ color: white;
+ border-radius: 50%;
+ width: 20px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 14px;
+ font-weight: bold;
+ opacity: 0;
+ transition: all 0.2s;
+}
+
+.file-thumbnail {
+ width: 80px;
+ height: 80px;
+ border-radius: 6px;
+ overflow: hidden;
+ margin-bottom: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #f8f9fa;
+}
+
+.file-thumbnail img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.video-thumbnail {
+ color: #666;
+}
+
+.file-info {
+ text-align: center;
+ width: 100%;
+}
+
+.file-name {
+ font-size: 12px;
+ color: #333;
+ margin-bottom: 4px;
+ font-weight: 500;
+}
+
+.file-size {
+ font-size: 11px;
+ color: #999;
+}
+
+/* Upload Progress */
+.progress-container {
+ margin-top: 20px;
+ width: 100%;
+ max-width: 400px;
+}
+
+.progress-bar {
+ width: 100%;
+ height: 8px;
+ background-color: #f0f0f0;
+ border-radius: 4px;
+ overflow: hidden;
+ margin-bottom: 8px;
+}
+
+.progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, #ff4757, #ff6b7a);
+ border-radius: 4px;
+ transition: width 0.3s ease;
+ position: relative;
+}
+
+.progress-fill::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
+ animation: shimmer 1.5s infinite;
+}
+
+@keyframes shimmer {
+ 0% { transform: translateX(-100%); }
+ 100% { transform: translateX(100%); }
+}
+
+.progress-text {
+ text-align: center;
+ font-size: 12px;
+ color: #666;
+ font-weight: 500;
+}
+
+/* Upload Info */
+.upload-info {
+ display: flex;
+ gap: 60px;
+ justify-content: center;
+ margin-top: 40px;
+ padding: 20px;
+ opacity: 1;
+ transition: opacity 0.3s ease;
+}
+
+.upload-info.fade-in {
+ animation: fadeIn 0.3s ease-in-out;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.info-item {
+ text-align: center;
+ flex: 1;
+ max-width: 300px;
+}
+
+.info-title {
+ font-size: 16px;
+ color: #333;
+ margin-bottom: 12px;
+ font-weight: 500;
+}
+
+.info-desc {
+ font-size: 13px;
+ color: #666;
+ line-height: 1.6;
+}
+
+/* Page Content Styles */
+.page-content {
+ padding: 40px;
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ margin: 20px 0;
+ min-height: 500px;
+}
+
+.page-header {
+ margin-bottom: 40px;
+ padding-bottom: 20px;
+ border-bottom: 1px solid #e8eaed;
+}
+
+.page-title {
+ font-size: 24px;
+ font-weight: 600;
+ color: #333;
+ margin: 0;
+}
+
+.page-body {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 400px;
+}
+
+.placeholder-content {
+ text-align: center;
+ max-width: 400px;
+}
+
+.placeholder-icon {
+ color: #ff4757;
+ margin-bottom: 20px;
+ display: flex;
+ justify-content: center;
+}
+
+.placeholder-title {
+ font-size: 20px;
+ font-weight: 500;
+ color: #333;
+ margin: 0 0 15px 0;
+}
+
+.placeholder-desc {
+ font-size: 14px;
+ color: #666;
+ line-height: 1.6;
+ margin: 0;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .sidebar {
+ transform: translateX(-100%);
+ transition: transform 0.3s;
+ }
+
+ .main-content {
+ margin-left: 0;
+ }
+
+ .header-title {
+ display: none;
+ }
+
+ .upload-area {
+ padding: 60px 20px;
+ margin: 0 10px 30px;
+ }
+
+ .upload-info {
+ flex-direction: column;
+ gap: 30px;
+ padding: 10px;
+ }
+
+ .content-wrapper {
+ padding: 15px;
+ }
+
+ .upload-tabs {
+ gap: 15px;
+ }
+
+ .page-content {
+ padding: 20px;
+ margin: 10px;
+ }
+
+ .page-title {
+ font-size: 20px;
+ }
+
+ .placeholder-title {
+ font-size: 18px;
+ }
+
+ .placeholder-desc {
+ font-size: 13px;
+ }
+}
diff --git a/API/API-TRM/WZY/xhs_front/src/App.jsx b/API/API-TRM/WZY/xhs_front/src/App.jsx
new file mode 100644
index 0000000..9a79c5c
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_front/src/App.jsx
@@ -0,0 +1,23 @@
+import React from 'react'
+import { BrowserRouter as Router } from 'react-router-dom'
+import Header from './components/Header'
+import Sidebar from './components/Sidebar'
+import AppRouter from './router'
+
+import './App.css'
+
+export default function App() {
+ return (
+ <Router>
+ <div className="app">
+ <Header />
+ <Sidebar />
+ <main className="main-content">
+ <div className="content-wrapper">
+ <AppRouter />
+ </div>
+ </main>
+ </div>
+ </Router>
+ )
+}
\ No newline at end of file
diff --git a/API/API-TRM/WZY/xhs_front/src/components/Header.jsx b/API/API-TRM/WZY/xhs_front/src/components/Header.jsx
new file mode 100644
index 0000000..60a50b7
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_front/src/components/Header.jsx
@@ -0,0 +1,20 @@
+import React from 'react'
+import { User } from 'lucide-react'
+import '../App.css' // 或者单独的 Header.css
+
+export default function Header() {
+ return (
+ <header className="header">
+ <div className="header-left">
+ <div className="logo">小红书</div>
+ <h1 className="header-title">创作服务平台</h1>
+ </div>
+ <div className="header-right">
+ <div className="user-info">
+ <User size={16} />
+ <span>小红薯63081EA1</span>
+ </div>
+ </div>
+ </header>
+ )
+}
\ No newline at end of file
diff --git a/API/API-TRM/WZY/xhs_front/src/components/HomeFeed.jsx b/API/API-TRM/WZY/xhs_front/src/components/HomeFeed.jsx
new file mode 100644
index 0000000..a7f70b5
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_front/src/components/HomeFeed.jsx
@@ -0,0 +1,72 @@
+import React, { useState } from 'react'
+import { ThumbsUp } from 'lucide-react'
+import '../style/HomeFeed.css'
+
+const mockItems = Array.from({ length: 20 }).map((_, i) => ({
+ id: i,
+ title: `示例标题 ${i + 1}`,
+ author: `作者 ${i + 1}`,
+ avatar: `https://i.pravatar.cc/40?img=${i + 1}`,
+ img: `https://picsum.photos/seed/${i}/300/200`,
+ likes: Math.floor(Math.random() * 1000)
+}))
+
+const categories = [
+ '推荐','穿搭','美食','彩妆','影视',
+ '职场','情感','家居','游戏','旅行','健身'
+]
+
+export default function HomeFeed() {
+ const [activeCat, setActiveCat] = useState('推荐')
+ const items = mockItems.filter(() => true) // 可按 activeCat 过滤
+
+ return (
+ <div className="home-feed">
+ {/* 顶部分类 */}
+ <nav className="feed-tabs">
+ {categories.map(cat => (
+ <button
+ key={cat}
+ className={cat === activeCat ? 'tab active' : 'tab'}
+ onClick={() => setActiveCat(cat)}
+ >
+ {cat}
+ </button>
+ ))}
+ </nav>
+
+ {/* 瀑布流卡片区 */}
+ <div className="feed-grid">
+ {items.map(item => (
+ <div key={item.id} className="feed-card">
+ {/* 封面图 */}
+ <img
+ className="card-img"
+ src={item.img}
+ alt={item.title}
+ />
+
+ {/* 标题 */}
+ <h3 className="card-title">{item.title}</h3>
+
+ {/* 底部作者 + 点赞 */}
+ <div className="card-footer">
+ <div className="card-author">
+ <img
+ className="avatar"
+ src={item.avatar}
+ alt={item.author}
+ />
+ <span className="username">{item.author}</span>
+ </div>
+ <div className="card-likes">
+ <ThumbsUp size={16} />
+ <span className="likes-count">{item.likes}</span>
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ )
+}
\ No newline at end of file
diff --git a/API/API-TRM/WZY/xhs_front/src/components/PlaceholderPage.jsx b/API/API-TRM/WZY/xhs_front/src/components/PlaceholderPage.jsx
new file mode 100644
index 0000000..b290eb4
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_front/src/components/PlaceholderPage.jsx
@@ -0,0 +1,55 @@
+import React from 'react'
+import {
+ Home,
+ BookOpen,
+ Activity,
+ Users
+} from 'lucide-react'
+import '../App.css' // 或 PlaceholderPage.css
+
+const icons = {
+ home: Home,
+ notebooks: BookOpen,
+ activity: Activity,
+ notes: BookOpen,
+ creator: Users,
+ journal: BookOpen,
+}
+
+const titles = {
+ home: '欢迎来到小红书创作平台',
+ notebooks: '笔记管理功能开发中',
+ activity: '活动中心功能开发中',
+ notes: '笔记灵感功能开发中',
+ creator: '创作学院功能开发中',
+ journal: '创作日刊功能开发中',
+}
+
+const descs = {
+ home: '在这里您可以管理您的创作内容,查看数据分析,获取创作灵感。',
+ notebooks: '这里将显示您的所有笔记,支持编辑、删除、分类等操作。',
+ activity: '这里将展示最新的平台活动,让您参与更多有趣的创作活动。',
+ notes: '这里将为您提供创作灵感和写作建议,帮助您创作更好的内容。',
+ creator: '这里将提供创作技巧教学和平台规则说明,助您成为优秀创作者。',
+ journal: '这里将展示创作相关的最新资讯和平台动态。',
+}
+
+export default function PlaceholderPage({ pageId }) {
+ const Icon = icons[pageId] || Home
+ return (
+ <div className="page-content">
+ <div className="page-header">
+ <h1 className="page-title">{titles[pageId]}</h1>
+ </div>
+ <div className="page-body">
+ <div className="placeholder-content">
+ <div className="placeholder-icon">
+ <Icon size={48} />
+ </div>
+ <h3 className="placeholder-title">{titles[pageId]}</h3>
+ <p className="placeholder-desc">{descs[pageId]}</p>
+ </div>
+ </div>
+ </div>
+ )
+}
\ No newline at end of file
diff --git a/API/API-TRM/WZY/xhs_front/src/components/Sidebar.jsx b/API/API-TRM/WZY/xhs_front/src/components/Sidebar.jsx
new file mode 100644
index 0000000..b6acad5
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_front/src/components/Sidebar.jsx
@@ -0,0 +1,95 @@
+import React, { useState, useEffect } from 'react'
+import { NavLink, useLocation, useNavigate } from 'react-router-dom'
+import {
+ Home,
+ BookOpen,
+ BarChart3,
+ Activity,
+ Users,
+ ChevronDown,
+} from 'lucide-react'
+
+
+const menuItems = [
+ { id: 'home', label: '首页', icon: Home, path: '/home' },
+ { id: 'notebooks', label: '笔记管理', icon: BookOpen, path: '/notebooks' },
+ {
+ id: 'dashboard',
+ label: '数据看板',
+ icon: BarChart3,
+ path: '/dashboard',
+ submenu: [
+ { id: 'overview', label: '账号概况', path: '/dashboard/overview' },
+ { id: 'content', label: '内容分析', path: '/dashboard/content' },
+ { id: 'fans', label: '粉丝数据', path: '/dashboard/fans' },
+ ]
+ },
+ { id: 'activity', label: '活动中心', icon: Activity, path: '/activity' },
+ { id: 'notes', label: '笔记灵感', icon: BookOpen, path: '/notes' },
+ { id: 'creator', label: '创作学院', icon: Users, path: '/creator' },
+ { id: 'journal', label: '创作日刊', icon: BookOpen, path: '/journal' },
+]
+
+export default function Sidebar() {
+ const [expandedMenu, setExpandedMenu] = useState(null)
+ const location = useLocation()
+ const navigate = useNavigate()
+
+ useEffect(() => {
+ if (location.pathname.startsWith('/dashboard')) {
+ setExpandedMenu('dashboard')
+ }
+ }, [location.pathname])
+
+ const toggleMenu = item => {
+ if (item.submenu) {
+ setExpandedMenu(expandedMenu === item.id ? null : item.id)
+ } else {
+ navigate(item.path)
+ setExpandedMenu(null)
+ }
+ }
+
+ return (
+ <aside className="sidebar">
+ <button className="publish-btn">发布笔记</button>
+ <nav className="nav-menu">
+ {menuItems.map(item => (
+ <div key={item.id} className="nav-item">
+ <a
+ href="#"
+ className={`nav-link${location.pathname === item.path ? ' active' : ''}`}
+ onClick={e => { e.preventDefault(); toggleMenu(item) }}
+ >
+ <item.icon size={16} />
+ <span>{item.label}</span>
+ {item.submenu && (
+ <ChevronDown
+ size={16}
+ style={{
+ marginLeft: 'auto',
+ transform: expandedMenu === item.id ? 'rotate(180deg)' : 'rotate(0deg)',
+ transition: 'transform 0.3s ease'
+ }}
+ />
+ )}
+ </a>
+ {item.submenu && expandedMenu === item.id && (
+ <div className="nav-submenu">
+ {item.submenu.map(sub => (
+ <NavLink
+ key={sub.id}
+ to={sub.path}
+ className={({ isActive }) => `nav-link${isActive ? ' active' : ''}`}
+ >
+ {sub.label}
+ </NavLink>
+ ))}
+ </div>
+ )}
+ </div>
+ ))}
+ </nav>
+ </aside>
+ )
+}
\ No newline at end of file
diff --git a/API/API-TRM/WZY/xhs_front/src/components/UploadPage.jsx b/API/API-TRM/WZY/xhs_front/src/components/UploadPage.jsx
new file mode 100644
index 0000000..e6db861
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_front/src/components/UploadPage.jsx
@@ -0,0 +1,200 @@
+import React, { useState } from 'react'
+import { Image, Video } from 'lucide-react'
+
+
+export default function UploadPage() {
+ const [activeTab, setActiveTab] = useState('image')
+ const [isDragOver, setIsDragOver] = useState(false)
+ const [isUploading, setIsUploading] = useState(false)
+ const [uploadedFiles, setUploadedFiles] = useState([])
+ const [uploadProgress, setUploadProgress] = useState(0)
+
+ const validateFiles = files => {
+ const imgTypes = ['image/jpeg','image/jpg','image/png','image/webp']
+ const vidTypes = ['video/mp4','video/mov','video/avi']
+ const types = activeTab==='video'? vidTypes : imgTypes
+ const max = activeTab==='video'? 2*1024*1024*1024 : 32*1024*1024
+
+ const invalid = files.filter(f => !types.includes(f.type) || f.size > max)
+ if (invalid.length) {
+ alert(`发现 ${invalid.length} 个无效文件,请检查文件格式和大小`)
+ return false
+ }
+ return true
+ }
+
+ const simulateUpload = files => {
+ setIsUploading(true)
+ setUploadProgress(0)
+ setUploadedFiles(files)
+ const iv = setInterval(() => {
+ setUploadProgress(p => {
+ if (p >= 100) {
+ clearInterval(iv)
+ setIsUploading(false)
+ alert(`成功上传了 ${files.length} 个文件`)
+ return 100
+ }
+ return p + 10
+ })
+ }, 200)
+ }
+
+ const handleFileUpload = () => {
+ if (isUploading) return
+ const input = document.createElement('input')
+ input.type = 'file'
+ input.accept = activeTab==='video'? 'video/*' : 'image/*'
+ input.multiple = activeTab==='image'
+ input.onchange = e => {
+ const files = Array.from(e.target.files)
+ if (files.length > 0 && validateFiles(files)) simulateUpload(files)
+ }
+ input.click()
+ }
+
+ const handleDragOver = e => { e.preventDefault(); e.stopPropagation(); setIsDragOver(true) }
+ const handleDragLeave = e => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false) }
+ const handleDrop = e => {
+ e.preventDefault(); e.stopPropagation(); setIsDragOver(false)
+ if (isUploading) return
+ const files = Array.from(e.dataTransfer.files)
+ if (files.length > 0 && validateFiles(files)) simulateUpload(files)
+ }
+
+ const clearFiles = () => setUploadedFiles([])
+ const removeFile = idx => setUploadedFiles(f => f.filter((_,i) => i!==idx))
+
+ return (
+ <>
+ <div className="upload-tabs">
+ <button
+ className={`upload-tab${activeTab==='video'?' active':''}`}
+ onClick={() => setActiveTab('video')}
+ >上传视频</button>
+ <button
+ className={`upload-tab${activeTab==='image'?' active':''}`}
+ onClick={() => setActiveTab('image')}
+ >上传图文</button>
+ </div>
+
+ <div
+ className={`upload-area${isDragOver?' drag-over':''}`}
+ onDragOver={handleDragOver}
+ onDragLeave={handleDragLeave}
+ onDrop={handleDrop}
+ >
+ <div className="upload-icon">
+ {activeTab==='video'? <Video/> : <Image/>}
+ </div>
+ <h2 className="upload-title">
+ {activeTab==='video'
+ ? '拖拽视频到此处或点击上传'
+ : '拖拽图片到此处或点击上传'
+ }
+ </h2>
+ <p className="upload-subtitle">(需支持上传格式)</p>
+ <button
+ className={`upload-btn${isUploading?' uploading':''}`}
+ onClick={handleFileUpload}
+ disabled={isUploading}
+ >
+ {isUploading
+ ? `上传中... ${uploadProgress}%`
+ : activeTab==='video'
+ ? '上传视频'
+ : '上传图片'
+ }
+ </button>
+
+ {isUploading && (
+ <div className="progress-container">
+ <div className="progress-bar">
+ <div
+ className="progress-fill"
+ style={{ width: `${uploadProgress}%` }}
+ />
+ </div>
+ <div className="progress-text">{uploadProgress}%</div>
+ </div>
+ )}
+ </div>
+
+ {uploadedFiles.length > 0 && (
+ <div className="file-preview-area">
+ <div className="preview-header">
+ <h3 className="preview-title">已上传文件 ({uploadedFiles.length})</h3>
+ <button className="clear-files-btn" onClick={clearFiles}>
+ 清除所有
+ </button>
+ </div>
+ <div className="file-grid">
+ {uploadedFiles.map((file, i) => (
+ <div key={i} className="file-item">
+ <button
+ className="remove-file-btn"
+ onClick={() => removeFile(i)}
+ title="删除文件"
+ >×</button>
+ {file.type.startsWith('image/') ? (
+ <div className="file-thumbnail">
+ <img src={URL.createObjectURL(file)} alt={file.name} />
+ </div>
+ ) : (
+ <div className="file-thumbnail video-thumbnail">
+ <Video size={24} />
+ </div>
+ )}
+ <div className="file-info">
+ <div className="file-name" title={file.name}>
+ {file.name.length > 20
+ ? file.name.slice(0,17) + '...'
+ : file.name
+ }
+ </div>
+ <div className="file-size">
+ {(file.size/1024/1024).toFixed(2)} MB
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+
+ <div className="upload-info fade-in">
+ {activeTab==='image' ? (
+ <>
+ <div className="info-item">
+ <h3 className="info-title">图片大小</h3>
+ <p className="info-desc">最大32MB</p>
+ </div>
+ <div className="info-item">
+ <h3 className="info-title">图片格式</h3>
+ <p className="info-desc">png/jpg/jpeg/webp</p>
+ </div>
+ <div className="info-item">
+ <h3 className="info-title">分辨率</h3>
+ <p className="info-desc">建议720×960及以上</p>
+ </div>
+ </>
+ ) : (
+ <>
+ <div className="info-item">
+ <h3 className="info-title">视频大小</h3>
+ <p className="info-desc">最大2GB,时长≤5分钟</p>
+ </div>
+ <div className="info-item">
+ <h3 className="info-title">视频格式</h3>
+ <p className="info-desc">mp4/mov</p>
+ </div>
+ <div className="info-item">
+ <h3 className="info-title">分辨率</h3>
+ <p className="info-desc">建议720P及以上</p>
+ </div>
+ </>
+ )}
+ </div>
+ </>
+ )
+}
\ No newline at end of file
diff --git a/API/API-TRM/WZY/xhs_front/src/index.css b/API/API-TRM/WZY/xhs_front/src/index.css
new file mode 100644
index 0000000..72c144a
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_front/src/index.css
@@ -0,0 +1,29 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ background-color: #f5f7fa;
+}
+
+button {
+ border: none;
+ background: none;
+ cursor: pointer;
+ font-family: inherit;
+}
+
+a {
+ text-decoration: none;
+ color: inherit;
+}
+
+#root {
+ width: 100%;
+ min-height: 100vh;
+}
diff --git a/API/API-TRM/WZY/xhs_front/src/main.jsx b/API/API-TRM/WZY/xhs_front/src/main.jsx
new file mode 100644
index 0000000..b9a1a6d
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_front/src/main.jsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.jsx'
+
+createRoot(document.getElementById('root')).render(
+ <StrictMode>
+ <App />
+ </StrictMode>,
+)
diff --git a/API/API-TRM/WZY/xhs_front/src/router/index.jsx b/API/API-TRM/WZY/xhs_front/src/router/index.jsx
new file mode 100644
index 0000000..64191ac
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_front/src/router/index.jsx
@@ -0,0 +1,24 @@
+import React from 'react'
+import { Routes, Route, Navigate } from 'react-router-dom'
+import UploadPage from '../components/UploadPage'
+import PlaceholderPage from '../components/PlaceholderPage'
+import HomeFeed from '../components/HomeFeed'
+
+export default function AppRouter() {
+ return (
+ <Routes>
+ <Route path="/" element={<Navigate to="/dashboard" replace />} />
+
+ <Route path="/home" element={<HomeFeed />} />
+ <Route path="/notebooks" element={<PlaceholderPage pageId="notebooks" />} />
+ <Route path="/activity" element={<PlaceholderPage pageId="activity" />} />
+ <Route path="/notes" element={<PlaceholderPage pageId="notes" />} />
+ <Route path="/creator" element={<PlaceholderPage pageId="creator" />} />
+ <Route path="/journal" element={<PlaceholderPage pageId="journal" />} />
+
+ <Route path="/dashboard/*" element={<UploadPage />} />
+
+ <Route path="*" element={<PlaceholderPage pageId="home" />} />
+ </Routes>
+ )
+}
\ No newline at end of file
diff --git a/API/API-TRM/WZY/xhs_front/src/style/HomeFeed.css b/API/API-TRM/WZY/xhs_front/src/style/HomeFeed.css
new file mode 100644
index 0000000..f1bf75d
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_front/src/style/HomeFeed.css
@@ -0,0 +1,116 @@
+/* --------- 容器 & Tabs --------- */
+.home-feed {
+ padding: 20px;
+}
+
+.feed-tabs {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ margin-bottom: 20px;
+}
+
+.feed-tabs .tab {
+ padding: 6px 12px;
+ border: none;
+ background: #f0f0f0;
+ border-radius: 16px;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+.feed-tabs .tab.active {
+ background: #ff4757;
+ color: #fff;
+}
+
+/* --------- 瀑布流布局 --------- */
+.feed-grid {
+ display: grid;
+ grid-gap: 16px;
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
+}
+
+/* --------- 卡片样式及最大高度限制 --------- */
+.feed-card {
+ display: flex;
+ flex-direction: column;
+ max-height: 360px; /* 卡片最大高度 */
+ overflow: hidden; /* 超出部分隐藏 */
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 1px 4px rgba(0,0,0,0.1);
+ transition: transform 0.2s;
+}
+
+.feed-card:hover {
+ transform: translateY(-4px);
+}
+
+/* 封面图固定高度 */
+.card-img {
+ width: 100%;
+ height: 180px; /* 固定图片区域高度 */
+ object-fit: cover;
+ flex-shrink: 0; /* 不随容器收缩 */
+}
+
+/* 标题填充剩余空间 */
+.card-title {
+ font-size: 14px;
+ color: #333;
+ margin: 12px;
+ line-height: 1.4;
+ flex: 1; /* 占满中间区域 */
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 2; /* 最多两行 */
+ -webkit-box-orient: vertical;
+}
+
+/* --------- 底部:作者 + 点赞 --------- */
+.card-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 12px;
+ border-top: 1px solid #f0f0f0;
+ background: #fff;
+ flex-shrink: 0; /* 保持在底部 */
+}
+
+/* 作者区域 */
+.card-author {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.card-author .avatar {
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ object-fit: cover;
+}
+
+.card-author .username {
+ font-size: 13px;
+ color: #333;
+}
+
+/* 点赞区域 */
+.card-likes {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.card-likes svg {
+ color: #ff4757;
+}
+
+.card-likes .likes-count {
+ font-size: 13px;
+ color: #666;
+}
\ No newline at end of file
diff --git a/API/API-TRM/WZY/xhs_front/vite.config.js b/API/API-TRM/WZY/xhs_front/vite.config.js
new file mode 100644
index 0000000..8b0f57b
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_front/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+})
diff --git a/API/API-TRM/WZY/xhs_server/__pycache__/app.cpython-312.pyc b/API/API-TRM/WZY/xhs_server/__pycache__/app.cpython-312.pyc
new file mode 100644
index 0000000..215bc7f
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/__pycache__/app.cpython-312.pyc
Binary files differ
diff --git a/API/API-TRM/WZY/xhs_server/__pycache__/config.cpython-312.pyc b/API/API-TRM/WZY/xhs_server/__pycache__/config.cpython-312.pyc
new file mode 100644
index 0000000..4e1a3be
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/__pycache__/config.cpython-312.pyc
Binary files differ
diff --git a/API/API-TRM/WZY/xhs_server/__pycache__/extensions.cpython-312.pyc b/API/API-TRM/WZY/xhs_server/__pycache__/extensions.cpython-312.pyc
new file mode 100644
index 0000000..27ba8c2
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/__pycache__/extensions.cpython-312.pyc
Binary files differ
diff --git a/API/API-TRM/WZY/xhs_server/all_tables.sql b/API/API-TRM/WZY/xhs_server/all_tables.sql
new file mode 100644
index 0000000..fd5d1e5
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/all_tables.sql
@@ -0,0 +1,182 @@
+/*
+数据库设计说明:
+1. 核心表结构:
+ users:存储用户信息,包含角色管理和账号状态
+ posts:核心内容表,支持多模态内容(图文/视频/文档)
+ behaviors:记录用户互动行为(点赞/收藏/浏览等)
+ comments:评论系统,支持多级回复
+ follows:用户社交关系
+2. 推荐系统支持:
+ posts.heat 字段存储动态计算的热度值
+ behaviors 表记录用户行为用于协同过滤
+ user_tags 表构建用户兴趣画像
+ 通过 post_tags 实现内容标签分类
+3. 多模态内容处理:
+ posts.media_urls 使用 JSON 类型存储多个资源 URL
+ posts.type 区分不同类型的内容(图文/视频/文档)
+4. 审核与安全:
+ audits 表记录内容审核历史
+ posts.status 管理内容生命周期状态
+ logs 表记录系统操作和访问日志
+5. 性能优化:
+ 为查询频繁字段添加索引(热度/行为类型/时间)
+ 使用 JSON 类型存储灵活数据(通知内容/媒体资源)
+ 通过 heat 字段预计算支持热门排序
+6. 扩展性设计:
+ 用户画像系统通过 user_tags 表实现
+ 通知系统支持多种互动类型
+ 行为表设计支持未来扩展新行为类型
+*/
+
+DROP DATABASE IF EXISTS redbook;
+
+-- 创建数据库
+CREATE DATABASE IF NOT EXISTS redbook DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;
+USE redbook;
+
+-- 用户表
+CREATE TABLE users (
+ id INT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID',
+ username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
+ password VARCHAR(255) NOT NULL COMMENT '加密密码',
+ email VARCHAR(100) NOT NULL UNIQUE COMMENT '邮箱',
+ avatar VARCHAR(255) COMMENT '头像URL',
+ role ENUM('user', 'admin') DEFAULT 'user' COMMENT '角色',
+ bio VARCHAR(255) COMMENT '个人简介',
+ status ENUM('active', 'banned', 'muted') DEFAULT 'active' COMMENT '账号状态',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
+) ENGINE=InnoDB COMMENT='用户表';
+
+-- 标签表
+CREATE TABLE tags (
+ id INT AUTO_INCREMENT PRIMARY KEY COMMENT '标签ID',
+ name VARCHAR(50) NOT NULL UNIQUE COMMENT '标签名称',
+ description VARCHAR(255) COMMENT '标签描述',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
+) ENGINE=InnoDB COMMENT='内容标签表';
+
+-- 话题/超话表
+CREATE TABLE topics (
+ id INT AUTO_INCREMENT PRIMARY KEY COMMENT '话题ID',
+ name VARCHAR(100) NOT NULL UNIQUE COMMENT '话题名称',
+ description TEXT COMMENT '话题描述',
+ status ENUM('active', 'archived') DEFAULT 'active' COMMENT '状态',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
+) ENGINE=InnoDB COMMENT='话题/超话表';
+
+-- 内容帖子表
+CREATE TABLE posts (
+ id INT AUTO_INCREMENT PRIMARY KEY COMMENT '帖子ID',
+ user_id INT NOT NULL COMMENT '作者ID',
+ topic_id INT COMMENT '所属话题ID',
+ type ENUM('text', 'image', 'video', 'document') DEFAULT 'text' COMMENT '内容类型',
+ title VARCHAR(255) NOT NULL COMMENT '标题',
+ content TEXT NOT NULL COMMENT '正文内容',
+ media_urls JSON COMMENT '媒体资源URL数组',
+ status ENUM('draft', 'pending', 'published', 'deleted', 'rejected') DEFAULT 'draft' COMMENT '状态',
+ heat INT DEFAULT 0 COMMENT '热度值',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (topic_id) REFERENCES topics(id) ON DELETE SET NULL
+) ENGINE=InnoDB COMMENT='内容帖子表';
+
+-- 帖子标签关联表
+CREATE TABLE post_tags (
+ post_id INT NOT NULL COMMENT '帖子ID',
+ tag_id INT NOT NULL COMMENT '标签ID',
+ PRIMARY KEY (post_id, tag_id),
+ FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
+ FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
+) ENGINE=InnoDB COMMENT='帖子标签关联表';
+
+-- 用户行为表
+CREATE TABLE behaviors (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '行为ID',
+ user_id INT NOT NULL COMMENT '用户ID',
+ post_id INT NOT NULL COMMENT '帖子ID',
+ type ENUM('like', 'comment', 'favorite', 'view', 'share') NOT NULL COMMENT '行为类型',
+ value INT DEFAULT 1 COMMENT '行为值',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '行为时间',
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE
+) ENGINE=InnoDB COMMENT='用户行为记录表';
+
+-- 评论表
+CREATE TABLE comments (
+ id INT AUTO_INCREMENT PRIMARY KEY COMMENT '评论ID',
+ post_id INT NOT NULL COMMENT '帖子ID',
+ user_id INT NOT NULL COMMENT '用户ID',
+ parent_id INT DEFAULT NULL COMMENT '父评论ID',
+ content TEXT NOT NULL COMMENT '评论内容',
+ status ENUM('active', 'deleted') DEFAULT 'active' COMMENT '状态',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (parent_id) REFERENCES comments(id) ON DELETE CASCADE
+) ENGINE=InnoDB COMMENT='评论表';
+
+-- 用户关注关系表
+CREATE TABLE follows (
+ follower_id INT NOT NULL COMMENT '关注者ID',
+ followee_id INT NOT NULL COMMENT '被关注者ID',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '关注时间',
+ PRIMARY KEY (follower_id, followee_id),
+ FOREIGN KEY (follower_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (followee_id) REFERENCES users(id) ON DELETE CASCADE
+) ENGINE=InnoDB COMMENT='用户关注关系表';
+
+-- 通知表
+CREATE TABLE notifications (
+ id INT AUTO_INCREMENT PRIMARY KEY COMMENT '通知ID',
+ user_id INT NOT NULL COMMENT '接收用户ID',
+ type ENUM('like', 'comment', 'follow', 'system', 'audit') NOT NULL COMMENT '通知类型',
+ content JSON NOT NULL COMMENT '通知内容',
+ is_read BOOLEAN DEFAULT FALSE COMMENT '是否已读',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+) ENGINE=InnoDB COMMENT='用户通知表';
+
+-- 审核记录表
+CREATE TABLE audits (
+ id INT AUTO_INCREMENT PRIMARY KEY COMMENT '审核ID',
+ post_id INT NOT NULL COMMENT '帖子ID',
+ admin_id INT NOT NULL COMMENT '管理员ID',
+ result ENUM('approved', 'rejected') NOT NULL COMMENT '审核结果',
+ reason VARCHAR(255) COMMENT '审核原因',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '审核时间',
+ FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
+ FOREIGN KEY (admin_id) REFERENCES users(id) ON DELETE CASCADE
+) ENGINE=InnoDB COMMENT='内容审核记录表';
+
+-- 日志表
+CREATE TABLE logs (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '日志ID',
+ user_id INT DEFAULT NULL COMMENT '用户ID',
+ type ENUM('access', 'error', 'behavior', 'system') NOT NULL COMMENT '日志类型',
+ content TEXT NOT NULL COMMENT '日志内容',
+ ip VARCHAR(45) COMMENT 'IP地址',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '记录时间',
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
+) ENGINE=InnoDB COMMENT='系统日志表';
+
+-- 用户兴趣标签表(用户画像)
+CREATE TABLE user_tags (
+ user_id INT NOT NULL COMMENT '用户ID',
+ tag_id INT NOT NULL COMMENT '标签ID',
+ weight FLOAT DEFAULT 1.0 COMMENT '兴趣权重',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ PRIMARY KEY (user_id, tag_id),
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
+) ENGINE=InnoDB COMMENT='用户兴趣标签表';
+
+-- 索引优化
+CREATE INDEX idx_posts_heat ON posts(heat);
+CREATE INDEX idx_behaviors_type ON behaviors(type);
+CREATE INDEX idx_notifications_read ON notifications(is_read);
+CREATE INDEX idx_logs_created ON logs(created_at);
+CREATE INDEX idx_comments_post ON comments(post_id);
\ No newline at end of file
diff --git a/API/API-TRM/WZY/xhs_server/app.py b/API/API-TRM/WZY/xhs_server/app.py
new file mode 100644
index 0000000..226bf18
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/app.py
@@ -0,0 +1,25 @@
+# app.py
+from flask import Flask
+from config import Config
+from extensions import db, migrate # ← 改自 extensions
+
+def create_app():
+ app = Flask(__name__)
+ app.config.from_object(Config)
+
+ db.init_app(app)
+ migrate.init_app(app, db)
+
+ # 在工厂函数里再导入、注册蓝图
+ from routes.posts import posts_bp
+ from routes.comments import comments_bp
+
+ app.register_blueprint(posts_bp, url_prefix='/posts')
+ app.register_blueprint(comments_bp, url_prefix='/posts/<int:post_id>/comments')
+ return app
+
+app = create_app()
+
+if __name__ == '__main__':
+ # 只有直接用 python app.py 时,这段才会执行
+ app.run(host='0.0.0.0', port=5000, debug=True)
\ No newline at end of file
diff --git a/API/API-TRM/WZY/xhs_server/config.py b/API/API-TRM/WZY/xhs_server/config.py
new file mode 100644
index 0000000..b83aead
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/config.py
@@ -0,0 +1,12 @@
+# config.py
+import os
+
+basedir = os.path.abspath(os.path.dirname(__file__))
+
+class Config:
+ SECRET_KEY = os.environ.get('SECRET_KEY', 'you-will-never-guess')
+ SQLALCHEMY_DATABASE_URI = os.environ.get(
+ 'DATABASE_URL',
+ 'mysql+pymysql://root:password@localhost/redbook?charset=utf8mb4'
+ )
+ SQLALCHEMY_TRACK_MODIFICATIONS = False
diff --git a/API/API-TRM/WZY/xhs_server/extensions.py b/API/API-TRM/WZY/xhs_server/extensions.py
new file mode 100644
index 0000000..3563787
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/extensions.py
@@ -0,0 +1,6 @@
+# extensions.py(根目录,同级于 app.py)
+from flask_sqlalchemy import SQLAlchemy
+from flask_migrate import Migrate
+
+db = SQLAlchemy()
+migrate = Migrate()
diff --git a/API/API-TRM/WZY/xhs_server/manage.py b/API/API-TRM/WZY/xhs_server/manage.py
new file mode 100644
index 0000000..15ab995
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/manage.py
@@ -0,0 +1,12 @@
+# manage.py
+from flask_script import Manager
+from flask_migrate import MigrateCommand
+
+from app import app, db
+
+manager = Manager(app)
+# 添加 db 子命令: migrate, upgrade, downgrade 等
+manager.add_command('db', MigrateCommand)
+
+if __name__ == '__main__':
+ manager.run()
diff --git a/API/API-TRM/WZY/xhs_server/models/__init__.py b/API/API-TRM/WZY/xhs_server/models/__init__.py
new file mode 100644
index 0000000..f7cef7b
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/models/__init__.py
@@ -0,0 +1,12 @@
+# models/__init__.py
+# --------------------------------------------------
+# 绝**对**不要**从** app.py 导入 db!
+# 改成:
+from extensions import db
+
+from .user import User
+from .topic import Topic
+from .tag import Tag
+from .post import Post, post_tags
+from .behavior import Behavior
+from .comment import Comment
diff --git a/API/API-TRM/WZY/xhs_server/models/__pycache__/__init__.cpython-312.pyc b/API/API-TRM/WZY/xhs_server/models/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..fe53b2a
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/models/__pycache__/__init__.cpython-312.pyc
Binary files differ
diff --git a/API/API-TRM/WZY/xhs_server/models/__pycache__/behavior.cpython-312.pyc b/API/API-TRM/WZY/xhs_server/models/__pycache__/behavior.cpython-312.pyc
new file mode 100644
index 0000000..71f3757
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/models/__pycache__/behavior.cpython-312.pyc
Binary files differ
diff --git a/API/API-TRM/WZY/xhs_server/models/__pycache__/comment.cpython-312.pyc b/API/API-TRM/WZY/xhs_server/models/__pycache__/comment.cpython-312.pyc
new file mode 100644
index 0000000..393c6d3
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/models/__pycache__/comment.cpython-312.pyc
Binary files differ
diff --git a/API/API-TRM/WZY/xhs_server/models/__pycache__/post.cpython-312.pyc b/API/API-TRM/WZY/xhs_server/models/__pycache__/post.cpython-312.pyc
new file mode 100644
index 0000000..1d64737
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/models/__pycache__/post.cpython-312.pyc
Binary files differ
diff --git a/API/API-TRM/WZY/xhs_server/models/__pycache__/tag.cpython-312.pyc b/API/API-TRM/WZY/xhs_server/models/__pycache__/tag.cpython-312.pyc
new file mode 100644
index 0000000..d76c0e0
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/models/__pycache__/tag.cpython-312.pyc
Binary files differ
diff --git a/API/API-TRM/WZY/xhs_server/models/__pycache__/topic.cpython-312.pyc b/API/API-TRM/WZY/xhs_server/models/__pycache__/topic.cpython-312.pyc
new file mode 100644
index 0000000..a779595
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/models/__pycache__/topic.cpython-312.pyc
Binary files differ
diff --git a/API/API-TRM/WZY/xhs_server/models/__pycache__/user.cpython-312.pyc b/API/API-TRM/WZY/xhs_server/models/__pycache__/user.cpython-312.pyc
new file mode 100644
index 0000000..e3841c7
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/models/__pycache__/user.cpython-312.pyc
Binary files differ
diff --git a/API/API-TRM/WZY/xhs_server/models/behavior.py b/API/API-TRM/WZY/xhs_server/models/behavior.py
new file mode 100644
index 0000000..aed413f
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/models/behavior.py
@@ -0,0 +1,13 @@
+# models/behavior.py
+from extensions import db
+from datetime import datetime
+
+class Behavior(db.Model):
+ __tablename__ = 'behaviors'
+
+ id = db.Column(db.BigInteger, primary_key=True)
+ user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
+ post_id = db.Column(db.Integer, db.ForeignKey('posts.id', ondelete='CASCADE'), nullable=False)
+ type = db.Column(db.Enum('like', 'comment', 'favorite', 'view', 'share'), nullable=False)
+ value = db.Column(db.Integer, default=1, nullable=False)
+ created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
diff --git a/API/API-TRM/WZY/xhs_server/models/comment.py b/API/API-TRM/WZY/xhs_server/models/comment.py
new file mode 100644
index 0000000..ce25287
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/models/comment.py
@@ -0,0 +1,24 @@
+# models/comment.py
+from extensions import db
+from datetime import datetime
+
+class Comment(db.Model):
+ __tablename__ = 'comments'
+
+ id = db.Column(db.Integer, primary_key=True)
+ post_id = db.Column(db.Integer, db.ForeignKey('posts.id', ondelete='CASCADE'), nullable=False)
+ user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
+ parent_id = db.Column(db.Integer, db.ForeignKey('comments.id', ondelete='CASCADE'))
+ content = db.Column(db.Text, nullable=False)
+ status = db.Column(db.Enum('active', 'deleted'), default='active', nullable=False)
+ created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
+ updated_at = db.Column(db.DateTime, default=datetime.utcnow,
+ onupdate=datetime.utcnow, nullable=False)
+
+ # self-referential replies
+ replies = db.relationship(
+ 'Comment',
+ backref=db.backref('parent', remote_side=[id]),
+ lazy='dynamic',
+ cascade='all, delete-orphan'
+ )
diff --git a/API/API-TRM/WZY/xhs_server/models/post.py b/API/API-TRM/WZY/xhs_server/models/post.py
new file mode 100644
index 0000000..ab99191
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/models/post.py
@@ -0,0 +1,32 @@
+# models/post.py
+from extensions import db
+from datetime import datetime
+
+# association table for Post <-> Tag
+post_tags = db.Table(
+ 'post_tags',
+ db.Column('post_id', db.Integer, db.ForeignKey('posts.id', ondelete='CASCADE'), primary_key=True),
+ db.Column('tag_id', db.Integer, db.ForeignKey('tags.id', ondelete='CASCADE'), primary_key=True)
+)
+
+class Post(db.Model):
+ __tablename__ = 'posts'
+
+ id = db.Column(db.Integer, primary_key=True)
+ user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
+ topic_id = db.Column(db.Integer, db.ForeignKey('topics.id', ondelete='SET NULL'))
+ type = db.Column(db.Enum('text', 'image', 'video', 'document'), default='text', nullable=False)
+ title = db.Column(db.String(255), nullable=False)
+ content = db.Column(db.Text, nullable=False)
+ media_urls = db.Column(db.JSON)
+ status = db.Column(db.Enum('draft', 'pending', 'published', 'deleted', 'rejected'),
+ default='draft', nullable=False)
+ heat = db.Column(db.Integer, default=0, nullable=False)
+ created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
+ updated_at = db.Column(db.DateTime, default=datetime.utcnow,
+ onupdate=datetime.utcnow, nullable=False)
+
+ # relationships
+ tags = db.relationship('Tag', secondary=post_tags, backref=db.backref('posts', lazy='dynamic'))
+ behaviors = db.relationship('Behavior', backref='post', lazy='dynamic', cascade='all, delete')
+ comments = db.relationship('Comment', backref='post', lazy='dynamic', cascade='all, delete')
diff --git a/API/API-TRM/WZY/xhs_server/models/tag.py b/API/API-TRM/WZY/xhs_server/models/tag.py
new file mode 100644
index 0000000..7753a38
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/models/tag.py
@@ -0,0 +1,13 @@
+# models/tag.py
+from extensions import db
+from datetime import datetime
+
+class Tag(db.Model):
+ __tablename__ = 'tags'
+
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String(50), unique=True, nullable=False)
+ description = db.Column(db.String(255))
+ created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
+
+ # posts relationship via association table defined in post.py
diff --git a/API/API-TRM/WZY/xhs_server/models/topic.py b/API/API-TRM/WZY/xhs_server/models/topic.py
new file mode 100644
index 0000000..d0e5a2e
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/models/topic.py
@@ -0,0 +1,14 @@
+# models/topic.py
+from extensions import db
+from datetime import datetime
+
+class Topic(db.Model):
+ __tablename__ = 'topics'
+
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String(100), unique=True, nullable=False)
+ description = db.Column(db.Text)
+ status = db.Column(db.Enum('active', 'archived'), default='active', nullable=False)
+ created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
+
+ posts = db.relationship('Post', backref='topic', lazy='dynamic', cascade='all, delete')
diff --git a/API/API-TRM/WZY/xhs_server/models/user.py b/API/API-TRM/WZY/xhs_server/models/user.py
new file mode 100644
index 0000000..f27e7d3
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/models/user.py
@@ -0,0 +1,68 @@
+# models/user.py
+
+from datetime import datetime
+from extensions import db
+
+# 关联表:用户关注关系
+follows = db.Table(
+ 'follows',
+ db.Column('follower_id', db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), primary_key=True),
+ db.Column('followee_id', db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), primary_key=True),
+ db.Column('created_at', db.DateTime, default=datetime.utcnow, nullable=False)
+)
+
+class User(db.Model):
+ __tablename__ = 'users'
+
+ id = db.Column(db.Integer, primary_key=True)
+ username = db.Column(db.String(50), unique=True, nullable=False)
+ password = db.Column(db.String(255), nullable=False)
+ email = db.Column(db.String(100), unique=True, nullable=False)
+ avatar = db.Column(db.String(255))
+ role = db.Column(db.Enum('user', 'admin'), default='user', nullable=False)
+ bio = db.Column(db.String(255))
+ status = db.Column(db.Enum('active', 'banned', 'muted'), default='active', nullable=False)
+ created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
+ updated_at = db.Column(
+ db.DateTime,
+ default=datetime.utcnow,
+ onupdate=datetime.utcnow,
+ nullable=False
+ )
+
+ # 用户发布的帖子
+ posts = db.relationship(
+ 'Post',
+ backref='author',
+ lazy='dynamic',
+ cascade='all, delete-orphan'
+ )
+
+ # 用户的互动行为
+ behaviors = db.relationship(
+ 'Behavior',
+ backref='user',
+ lazy='dynamic',
+ cascade='all, delete-orphan'
+ )
+
+ # 用户发表的评论
+ comments = db.relationship(
+ 'Comment',
+ backref='user',
+ lazy='dynamic',
+ cascade='all, delete-orphan'
+ )
+
+ # 用户关注的对象列表
+ following = db.relationship(
+ 'User',
+ secondary=follows,
+ primaryjoin=(id == follows.c.follower_id),
+ secondaryjoin=(id == follows.c.followee_id),
+ backref=db.backref('followers', lazy='dynamic'),
+ lazy='dynamic'
+ )
+
+ def __repr__(self):
+ return f"<User {self.username!r} id={self.id}>"
diff --git a/API/API-TRM/WZY/xhs_server/requirements.txt b/API/API-TRM/WZY/xhs_server/requirements.txt
new file mode 100644
index 0000000..4db9d23
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/requirements.txt
@@ -0,0 +1,6 @@
+# requirements.txt
+Flask>=2.0
+Flask-SQLAlchemy>=2.5
+Flask-Migrate>=3.1
+Flask-Script>=2.0
+PyMySQL>=1.0
diff --git a/API/API-TRM/WZY/xhs_server/routes/__init__.py b/API/API-TRM/WZY/xhs_server/routes/__init__.py
new file mode 100644
index 0000000..5410dd3
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/routes/__init__.py
@@ -0,0 +1,4 @@
+# routes/__init__.py
+from flask import Blueprint
+
+# 可以在这里统一注册蓝图,也可以直接在 app.py 中 import
diff --git a/API/API-TRM/WZY/xhs_server/routes/__pycache__/__init__.cpython-312.pyc b/API/API-TRM/WZY/xhs_server/routes/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..acde7ed
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/routes/__pycache__/__init__.cpython-312.pyc
Binary files differ
diff --git a/API/API-TRM/WZY/xhs_server/routes/__pycache__/comments.cpython-312.pyc b/API/API-TRM/WZY/xhs_server/routes/__pycache__/comments.cpython-312.pyc
new file mode 100644
index 0000000..4bd83ee
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/routes/__pycache__/comments.cpython-312.pyc
Binary files differ
diff --git a/API/API-TRM/WZY/xhs_server/routes/__pycache__/posts.cpython-312.pyc b/API/API-TRM/WZY/xhs_server/routes/__pycache__/posts.cpython-312.pyc
new file mode 100644
index 0000000..119a503
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/routes/__pycache__/posts.cpython-312.pyc
Binary files differ
diff --git a/API/API-TRM/WZY/xhs_server/routes/comments.py b/API/API-TRM/WZY/xhs_server/routes/comments.py
new file mode 100644
index 0000000..2d5b654
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/routes/comments.py
@@ -0,0 +1,46 @@
+# routes/comments.py
+from flask import Blueprint, request, jsonify, abort
+from extensions import db
+from models.comment import Comment
+from models.behavior import Behavior
+
+comments_bp = Blueprint('comments', __name__)
+
+@comments_bp.route('', methods=['POST'])
+def add_comment(post_id):
+ data = request.get_json() or {}
+ user_id = data.get('user_id')
+ content = data.get('content')
+ if not user_id or not content:
+ return jsonify({'error': 'user_id and content required'}), 400
+
+ comment = Comment(
+ post_id=post_id,
+ user_id=user_id,
+ content=content,
+ parent_id=data.get('parent_id')
+ )
+ db.session.add(comment)
+ # 记录行为
+ beh = Behavior(user_id=user_id, post_id=post_id, type='comment')
+ db.session.add(beh)
+ db.session.commit()
+ return jsonify({'id': comment.id}), 201
+
+@comments_bp.route('', methods=['GET'])
+def list_comments(post_id):
+ def serialize(c):
+ return {
+ 'id': c.id,
+ 'user_id': c.user_id,
+ 'content': c.content,
+ 'created_at': c.created_at.isoformat(),
+ 'replies': [serialize(r) for r in c.replies]
+ }
+
+ comments = Comment.query.filter_by(
+ post_id=post_id,
+ status='active',
+ parent_id=None
+ ).order_by(Comment.created_at.asc()).all()
+ return jsonify([serialize(c) for c in comments])
diff --git a/API/API-TRM/WZY/xhs_server/routes/posts.py b/API/API-TRM/WZY/xhs_server/routes/posts.py
new file mode 100644
index 0000000..e01bdd8
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/routes/posts.py
@@ -0,0 +1,150 @@
+# routes/posts.py
+from flask import Blueprint, request, jsonify, abort
+from extensions import db
+from models.post import Post
+from models.behavior import Behavior
+
+posts_bp = Blueprint('posts', __name__)
+
+@posts_bp.route('', methods=['POST'])
+def create_post():
+ data = request.get_json() or {}
+ post = Post(**data)
+ db.session.add(post)
+ db.session.commit()
+ return jsonify({'id': post.id}), 201
+
+@posts_bp.route('', methods=['GET'])
+def list_posts():
+ posts = Post.query.filter_by(status='published').all()
+ return jsonify([{
+ 'id': p.id,
+ 'title': p.title,
+ 'heat': p.heat,
+ 'created_at': p.created_at.isoformat()
+ } for p in posts])
+
+@posts_bp.route('/<int:post_id>', methods=['GET'])
+def get_post(post_id):
+ post = Post.query.get_or_404(post_id)
+ return jsonify({
+ 'id': post.id,
+ 'user_id': post.user_id,
+ 'title': post.title,
+ 'content': post.content,
+ 'media_urls': post.media_urls,
+ 'status': post.status,
+ 'heat': post.heat,
+ 'created_at': post.created_at.isoformat(),
+ 'updated_at': post.updated_at.isoformat()
+ })
+
+@posts_bp.route('/<int:post_id>', methods=['PUT'])
+def update_post(post_id):
+ """
+ 修改帖子
+ URL 参数:
+ post_id - 要修改的帖子 ID
+ JSON Body 可选字段:
+ title (string)
+ content (string)
+ topic_id (int) — 必须是 topics 表中已有的 ID
+ media_urls (list) — 字符串数组
+ status (string) — 'draft','pending','published','deleted','rejected'
+ """
+ post = Post.query.get_or_404(post_id)
+ data = request.get_json() or {}
+ # 只更新客户端传来的字段
+ for key in ('title','content','topic_id','media_urls','status'):
+ if key in data:
+ setattr(post, key, data[key])
+ db.session.commit()
+ return '', 204
+
+@posts_bp.route('/<int:post_id>', methods=['DELETE'])
+def delete_post(post_id):
+ post = Post.query.get_or_404(post_id)
+ db.session.delete(post)
+ db.session.commit()
+ return '', 204
+
+
+@posts_bp.route('/<int:post_id>/<action>', methods=['POST'])
+def post_action(post_id, action):
+ """
+ 支持的 action: like, favorite, view, share
+ 对于 like 和 favorite,保证每个用户每帖只做一次。
+ """
+ if action not in ('like', 'favorite', 'view', 'share'):
+ abort(400, 'Invalid action')
+
+ data = request.get_json() or {}
+ user_id = data.get('user_id')
+ if not user_id:
+ abort(400, 'user_id required')
+
+ # 对 like/favorite 做去重检查
+ if action in ('like', 'favorite'):
+ exists = Behavior.query.filter_by(
+ user_id=user_id,
+ post_id=post_id,
+ type=action
+ ).first()
+ if exists:
+ return jsonify({'error': f'already {action}d'}), 400
+
+ # 创建行为记录
+ beh = Behavior(user_id=user_id, post_id=post_id, type=action)
+ db.session.add(beh)
+
+ # 更新热度
+ post = Post.query.get_or_404(post_id)
+ post.heat += 1
+
+ db.session.commit()
+ return '', 201
+
+
+# 取消点赞
+@posts_bp.route('/<int:post_id>/like', methods=['DELETE'])
+def unlike(post_id):
+ user_id = request.get_json(silent=True) and request.get_json().get('user_id')
+ if not user_id:
+ abort(400, 'user_id required')
+ # 查找已有的 like 行为
+ beh = Behavior.query.filter_by(
+ user_id=user_id,
+ post_id=post_id,
+ type='like'
+ ).first()
+ if not beh:
+ return jsonify({'error': 'not liked yet'}), 400
+
+ db.session.delete(beh)
+ # 更新热度,确保不降到负数
+ post = Post.query.get_or_404(post_id)
+ post.heat = max(post.heat - 1, 0)
+ db.session.commit()
+ return '', 204
+
+# 取消收藏
+@posts_bp.route('/<int:post_id>/favorite', methods=['DELETE'])
+def unfavorite(post_id):
+ user_id = request.get_json(silent=True) and request.get_json().get('user_id')
+ if not user_id:
+ abort(400, 'user_id required')
+ # 查找已有的 favorite 行为
+ beh = Behavior.query.filter_by(
+ user_id=user_id,
+ post_id=post_id,
+ type='favorite'
+ ).first()
+ if not beh:
+ return jsonify({'error': 'not favorited yet'}), 400
+
+ db.session.delete(beh)
+ # 更新热度
+ post = Post.query.get_or_404(post_id)
+ post.heat = max(post.heat - 1, 0)
+ db.session.commit()
+ return '', 204
\ No newline at end of file
diff --git a/API/API-TRM/WZY/xhs_server/utils/auth.py b/API/API-TRM/WZY/xhs_server/utils/auth.py
new file mode 100644
index 0000000..24704d1
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/utils/auth.py
@@ -0,0 +1,38 @@
+# utils/auth.py
+import os
+import jwt
+from functools import wraps
+from flask import request, jsonify, current_app
+from models.user import User
+from app import db
+
+def generate_token(user_id):
+ payload = {
+ 'user_id': user_id,
+ # you can add exp, iat here
+ }
+ token = jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256')
+ return token
+
+def verify_token(token):
+ try:
+ payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
+ user = User.query.get(payload['user_id'])
+ return user
+ except Exception:
+ return None
+
+def login_required(f):
+ @wraps(f)
+ def decorated(*args, **kwargs):
+ auth_header = request.headers.get('Authorization', None)
+ if not auth_header or not auth_header.startswith('Bearer '):
+ return jsonify({'error': 'Authorization header missing or invalid'}), 401
+ token = auth_header.split()[1]
+ user = verify_token(token)
+ if not user or user.status != 'active':
+ return jsonify({'error': 'Invalid or expired token'}), 401
+ # attach user to request context if needed
+ request.current_user = user
+ return f(*args, **kwargs)
+ return decorated
diff --git a/API/API-TRM/WZY/xhs_server/utils/serializers.py b/API/API-TRM/WZY/xhs_server/utils/serializers.py
new file mode 100644
index 0000000..1ed5445
--- /dev/null
+++ b/API/API-TRM/WZY/xhs_server/utils/serializers.py
@@ -0,0 +1,16 @@
+# utils/serializers.py
+from marshmallow import Schema, fields, validate, ValidationError
+
+class PostSchema(Schema):
+ user_id = fields.Int(required=True)
+ topic_id = fields.Int(required=False, allow_none=True)
+ type = fields.Str(validate=validate.OneOf(['text','image','video','document']), missing='text')
+ title = fields.Str(required=True, validate=validate.Length(min=1, max=255))
+ content = fields.Str(required=True)
+ media_urls = fields.List(fields.Url(), required=False)
+ status = fields.Str(validate=validate.OneOf(['draft','pending','published','deleted','rejected']), missing='draft')
+
+class CommentSchema(Schema):
+ user_id = fields.Int(required=True)
+ content = fields.Str(required=True, validate=validate.Length(min=1))
+ parent_id = fields.Int(required=False, allow_none=True)