From 42855274cc87a045e181bcb53b2e6fc4de608a66 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Tue, 6 Jan 2026 12:36:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=AA=E8=82=A1=E8=AE=BA=E5=9D=9B=E9=87=8D?= =?UTF-8?q?=E5=81=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- community_admin_setup.sql | 59 +++++++ community_api.py | 88 ++++++++++ init_community_es.py | 158 ++++++++++++++++++ .../MessageArea/TextChannel/MessageItem.tsx | 41 +++-- .../MessageArea/TextChannel/MessageList.tsx | 8 +- 5 files changed, 336 insertions(+), 18 deletions(-) create mode 100644 community_admin_setup.sql create mode 100644 init_community_es.py diff --git a/community_admin_setup.sql b/community_admin_setup.sql new file mode 100644 index 00000000..687b33f6 --- /dev/null +++ b/community_admin_setup.sql @@ -0,0 +1,59 @@ +-- 社区管理员权限设置 +-- 在 MySQL 中执行 + +-- 1. 创建管理员表(如果不存在) +CREATE TABLE IF NOT EXISTS community_admins ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL UNIQUE, + role ENUM('admin', 'moderator') DEFAULT 'moderator', + permissions JSON, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_user_id (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. 添加用户 ID=65 为管理员 +INSERT INTO community_admins (user_id, role, permissions) +VALUES (65, 'admin', JSON_OBJECT( + 'manage_channels', true, + 'manage_messages', true, + 'manage_users', true, + 'manage_posts', true, + 'pin_messages', true, + 'delete_messages', true, + 'announce', true, + 'ban_users', true +)) +ON DUPLICATE KEY UPDATE + role = 'admin', + permissions = JSON_OBJECT( + 'manage_channels', true, + 'manage_messages', true, + 'manage_users', true, + 'manage_posts', true, + 'pin_messages', true, + 'delete_messages', true, + 'announce', true, + 'ban_users', true + ); + +-- 3. 查看管理员列表 +SELECT ca.*, u.username +FROM community_admins ca +LEFT JOIN users u ON ca.user_id = u.id; + +-- 管理员权限说明: +-- admin (管理员): 拥有所有权限 +-- - manage_channels: 创建/编辑/删除频道 +-- - manage_messages: 编辑/删除任何消息 +-- - manage_users: 踢出/禁言用户 +-- - manage_posts: 编辑/删除任何帖子 +-- - pin_messages: 置顶消息 +-- - delete_messages: 删除消息 +-- - announce: 在公告频道发布消息 +-- - ban_users: 封禁用户 + +-- moderator (版主): 拥有部分权限 +-- - pin_messages: 置顶消息 +-- - delete_messages: 删除消息 +-- - manage_posts: 管理帖子(锁定/置顶) diff --git a/community_api.py b/community_api.py index 2334cf2e..28bfe15c 100644 --- a/community_api.py +++ b/community_api.py @@ -75,6 +75,67 @@ def login_required(f): return decorated_function +def get_user_admin_info(user_id): + """获取用户管理员信息""" + try: + with get_db_engine().connect() as conn: + sql = text(""" + SELECT role, permissions + FROM community_admins + WHERE user_id = :user_id + """) + result = conn.execute(sql, {'user_id': int(user_id)}).fetchone() + if result: + import json + permissions = result.permissions + if isinstance(permissions, str): + permissions = json.loads(permissions) + return { + 'role': result.role, + 'permissions': permissions or {}, + 'isAdmin': result.role == 'admin', + 'isModerator': result.role in ['admin', 'moderator'] + } + except Exception as e: + print(f"[Community API] 获取管理员信息失败: {e}") + return None + + +def check_permission(user_id, permission): + """检查用户是否有指定权限""" + admin_info = get_user_admin_info(user_id) + if not admin_info: + return False + if admin_info['isAdmin']: + return True + return admin_info['permissions'].get(permission, False) + + +def admin_required(permission=None): + """管理员权限验证装饰器""" + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + user = get_current_user() + if not user: + return jsonify({'code': 401, 'message': '请先登录'}), 401 + g.current_user = user + + # 检查权限 + if permission: + if not check_permission(user['id'], permission): + return jsonify({'code': 403, 'message': '无权限执行此操作'}), 403 + else: + admin_info = get_user_admin_info(user['id']) + if not admin_info: + return jsonify({'code': 403, 'message': '需要管理员权限'}), 403 + + g.admin_info = get_user_admin_info(user['id']) + return f(*args, **kwargs) + return decorated_function + return decorator + + def api_response(data=None, message='success', code=200): """统一 API 响应格式""" return jsonify({ @@ -92,6 +153,33 @@ def api_error(message, code=400): }), code if code >= 400 else 400 +# ============================================================ +# 用户管理员状态 API +# ============================================================ + +@community_bp.route('/me/admin-status', methods=['GET']) +@login_required +def get_my_admin_status(): + """获取当前用户的管理员状态""" + user = g.current_user + admin_info = get_user_admin_info(user['id']) + + if admin_info: + return api_response({ + 'isAdmin': admin_info['isAdmin'], + 'isModerator': admin_info['isModerator'], + 'role': admin_info['role'], + 'permissions': admin_info['permissions'] + }) + else: + return api_response({ + 'isAdmin': False, + 'isModerator': False, + 'role': None, + 'permissions': {} + }) + + # ============================================================ # 频道相关 API # ============================================================ diff --git a/init_community_es.py b/init_community_es.py new file mode 100644 index 00000000..02f964ae --- /dev/null +++ b/init_community_es.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +""" +初始化社区 ES 索引 +运行: python init_community_es.py +""" + +from elasticsearch import Elasticsearch + +# ES 客户端 +es_client = Elasticsearch( + hosts=["http://222.128.1.157:19200"], + request_timeout=30 +) + +# 索引配置 +INDICES = { + 'community_messages': { + 'mappings': { + 'properties': { + 'id': {'type': 'keyword'}, + 'channel_id': {'type': 'keyword'}, + 'thread_id': {'type': 'keyword'}, + 'author_id': {'type': 'keyword'}, + 'author_name': {'type': 'text', 'fields': {'keyword': {'type': 'keyword'}}}, + 'author_avatar': {'type': 'keyword'}, + 'content': {'type': 'text', 'analyzer': 'ik_max_word'}, + 'type': {'type': 'keyword'}, + 'mentioned_users': {'type': 'keyword'}, + 'mentioned_stocks': {'type': 'keyword'}, + 'mentioned_everyone': {'type': 'boolean'}, + 'reply_to': { + 'type': 'object', + 'properties': { + 'messageId': {'type': 'keyword'}, + 'authorId': {'type': 'keyword'}, + 'authorName': {'type': 'text'}, + 'contentPreview': {'type': 'text'} + } + }, + 'reactions': {'type': 'object', 'enabled': False}, + 'reaction_count': {'type': 'integer'}, + 'is_pinned': {'type': 'boolean'}, + 'is_edited': {'type': 'boolean'}, + 'is_deleted': {'type': 'boolean'}, + 'created_at': {'type': 'date'}, + 'edited_at': {'type': 'date'} + } + }, + 'settings': { + 'number_of_shards': 1, + 'number_of_replicas': 0 + } + }, + 'community_forum_posts': { + 'mappings': { + 'properties': { + 'id': {'type': 'keyword'}, + 'channel_id': {'type': 'keyword'}, + 'author_id': {'type': 'keyword'}, + 'author_name': {'type': 'text', 'fields': {'keyword': {'type': 'keyword'}}}, + 'author_avatar': {'type': 'keyword'}, + 'title': {'type': 'text', 'analyzer': 'ik_max_word'}, + 'content': {'type': 'text', 'analyzer': 'ik_max_word'}, + 'tags': {'type': 'keyword'}, + 'stock_symbols': {'type': 'keyword'}, + 'is_pinned': {'type': 'boolean'}, + 'is_locked': {'type': 'boolean'}, + 'is_deleted': {'type': 'boolean'}, + 'reply_count': {'type': 'integer'}, + 'view_count': {'type': 'integer'}, + 'like_count': {'type': 'integer'}, + 'last_reply_at': {'type': 'date'}, + 'last_reply_by': {'type': 'keyword'}, + 'created_at': {'type': 'date'}, + 'updated_at': {'type': 'date'} + } + }, + 'settings': { + 'number_of_shards': 1, + 'number_of_replicas': 0 + } + }, + 'community_forum_replies': { + 'mappings': { + 'properties': { + 'id': {'type': 'keyword'}, + 'post_id': {'type': 'keyword'}, + 'channel_id': {'type': 'keyword'}, + 'author_id': {'type': 'keyword'}, + 'author_name': {'type': 'text', 'fields': {'keyword': {'type': 'keyword'}}}, + 'author_avatar': {'type': 'keyword'}, + 'content': {'type': 'text', 'analyzer': 'ik_max_word'}, + 'reply_to': {'type': 'object', 'enabled': False}, + 'reactions': {'type': 'object', 'enabled': False}, + 'like_count': {'type': 'integer'}, + 'is_solution': {'type': 'boolean'}, + 'is_deleted': {'type': 'boolean'}, + 'created_at': {'type': 'date'}, + 'edited_at': {'type': 'date'} + } + }, + 'settings': { + 'number_of_shards': 1, + 'number_of_replicas': 0 + } + } +} + + +def create_index(index_name, config): + """创建索引""" + if es_client.indices.exists(index=index_name): + print(f"✓ 索引 {index_name} 已存在") + return True + + try: + es_client.indices.create(index=index_name, body=config) + print(f"✓ 创建索引 {index_name} 成功") + return True + except Exception as e: + print(f"✗ 创建索引 {index_name} 失败: {e}") + return False + + +def check_es_connection(): + """检查 ES 连接""" + try: + info = es_client.info() + print(f"✓ ES 连接成功: {info['cluster_name']} (v{info['version']['number']})") + return True + except Exception as e: + print(f"✗ ES 连接失败: {e}") + return False + + +def main(): + print("=" * 50) + print("初始化社区 ES 索引") + print("=" * 50) + + # 检查连接 + if not check_es_connection(): + return + + print() + + # 创建索引 + for index_name, config in INDICES.items(): + create_index(index_name, config) + + print() + print("=" * 50) + print("初始化完成") + print("=" * 50) + + +if __name__ == '__main__': + main() diff --git a/src/views/StockCommunity/components/MessageArea/TextChannel/MessageItem.tsx b/src/views/StockCommunity/components/MessageArea/TextChannel/MessageItem.tsx index 4ebcbfcd..974207bc 100644 --- a/src/views/StockCommunity/components/MessageArea/TextChannel/MessageItem.tsx +++ b/src/views/StockCommunity/components/MessageArea/TextChannel/MessageItem.tsx @@ -10,7 +10,6 @@ import { Avatar, IconButton, HStack, - useColorModeValue, Tooltip, Menu, MenuButton, @@ -50,12 +49,12 @@ const MessageItem: React.FC = ({ }) => { const [isHovered, setIsHovered] = useState(false); - // 颜色 - const hoverBg = useColorModeValue('gray.50', 'gray.700'); - const textColor = useColorModeValue('gray.800', 'gray.100'); - const mutedColor = useColorModeValue('gray.500', 'gray.400'); - const mentionBg = useColorModeValue('blue.50', 'blue.900'); - const replyBg = useColorModeValue('gray.100', 'gray.700'); + // 深色主题颜色(HeroUI 风格) + const hoverBg = 'rgba(255, 255, 255, 0.05)'; + const textColor = 'gray.100'; + const mutedColor = 'gray.400'; + const mentionBg = 'rgba(59, 130, 246, 0.2)'; + const replyBg = 'rgba(255, 255, 255, 0.05)'; // 格式化时间 const formatTime = (dateStr: string) => { @@ -246,9 +245,11 @@ const MessageItem: React.FC = ({ position="absolute" top="-10px" right="8px" - bg={useColorModeValue('white', 'gray.700')} - borderRadius="md" - boxShadow="md" + bg="rgba(17, 24, 39, 0.95)" + border="1px solid" + borderColor="rgba(255, 255, 255, 0.1)" + borderRadius="lg" + boxShadow="0 4px 20px rgba(0, 0, 0, 0.4)" p={1} spacing={0} > @@ -258,6 +259,8 @@ const MessageItem: React.FC = ({ icon={} size="sm" variant="ghost" + color="gray.400" + _hover={{ bg: 'whiteAlpha.100', color: 'yellow.400' }} /> @@ -266,6 +269,8 @@ const MessageItem: React.FC = ({ icon={} size="sm" variant="ghost" + color="gray.400" + _hover={{ bg: 'whiteAlpha.100', color: 'purple.400' }} onClick={() => onReply(message)} /> @@ -275,6 +280,8 @@ const MessageItem: React.FC = ({ icon={} size="sm" variant="ghost" + color="gray.400" + _hover={{ bg: 'whiteAlpha.100', color: 'blue.400' }} onClick={() => onThreadOpen?.(message.id)} /> @@ -285,11 +292,17 @@ const MessageItem: React.FC = ({ icon={} size="sm" variant="ghost" + color="gray.400" + _hover={{ bg: 'whiteAlpha.100', color: 'white' }} /> - - }>置顶消息 - }>编辑 - } color="red.500">删除 + + } bg="transparent" _hover={{ bg: 'whiteAlpha.100' }} color="gray.300">置顶消息 + } bg="transparent" _hover={{ bg: 'whiteAlpha.100' }} color="gray.300">编辑 + } bg="transparent" _hover={{ bg: 'rgba(239, 68, 68, 0.2)' }} color="red.400">删除 diff --git a/src/views/StockCommunity/components/MessageArea/TextChannel/MessageList.tsx b/src/views/StockCommunity/components/MessageArea/TextChannel/MessageList.tsx index 7ba85a15..381c9635 100644 --- a/src/views/StockCommunity/components/MessageArea/TextChannel/MessageList.tsx +++ b/src/views/StockCommunity/components/MessageArea/TextChannel/MessageList.tsx @@ -7,7 +7,6 @@ import { Box, Flex, Text, - useColorModeValue, } from '@chakra-ui/react'; import { format, isToday, isYesterday, isSameDay } from 'date-fns'; import { zhCN } from 'date-fns/locale'; @@ -31,9 +30,10 @@ const MessageList: React.FC = ({ onReply, onThreadOpen, }) => { - const dividerColor = useColorModeValue('gray.300', 'gray.600'); - const dividerTextColor = useColorModeValue('gray.500', 'gray.400'); - const dividerBg = useColorModeValue('white', 'gray.800'); + // 深色主题颜色(HeroUI 风格) + const dividerColor = 'rgba(255, 255, 255, 0.1)'; + const dividerTextColor = 'gray.400'; + const dividerBg = 'rgba(15, 23, 42, 0.9)'; // 按日期分组消息 const messageGroups = useMemo(() => {