前言

在快节奏的现代生活中,情绪困扰常常被人们压抑在心。一个安全、匿名、有回应的倾诉空间,能成为许多人情绪的出口。本文介绍我开发的一个开源项目——情绪互助平台,一个结合了 AI 辅助审核、管理员管控、实时消息通知和温暖 UI 的社区系统。项目包含完整的前端界面(原生 HTML/CSS/JS)和后端 API(FastAPI + SQLite),代码清晰易懂,适合学习全栈开发、社区产品设计和 AI 集成实践。

项目概述

情绪互助平台允许用户匿名发布自己的情绪需求,其他用户可以回应、评论,而 AI 会自动为每个通过审核的帖子生成一句温暖的回复。所有用户发布的帖子和评论都会经过智能审核(集成 Dify 工作流或本地敏感词过滤),违规用户会被记录违规次数,达到阈值后自动禁言或注销。管理员拥有独立后台,可管理需求、评论、用户账号,并查看情绪热词统计。

技术亮点

  • 前端:原生 JavaScript + 毛玻璃 CSS 设计,无框架依赖,易于部署
  • 后端:FastAPI + SQLAlchemy + SQLite,JWT 身份认证,RESTful API
  • AI 集成:支持 Dify 工作流(发帖分析、评论审核),同时提供本地降级规则
  • 特色功能:游客模式、AI 自动评论、违规计次禁言、未读消息小红点、表情包选择器、情绪词云统计

前端设计

前端通过 backdrop-filter: blur() 和半透明白色遮罩,实现现代毛玻璃卡片风格。每次加载页面会从一组精选的 Pexels 风景图中随机选择背景,营造宁静、治愈的视觉氛围。

body::before {
  content: "";
  position: fixed;
  background: rgba(255, 255, 255, 0.85);
  backdrop-filter: blur(2px);
  z-index: -1;
}
.card {
  background: rgba(255, 255, 255, 0.92);
  backdrop-filter: blur(8px);
  border-radius: 24px;
  transition: all 0.3s ease;
}
  • 用户系统与游客模式
    • 普通用户:注册/登录后可以发布需求、评论、接收消息、修改个人资料
    • 管理员:拥有后台入口,可审核内容、管理用户、查看统计
    • 游客:无需登录即可浏览所有已通过的帖子,但不能互动,降低使用门槛
      前端通过 sessionStorage 存储游客标志,并在每个 API 请求中动态判断是否需要携带 token。
  • 情感化交互细节
    • 表情包选择器:评论和发帖的文本框旁有一个 😊 按钮,点击可弹出常用 emoji 面板,提升表达丰富度。
    • 未读消息提醒:导航栏铃铛图标旁出现小红点,点击可查看系统通知和他人回应。
    • 弹窗模态框:查看帖子详情、发布新需求、消息列表均使用自定义模态框,避免页面跳转,体验流畅。
      在这里插入图片描述
      在这里插入图片描述

后端架构:FastAPI 驱动的坚实底座

  • 数据库设计

    • users:用户名、密码哈希、昵称、角色(user/admin)、违规次数、禁言截止时间、软删除标志
    • posts:需求内容、审核状态(pending/approved/rejected)、驳回理由、发布时间
    • comments:所属帖子、用户 ID(空表示 AI 生成)、内容、审核状态、是否 AI 生成
    • messages:系统通知或评论提醒,支持已读标记
    • 情绪热词统计通过实时查询帖子内容提取关键词实现
  • JWT身份验证与权限控制

    • 每个需要登录的接口都依赖 get_current_user 依赖项,从 Authorization 头解析 token。管理员接口额外使用 get_current_admin 进行角色校验。用户被禁言后,发布和评论接口会返回 403 错误。
  • AI审核流程:Dify+本地部署

    • 这是项目的核心特色。当用户发布一个新需求时,后端调用 Dify 工作流分析文本内容,返回 needs_review 标志和可选的 ai_response:
      • 若 needs_review = True(例如包含高危词),帖子状态设为 pending,等待管理员审核;
      • 否则自动设为 approved,并立即生成一条 AI 评论,同时通知用户。
    • 若 Dify 服务不可用(网络错误或未配置 key),系统自动降级为关键词匹配,确保核心功能不中断。
    • 评论的审核流程类似:调用 Dify 的评论审核工作流,返回 pass 或 nopass。未通过的评论进入 pending 状态,由管理员人工判断。管理员通过或驳回时,系统会发送消息给原帖作者,并视情况增加用户违规次数。
  • 违规与禁言机制

    • 每个用户初始违规次数为 0。每次发布不当内容被管理员驳回,violation_count++:
      • 第 1~2 次:仅记录并发送警告消息
      • 第 3 次:设置 ban_until = now + 15 days,用户无法发帖/评论
      • 第 4 次:is_deleted = True,账号永久注销
    • 管理员可在后台直接删除用户,也是软删除。
      在这里插入图片描述
  • 消息通知系统

    • 所有重要事件都会生成一条 Message 记录:

      • 帖子/评论审核通过/驳回
      • 自己的帖子收到新评论
      • AI 生成的自动回复
      • 违规警告或账号状态变更
        在这里插入图片描述
    • 前端轮询(或用户主动点击)获取未读消息,并支持一键已读。

管理员后台:内容治理的指挥中心

  • 管理员登录后界面切换为后台模式,包含四个板块:
  1. 需求审核:列出所有状态为 pending 的帖子。管理员可通过(触发 AI 评论)或驳回(需填写原因并增加用户违规)。
  2. 评论审核:待审核的评论列表,支持通过/驳回,驳回同样增加用户违规。
  3. 账号管理:展示所有普通用户,显示违规次数、禁言状态,支持直接删除账号。
  4. 情绪热词统计:按日/周/月统计已通过帖子中出现的高频情绪词(如“焦虑”“孤独”“压力”),生成词云式排行榜。
  • 后台顶部会显示待审核数量,并且有新需求时会在管理员登录后弹出提醒弹窗,确保及时处理。

部署与运行

  • 后端启动
# 安装依赖
pip install fastapi uvicorn sqlalchemy python-jose python-multipart requests python-dotenv

# 可选:配置 .env 文件
DATABASE_URL=sqlite:///./empathy.db
SECRET_KEY=your-secret-key
DIFY_API_URL=http://localhost/v1/workflows/run
DIFY_API_KEY=app-xxxxx

# 启动服务
uvicorn main:app --reload --port 5000

