增加文件系统存储功能,修复上传功能
Change-Id: I7b76c0a78fa0a02018fa76e932d283e29b35d4be
diff --git a/.gitignore b/.gitignore
index d2bc02a..d18094f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,4 @@
.env.development
package-lock.json
Merge/front/node_modules
+Merge/back_jwlll/models
diff --git a/Merge/back_jwlll/__pycache__/word2vec_helper.cpython-310.pyc b/Merge/back_jwlll/__pycache__/word2vec_helper.cpython-310.pyc
new file mode 100644
index 0000000..7256c16
--- /dev/null
+++ b/Merge/back_jwlll/__pycache__/word2vec_helper.cpython-310.pyc
Binary files differ
diff --git a/Merge/back_jwlll/app.py b/Merge/back_jwlll/app.py
index 940c564..bd7e1be 100644
--- a/Merge/back_jwlll/app.py
+++ b/Merge/back_jwlll/app.py
@@ -1069,7 +1069,7 @@
if __name__ == "__main__":
try:
logger.info("搜索推荐服务启动中...")
- app.run(host="0.0.0.0", port=5000)
+ app.run(host="0.0.0.0", port=5717)
except Exception as e:
logger.error(f"启动异常: {e}")
import traceback
diff --git a/Merge/back_wzy/routes/__pycache__/posts.cpython-310.pyc b/Merge/back_wzy/routes/__pycache__/posts.cpython-310.pyc
index 7d61b05..45d3343 100644
--- a/Merge/back_wzy/routes/__pycache__/posts.cpython-310.pyc
+++ b/Merge/back_wzy/routes/__pycache__/posts.cpython-310.pyc
Binary files differ
diff --git a/Merge/back_wzy/routes/posts.py b/Merge/back_wzy/routes/posts.py
index 14ff64c..a016f69 100644
--- a/Merge/back_wzy/routes/posts.py
+++ b/Merge/back_wzy/routes/posts.py
@@ -4,16 +4,47 @@
from extensions import db
from models.post import Post
from models.behavior import Behavior
+from utils.Fpost import Fpost
+import json
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
+ try:
+ # 获取文本字段
+ user_id = request.form.get('user_id')
+ title = request.form.get('title')
+ content = request.form.get('content')
+ status = request.form.get('status', 'published')
+ topic_id = request.form.get('topic_id')
+ media_count = int(request.form.get('media_count', 0))
+
+ if not user_id or not title or not content:
+ return jsonify({'error': '缺少必要字段'}), 400
+
+ # 获取上传的文件
+ files = []
+ for i in range(media_count):
+ file_key = f'media_{i}'
+ if file_key in request.files:
+ files.append(request.files[file_key])
+
+ # 使用 Fpost 创建帖子
+ fpost = Fpost(db.session)
+ new_post = fpost.create_post_with_files(
+ user_id=int(user_id),
+ title=title,
+ content=content,
+ topic_id=int(topic_id) if topic_id else None,
+ status=status,
+ files=files
+ )
+
+ return jsonify({'id': new_post.id}), 201
+
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
@posts_bp.route('', methods=['GET'])
def list_posts():
@@ -59,14 +90,64 @@
def update_post(post_id):
"""
修改帖子字段(可选字段:title, content, topic_id, media_urls, status)
+ 支持FormData和JSON两种格式
"""
- 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
+ try:
+ fpost = Fpost(db.session)
+
+ # 检查是否是FormData请求
+ if request.content_type and 'multipart/form-data' in request.content_type:
+ # FormData请求
+ title = request.form.get('title')
+ content = request.form.get('content')
+ status = request.form.get('status')
+ topic_id = request.form.get('topic_id')
+ media_count = int(request.form.get('media_count', 0))
+ existing_media_urls_str = request.form.get('existing_media_urls')
+
+ # 解析现有媒体URLs
+ existing_media_urls = None
+ if existing_media_urls_str:
+ try:
+ existing_media_urls = json.loads(existing_media_urls_str)
+ except:
+ existing_media_urls = None
+
+ # 获取新上传的文件
+ files = []
+ for i in range(media_count):
+ file_key = f'media_{i}'
+ if file_key in request.files:
+ files.append(request.files[file_key])
+
+ # 更新帖子
+ updated_post = fpost.update_post_with_files(
+ post_id=post_id,
+ title=title,
+ content=content,
+ topic_id=int(topic_id) if topic_id else None,
+ status=status,
+ files=files if files else None,
+ existing_media_urls=existing_media_urls
+ )
+
+ else:
+ # JSON请求(保持原有逻辑)
+ 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()
+ updated_post = post
+
+ if not updated_post:
+ return jsonify({'error': '帖子不存在'}), 404
+
+ return '', 204
+
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
@posts_bp.route('/<int:post_id>', methods=['DELETE'])
def delete_post(post_id):
diff --git a/Merge/back_wzy/utils/Fpost.py b/Merge/back_wzy/utils/Fpost.py
index c8b5f48..894a831 100644
--- a/Merge/back_wzy/utils/Fpost.py
+++ b/Merge/back_wzy/utils/Fpost.py
@@ -6,9 +6,22 @@
from sqlalchemy.orm import Session
from models.logs import Log
from models.syscost import PerformanceData
+import os
+import requests
+from werkzeug.utils import secure_filename
+import uuid
+
class Fpost:
def __init__(self,session:Session):
self.session=session
+ # 配置文件存储节点(docker容器的实际路径)
+ self.storage_nodes = [
+ '/home/tianruiming/docker02/static',
+ '/home/tianruiming/docker03/static',
+ '/home/tianruiming/docker04/static'
+ ]
+ # 默认访问节点(LVS负载均衡器)
+ self.access_node = '192.168.5.231:8080'
return
@@ -153,4 +166,131 @@
def getsyscost(self):
res= self.session.query(PerformanceData).all()
- return res
\ No newline at end of file
+ return res
+
+ def save_files_to_storage(self, files, post_id):
+ """
+ 将文件保存到所有存储节点
+ :param files: 文件列表
+ :param post_id: 帖子ID
+ :return: 媒体URL列表
+ """
+ media_urls = []
+
+ for file in files:
+ if file and file.filename:
+ # 生成安全的文件名
+ original_filename = secure_filename(file.filename)
+ # 生成唯一文件名避免冲突
+ unique_id = str(uuid.uuid4())
+ file_extension = os.path.splitext(original_filename)[1]
+ unique_filename = f"{unique_id}{file_extension}"
+
+ # 读取文件内容
+ file_content = file.read()
+ file.seek(0) # 重置文件指针
+
+ # 保存到所有存储节点
+ success_count = 0
+ for node_path in self.storage_nodes:
+ try:
+ # 创建目录路径:/home/tianruiming/docker0X/static/{post_id}/
+ node_dir = os.path.join(node_path, str(post_id))
+ os.makedirs(node_dir, exist_ok=True)
+
+ # 完整文件路径
+ full_file_path = os.path.join(node_dir, unique_filename)
+
+ # 写入文件到存储节点
+ with open(full_file_path, 'wb') as f:
+ f.write(file_content)
+
+ success_count += 1
+ print(f"Successfully saved file to {full_file_path}")
+
+ except Exception as e:
+ print(f"Failed to save file to node {node_path}: {str(e)}")
+
+ if success_count > 0:
+ # 生成访问URL,格式:http://192.168.5.231:8080/static/{post_id}/{filename}
+ media_url = f"http://{self.access_node}/static/{post_id}/{unique_filename}"
+ media_urls.append(media_url)
+ else:
+ raise Exception(f"Failed to save file {original_filename} to any storage node")
+
+ return media_urls
+
+ def create_post_with_files(self, user_id, title, content, topic_id, status, files):
+ """
+ 创建带文件的帖子
+ :param user_id: 用户ID
+ :param title: 标题
+ :param content: 内容
+ :param topic_id: 话题ID
+ :param status: 状态
+ :param files: 文件列表
+ :return: 帖子对象
+ """
+ # 先创建帖子获取ID
+ new_post = post(
+ user_id=user_id,
+ title=title,
+ content=content,
+ topic_id=topic_id if topic_id else None,
+ status=status,
+ created_at=datetime.now(),
+ updated_at=datetime.now()
+ )
+
+ self.session.add(new_post)
+ self.session.flush() # 获取ID但不提交
+
+ # 保存文件
+ if files:
+ media_urls = self.save_files_to_storage(files, new_post.id)
+ new_post.media_urls = media_urls
+ else:
+ new_post.media_urls = []
+
+ self.session.commit()
+ return new_post
+
+ def update_post_with_files(self, post_id, title=None, content=None, topic_id=None, status=None, files=None, existing_media_urls=None):
+ """
+ 更新带文件的帖子
+ :param post_id: 帖子ID
+ :param title: 标题
+ :param content: 内容
+ :param topic_id: 话题ID
+ :param status: 状态
+ :param files: 新文件列表
+ :param existing_media_urls: 现有媒体URL
+ :return: 更新后的帖子对象
+ """
+ post_obj = self.session.query(post).filter(post.id == post_id).first()
+ if not post_obj:
+ return None
+
+ # 更新基本信息
+ if title is not None:
+ post_obj.title = title
+ if content is not None:
+ post_obj.content = content
+ if topic_id is not None:
+ post_obj.topic_id = topic_id
+ if status is not None:
+ post_obj.status = status
+
+ post_obj.updated_at = datetime.now()
+
+ # 处理文件
+ if files:
+ # 有新文件,保存新文件
+ media_urls = self.save_files_to_storage(files, post_id)
+ post_obj.media_urls = media_urls
+ elif existing_media_urls is not None:
+ # 保留现有媒体URL
+ post_obj.media_urls = existing_media_urls
+
+ self.session.commit()
+ return post_obj
\ No newline at end of file
diff --git a/Merge/back_wzy/utils/__pycache__/Fpost.cpython-310.pyc b/Merge/back_wzy/utils/__pycache__/Fpost.cpython-310.pyc
index 92bc6b9..b261f17 100644
--- a/Merge/back_wzy/utils/__pycache__/Fpost.cpython-310.pyc
+++ b/Merge/back_wzy/utils/__pycache__/Fpost.cpython-310.pyc
Binary files differ
diff --git a/Merge/front/src/api/posts_wzy.js b/Merge/front/src/api/posts_wzy.js
index d901500..4992f95 100644
--- a/Merge/front/src/api/posts_wzy.js
+++ b/Merge/front/src/api/posts_wzy.js
@@ -36,11 +36,11 @@
* 发布新帖
* POST /posts
*/
-export async function createPost(payload) {
+export async function createPost(formData) {
const res = await fetch(`${BASE}/posts`, {
method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(payload)
+ // 不设置Content-Type,让浏览器自动设置multipart/form-data
+ body: formData
})
if (!res.ok) {
const err = await res.json().catch(() => null)
@@ -53,11 +53,12 @@
* 修改帖子
* PUT /posts/{postId}
*/
-export async function updatePost(postId, payload) {
+export async function updatePost(postId, formData) {
const res = await fetch(`${BASE}/posts/${postId}`, {
method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(payload)
+ // 如果是FormData则不设置Content-Type,否则设置JSON
+ headers: formData instanceof FormData ? {} : { 'Content-Type': 'application/json' },
+ body: formData instanceof FormData ? formData : JSON.stringify(formData)
})
if (!res.ok) throw new Error(`updatePost(${postId}): ${res.status}`)
// 204 No Content
diff --git a/Merge/front/src/components/CreatePost.jsx b/Merge/front/src/components/CreatePost.jsx
index c11e247..2b365db 100644
--- a/Merge/front/src/components/CreatePost.jsx
+++ b/Merge/front/src/components/CreatePost.jsx
@@ -51,12 +51,12 @@
.catch(err => setError(err.message))
.finally(() => setLoading(false))
}, [isEdit, postId])
- // 上传回调
+
+ // 上传回调 - 保存原始文件对象
const handleUploadComplete = async uploadedFiles => {
- // TODO: 真正上传到服务器后替换为服务端 URL
- const urls = await Promise.all(
- uploadedFiles.map(f => URL.createObjectURL(f))
- )
+ setFiles(uploadedFiles)
+ // 为预览创建临时URLs
+ const urls = uploadedFiles.map(f => URL.createObjectURL(f))
setMediaUrls(urls)
setStep('detail')
}
@@ -72,25 +72,42 @@
return
}
setError(null)
+
try {
+ // 创建FormData对象
+ const formData = new FormData()
+
+ // 添加文本字段
+ formData.append('user_id', currentUserId)
+ formData.append('title', title.trim())
+ formData.append('content', content.trim())
+ formData.append('status', status)
+ if (topicId) {
+ formData.append('topic_id', topicId)
+ }
+
if (isEdit) {
- await updatePost(postId, {
- title: title.trim(),
- content: content.trim(),
- topic_id: topicId || undefined,
- media_urls: mediaUrls,
- status
- })
+ // 编辑模式:如果有新文件,添加新文件;否则保留现有URLs
+ if (files.length > 0) {
+ files.forEach((file, index) => {
+ formData.append(`media_${index}`, file)
+ })
+ formData.append('media_count', files.length)
+ } else {
+ // 保留现有的media_urls
+ formData.append('existing_media_urls', JSON.stringify(mediaUrls))
+ }
+
+ await updatePost(postId, formData)
alert('更新成功!')
} else {
- await createPost({
- user_id: currentUserId,
- topic_id: topicId || undefined,
- title: title.trim(),
- content: content.trim(),
- media_urls: mediaUrls,
- status
+ // 创建模式:添加文件
+ files.forEach((file, index) => {
+ formData.append(`media_${index}`, file)
})
+ formData.append('media_count', files.length)
+
+ await createPost(formData)
alert('发布成功!')
}
navigate('/notebooks', { replace: true })