前端无需构建,将 front.html 中的 API_BASE 改为实际后端地址(如 http://localhost:5000/api),直接使用浏览器打开即可。

Dify工作流

  • 若需完整 AI 能力,请自行部署 Dify 社区版,并创建两个工作流:
    • 发帖分析工作流:输入变量 text,输出 needs_review(bool)、ai_response(string)、class_name(string)
    • 评论审核工作流:输入变量 input,输出 class_name(值为 “pass” 或 “nopass”)
  • 也可直接使用项目内置的关键词降级规则,无需任何 AI 服务即可运行。
    在这里插入图片描述

项目展望与优化方向

当前版本已实现完整的社区闭环,但仍有一些可扩展之处:

  • 消息实时推送:可用 WebSocket 替代轮询,提升体验
  • 更丰富的内容展示:支持图片上传、心情图标
  • AI 回复个性化:根据用户历史情绪推荐不同风格的回复
  • 部署简化:提供 Docker Compose 一键启动(前端 + 后端 + 可选 Dify)

代码(直接放在一起了 获取代码的可以自己分区)

  • 前端
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
    <title>情绪互助平台 | 温暖倾诉社区</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
        }

        /* 全局背景图片 (随机选择) */
        body {
            min-height: 100vh;
            background-size: cover;
            background-position: center;
            background-attachment: fixed;
            position: relative;
        }
        /* 半透明遮罩,保证文字可读 */
        body::before {
            content: "";
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(255, 255, 255, 0.85);
            backdrop-filter: blur(2px);
            z-index: -1;
        }

        /* 容器与布局 */
        .app-container {
            max-width: 1280px;
            margin: 0 auto;
            padding: 20px;
            position: relative;
            z-index: 1;
        }

        /* 卡片样式 - 毛玻璃效果 */
        .card {
            background: rgba(255, 255, 255, 0.92);
            backdrop-filter: blur(8px);
            border-radius: 24px;
            box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
            padding: 20px;
            margin-bottom: 20px;
            transition: all 0.3s ease;
            border: 1px solid rgba(255,255,255,0.3);
        }
        .card:hover {
            transform: translateY(-3px);
            background: rgba(255, 255, 255, 0.97);
            box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
        }

        .btn {
            border: none;
            background: rgba(238, 242, 255, 0.9);
            padding: 8px 20px;
            border-radius: 40px;
            font-size: 14px;
            font-weight: 500;
            cursor: pointer;
            transition: 0.2s;
            color: #1f2937;
        }

        .btn-primary {
            background: linear-gradient(135deg, #6366f1, #8b5cf6);
            color: white;
            box-shadow: 0 4px 12px rgba(99,102,241,0.3);
        }
        .btn-primary:hover {
            transform: translateY(-2px);
            box-shadow: 0 8px 20px rgba(99,102,241,0.4);
        }

        .btn-danger {
            background: #fee2e2;
            color: #b91c1c;
        }
        .btn-outline {
            background: transparent;
            border: 1px solid #6366f1;
            color: #6366f1;
        }
        .btn-outline:hover {
            background: #6366f1;
            color: white;
        }

        input, textarea, select {
            width: 100%;
            padding: 10px 14px;
            border: 1px solid #e2e8f0;
            border-radius: 20px;
            font-size: 14px;
            outline: none;
            background: rgba(255,255,255,0.9);
        }
        input:focus, textarea:focus {
            border-color: #6366f1;
            box-shadow: 0 0 0 2px rgba(99,102,241,0.2);
        }

        /* 导航栏 */
        .navbar {
            display: flex;
            justify-content: space-between;
            align-items: center;
            background: rgba(255, 255, 255, 0.85);
            backdrop-filter: blur(12px);
            border-radius: 60px;
            padding: 10px 24px;
            margin-bottom: 24px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.05);
        }
        .logo {
            font-weight: 800;
            font-size: 1.6rem;
            background: linear-gradient(135deg, #6366f1, #a855f7);
            -webkit-background-clip: text;
            background-clip: text;
            color: transparent;
        }
        .user-info {
            display: flex;
            align-items: center;
            gap: 20px;
        }
        .avatar {
            width: 40px;
            height: 40px;
            background: #818cf8;
            border-radius: 50%;
            background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>');
            background-size: 60%;
            background-position: center 55%;
            background-repeat: no-repeat;
        }

        /* 欢迎页大卡片 */
        .welcome-hero {
            min-height: 70vh;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            text-align: center;
            gap: 32px;
        }
        .welcome-title {
            font-size: 3rem;
            font-weight: 800;
            background: linear-gradient(135deg, #4f46e5, #c084fc);
            -webkit-background-clip: text;
            background-clip: text;
            color: transparent;
            text-shadow: 0 2px 10px rgba(0,0,0,0.05);
        }
        .welcome-sub {
            font-size: 1.2rem;
            color: #334155;
            max-width: 600px;
        }
        .btn-group {
            display: flex;
            gap: 20px;
            flex-wrap: wrap;
            justify-content: center;
        }
        .btn-large {
            padding: 12px 32px;
            font-size: 1.1rem;
        }

        /* 帖子卡片新样式 */
        .post-item {
            background: rgba(255, 255, 255, 0.9);
            backdrop-filter: blur(4px);
            border-left: 5px solid #818cf8;
            padding: 20px;
            margin-bottom: 18px;
            border-radius: 28px;
            box-shadow: 0 5px 15px rgba(0,0,0,0.05);
            cursor: pointer;
            transition: all 0.25s;
        }
        .post-item:hover {
            transform: translateY(-3px);
            background: white;
            box-shadow: 0 12px 25px rgba(0,0,0,0.1);
            border-left-color: #a855f7;
        }

        /* 其他组件样式 */
        .tabs {
            display: flex;
            gap: 12px;
            margin-bottom: 24px;
            flex-wrap: wrap;
        }
        .tab-btn {
            background: rgba(241, 245, 249, 0.8);
            border-radius: 40px;
            padding: 8px 24px;
            border: none;
            cursor: pointer;
            font-weight: 500;
        }
        .tab-btn.active {
            background: #6366f1;
            color: white;
        }
        .modal-mask {
            position: fixed;
            top:0;left:0;width:100%;height:100%;
            background: rgba(0,0,0,0.6);
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 1000;
        }
        .modal-content {
            background: white;
            max-width: 500px;
            width: 90%;
            border-radius: 32px;
            padding: 28px;
            animation: fadeSlideUp 0.25s ease;
        }
        @keyframes fadeSlideUp {
            from { opacity: 0; transform: translateY(20px);}
            to { opacity: 1; transform: translateY(0);}
        }
        .warning { color: #b91c1c; font-size: 14px; margin-top: 8px; }
        .text-small { font-size: 12px; color: #4b5563; }
        .flex-between { display: flex; justify-content: space-between; align-items: center; }
        .loading { text-align: center; padding: 40px; color: #4b5563; }

        /* 表情选择器 */
        .emoji-picker-wrapper { position: relative; margin-top: 8px; }
        .emoji-trigger-btn {
            background: #f1f5f9;
            border: none;
            padding: 6px 12px;
            border-radius: 30px;
            font-size: 18px;
            cursor: pointer;
            display: inline-flex;
            align-items: center;
            gap: 6px;
        }
        .emoji-panel {
            position: absolute;
            bottom: 100%;
            left: 0;
            margin-bottom: 8px;
            background: white;
            border-radius: 20px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.1);
            padding: 12px;
            width: 260px;
            display: grid;
            grid-template-columns: repeat(6, 1fr);
            gap: 8px;
            z-index: 100;
            border: 1px solid #eef2ff;
        }
        .emoji-item { font-size: 24px; text-align: center; cursor: pointer; padding: 6px; border-radius: 16px; transition: 0.1s; }
        .emoji-item:hover { background: #f1f5f9; transform: scale(1.15); }
        .badge { background: #ef4444; color: white; font-size: 11px; padding: 2px 8px; border-radius: 20px; margin-left: 6px; }
    </style>
</head>
<body>
<div id="app" class="app-container"></div>

<script>
    // API配置
    const API_BASE = 'http://localhost:5000/api';
    const STORAGE_KEYS = { token: 'empathy_token', currentUser: 'empathy_current_user' };

    // 随机背景图片库 (精美免费图片)
    const BG_IMAGES = [
        'https://images.pexels.com/photos/669615/pexels-photo-669615.jpeg?auto=compress&cs=tinysrgb&w=1600',
        'https://images.pexels.com/photos/1261728/pexels-photo-1261728.jpeg?auto=compress&cs=tinysrgb&w=1600',
        'https://images.pexels.com/photos/1423600/pexels-photo-1423600.jpeg?auto=compress&cs=tinysrgb&w=1600',
        'https://images.pexels.com/photos/235985/pexels-photo-235985.jpeg?auto=compress&cs=tinysrgb&w=1600',
        'https://images.pexels.com/photos/532826/pexels-photo-532826.jpeg?auto=compress&cs=tinysrgb&w=1600',
        'https://images.pexels.com/photos/1287145/pexels-photo-1287145.jpeg?auto=compress&cs=tinysrgb&w=1600'
    ];
    function setRandomBackground() {
        const randomIndex = Math.floor(Math.random() * BG_IMAGES.length);
        document.body.style.backgroundImage = `url(${BG_IMAGES[randomIndex]})`;
    }
    setRandomBackground();

    // 游客模式管理
    function setGuestMode(isGuest) {
        if (isGuest) {
            sessionStorage.setItem('guest_mode', 'true');
            localStorage.removeItem(STORAGE_KEYS.token);
            localStorage.removeItem(STORAGE_KEYS.currentUser);
        } else {
            sessionStorage.removeItem('guest_mode');
        }
    }
    function isGuestActive() {
        return sessionStorage.getItem('guest_mode') === 'true';
    }
    function getEffectiveUser() {
        const loggedUser = localStorage.getItem(STORAGE_KEYS.currentUser);
        if (loggedUser) return JSON.parse(loggedUser);
        if (isGuestActive()) {
            return { role: 'guest', nickname: '游客', id: null, username: 'guest', violation_count: 0 };
        }
        return null;
    }

    // HTTP请求 (保持原样)
    async function apiRequest(url, method = 'GET', data = null, requiresAuth = true) {
        const headers = { 'Content-Type': 'application/json' };
        if (requiresAuth) {
            const token = localStorage.getItem(STORAGE_KEYS.token);
            if (token) headers['Authorization'] = `Bearer ${token}`;
            else if (!isGuestActive()) throw new Error('请先登录');
        }
        const options = { method, headers, body: data ? JSON.stringify(data) : null };
        const response = await fetch(`${API_BASE}${url}`, options);
        if (!response.ok) {
            const result = await response.json();
            throw new Error(result.detail || '请求失败');
        }
        return await response.json();
    }

    // 表情包组件
    const DEFAULT_EMOJIS = ['😊', '😢', '❤️', '👍', '😂', '😭', '🥺', '😡', '💪', '🌟', '🔥', '🌈', '🌸', '🍀', '✨', '🎉', '💔', '🤗', '😇', '🥰'];
    function attachEmojiPicker(textarea) {
        if (!textarea || textarea.parentElement?.querySelector('.emoji-picker-wrapper')) return;
        const wrapper = document.createElement('div'); wrapper.className = 'emoji-picker-wrapper';
        const triggerBtn = document.createElement('button'); triggerBtn.type = 'button'; triggerBtn.className = 'emoji-trigger-btn'; triggerBtn.innerHTML = '😊 表情';
        const panel = document.createElement('div'); panel.className = 'emoji-panel'; panel.style.display = 'none';
        DEFAULT_EMOJIS.forEach(emoji => {
            const emojiSpan = document.createElement('span'); emojiSpan.textContent = emoji; emojiSpan.className = 'emoji-item';
            emojiSpan.addEventListener('click', () => { insertAtCursor(textarea, emoji); panel.style.display = 'none'; textarea.focus(); });
            panel.appendChild(emojiSpan);
        });
        triggerBtn.addEventListener('click', (e) => { e.stopPropagation(); document.querySelectorAll('.emoji-panel').forEach(p => p.style.display = 'none'); panel.style.display = panel.style.display === 'flex' ? 'none' : 'flex'; });
        wrapper.appendChild(triggerBtn); wrapper.appendChild(panel);
        textarea.parentNode.insertBefore(wrapper, textarea.nextSibling);
        document.addEventListener('click', function closePanel(e) { if (!wrapper.contains(e.target)) panel.style.display = 'none'; });
    }
    function insertAtCursor(textarea, text) {
        const start = textarea.selectionStart, end = textarea.selectionEnd;
        const value = textarea.value;
        textarea.value = value.substring(0, start) + text + value.substring(end);
        textarea.selectionStart = textarea.selectionEnd = start + text.length;
        textarea.dispatchEvent(new Event('input'));
    }
    function escapeHtml(str) { if (!str) return ''; return str.replace(/[&<>]/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[m])); }

    // 全局渲染控制
    const app = document.getElementById('app');
    let adminTab = 'review-posts';

    // 欢迎页面
    function renderWelcome() {
        app.innerHTML = `
            <div class="welcome-hero">
                <div class="welcome-title">🌱 情绪互助平台</div>
                <div class="welcome-sub">在这里,每一份情绪都被温柔接住。<br>匿名倾诉,真诚回应,让温暖传递。</div>
                <div class="btn-group">
                    <button id="welcomeUserLogin" class="btn btn-primary btn-large">👤 用户登录</button>
                    <button id="welcomeAdminLogin" class="btn btn-outline btn-large">🛡️ 管理员登录</button>
                    <button id="welcomeGuest" class="btn btn-large">👀 游客浏览</button>
                </div>
                <div class="text-small" style="margin-top: 30px;">💬 游客可查看动态,参与互动请登录</div>
            </div>
        `;
        document.getElementById('welcomeUserLogin')?.addEventListener('click', () => renderLoginRegister('user'));
        document.getElementById('welcomeAdminLogin')?.addEventListener('click', () => renderLoginRegister('admin'));
        document.getElementById('welcomeGuest')?.addEventListener('click', () => {
            setGuestMode(true);
            render();
        });
    }

    // 登录注册界面 (保留返回欢迎页)
    function renderLoginRegister(roleHint = 'user') {
        let remember = localStorage.getItem('remember_username') || '';
        app.innerHTML = `
            <div class="card" style="max-width: 450px; margin: 40px auto;">
                <button id="backToWelcome" class="btn btn-small" style="margin-bottom: 16px;">← 返回首页</button>
                <h2 style="margin-bottom: 20px;">🔐 ${roleHint === 'admin' ? '管理员' : '用户'}登录 / 注册</h2>
                <div id="loginForm">
                    <input type="text" id="loginUsername" placeholder="用户名" value="${remember}"><br><br>
                    <input type="password" id="loginPassword" placeholder="密码"><br><br>
                    <label><input type="checkbox" id="rememberPwd"> 记住用户名</label><br><br>
                    <button id="doLoginBtn" class="btn btn-primary">登录</button>
                    <button id="goRegBtn" class="btn" style="margin-left: 10px;">注册新账号</button>
                </div>
                <div id="regForm" style="display:none;">
                    <input type="text" id="regUsername" placeholder="用户名"><br><br>
                    <input type="password" id="regPassword" placeholder="密码"><br><br>
                    <input type="text" id="regNickname" placeholder="昵称(唯一)"><br><br>
                    <button id="doRegBtn" class="btn btn-primary">注册</button>
                    <button id="backLoginBtn" class="btn">返回登录</button>
                </div>
            </div>
        `;
        document.getElementById('backToWelcome')?.addEventListener('click', () => renderWelcome());
        document.getElementById('doLoginBtn')?.addEventListener('click', async () => {
            const username = document.getElementById('loginUsername').value;
            const pwd = document.getElementById('loginPassword').value;
            try {
                const result = await apiRequest('/login', 'POST', { username, password: pwd }, false);
                localStorage.setItem(STORAGE_KEYS.token, result.access_token);
                const user = await apiRequest('/users/me');
                localStorage.setItem(STORAGE_KEYS.currentUser, JSON.stringify(user));
                setGuestMode(false);  // 清除游客标记
                if (document.getElementById('rememberPwd').checked) localStorage.setItem('remember_username', username);
                else localStorage.removeItem('remember_username');
                render();
            } catch (error) { alert('登录失败: ' + error.message); }
        });
        document.getElementById('goRegBtn')?.addEventListener('click', () => { document.getElementById('loginForm').style.display = 'none'; document.getElementById('regForm').style.display = 'block'; });
        document.getElementById('backLoginBtn')?.addEventListener('click', () => { document.getElementById('loginForm').style.display = 'block'; document.getElementById('regForm').style.display = 'none'; });
        document.getElementById('doRegBtn')?.addEventListener('click', async () => {
            const username = document.getElementById('regUsername').value, pwd = document.getElementById('regPassword').value, nickname = document.getElementById('regNickname').value;
            if (!username || !pwd || !nickname) return alert('请填写完整');
            try {
                await apiRequest('/register', 'POST', { username, password: pwd, nickname }, false);
                alert('注册成功,请登录');
                document.getElementById('regForm').style.display = 'none'; document.getElementById('loginForm').style.display = 'block';
            } catch (error) { alert('注册失败: ' + error.message); }
        });
    }

    async function render() {
        const user = getEffectiveUser();
        if (!user) { renderWelcome(); return; }
        if (user.role === 'admin') await renderAdminPanel(user);
        else await renderUserPanel(user);
    }

    // 普通用户面板 (含游客模式适配)
    async function renderUserPanel(user) {
        const isGuest = user.role === 'guest';
        let unreadCount = 0;
        if (!isGuest) {
            try { const messages = await apiRequest('/messages'); unreadCount = messages.filter(m => !m.is_read).length; } catch(e) { console.log(e); }
        }
        app.innerHTML = `
            <div class="navbar">
                <div class="logo">💬 情绪互助</div>
                <div class="user-info">
                    <span>${user.nickname} ${isGuest ? '(游客)' : ''}</span>
                    ${!isGuest ? `<div class="msg-badge" id="msgIcon">🔔 ${unreadCount > 0 ? '<span class="red-dot" style="background:red; display:inline-block; width:10px;height:10px; border-radius:50%;"></span>' : ''}</div>` : ''}
                    <div class="avatar"></div>
                    <button id="logoutOrHomeBtn" class="btn">${isGuest ? '🏠 返回首页' : '退出'}</button>
                </div>
            </div>
            <div class="tabs">
                <button data-tab="feed" class="tab-btn active">📖 温暖动态</button>
                ${!isGuest ? '<button data-tab="profile" class="tab-btn">👤 个人中心</button>' : ''}
                ${!isGuest ? '<button id="newPostBtn" class="btn btn-primary">➕ 发布需求</button>' : '<span class="text-small" style="margin-left: auto;">游客模式 · 仅可阅览</span>'}
            </div>
            <div id="feedContainer"></div>
            <div id="profileContainer" style="display:none;"></div>
        `;
        document.querySelectorAll('.tab-btn[data-tab]').forEach(btn => {
            btn.addEventListener('click', async () => {
                const tab = btn.dataset.tab;
                document.getElementById('feedContainer').style.display = tab === 'feed' ? 'block' : 'none';
                if (document.getElementById('profileContainer')) document.getElementById('profileContainer').style.display = tab === 'profile' ? 'block' : 'none';
                if (tab === 'feed') await renderFeed(user);
                if (tab === 'profile' && !isGuest) renderProfile(user);
            });
        });
        document.getElementById('logoutOrHomeBtn').onclick = () => {
            if (isGuest) { setGuestMode(false); renderWelcome(); }
            else { localStorage.removeItem(STORAGE_KEYS.token); localStorage.removeItem(STORAGE_KEYS.currentUser); setGuestMode(false); renderWelcome(); }
        };
        if (!isGuest) document.getElementById('newPostBtn')?.addEventListener('click', () => showNewPostModal(user));
        if (!isGuest) document.getElementById('msgIcon')?.addEventListener('click', () => showMessagesModal(user));
        await renderFeed(user);
        if (!isGuest) renderProfile(user);
    }

    async function renderFeed(user) {
        const container = document.getElementById('feedContainer');
        if (!container) return;
        container.innerHTML = '<div class="loading">✨ 加载温暖动态...</div>';
        try {
            const posts = await apiRequest('/posts');
            if (posts.length === 0) { container.innerHTML = '<div class="card">🌟 暂无需求动态,成为第一个分享的人吧~</div>'; return; }
            container.innerHTML = posts.map(post => `
                <div class="post-item" data-postid="${post.id}">
                    <div class="flex-between"><strong>${escapeHtml(post.nickname)}</strong><span class="text-small">${new Date(post.created_at).toLocaleString()}</span></div>
                    <div style="margin: 10px 0; font-size: 15px;">${escapeHtml(post.content)}</div>
                    <div class="text-small">💬 ${post.comment_count} 条回应</div>
                </div>
            `).join('');
            document.querySelectorAll('.post-item').forEach(el => el.addEventListener('click', () => showPostDetail(parseInt(el.dataset.postid), user)));
        } catch (error) { container.innerHTML = '<div class="card">加载失败,请重试</div>'; }
    }

    async function showPostDetail(postId, user) {
        const modalDiv = document.createElement('div'); modalDiv.className = 'modal-mask';
        try {
            const post = await apiRequest(`/posts/${postId}`);
            const comments = await apiRequest(`/posts/${postId}/comments`);
            const isGuest = user.role === 'guest';
            const isBannedUser = (!isGuest && user.ban_until && new Date(user.ban_until) > new Date());
            const isAdmin = user.role === 'admin';
            const isOwnPost = !isGuest && post.user_id === user.id;
            const canDeletePost = isOwnPost || isAdmin;
            
            modalDiv.innerHTML = `
                <div class="modal-content">
                    <div class="flex-between">
                        <h3>${escapeHtml(post.nickname)} 的倾诉</h3>
                        ${canDeletePost ? `<button id="deletePostBtn" class="btn btn-danger" style="font-size:12px">🗑️ 删除</button>` : ''}
                    </div>
                    <p style="margin:12px 0">${escapeHtml(post.content)}</p>
                    <small>${new Date(post.created_at).toLocaleString()}</small><hr style="margin:12px 0">
                    <h4>💬 回应区</h4>
                    <div id="commentList">${comments.length > 0 ? comments.map(c => ` <div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:8px;"> <div><strong>${c.is_ai_generated ? '🤖 AI' : escapeHtml(c.nickname)}</strong>: ${escapeHtml(c.content)}</div> ${(!isGuest && (c.user_id === user.id || isAdmin) && !c.is_ai_generated) ? `<button class="btn btn-danger" style="font-size:10px; padding:2px 8px;" data-commentid="${c.id}">删除</button>` : ''} </div> `).join('') : '✨ 还没有回应,来当第一个温暖的人吧'}</div>
                    ${!isGuest && !isBannedUser ? `<div style="margin-top: 16px;"><textarea id="newComment" rows="2" placeholder="写下你的回应... 可以使用表情包"></textarea><button id="submitCommentBtn" class="btn btn-primary" style="margin-top:8px">💬 发送回应</button></div>` : (isGuest ? `<div class="warning">🔒 游客无法评论,请登录后参与互动~</div>` : (isBannedUser ? `<div class="warning">⛔ 您已被禁言,无法评论</div>` : ''))}
                    <button id="closeModal" class="btn" style="margin-top:12px">关闭</button>
                </div>
            `;
            document.body.appendChild(modalDiv);
            
            if (canDeletePost) {
                document.getElementById('deletePostBtn')?.addEventListener('click', async () => {
                    if (confirm('确定要删除这个帖子吗?')) {
                        try {
                            await apiRequest(`/posts/${postId}`, 'DELETE');
                            alert('删除成功');
                            modalDiv.remove();
                            render();
                        } catch (error) {
                            alert('删除失败: ' + error.message);
                        }
                    }
                });
            }
            
            modalDiv.querySelectorAll('.btn-danger[data-commentid]').forEach(btn => {
                btn.addEventListener('click', async () => {
                    const commentId = parseInt(btn.dataset.commentid);
                    if (confirm('确定要删除这个评论吗?')) {
                        try {
                            await apiRequest(`/comments/${commentId}`, 'DELETE');
                            alert('删除成功');
                            modalDiv.remove();
                            showPostDetail(postId, user);
                        } catch (error) {
                            alert('删除失败: ' + error.message);
                        }
                    }
                });
            });
            
            const commentTextarea = modalDiv.querySelector('#newComment');
            if (commentTextarea) attachEmojiPicker(commentTextarea);
            const submitBtn = document.getElementById('submitCommentBtn');
            submitBtn?.addEventListener('click', async () => {
                const content = document.getElementById('newComment').value.trim();
                if (!content) return alert('内容不能为空');
                if (isGuest) return alert('请先登录再评论');
                
                submitBtn.disabled = true;
                submitBtn.textContent = '等待中...';
                
                try { 
                const result = await apiRequest(`/posts/${postId}/comments`, 'POST', { content });
                
                submitBtn.disabled = false;
                submitBtn.textContent = '💬 发送回应';
                
                if (result.status === 'pending') {
                    alert('评论已提交,需要管理员审核后才会显示');
                } else {
                    alert('评论发布成功!');
                }
                
                modalDiv.remove(); 
                showPostDetail(postId, user); 
            }
            catch (error) { 
                submitBtn.disabled = false;
                submitBtn.textContent = '💬 发送回应';
                alert('发送失败: ' + error.message);
            }
            });
            document.getElementById('closeModal').onclick = () => modalDiv.remove();
        } catch (error) { alert('加载失败'); modalDiv.remove(); }
    }

    function renderProfile(user) {
        const container = document.getElementById('profileContainer');
        if (!container) return;
        const banStatus = (user.ban_until && new Date(user.ban_until) > new Date()) ? `<div class="warning">禁言中</div>` : '';
        container.innerHTML = `
            <div class="card"><h3>个人资料</h3><div>昵称: ${user.nickname} <button id="changeNicknameBtn" class="btn btn-small">修改</button></div>
            <div>用户名: ${user.username}</div><div>违规次数: ${user.violation_count}</div>${banStatus}<hr>
            <button id="changePwdBtn" class="btn">修改密码</button>
            <button id="logoutAccountBtn" class="btn btn-danger">注销账号</button></div>`;
        document.getElementById('changeNicknameBtn')?.addEventListener('click', async () => { const nn = prompt('新昵称'); if(nn){ await apiRequest('/users/me', 'PUT', { nickname: nn }); alert('成功'); render(); } });
        document.getElementById('changePwdBtn')?.addEventListener('click', async () => { const oldPwd = prompt('原密码'), newPwd = prompt('新密码'); if(oldPwd && newPwd){ await apiRequest('/users/me/change-password', 'POST', { old_password: oldPwd, new_password: newPwd }); alert('成功'); render(); } });
        document.getElementById('logoutAccountBtn')?.addEventListener('click', async () => { if(confirm('注销账号不可逆')){ await apiRequest('/users/me', 'DELETE'); localStorage.clear(); setGuestMode(false); renderWelcome(); } });
    }

    function showNewPostModal(user) { 
        if (user.role === 'guest') return alert('游客无法发布'); 
        if (isBanned(user)) return alert('您已被禁言'); 
        
        const modal = document.createElement('div'); 
        modal.className = 'modal-mask'; 
        modal.innerHTML = `<div class="modal-content"><textarea id="postContent" rows="4" placeholder="写下你的情绪需求... 💬"></textarea><div style="display:flex; justify-content:flex-end; gap:8px; margin-top:16px;"><button id="submitPost" class="btn btn-primary">✨ 发布(需审核)</button><button id="cancelPost" class="btn">取消</button></div></div>`; 
        document.body.appendChild(modal); 
        
        const textarea = modal.querySelector('#postContent'); 
        if (textarea) attachEmojiPicker(textarea); 
        
        const submitBtn = document.getElementById('submitPost');
        
        submitBtn.onclick = async () => { 
            const content = document.getElementById('postContent').value.trim(); 
            if (!content) return alert('内容不能为空'); 
            
            submitBtn.disabled = true;
            submitBtn.textContent = '等待中...';
            
            try { 
                await apiRequest('/posts', 'POST', { content }); 
                alert('提交成功,等待审核'); 
                modal.remove(); 
                render(); 
            } catch (error) { 
                submitBtn.disabled = false;
                submitBtn.textContent = '✨ 发布(需审核)';
                alert('发布失败: ' + error.message); 
            } 
        }; 
        
        document.getElementById('cancelPost').onclick = () => modal.remove(); 
    }
    async function showMessagesModal(user) { try { const msgs = await apiRequest('/messages'); if (msgs.length===0) { alert('暂无消息'); return; } alert(msgs.map(m=>`${m.type==='comment'?'💬回应':'📢系统'}: ${m.content}`).join('\n\n')); await apiRequest('/messages/read', 'PUT'); render(); } catch(e) { alert('获取消息失败'); } }
    function isBanned(user) { return user.ban_until && new Date(user.ban_until) > new Date(); }

    // 管理员面板 (保持原有逻辑,仅优化样式)
    async function renderAdminPanel(admin, showAlert = true) {
        let pendingCount = 0;
        try { const posts = await apiRequest('/admin/pending-posts'); pendingCount = posts.length; } catch(e) {}
        app.innerHTML = `<div class="navbar"><div class="logo">🛡️ 管理后台</div><div><button id="adminLogout" class="btn">退出登录</button></div></div>
            <div class="tabs"><button data-admin="review-posts" class="tab-btn active">📋 需求审核 ${pendingCount?`<span class="badge">${pendingCount}</span>`:''}</button>
            <button data-admin="review-comments" class="tab-btn">💬 评论审核</button><button data-admin="user-manage" class="tab-btn">👥 账号管理</button>
            <button data-admin="stats" class="tab-btn">📊 情绪热词</button></div><div id="adminContent" class="loading">加载中...</div>`;
        if (pendingCount > 0 && showAlert) showReviewAlert(pendingCount, admin);
        document.getElementById('adminLogout').onclick = () => { localStorage.clear(); setGuestMode(false); renderWelcome(); };
        document.querySelectorAll('[data-admin]').forEach(btn => btn.addEventListener('click', () => { adminTab = btn.dataset.admin; renderAdminPanel(admin, false); }));
        if (adminTab === 'review-posts') await renderReviewPosts();
        if (adminTab === 'review-comments') await renderReviewComments();
        if (adminTab === 'user-manage') await renderUserManage();
        if (adminTab === 'stats') renderStats();
    }
    async function renderReviewPosts() { const cont = document.getElementById('adminContent'); try { const posts = await apiRequest('/admin/pending-posts'); cont.innerHTML = posts.length ? posts.map(p => `<div class="card"><strong>${p.nickname}</strong>: ${p.content}<br><button class="btn btn-primary" data-approve="${p.id}">通过(触发AI)</button><button class="btn btn-danger" data-reject="${p.id}">驳回</button></div>`).join('') : '<div class="card">无待审核需求</div>'; document.querySelectorAll('[data-approve]').forEach(btn => btn.onclick = async () => { await apiRequest(`/admin/posts/${btn.dataset.approve}/approve`, 'POST'); await renderReviewPosts(); }); document.querySelectorAll('[data-reject]').forEach(btn => btn.onclick = async () => { const reason = prompt('驳回原因'); await apiRequest(`/admin/posts/${btn.dataset.reject}/reject`, 'POST', { reason }); await renderReviewPosts(); }); } catch(e){ cont.innerHTML='<div class="card">加载失败</div>'; } }
    async function renderReviewComments() { 
        const cont = document.getElementById('adminContent'); 
        try { 
            const comments = await apiRequest('/admin/pending-comments'); 
            cont.innerHTML = comments.length ? comments.map(c => `<div class="card"><strong>${c.nickname}</strong> 回应了需求: "${c.post_content}"<br><div style="margin-top:8px;">评论内容: ${c.content}</div><br><button class="btn btn-primary" data-approve="${c.id}">✅ 通过审核</button><button class="btn btn-danger" data-reject="${c.id}">❌ 驳回</button></div>`).join('') : '<div class="card">暂无待审核评论</div>'; 
            document.querySelectorAll('[data-approve]').forEach(btn => btn.onclick = async () => { 
                await apiRequest(`/admin/comments/${btn.dataset.approve}/approve`, 'POST'); 
                await renderReviewComments(); 
            }); 
            document.querySelectorAll('[data-reject]').forEach(btn => btn.onclick = async () => { 
                if(confirm('确定驳回此评论并记录用户违规?')){ 
                    await apiRequest(`/admin/comments/${btn.dataset.reject}/reject`, 'POST'); 
                    await renderReviewComments(); 
                } 
            }); 
        } catch(e){ cont.innerHTML='<div class="card">加载失败</div>'; } 
    }
    async function renderUserManage() { const cont = document.getElementById('adminContent'); try { const users = await apiRequest('/admin/users'); cont.innerHTML = `<table class="admin-table"><tr><th>昵称</th><th>违规次数</th><th>状态</th><th>操作</th></tr>${users.map(u => `<tr><td>${u.nickname}</td><td>${u.violation_count}</td><td>${u.is_deleted ? '已注销' : (u.ban_until && new Date(u.ban_until) > new Date() ? '禁言中' : '正常')}</td><td><button data-del="${u.id}" class="btn btn-danger">删除账号</button></td></tr>`).join('')}</table>`; document.querySelectorAll('[data-del]').forEach(btn => btn.onclick = async () => { if(confirm('删除用户')){ await apiRequest(`/admin/users/${btn.dataset.del}`, 'DELETE'); await renderUserManage(); } }); } catch(e){ cont.innerHTML='<div class="card">加载失败</div>'; } }
    function renderStats() { const cont = document.getElementById('adminContent'); cont.innerHTML = `<select id="statRange"><option value="day">今日</option><option value="week">本周</option><option value="month">本月</option></select><button id="refreshStat" class="btn btn-primary">刷新</button><div id="wordCloud"></div>`; const update = async () => { const range = document.getElementById('statRange').value; const wordCloud = document.getElementById('wordCloud'); wordCloud.innerHTML = '<div class="loading">加载中...</div>'; try { const stats = await apiRequest(`/admin/stats/keywords?range=${range}`); wordCloud.innerHTML = `<div class="card"><h3>🔥 需求词排行</h3>${stats.top_keywords.length ? stats.top_keywords.map(t => `<div>${t.word} : ${t.count}次</div>`).join('') : '暂无数据'}</div>`; } catch(e){ wordCloud.innerHTML = '<div class="card">加载失败</div>'; } }; document.getElementById('refreshStat')?.addEventListener('click', update); update(); }
    function showReviewAlert(count, admin) { const modal = document.createElement('div'); modal.className = 'modal-overlay'; modal.innerHTML = `<div class="modal-content"><div class="modal-header"><h3>🔔 有待审核需求</h3><button class="modal-close" onclick="this.parentElement.parentElement.parentElement.remove()">&times;</button></div><div class="modal-body"><p>检测到 <strong>${count}</strong> 条新需求需要您人工处理</p><p>AI分析后需要管理员审核</p></div><div class="modal-footer"><button id="goReviewBtn" class="btn btn-primary">立即审核</button><button class="btn" onclick="this.parentElement.parentElement.remove()">稍后</button></div></div>`; document.body.appendChild(modal); document.getElementById('goReviewBtn')?.addEventListener('click', () => { modal.remove(); adminTab = 'review-posts'; renderAdminPanel(admin, false); }); }

    render();
</script>
</body>
</html>
  • 后端
# main.py
# FastAPI 后端 for 情绪互助平台
# 运行方式: uvicorn main:app --reload
# 需要安装: fastapi, uvicorn, sqlalchemy, python-jose, python-multipart, python-multipart

from fastapi import FastAPI, HTTPException, Depends, status, Header
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime, Text, func
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from pydantic import BaseModel, Field
from typing import List, Optional
from datetime import datetime, timedelta
import hashlib
import secrets
import requests
import os
import json
from dotenv import load_dotenv

# 加载环境变量
load_dotenv()

# ---------- 配置 ----------
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./empathy.db")
SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-in-production-2024")

# Dify 配置
DIFY_API_URL = os.getenv("DIFY_API_URL", "http://localhost/v1/workflows/run")
DIFY_API_KEY = os.getenv("DIFY_API_KEY", "")
DIFY_RESPONSE_MODE = os.getenv("DIFY_RESPONSE_MODE", "blocking")
DIFY_TIMEOUT = int(os.getenv("DIFY_TIMEOUT", "30"))

ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "10080"))

# ---------- 数据库 ----------
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()


class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, index=True)
    username = Column(String(50), unique=True, index=True)
    password_hash = Column(String(128))
    nickname = Column(String(50), unique=True)
    avatar = Column(String(200), default="/static/default-avatar.png")
    role = Column(String(20), default="user")  # user / admin
    violation_count = Column(Integer, default=0)
    ban_until = Column(DateTime, nullable=True)
    is_deleted = Column(Boolean, default=False)
    created_at = Column(DateTime, default=datetime.utcnow)


class Post(Base):
    __tablename__ = "posts"
    id = Column(Integer, primary_key=True, index=True)
    user_id = Column(Integer, index=True)
    content = Column(Text)
    status = Column(String(20), default="pending")  # pending, approved, rejected
    reject_reason = Column(String(200), nullable=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)


class Comment(Base):
    __tablename__ = "comments"
    id = Column(Integer, primary_key=True, index=True)
    post_id = Column(Integer, index=True)
    user_id = Column(Integer, nullable=True)  # null 表示 AI 生成
    content = Column(Text)
    status = Column(String(20), default="visible")  # visible, hidden
    is_ai_generated = Column(Boolean, default=False)
    created_at = Column(DateTime, default=datetime.utcnow)


class Message(Base):
    __tablename__ = "messages"
    id = Column(Integer, primary_key=True, index=True)
    user_id = Column(Integer, index=True)
    type = Column(String(20))  # comment, system
    content = Column(Text)
    related_id = Column(Integer, nullable=True)  # post_id or comment_id
    is_read = Column(Boolean, default=False)
    created_at = Column(DateTime, default=datetime.utcnow)


Base.metadata.create_all(bind=engine)


# ---------- Pydantic 模型 ----------
class UserCreate(BaseModel):
    username: str
    password: str
    nickname: str


class UserLogin(BaseModel):
    username: str
    password: str


class UserOut(BaseModel):
    id: int
    username: str
    nickname: str
    avatar: str
    role: str
    violation_count: int
    ban_until: Optional[datetime] = None


class Token(BaseModel):
    access_token: str
    token_type: str


class PostCreate(BaseModel):
    content: str


class PostOut(BaseModel):
    id: int
    user_id: int
    nickname: str
    avatar: str
    content: str
    status: str
    reject_reason: Optional[str] = None
    created_at: datetime
    comment_count: int = 0


class CommentCreate(BaseModel):
    content: str


class CommentOut(BaseModel):
    id: int
    user_id: Optional[int]
    nickname: Optional[str]
    avatar: Optional[str]
    content: str
    is_ai_generated: bool
    created_at: datetime
    status: Optional[str] = None


class MessageOut(BaseModel):
    id: int
    type: str
    content: str
    related_id: Optional[int]
    is_read: bool
    created_at: datetime


class PasswordChange(BaseModel):
    old_password: str
    new_password: str


class AdminPostApprove(BaseModel):
    reason: Optional[str] = None


# ---------- 工具函数 ----------
def get_password_hash(password: str) -> str:
    return hashlib.sha256(password.encode()).hexdigest()


def verify_password(plain: str, hashed: str) -> bool:
    return get_password_hash(plain) == hashed


def create_access_token(data: dict, expires_delta: timedelta = None):
    from jose import jwt
    to_encode = data.copy()
    expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)


def call_dify_workflow(text: str) -> dict:
    """调用 Dify 工作流,返回情绪分类和 AI 回复"""

    # 检查配置
    if not DIFY_API_KEY:
        print("DIFY_API_KEY 未配置,使用降级模式")
        return {"needs_review": True, "ai_response": "", "class_name": "配置错误"}

    print(f"调用 Dify 发帖工作流: {DIFY_API_URL}")
    print(f"输入内容: {text[:100]}...")

    headers = {
        'Authorization': f'Bearer {DIFY_API_KEY}',
        'Content-Type': 'application/json'
    }

    payload = {
        "inputs": {"text": text},
        "response_mode": "streaming",
        "user": "abc-123"
    }

    try:
        print(f"发送请求: {payload}")
        response = requests.post(DIFY_API_URL, headers=headers, json=payload, timeout=DIFY_TIMEOUT, stream=True)
        print(f"响应状态码: {response.status_code}")

        response.raise_for_status()

        ai_response = ""
        class_name = "normal"
        needs_review = False
        raw_response = ""

        for line in response.iter_lines():
            if line:
                try:
                    line_text = line.decode('utf-8')
                    if line_text.startswith('data: '):
                        json_str = line_text[6:]
                        try:
                            data = json.loads(json_str)
                            raw_response = json_str

                            if isinstance(data, dict):
                                if "data" in data and "outputs" in data["data"]:
                                    outputs = data["data"]["outputs"]
                                    if "text" in outputs:
                                        ai_response = outputs["text"]
                                    if "class_name" in outputs:
                                        class_name = outputs["class_name"]
                                    if "needs_review" in outputs:
                                        needs_review_val = outputs["needs_review"]
                                        if isinstance(needs_review_val, bool):
                                            needs_review = needs_review_val
                                        elif isinstance(needs_review_val, str):
                                            needs_review = needs_review_val.lower() == "true"
                                elif "outputs" in data:
                                    outputs = data["outputs"]
                                    if "text" in outputs:
                                        ai_response = outputs["text"]
                                    if "class_name" in outputs:
                                        class_name = outputs["class_name"]
                                elif "text" in data:
                                    ai_response = data["text"]
                                    if "class_name" in data:
                                        class_name = data["class_name"]
                        except json.JSONDecodeError:
                            continue
                except UnicodeDecodeError:
                    continue

        print(f"原始响应: {raw_response[:200] if raw_response else 'N/A'}...")

        if not ai_response:
            ai_response = "感谢你的分享,我们一直在这里支持你"

        keywords_for_review = ["自杀", "自残", "暴力", "伤害", "绝望"]
        needs_review = any(kw in text for kw in keywords_for_review)

        print(f"解析结果: needs_review={needs_review}, class_name={class_name}, ai_response={ai_response[:50]}...")

        return {
            "needs_review": needs_review,
            "ai_response": ai_response,
            "class_name": class_name
        }

    except requests.exceptions.ConnectionError as e:
        print(f"❌ Dify API 连接失败: 无法连接到 {DIFY_API_URL}")
        print(f"   错误详情: {str(e)}")
        print(f"   请检查:")
        print(f"   1. Dify 服务是否启动")
        print(f"   2. API URL 是否正确")
        print(f"   3. 网络连接是否正常")
        return {"needs_review": True, "ai_response": "", "class_name": "连接失败"}

    except requests.exceptions.Timeout as e:
        print(f"❌ Dify API 请求超时: {str(e)}")
        return {"needs_review": True, "ai_response": "", "class_name": "请求超时"}

    except requests.exceptions.HTTPError as e:
        print(f"❌ Dify API HTTP 错误: {e}")
        print(f"   响应内容: {response.text if 'response' in locals() else 'N/A'}")
        return {"needs_review": True, "ai_response": "",
                "class_name": f"HTTP错误: {response.status_code}"}

    except Exception as e:
        print(f"❌ Dify API 调用失败: {str(e)}")
        import traceback
        traceback.print_exc()
        return {"needs_review": True, "ai_response": "", "class_name": f"错误: {str(e)}"}


def call_dify_comment_workflow(comment_content: str) -> dict:
    """调用 Dify 评论审核工作流,返回审核结果"""

    if not DIFY_API_KEY:
        print("DIFY_API_KEY 未配置,评论审核使用默认规则")
        return {"approved": False, "class_name": "nopass"}

    print(f"调用 Dify 评论审核工作流")
    print(f"评论内容: {comment_content[:100]}...")

    headers = {
        'Authorization': f'Bearer {DIFY_API_KEY}',
        'Content-Type': 'application/json'
    }

    payload = {
        "inputs": {"input": comment_content},
        "response_mode": "streaming",
        "user": "abc-123"
    }

    try:
        print(f"发送请求: {payload}")
        response = requests.post(DIFY_API_URL, headers=headers, json=payload, timeout=DIFY_TIMEOUT, stream=True)
        print(f"响应状态码: {response.status_code}")

        response.raise_for_status()

        class_name = "nopass"
        approved = False
        raw_response = ""
        
        # 先尝试作为流式响应处理
        lines = list(response.iter_lines())
        
        # 如果没有流式数据,尝试直接解析为JSON
        if not lines or all(not line for line in lines):
            print("未检测到流式数据,尝试直接解析JSON响应")
            try:
                result = response.json()
                raw_response = json.dumps(result)
                
                if isinstance(result, dict):
                    if "data" in result and "outputs" in result["data"]:
                        outputs = result["data"]["outputs"]
                        if "class_name" in outputs:
                            class_name = outputs["class_name"]
                    elif "outputs" in result:
                        outputs = result["outputs"]
                        if "class_name" in outputs:
                            class_name = outputs["class_name"]
                    elif "class_name" in result:
                        class_name = result["class_name"]
            except:
                pass
        else:
            # 处理流式响应
            for line in lines:
                if line:
                    try:
                        line_text = line.decode('utf-8')
                        if line_text.startswith('data: '):
                            json_str = line_text[6:]
                            try:
                                data = json.loads(json_str)
                                raw_response = json_str

                                if isinstance(data, dict):
                                    if "data" in data and "outputs" in data["data"]:
                                        outputs = data["data"]["outputs"]
                                        if "class_name" in outputs:
                                            class_name = outputs["class_name"]
                                    elif "outputs" in data:
                                        outputs = data["outputs"]
                                        if "class_name" in outputs:
                                            class_name = outputs["class_name"]
                                    elif "class_name" in data:
                                        class_name = data["class_name"]
                            except json.JSONDecodeError:
                                continue
                    except UnicodeDecodeError:
                        continue

        print(f"原始响应: {raw_response[:200] if raw_response else 'N/A'}...")

        class_name = str(class_name).lower().strip()
        approved = (class_name == "pass")

        print(f"审核结果: class_name={class_name}, approved={approved}")

        return {
            "approved": approved,
            "class_name": class_name
        }

    except requests.exceptions.ConnectionError as e:
        print(f"Dify 评论审核 API 连接失败: {str(e)}")
        print("使用本地审核规则")
        
        # 本地审核规则:如果没有敏感词,则通过
        sensitive_keywords = ["自杀", "自残", "暴力", "伤害", "杀人", "骂人", "脏话", "政治", "色情", "赌博",
                             "傻逼", "傻比", "操", "草", "日", "妈", "娘", "狗屁", "他妈的", "你妈",
                             "卧槽", "滚蛋", "去死", "废物", "垃圾", "脑残", "智障", "煞笔",
                             "Fuck", "Shit", "Bitch", "Asshole", "Damn"]
        needs_review = any(keyword in comment_content for keyword in sensitive_keywords)
        
        return {
            "approved": not needs_review,
            "class_name": "pass" if not needs_review else "nopass"
        }

    except requests.exceptions.Timeout as e:
        print(f"Dify 评论审核 API 请求超时: {str(e)}")
        print("使用本地审核规则")
        
        # 本地审核规则:如果没有敏感词,则通过
        sensitive_keywords = ["自杀", "自残", "暴力", "伤害", "杀人", "骂人", "脏话", "政治", "色情", "赌博",
                             "傻逼", "傻比", "操", "草", "日", "妈", "娘", "狗屁", "他妈的", "你妈",
                             "卧槽", "滚蛋", "去死", "废物", "垃圾", "脑残", "智障", "煞笔",
                             "Fuck", "Shit", "Bitch", "Asshole", "Damn"]
        needs_review = any(keyword in comment_content for keyword in sensitive_keywords)
        
        return {
            "approved": not needs_review,
            "class_name": "pass" if not needs_review else "nopass"
        }

    except requests.exceptions.HTTPError as e:
        print(f"Dify 评论审核 API HTTP 错误: {e}")
        print(f"响应内容: {response.text if hasattr(response, 'text') else 'N/A'}")
        print("使用本地审核规则")
        
        # 本地审核规则:如果没有敏感词,则通过
        sensitive_keywords = ["自杀", "自残", "暴力", "伤害", "杀人", "骂人", "脏话", "政治", "色情", "赌博",
                             "傻逼", "傻比", "操", "草", "日", "妈", "娘", "狗屁", "他妈的", "你妈",
                             "卧槽", "滚蛋", "去死", "废物", "垃圾", "脑残", "智障", "煞笔",
                             "Fuck", "Shit", "Bitch", "Asshole", "Damn"]
        needs_review = any(keyword in comment_content for keyword in sensitive_keywords)
        
        return {
            "approved": not needs_review,
            "class_name": "pass" if not needs_review else "nopass"
        }

    except Exception as e:
        print(f"Dify 评论审核 API 调用失败: {str(e)}")
        import traceback
        traceback.print_exc()
        print("使用本地审核规则")
        
        # 本地审核规则:如果没有敏感词,则通过
        sensitive_keywords = ["自杀", "自残", "暴力", "伤害", "杀人", "骂人", "脏话", "政治", "色情", "赌博",
                             "傻逼", "傻比", "操", "草", "日", "妈", "娘", "狗屁", "他妈的", "你妈",
                             "卧槽", "滚蛋", "去死", "废物", "垃圾", "脑残", "智障", "煞笔",
                             "Fuck", "Shit", "Bitch", "Asshole", "Damn"]
        needs_review = any(keyword in comment_content for keyword in sensitive_keywords)
        
        return {
            "approved": not needs_review,
            "class_name": "pass" if not needs_review else "nopass"
        }


def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


def get_current_user(authorization: str = Header(None), db: Session = Depends(get_db)):
    from jose import jwt, JWTError
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )

    if authorization is None:
        raise credentials_exception

    try:
        token = authorization.replace("Bearer ", "")
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        user_id_str = payload.get("sub")
        if user_id_str is None:
            raise credentials_exception
        user_id = int(user_id_str)
    except JWTError:
        raise credentials_exception

    user = db.query(User).filter(User.id == user_id, User.is_deleted == False).first()
    if user is None:
        raise credentials_exception
    return user


def get_current_admin(user: User = Depends(get_current_user)):
    if user.role != "admin":
        raise HTTPException(status_code=403, detail="Admin permission required")
    return user


def is_user_banned(user: User) -> bool:
    return user.ban_until is not None and user.ban_until > datetime.utcnow()


def add_violation(db: Session, user_id: int, reason: str):
    user = db.query(User).filter(User.id == user_id).first()
    if not user or user.role == "admin":
        return
    user.violation_count += 1
    msg = f"违规行为: {reason},当前违规次数 {user.violation_count}"
    if user.violation_count >= 4:
        user.is_deleted = True
        msg = "您的账号因多次违规已被永久注销。"
        db.add(Message(user_id=user_id, type="system", content=msg))
    elif user.violation_count == 3:
        user.ban_until = datetime.utcnow() + timedelta(days=15)
        msg += ",您已被禁言15天。"
        db.add(Message(user_id=user_id, type="system", content=msg))
    else:
        db.add(Message(user_id=user_id, type="system", content=msg))
    db.commit()


def add_ai_comment(db: Session, post_id: int, post_user_id: int, post_content: str = None):
    """管理员审核通过后,调用 Dify 生成 AI 评论"""
    if not post_content:
        # 如果没有传内容,从数据库获取
        post = db.query(Post).filter(Post.id == post_id).first()
        post_content = post.content if post else ""

    dify_result = call_dify_workflow(post_content)
    ai_text = dify_result.get("ai_response")

    if not ai_text:
        # 降级备用文本
        ai_text = "感谢你的分享,我们一直在这里支持你 🌸"

    comment = Comment(
        post_id=post_id,
        user_id=None,
        content=ai_text,
        is_ai_generated=True,
        status="visible"
    )
    db.add(comment)
    db.add(Message(
        user_id=post_user_id,
        type="system",
        content=f"你的需求已通过审核,AI 为你送上一份温暖回应:{ai_text}",
        related_id=post_id
    ))
    db.commit()


def extract_keywords(text: str) -> List[str]:
    keywords = ["焦虑", "孤独", "压力", "抑郁", "失眠", "迷茫", "悲伤", "愤怒", "恐惧", "无助",
                "痛苦", "自卑", "烦躁"]
    found = [kw for kw in keywords if kw in text]
    return found if found else ["情绪倾诉"]


# ---------- API 路由 ----------
app = FastAPI(title="情绪互助平台 API", version="1.0")

# CORS 配置
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


# 初始化管理员账户
def init_admin(db: Session):
    admin = db.query(User).filter(User.username == "admin").first()
    if not admin:
        admin = User(
            username="admin",
            password_hash=get_password_hash("admin123"),
            nickname="管理员",
            role="admin"
        )
        db.add(admin)
        db.commit()


# 启动时初始化管理员
with SessionLocal() as db:
    init_admin(db)


# ---------- 用户认证 ----------
@app.post("/api/register", response_model=UserOut, summary="用户注册")
def register(user: UserCreate, db: Session = Depends(get_db)):
    if db.query(User).filter(User.username == user.username).first():
        raise HTTPException(status_code=400, detail="用户名已存在")
    if db.query(User).filter(User.nickname == user.nickname).first():
        raise HTTPException(status_code=400, detail="昵称已被使用")
    hashed = get_password_hash(user.password)
    db_user = User(
        username=user.username,
        password_hash=hashed,
        nickname=user.nickname,
        role="user"
    )
    db.add(db_user)
    db.commit()
    db.refresh(db_user)
    return db_user


@app.post("/api/login", response_model=Token, summary="用户登录")
def login(login: UserLogin, db: Session = Depends(get_db)):
    user = db.query(User).filter(User.username == login.username, User.is_deleted == False).first()
    if not user or not verify_password(login.password, user.password_hash):
        raise HTTPException(status_code=401, detail="用户名或密码错误")
    access_token = create_access_token(
        data={"sub": str(user.id)},
        expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    )
    return {"access_token": access_token, "token_type": "bearer"}


@app.get("/api/users/me", response_model=UserOut, summary="获取当前用户信息")
def get_me(current_user: User = Depends(get_current_user)):
    return current_user


@app.put("/api/users/me", summary="更新个人资料")
def update_profile(nickname: Optional[str] = None, current_user: User = Depends(get_current_user),
                   db: Session = Depends(get_db)):
    if nickname:
        exist = db.query(User).filter(User.nickname == nickname, User.id != current_user.id).first()
        if exist:
            raise HTTPException(status_code=400, detail="昵称已存在")
        current_user.nickname = nickname
    db.commit()
    return {"msg": "更新成功"}


@app.post("/api/users/me/change-password", summary="修改密码")
def change_password(pwd: PasswordChange, current_user: User = Depends(get_current_user),
                    db: Session = Depends(get_db)):
    if not verify_password(pwd.old_password, current_user.password_hash):
        raise HTTPException(status_code=400, detail="原密码错误")
    current_user.password_hash = get_password_hash(pwd.new_password)
    db.commit()
    return {"msg": "密码修改成功"}


@app.delete("/api/users/me", summary="注销账号")
def delete_account(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    current_user.is_deleted = True
    db.commit()
    return {"msg": "账号已注销"}


# ---------- 需求贴 ----------
@app.get("/api/posts", response_model=List[PostOut], summary="获取需求动态流")
def get_feed(skip: int = 0, limit: int = 20, db: Session = Depends(get_db)):
    posts = db.query(Post).filter(Post.status == "approved").order_by(
        Post.created_at.desc()).offset(skip).limit(limit).all()
    result = []
    for p in posts:
        user = db.query(User).filter(User.id == p.user_id).first()
        comment_count = db.query(Comment).filter(Comment.post_id == p.id,
                                                 Comment.status == "visible").count()
        result.append(PostOut(
            id=p.id, user_id=p.user_id,
            nickname=user.nickname if user else "已注销",
            avatar=user.avatar if user else "",
            content=p.content, status=p.status,
            reject_reason=p.reject_reason,
            created_at=p.created_at,
            comment_count=comment_count
        ))
    return result


@app.get("/api/posts/{post_id}", response_model=PostOut, summary="获取单个需求详情")
def get_post(post_id: int, db: Session = Depends(get_db)):
    post = db.query(Post).filter(Post.id == post_id).first()
    if not post:
        raise HTTPException(status_code=404, detail="需求不存在")
    user = db.query(User).filter(User.id == post.user_id).first()
    comment_count = db.query(Comment).filter(Comment.post_id == post.id,
                                             Comment.status == "visible").count()
    return PostOut(
        id=post.id, user_id=post.user_id,
        nickname=user.nickname if user else "已注销",
        avatar=user.avatar if user else "",
        content=post.content, status=post.status,
        reject_reason=post.reject_reason,
        created_at=post.created_at,
        comment_count=comment_count
    )


@app.post("/api/posts", response_model=PostOut, summary="发布新需求")
def create_post(post: PostCreate, current_user: User = Depends(get_current_user),
                db: Session = Depends(get_db)):
    if is_user_banned(current_user):
        raise HTTPException(status_code=403, detail="您已被禁言,无法发布需求")

    # 调用 Dify 工作流分析
    dify_result = call_dify_workflow(post.content)
    needs_review = dify_result["needs_review"]
    ai_response = dify_result["ai_response"]

    # 根据 AI 分析结果决定帖子状态
    if needs_review:
        status = "pending"
        user_msg = "您的新需求已提交审核,请等待管理员审核"
    else:
        status = "approved"
        user_msg = "您的需求已自动通过审核并发布"

    new_post = Post(
        user_id=current_user.id,
        content=post.content,
        status=status
    )
    db.add(new_post)
    db.commit()
    db.refresh(new_post)

    # 给用户发送系统消息
    db.add(Message(
        user_id=current_user.id,
        type="system",
        content=user_msg,
        related_id=new_post.id
    ))

    # 如果自动通过且有 AI 回复,立即添加 AI 评论
    if not needs_review and ai_response:
        ai_comment = Comment(
            post_id=new_post.id,
            user_id=None,
            content=ai_response,
            is_ai_generated=True,
            status="visible"
        )
        db.add(ai_comment)
        db.add(Message(
            user_id=current_user.id,
            type="system",
            content=f"AI 为你送上一份暖心回应:{ai_response}",
            related_id=new_post.id
        ))
    elif needs_review:
        # 需要人工审核,通知所有管理员
        admins = db.query(User).filter(User.role == "admin", User.is_deleted == False).all()
        for admin in admins:
            db.add(Message(
                user_id=admin.id,
                type="system",
                content=f"📢 新需求需要审核 (ID: {new_post.id})",
                related_id=new_post.id
            ))

    db.commit()

    # 返回信息
    return PostOut(
        id=new_post.id,
        user_id=new_post.user_id,
        nickname=current_user.nickname,
        avatar=current_user.avatar,
        content=new_post.content,
        status=new_post.status,
        created_at=new_post.created_at,
        comment_count=1 if (not needs_review and ai_response) else 0
    )


@app.get("/api/posts/my", response_model=List[PostOut], summary="获取我的需求")
def get_my_posts(status: Optional[str] = None, current_user: User = Depends(get_current_user),
                 db: Session = Depends(get_db)):
    query = db.query(Post).filter(Post.user_id == current_user.id)
    if status:
        query = query.filter(Post.status == status)
    posts = query.order_by(Post.created_at.desc()).all()
    return [
        PostOut(
            id=p.id, user_id=p.user_id, nickname=current_user.nickname, avatar=current_user.avatar,
            content=p.content, status=p.status, reject_reason=p.reject_reason,
            created_at=p.created_at, comment_count=0
        ) for p in posts
    ]


@app.delete("/api/posts/{post_id}", summary="删除需求")
def delete_post(post_id: int, current_user: User = Depends(get_current_user),
                db: Session = Depends(get_db)):
    post = db.query(Post).filter(Post.id == post_id).first()
    if not post:
        raise HTTPException(status_code=404, detail="需求不存在")
    
    # 用户可以删除自己的帖子,管理员可以删除所有帖子
    if post.user_id != current_user.id and current_user.role != "admin":
        raise HTTPException(status_code=403, detail="无权删除此需求")
    
    db.delete(post)
    db.commit()
    return {"msg": "删除成功"}


# ---------- 评论 ----------
@app.get("/api/posts/{post_id}/comments", response_model=List[CommentOut], summary="获取需求评论")
def get_comments(post_id: int, db: Session = Depends(get_db)):
    comments = db.query(Comment).filter(Comment.post_id == post_id,
                                        Comment.status == "visible").order_by(
        Comment.created_at.asc()).all()
    result = []
    for c in comments:
        user = db.query(User).filter(User.id == c.user_id).first() if c.user_id else None
        result.append(CommentOut(
            id=c.id, user_id=c.user_id,
            nickname=user.nickname if user else "AI助手",
            avatar=user.avatar if user else "/static/ai-avatar.png",
            content=c.content, is_ai_generated=c.is_ai_generated, created_at=c.created_at
        ))
    return result


@app.post("/api/posts/{post_id}/comments", response_model=CommentOut, summary="发表评论")
def create_comment(post_id: int, comment: CommentCreate,
                   current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    if is_user_banned(current_user):
        raise HTTPException(status_code=403, detail="您已被禁言,无法评论")
    post = db.query(Post).filter(Post.id == post_id, Post.status == "approved").first()
    if not post:
        raise HTTPException(status_code=404, detail="需求不存在或未审核通过")

    dify_result = call_dify_comment_workflow(comment.content)
    approved = dify_result.get("approved", False)
    class_name = dify_result.get("class_name", "nopass")

    if approved or class_name == "pass":
        comment_status = "visible"
        user_msg = "您的评论已发布。"
    else:
        comment_status = "pending"
        user_msg = "您的评论需要审核,通过后会显示。"

        admins = db.query(User).filter(User.role == "admin", User.is_deleted == False).all()
        for admin in admins:
            db.add(Message(
                user_id=admin.id,
                type="system",
                content=f"📢 新评论需要审核 (帖子ID: {post_id})",
                related_id=post_id
            ))

    new_comment = Comment(
        post_id=post_id,
        user_id=current_user.id,
        content=comment.content,
        status=comment_status,
        is_ai_generated=False
    )
    db.add(new_comment)
    db.commit()
    db.refresh(new_comment)

    if comment_status == "visible":
        db.add(Message(user_id=post.user_id, type="comment",
                       content=f"{current_user.nickname} 回应了你的需求: {comment.content[:50]}",
                       related_id=post_id))
    db.commit()

    return CommentOut(
        id=new_comment.id, user_id=current_user.id, nickname=current_user.nickname,
        avatar=current_user.avatar,
        content=new_comment.content, is_ai_generated=False, created_at=new_comment.created_at,
        status=new_comment.status
    )


@app.delete("/api/comments/{comment_id}", summary="删除评论")
def delete_comment(comment_id: int, current_user: User = Depends(get_current_user), 
                   db: Session = Depends(get_db)):
    comment = db.query(Comment).filter(Comment.id == comment_id).first()
    if not comment:
        raise HTTPException(status_code=404, detail="评论不存在")
    
    # 用户可以删除自己的评论,管理员可以删除所有评论
    if comment.user_id != current_user.id and current_user.role != "admin":
        raise HTTPException(status_code=403, detail="无权删除此评论")
    
    db.delete(comment)
    db.commit()
    return {"msg": "删除成功"}


# ---------- 消息通知 ----------
@app.get("/api/messages", response_model=List[MessageOut], summary="获取消息列表")
def get_messages(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    msgs = db.query(Message).filter(Message.user_id == current_user.id).order_by(
        Message.created_at.desc()).all()
    return msgs


@app.put("/api/messages/read", summary="标记消息已读")
def mark_messages_read(current_user: User = Depends(get_current_user),
                       db: Session = Depends(get_db)):
    db.query(Message).filter(Message.user_id == current_user.id, Message.is_read == False).update(
        {"is_read": True})
    db.commit()
    return {"msg": "已标记已读"}


# ---------- 管理员专用 ----------
@app.get("/api/admin/pending-posts", response_model=List[PostOut], summary="获取待审核需求")
def get_pending_posts(admin: User = Depends(get_current_admin), db: Session = Depends(get_db)):
    posts = db.query(Post).filter(Post.status == "pending").all()
    result = []
    for p in posts:
        user = db.query(User).filter(User.id == p.user_id).first()
        result.append(PostOut(
            id=p.id, user_id=p.user_id, nickname=user.nickname, avatar=user.avatar,
            content=p.content, status=p.status, created_at=p.created_at, comment_count=0
        ))
    return result


@app.post("/api/admin/posts/{post_id}/approve")
def approve_post(post_id: int, admin: User = Depends(get_current_admin),
                 db: Session = Depends(get_db)):
    post = db.query(Post).filter(Post.id == post_id).first()
    if not post or post.status != "pending":
        raise HTTPException(status_code=404, detail="需求不存在或已处理")
    post.status = "approved"
    post.reject_reason = None
    db.commit()

    # 调用新的 add_ai_comment,传入帖子内容
    add_ai_comment(db, post.id, post.user_id, post.content)
    return {"msg": "已通过,AI已回复"}


@app.post("/api/admin/posts/{post_id}/reject", summary="驳回需求")
def reject_post(post_id: int, data: AdminPostApprove, admin: User = Depends(get_current_admin),
                db: Session = Depends(get_db)):
    post = db.query(Post).filter(Post.id == post_id).first()
    if not post or post.status != "pending":
        raise HTTPException(status_code=404, detail="需求不存在或已处理")
    post.status = "rejected"
    post.reject_reason = data.reason or "未通过审核"
    db.commit()
    db.add(Message(user_id=post.user_id, type="system",
                   content=f"您的需求未通过审核,原因:{post.reject_reason}", related_id=post.id))
    db.commit()
    add_violation(db, post.user_id, f"发布违规需求: {post.content[:50]}")
    return {"msg": "已驳回并记录违规"}


@app.get("/api/admin/comments", summary="获取所有评论")
def get_all_comments(admin: User = Depends(get_current_admin), db: Session = Depends(get_db)):
    comments = db.query(Comment).filter(Comment.user_id.isnot(None),
                                        Comment.status == "visible").all()
    result = []
    for c in comments:
        user = db.query(User).filter(User.id == c.user_id).first()
        result.append({
            "id": c.id, "post_id": c.post_id, "user_id": c.user_id,
            "nickname": user.nickname if user else "未知",
            "content": c.content, "created_at": c.created_at
        })
    return result


@app.post("/api/admin/comments/{comment_id}/hide", summary="隐藏违规评论")
def hide_comment(comment_id: int, admin: User = Depends(get_current_admin),
                 db: Session = Depends(get_db)):
    comment = db.query(Comment).filter(Comment.id == comment_id).first()
    if not comment or comment.user_id is None:
        raise HTTPException(status_code=404, detail="评论不存在或为AI评论")
    comment.status = "hidden"
    db.commit()
    add_violation(db, comment.user_id, f"发表违规评论: {comment.content[:50]}")
    return {"msg": "已隐藏评论并记录违规"}


@app.get("/api/admin/pending-comments", summary="获取待审核评论")
def get_pending_comments(admin: User = Depends(get_current_admin), db: Session = Depends(get_db)):
    comments = db.query(Comment).filter(Comment.status == "pending").order_by(
        Comment.created_at.desc()).all()
    result = []
    for c in comments:
        user = db.query(User).filter(User.id == c.user_id).first()
        post = db.query(Post).filter(Post.id == c.post_id).first()
        result.append({
            "id": c.id,
            "post_id": c.post_id,
            "post_content": post.content[:50] + "..." if post else "未知",
            "user_id": c.user_id,
            "nickname": user.nickname if user else "未知",
            "content": c.content,
            "created_at": c.created_at
        })
    return result


@app.post("/api/admin/comments/{comment_id}/approve", summary="审核通过评论")
def approve_comment(comment_id: int, admin: User = Depends(get_current_admin),
                   db: Session = Depends(get_db)):
    comment = db.query(Comment).filter(Comment.id == comment_id,
                                       Comment.status == "pending").first()
    if not comment:
        raise HTTPException(status_code=404, detail="评论不存在或已处理")
    comment.status = "visible"
    db.commit()

    post = db.query(Post).filter(Post.id == comment.post_id).first()
    if post:
        db.add(Message(user_id=post.user_id, type="comment",
                       content=f"{comment.user.nickname if comment.user else '用户'} 回应了你的需求: {comment.content[:50]}",
                       related_id=comment.post_id))
        db.commit()

    return {"msg": "评论已通过审核"}


@app.post("/api/admin/comments/{comment_id}/reject", summary="驳回评论")
def reject_comment(comment_id: int, admin: User = Depends(get_current_admin),
                   db: Session = Depends(get_db)):
    comment = db.query(Comment).filter(Comment.id == comment_id,
                                       Comment.status == "pending").first()
    if not comment:
        raise HTTPException(status_code=404, detail="评论不存在或已处理")
    comment.status = "hidden"
    db.commit()

    add_violation(db, comment.user_id, f"发表违规评论: {comment.content[:50]}")
    db.add(Message(user_id=comment.user_id, type="system",
                   content="您的评论因违规未通过审核,并被记录违规一次。",
                   related_id=comment.post_id))
    db.commit()

    return {"msg": "评论已驳回并记录违规"}


@app.get("/api/admin/users", summary="获取所有用户")
def get_all_users(admin: User = Depends(get_current_admin), db: Session = Depends(get_db)):
    users = db.query(User).filter(User.role != "admin").all()
    return [
        {
            "id": u.id, "username": u.username, "nickname": u.nickname,
            "violation_count": u.violation_count, "is_deleted": u.is_deleted,
            "ban_until": u.ban_until, "created_at": u.created_at
        } for u in users
    ]


@app.delete("/api/admin/users/{user_id}", summary="删除用户")
def admin_delete_user(user_id: int, admin: User = Depends(get_current_admin),
                      db: Session = Depends(get_db)):
    user = db.query(User).filter(User.id == user_id, User.role != "admin").first()
    if not user:
        raise HTTPException(status_code=404, detail="用户不存在")
    user.is_deleted = True
    db.commit()
    db.add(Message(user_id=user_id, type="system", content="您的账号已被管理员删除"))
    db.commit()
    return {"msg": "用户已软删除"}


@app.get("/api/admin/stats/keywords", summary="情绪热词统计")
def get_keyword_stats(range: str = "day", admin: User = Depends(get_current_admin),
                      db: Session = Depends(get_db)):
    now = datetime.utcnow()
    if range == "day":
        start = now - timedelta(days=1)
    elif range == "week":
        start = now - timedelta(days=7)
    else:
        start = now - timedelta(days=30)
    posts = db.query(Post).filter(Post.status == "approved", Post.created_at >= start).all()
    word_count = {}
    for p in posts:
        words = extract_keywords(p.content)
        for w in words:
            word_count[w] = word_count.get(w, 0) + 1
    sorted_words = sorted(word_count.items(), key=lambda x: x[1], reverse=True)[:10]
    return {"range": range, "top_keywords": [{"word": w, "count": c} for w, c in sorted_words]}


# 健康检查
@app.get("/", summary="健康检查")
def root():
    return {"message": "情绪互助平台 API 运行中"}

# if __name__ == "__main__":
#     import uvicorn
#     uvicorn.run(app, host="127.0.0.1", port=8000)

结语

情绪互助平台不仅是一个技术练习项目,更是一次对“技术向善”的实践。它展示了如何用全栈开发能力快速搭建一个带内容审核的 UGC 社区,并巧妙地将 AI 引入审核流,降低管理员负担。

每一份情绪都值得被温柔接住。希望这个项目能启发更多人用代码搭建有温度的互联网角落。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