关于开发社区网站小组发帖功能和多级回复的代码

init.php

<?php
// init.php - 数据库初始化脚本
include "config.php";

if (!$pdo) {
    die("数据库连接失败");
}

// 设置错误模式为异常,便于捕获
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// ------------------------------
// 1. 用户表
// ------------------------------
$sqlUser = "CREATE TABLE IF NOT EXISTS user (
    id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    account VARCHAR(100) NOT NULL UNIQUE,
    introduction TEXT,
    password VARCHAR(100) NOT NULL,
    headImgUrl VARCHAR(250),
    userName VARCHAR(20) UNIQUE
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB";

// ------------------------------
// 2. 作品表(依赖 user)
// ------------------------------
$sqlArtwork = "CREATE TABLE IF NOT EXISTS artworks (
    id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(80) NOT NULL,
    art_type ENUM('music','voice','novel','comic','game','chahua') NOT NULL,
    contentUrl VARCHAR(128) NOT NULL,
    progress INT(11) NOT NULL DEFAULT 0,
    description TEXT NOT NULL,
    privacy ENUM('public', 'private') NOT NULL DEFAULT 'private',
    creator_uid INT(11) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (creator_uid) REFERENCES user(id) ON DELETE CASCADE,
    UNIQUE KEY unique_user_artwork (creator_uid, title)
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB";

// ------------------------------
// 3. 小组表
// ------------------------------
$sqlGroups = "CREATE TABLE IF NOT EXISTS groups (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    description TEXT,
    creator_uid INT(11) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB";

// ------------------------------
// 4. 小组成员表(依赖 groups 和 user)
// ------------------------------
$sqlGroupMembers = "CREATE TABLE IF NOT EXISTS group_members (
    id INT(11) PRIMARY KEY AUTO_INCREMENT,
    group_id INT(11) NOT NULL,
    member_id INT(11) NOT NULL,
    role ENUM('member', 'creator', 'admin') DEFAULT 'member',
    joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE,
    FOREIGN KEY (member_id) REFERENCES user(id) ON DELETE CASCADE,
    UNIQUE KEY unique_member_per_group (group_id, member_id)
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB";

// ------------------------------
// 5. 帖子表(依赖 groups)
// ------------------------------
$sqlPosts = "CREATE TABLE IF NOT EXISTS posts (
    id INT PRIMARY KEY AUTO_INCREMENT,
    group_id INT NOT NULL,
    user_id INT NOT NULL,
    title VARCHAR(200) NOT NULL,
    content TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB";

// ------------------------------
// 6. 评论表(依赖 posts,自关联)
// ------------------------------
$sqlComments = "CREATE TABLE IF NOT EXISTS comments (
    id INT PRIMARY KEY AUTO_INCREMENT,
    post_id INT NOT NULL,
    user_id INT NOT NULL,
    parent_id INT NULL,
    content TEXT NOT NULL,
    depth INT DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
    FOREIGN KEY (parent_id) REFERENCES comments(id) ON DELETE CASCADE
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB";

// ------------------------------
// 执行建表(按依赖顺序)
// ------------------------------
try {
    $pdo->exec($sqlUser);
    echo "✅ 用户表创建成功<br>";

    $pdo->exec($sqlArtwork);
    echo "✅ 作品表创建成功<br>";

    $pdo->exec($sqlGroups);
    echo "✅ 小组表创建成功<br>";

    $pdo->exec($sqlGroupMembers);
    echo "✅ 小组成员表创建成功<br>";

    $pdo->exec($sqlPosts);
    echo "✅ 帖子表创建成功<br>";

    $pdo->exec($sqlComments);
    echo "✅ 评论表创建成功<br>";

    // ------------------------------
    // 7. 创建索引(MySQL 5.x 兼容写法:不检测存在,直接创建,重复则捕获异常)
    // ------------------------------
    $indexes = [
        "CREATE INDEX idx_comments_post_id ON comments(post_id)",
        "CREATE INDEX idx_comments_parent_id ON comments(parent_id)",
        "CREATE INDEX idx_comments_created_at ON comments(created_at)"
    ];
    foreach ($indexes as $indexSql) {
        try {
            $pdo->exec($indexSql);
        } catch (PDOException $e) {
            // 索引可能已存在,忽略错误(或输出提示)
            // 如果你想严格检查,可以在此处记录日志,但不影响后续运行
        }
    }
    echo "✅ 评论表索引创建成功<br>";

    echo "<br>🎉 数据库初始化完成!所有表和外键已建立。";
} catch (PDOException $e) {
    echo "数据库操作失败: " . $e->getMessage();
    // 可打印详细错误码以便调试(生产环境建议关闭)
    // echo "<br>错误码: " . $e->getCode();
}
?>

groupViewer.php

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?php echo $_GET["groupName"];?></title>
    <script src="/js/thirdparty/artDialog-7.0.0/lib/jquery-1.10.2.js"></script>
    <script src="/js/thirdparty/artDialog-7.0.0/dist/dialog.js"></script>
</head>
<body>
    <?php echo "<h2>" . $_GET["groupName"] . "</h2>";?>
    <button onclick="joinGroup()">加入这个小组</button>
    <button onclick="publishNew()">发表新主题</button>
    <h2>小组最新讨论</h2>
    <?php echo "<p>创建于" . $_GET["created_at"] . "</p>"; ?>
    <ol>
        <?php
            include "../php/config.php";
            try
            {
                $stmt = $pdo->prepare("SELECT * FROM posts WHERE group_id = :group_id");
                $stmt->bindParam(":group_id", $_GET["id"]);
                $stmt->execute();
                $result = $stmt->fetchAll(PDO::FETCH_ASSOC);
                foreach ($result as $row)
                {
                    // 1. 对参数值进行 URL 编码
                    $safeTitle = urlencode($row["title"]);
                    $safeUserId = urlencode($row["user_id"]);
                    $safeCreatedAt = urlencode($row["created_at"]);
                    $safePostId = urlencode($row["id"]);
                    $displayTitle = htmlspecialchars($row["title"], ENT_QUOTES, 'UTF-8');
                    echo "<li><a href=\"tieziViewer.php?title={$safeTitle}&user_id={$safeUserId}&created_at={$safeCreatedAt}&post_id={$safePostId}&group_id={$_GET['id']}\">{$displayTitle}</a></li>";
                }
            }catch (PDOException $e)
            {
                echo "Error: " . $e->getMessage();
            }
        ?>
    </ol>
    <script>
        function joinGroup()
        {
            var data = new FormData()
            data.append("action", "joinGroup")
            const groupID = "<?php echo htmlspecialchars($_GET['id'], ENT_QUOTES, 'UTF-8'); ?>";
            data.append("group_id", groupID)
            const request = new XMLHttpRequest()
            request.open("POST","../php/group.php")
            request.onload = function()
            {
                if (request.status == 200)
                {
                    var json = JSON.parse(request.responseText)
                    console.log(json)
                    if (json["status"] == "success")
                    {
                        var tip = dialog({title:"提示",content:json["message"]})
                        tip.show()
                    }
                    else
                    {
                        var tip = dialog({title:"提示",content:json["message"]})
                        tip.show()
                    }
                }
            }
            request.send(data)
        }
        function publishNew()
        {
            var d = dialog({
                title: "发表新主题",
                content: `
                    <input id=title type="" placeholder="标题"><br/>
                    <textarea id=content placeholder="内容"></textarea>
                `,
                ok: function()
                {
                    var title = document.getElementById("title").value
                    var content = document.getElementById("content").value
                    var data = new FormData()
                    data.append("action", "addNewTieZi")
                    data.append("group_id", "<?php echo htmlspecialchars($_GET['id'], ENT_QUOTES, 'UTF-8'); ?>")
                    data.append("title", title)
                    data.append("content", content)
                    const request = new XMLHttpRequest()
                    request.open("POST","../php/group.php")
                    request.onload = function()
                    {
                        if (request.status == 200)
                        {
                            console.log(request.responseText)
                            var json = JSON.parse(request.responseText)
                            console.log(json)
                            if (json["status"] == "success")
                            {
                                var tip = dialog({
                                    title: "提示",
                                    content: json["message"],
                                    onclose: function()
                                    {
                                        location.reload()
                                    }

                                })
                                tip.show()
                            }
                            else{
                                var tip = dialog({title:"提示",content: json["message"]})
                                tip.show()
                            }
                        }
                    }
                    request.send(data)
                }
            })
            d.show();
        }
    </script>
</body>
</html>

grop.php

<?php
session_start();
include "config.php";
/* 
2. 嵌套回复的核心原理
邻接表模型:每一行只记录自己的直接父级(parent_id)。
查询时,从根(parent_id IS NULL)开始,递归获取所有子节点。

深度字段(depth):可选但推荐。在插入回复时,通过 parent_id 查出父回复的 depth + 1 写入。
这样前端可以直接用 depth * 24px 等做缩进,无需递归计算。

排序:通常按 created_at 正序,也可以实现“按楼层”或“点赞”排序。
4. 性能考虑与扩展
深嵌套查询:递归 CTE 在深度超过 10 级时效率尚可,但若帖子有上万条回复,建议使用 闭包表(Closure Table)或 路径枚举,不过邻接表 + depth 已满足绝大多数社区需求。

回复计数:可在 posts 表增加 comment_count 冗余字段,通过触发器或业务代码更新,避免每次 COUNT(*)。

小组内的帖子数:同样可冗余到 groups 表。

5. 与前端“树形DOM+缩进折叠”的配合
后端返回树形 JSON(如上面的 $tree),前端递归渲染即可。

缩进:每个回复项使用 margin-left: ${depth * 24}px,或者用 CSS 类 .depth-0、.depth-1 控制。

折叠状态:前端维护一个 collapsedCommentIds 集合,渲染时跳过渲染子节点或使用 display: none。

6. 小结
表    作用    关键字段
groups    小组容器    id, name
posts    帖子    id, group_id, title, content
comments    支持无限嵌套的回复    id, post_id, parent_id, depth
这套设计简洁、扩展性好,完全能满足你“小组内多帖子,每个帖子有任意多级回复”的需求,且不会引入传统论坛的重度依赖。
*/
if ($_SERVER["REQUEST_METHOD"] == "POST")
{
    $action = $_POST['action'];
    if ($action == "newGroup")
    {
        newGroup($_POST["groupName"], $_SESSION["uid"]);
    }
    else if ($action == "joinGroup")
    {
        joinGroup($_POST["group_id"], $_SESSION["uid"]);
    }
    else if ($action == "addNewTieZi")
    {
        addNewTieZi($_POST["group_id"], $_SESSION["uid"], $_POST["title"], $_POST["content"]);
    }
    // 一级回复
    else if ($action == "replyLouZhu")
    {
        replyLouZhu($_POST["post_id"], $_SESSION["uid"], $_POST["content"]);
    }
    else if ($action == "reply") {
        reply($_POST["post_id"], $_SESSION["uid"], $_POST["parent_id"], $_POST["content"],$_POST["depth"]);
    }
    // 帖子顶层修改
    else if ($action == "modifyTieZiRoot")
    {
        modifyTieZiRoot($_POST["post_id"], $_SESSION["uid"], $_POST["title"], $_POST["content"]);
    }
    else if ($action == "deleteTieZi")
    {
        deleteTieZi($_POST["post_id"], $_POST["group_id"], $_SESSION["uid"], );
    }
    else if ($action == "editLayered")
    {
        modifyLayered($_POST["comment_id"], $_SESSION["uid"], $_POST["content"]);
    }
    else if ($action == "deleteLayered")
    {
        deleteLayered($_POST["comment_id"], $_SESSION["uid"]);
    }
}
else if ($_SERVER["REQUEST_METHOD"] == "GET")
{
    $action = $_GET['action'];
    if ($action == "queryAllGroupName")
    {
        queryAllGroupName();
    }
}
function modifyTieZiRoot($postId, $actionUser, $title, $content)
{
    global $pdo;
    $data = array();
    try {
        $stmt = $pdo->prepare("UPDATE posts SET title = :title, content = :content WHERE id = :id AND user_id = :user_id");
        $stmt->bindParam(":title", $title);
        $stmt->bindParam(":content", $content);
        $stmt->bindParam(":id", $postId);
        $stmt->bindParam(":user_id", $actionUser);
        if ($stmt->execute())
        {
            $data["status"] = "success";
            $data["message"] = "修改成功";
            echo json_encode($data);
        }
        else{
            $data["status"] = "error";
            $data["message"] = "修改失败:数据库执行错误";
            echo json_encode($data);
        }
    }catch(PDOException $e)
    {
        $data["status"] = "error";
        $data["message"] = "修改失败:" . $e->getMessage();
        echo json_encode($data);
    }
}
// 多级回复编辑
function modifyLayered($commentId, $actionUser, $content)
{
    global $pdo;
    $data = array();
    try {
        $stmt = $pdo->prepare("UPDATE comments SET content = :content WHERE id = :id AND user_id = :user_id");
        $stmt->bindParam(":content", $content);
        $stmt->bindParam(":id", $commentId);
        $stmt->bindParam(":user_id", $actionUser);
        if($stmt->execute())
        {
            $data["status"] = "success";
            $data["message"] = "修改成功";
            echo json_encode($data);
        }
        else{
            $data["status"] = "error";
            $data["message"] = "修改失败:数据库执行错误";
            echo json_encode($data);
        }
    }
    catch(PDOException $e)
    {
        $data["status"] = "error";
        $data["message"] = "修改失败:" . $e->getMessage();
        echo json_encode($data);
    }
}
function deleteLayered($commentId, $actionUser)
{
    global $pdo;
    $data = array();
    try{
        // 先检查权限:该评论是否属于当前用户
        $stmt = $pdo->prepare("SELECT user_id FROM comments WHERE id = ?");
        $stmt->execute([$commentId]);
        $comment = $stmt->fetch();
        if (!$comment || $comment['user_id'] != $actionUser) {
            $data["status"] = "error";
            $data["message"] = "无权删除此回复";
            echo json_encode($data);
            return;
        }
        // 如果设置了外键 ON DELETE CASCADE,只需删除本评论,子评论会自动删除
        $stmt = $pdo->prepare("DELETE FROM comments WHERE id = ?");
        $stmt->execute([$commentId]);
        
        $data["status"] = "success";
        $data["message"] = "删除成功";
        echo json_encode($data);
    }
    catch(PDOException $e)
    {
        $data["status"] = "error";
        $data["message"] = "修改失败:" . $e->getMessage();
        echo json_encode($data);
    }
}
// 查询数据表 返回树
function queryGroupJson($groupName)
{
    /*global $pdo;
    // 查出某个帖子下所有评论(扁平数组)
    $rows = $pdo->query("SELECT * FROM comments WHERE post_id = ? ORDER BY created_at", [$postId]);

    // 构建树形结构
    $comments = [];
    foreach ($rows as $row) {
        $comments[$row['id']] = $row;
    }
    $tree = [];
    foreach ($comments as &$item) {
        if ($item['parent_id'] === null) {
            $tree[] = &$item;
        } else {
            $comments[$item['parent_id']]['children'][] = &$item;
        }
    }
    // 最后 $tree 就是多级嵌套的回复树
    echo json_encode($tree);*/
}
function queryGroup($groupId)
{
    global $pdo;
    //$pdo->prepare("SELECT  FROM comments WHERE id = ?")
}
function queryAllGroupName()
{
    global $pdo;
    $sql = "SELECT name,id,created_at from groups";
    $data = array();
    try {
        $stmt = $pdo->prepare($sql);
        $stmt->execute(); // 【关键修改】使用 execute() 而不是 exec()
        $result = $stmt->fetchAll(PDO::FETCH_ASSOC);
        $data["status"] =  "success";
        $data["content"] = $result;
        // 返回 JSON
        echo json_encode($data);
    } catch (PDOException $e) {
        // 出错时返回错误 JSON,防止前端解析空字符串
        $data["status"] = "error";
        $data["message"] = "查询失败:数据库执行错误" . $e->getMessage();
        echo json_encode($data);
    }
}
function newGroup($groupName, $actionUser)
{
    global $pdo;
    $data = array();
    try {
        $stmt = $pdo->prepare("INSERT INTO groups (name, creator_uid) VALUES (:name, :creator_uid) ");
        $stmt->bindParam(":name",$groupName);
        $stmt->bindParam(":creator_uid",$actionUser);
        if($stmt->execute())
        {
            $data["status"] = "success";
            $data["message"] = "创建成功";
        }
        else{
            $data["status"] = "error";
            $data["message"] = "创建失败:数据库执行错误";
        }       
    }catch(PDOException $e)
    {
        $data["status"] = "error";
        $data["message"] = "创建失败:" . $e->getMessage();
    }

    echo json_encode($data);
}
function joinGroup($groupId, $actionUser)
{
    global $pdo;
    $data = array();
    try 
    {
        $stmt = $pdo->prepare("INSERT INTO group_members (group_id, member_id, role) VALUES (:group_id, :member_id, :role)");
        $stmt->bindParam(":group_id", $groupId);
        $stmt->bindParam(":member_id", $actionUser);
        $stmt->bindValue(":role", "member");
        if ($stmt->execute())
        {
            $data["status"] = "success";
            $data["message"] = "加入小组成功";
            echo json_encode($data);
        }
        else
        {
            $data["status"] = "error";
            $data["message"] = "加入小组失败:数据库执行错误";
            echo json_encode($data);
        }
    }
    catch(PDOException $e)
    {
        $data["status"] = "error";
        $data["message"] = "加入小组失败:数据库执行错误" . $e->getMessage();
        echo json_encode($data);
    }
}
function quitGroup($groupName, $actionUser)
{
    $data = array();
    $data["tip"] = "已退出";
    echo json_encode($data);
}
function addNewTieZi($groupId, $actionUser, $title, $content)
{
    global $pdo;
    $data = array();
    $stmt = $pdo->prepare("INSERT INTO posts (group_id, user_id, title, content) VALUES (:group_id, :user_id, :title, :content)");
    $stmt->bindParam(":group_id", $groupId);
    $stmt->bindParam(":user_id", $actionUser);
    $stmt->bindParam(":title", $title);
    $stmt->bindParam(":content", $content);
    try{
        if ($stmt->execute())
        {
            $data["status"] = "success";
            $data["message"] = "发布成功";
            echo json_encode($data);
        }
        else
        {
            $data["status"] = "error";
            $data["message"] = "发布失败:数据库执行错误";
            echo json_encode($data);
        }
    }
    catch (PDOException $e)
    {
        $data["status"] = "error";
        $errCode = $e->getCode();
        $errMsg = $e->getMessage();
        //$data["code"] = $e->getCode();
        // 23000 通常代表完整性约束违规(如外键失败、非空约束等)
        if ($errCode == '23000' || strpos($errMsg, 'user_id cannot be null') !== false) {
            $data["message"] = "您尚未登录或会话已过期,请重新登录后再试!";
        } else {
            // 其他错误记录日志,但不一定把详细 SQL 错误抛给前端(安全考虑)
            error_log("发生了一个错误\n", 3, __DIR__ . "/my_errors.log");
            $data["message"] = "发布失败:系统内部错误,请联系管理员。";
        }
        
        echo json_encode($data);
    }
    /*if ($actionUser is not in $groupName)
    {
        $data["tip"] = "你不在这个小组里,无法发布帖子";
    }
    else
    {
        $data=["tip"] = "发布成功";
    }*/
}
// 一级回复
function replyLouZhu($postId, $actionUser, $content)
{
    global $pdo;
    $data = array();
    try{
        $stmt = $pdo->prepare("INSERT INTO comments (post_id, user_id, content) VALUES (:post_id, :user_id, :content)");
        $stmt->bindParam(":post_id", $postId);
        $stmt->bindParam(":user_id", $actionUser);
        $stmt->bindParam(":content", $content);
        if ($stmt->execute())
        {
            $data["status"] = "success";
            $data["message"] = "回复成功";
            echo json_encode($data);
        }
        else
        {
            $data["status"] = "error";
            $data["message"] = "回复失败:数据库执行错误";
            echo json_encode($data);
        }
    }catch (PDOException $e)
    {
        $data["status"] = "error";
        $errCode = $e->getCode();
        $errMsg = $e->getMessage();
        // 23000 通常代表完整性约束违规(如外键失败、非空约束等)
        if ($errCode == '23000' || strpos($errMsg, 'user_id cannot be null') !== false) 
        {
            $data["message"] = "您尚未登录或会话已过期,请重新登录后再试!";
            echo json_encode($data);
        } else {
            // 其他错误记录日志,但不一定把详细 SQL 错误抛给前端(安全考虑)
            error_log("发生了一个错误\n", 3, __DIR__ . "/my_errors.log");
            $data["message"] = "发布失败:系统内部错误,请联系管理员。";
            echo json_encode($data);
        }
    }
}
// 多级回复
function reply($postId, $actionUser, $parent_id, $content)
{
    global $pdo;
    try{
        $stmt = $pdo->prepare("SELECT depth FROM commments WHERE id = :parent_id");
        $stmt->bindParam(":parent_id", $parent_id);
        $parent = $stmt->fetch(PDO::FETCH_ASSOC);
        $newDepth = $parent ? $parent['depth'] + 1 : 0;  // 如果父评论不存在,深度设为0(防错)
        $stmt = $pdo->prepare("INSERT INTO comments (post_id, user_id, parent_id, content, depth) VALUES (:post_id, :user_id, :parent_id, :content, :depth)");
        $stmt->bindParam(":post_id", $postId);
        $stmt->bindParam(":user_id", $actionUser);
        $stmt->bindParam(":parent_id", $parent_id);
        $stmt->bindParam(":content", $content);
        $stmt->bindParam(":depth", $newDepth);
        if ($stmt->execute())
        {
            $data["status"] = "success";
            $data["message"] = "回复成功";
            echo json_encode($data);
        }
        else
        {
            $data["status"] = "error";
            $data["message"] = "回复失败:数据库执行错误";
            echo json_encode($data);
        }
    }catch (PDOException $e)
    {
        $data["status"] = "error";
        $errCode = $e->getCode();
        $errMsg = $e->getMessage();
        // 23000 通常代表完整性约束违规(如外键失败、非空约束等)
        if ($errCode == '23000' || strpos($errMsg, 'user_id cannot be null') !== false) 
        {
            $data["message"] = "您尚未登录或会话已过期,请重新登录后再试!";
        }
        else
        {
            // 其他错误记录日志,但不一定把详细 SQL 错误抛给前端(安全考虑)
            error_log("发生了一个错误\n", 3, __DIR__ . "/my_errors.log");
            $data["message"] = "发布失败:系统内部错误,请联系管理员。";
        }
    }
}
function deleteTieZi($postId, $groupId, $actionUser)
{
    global $pdo;
    $data = array();
    try {
        // 只需删除帖子,comments 的外键 ON DELETE CASCADE 会自动删除所有评论
        $stmt = $pdo->prepare("DELETE FROM posts WHERE id = :id AND group_id = :group_id AND user_id = :user_id");
        $stmt->bindParam(":id", $postId);
        $stmt->bindParam(":group_id", $groupId);
        $stmt->bindParam(":user_id", $actionUser);
        
        if ($stmt->execute()) {
            $data["status"] = "success";
            $data["message"] = "删除成功";
        } else {
            $data["status"] = "error";
            $data["message"] = "删除失败:数据库执行错误";
        }
        echo json_encode($data);
    } catch (PDOException $e) {
        $data["status"] = "error";
        $errCode = $e->getCode();
        $errMsg = $e->getMessage();
        $data["code"] = $errCode;
        $data["debug"] = $errMsg;
        if ($errCode == '23000' || strpos($errMsg, 'user_id cannot be null') !== false) {
            $data["message"] = "您尚未登录或会话已过期";
        } else {
            $data["message"] = "删除失败:系统内部错误,请联系管理员。";
            error_log("删除帖子错误: " . $errMsg . "\n", 3, __DIR__ . "/my_errors.log");
        }
        echo json_encode($data);
    }
}
?>

tieziViewer.php

<?php
session_start();
include "../php/config.php";
$data = array();
try{
    $stmt = $pdo->prepare("SELECT userName, headImgUrl  FROM user WHERE id = :id");
    $stmt->bindParam(":id", $_GET["user_id"]);
    $stmt->execute();
    $result = $stmt->fetch(PDO::FETCH_ASSOC);
    $user = $result["userName"];
    $creator_head = "<img alt=无法显示头像 width=100px src=" . $result["headImgUrl"] .">";
    if ($user == null)
    {
        $user = "匿名用户";
    }
}catch(PDOException $e)
{
    echo $e->getMessage();
}

try{
    $stmt = $pdo->prepare("SELECT title, content FROM posts WHERE id = :id");
    $stmt->bindParam(":id", $_GET["post_id"]);
    $stmt->execute();
    $result = $stmt->fetch(PDO::FETCH_ASSOC);
    $tieZiContent = $result["content"];
    $tieZiTitle = $result["title"];
}catch(PDOException $e)
{
    die('数据库错误:' . $e->getMessage());
}

// 获取该帖子的所有评论(扁平数据)
try{
    $stmt = $pdo->prepare("SELECT * FROM comments WHERE post_id = :post_id ORDER BY created_at ASC");
    $stmt->bindParam(":post_id", $_GET["post_id"]);
    $stmt->execute();
    $commentsFlat = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
catch(PDOException $e)
{
    die('数据库错误:' . $e->getMessage());
}
// 构建评论树(邻接表 → 树形结构)
function buildTree($comments, $parentId = null) {
    $tree = [];
    foreach ($comments as $comment) {
        if ($comment['parent_id'] == $parentId) {
            $children = buildTree($comments, $comment['id']);
            if ($children) {
                $comment['children'] = $children;
            } else {
                $comment['children'] = [];
            }
            $tree[] = $comment;
        }
    }
    return $tree;
}

$commentTree = buildTree($commentsFlat);
// 递归生成评论HTML(带缩进和折叠按钮)
function renderComment($comment, $depth = 0) {
    global $pdo;
    $indentStyle = $depth > 0 ? 'style="margin-left: ' . ($depth * 28) . 'px;"' : '';
    $collapseId = 'comment-' . $comment['id'];
    $childrenHtml = '';
    if (!empty($comment['children'])) {
        $childrenHtml = '<div class="comment-children" id="children-' . $comment['id'] . '">';
        foreach ($comment['children'] as $child) {
            $childrenHtml .= renderComment($child, $depth + 1);
        }
        $childrenHtml .= '</div>';
    }
    $hasChildren = !empty($comment['children']);
    // $author = htmlspecialchars($comment['user_id'] ?? '匿名用户', ENT_QUOTES);
    $stmt = $pdo->prepare("SELECT userName, headImgUrl FROM user WHERE id = :id");
    $stmt->bindParam(':id', $comment['user_id']);
    $stmt->execute();
    $result = $stmt->fetch(PDO::FETCH_ASSOC);
    $author = $result["userName"] ?? "匿名用户";
    $authorHead = <<<AUTHORHEAD
        <img alt=无法显示头像 width=100px src="{$result['headImgUrl']}">
AUTHORHEAD;
    $content = nl2br(htmlspecialchars($comment['content'], ENT_QUOTES));
    $time = htmlspecialchars($comment['created_at'], ENT_QUOTES);
    // ✅ 根据登录状态和权限生成编辑/删除按钮
    $actionButtons = '';
    if (isset($_SESSION["uid"]) && $_SESSION["uid"] == $comment['user_id']) {
        // 将评论的 id 和 depth 作为参数传递给 JS 函数
        $actionButtons = <<<BTN
            <button class="edit-btn" onclick="editLayered({$comment['id']}, '{$comment['content']}')">编辑</button>
            <button class="delete-btn" onclick="deleteLayered({$comment['id']}, {$comment['depth']})">删除</button>
BTN;
    }
    return <<<HTML
    <div class="comment-item" data-comment-id="{$comment['id']}" {$indentStyle}>
        <div class="comment-meta">
            {$authorHead}
            <strong>{$author}</strong> <span class="time">{$time}</span>
            <button class="reply-btn" data-parent-id="{$comment['id']}" data-author="{$author}">回复</button>
            {$hasChildren}
            <button class="collapse-btn" data-target="children-{$comment['id']}">折叠</button>
            {$actionButtons}
            </div>
        <div class="comment-content">{$content}</div>
        {$childrenHtml}
        <div class="reply-form-container" id="reply-form-{$comment['id']}" style="display:none;">
            <form method="post" class="inline-reply-form">
                <input type="hidden" name="action" value="reply">
                <input type="hidden" name="post_id" value="{$_GET["post_id"]}">
                <input type="hidden" name="parent_id" value="{$comment['id']}">>
                <textarea name="content" rows="2" placeholder="回复 {$author}..."></textarea>
                <button type="submit">提交回复</button>
                <button type="button" class="cancel-reply">取消</button>
            </form>
        </div>
    </div>
HTML;
}
// 生成所有顶级评论的HTML
$commentsHtml = '';
foreach ($commentTree as $rootComment) {
    $commentsHtml .= renderComment($rootComment);
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?php echo $tieZiTitle;?></title>
    <script src="/js/thirdparty/artDialog-7.0.0/lib/jquery-1.10.2.js"></script>
    <script src="/js/thirdparty/artDialog-7.0.0/dist/dialog.js"></script>
    <style>
        body {
            font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
            max-width: 900px;
            margin: 20px auto;
            padding: 20px;
            background: #f5f7fb;
        }
        .post-container {
            background: white;
            border-radius: 24px;
            padding: 24px;
            margin-bottom: 30px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.05);
        }
        .post-title {
            font-size: 1.8rem;
            font-weight: 700;
            margin-bottom: 12px;
        }
        .post-meta {
            color: #64748b;
            font-size: 0.85rem;
            margin-bottom: 20px;
            border-bottom: 1px solid #eef2ff;
            padding-bottom: 12px;
        }
        .post-content {
            line-height: 1.6;
            font-size: 1rem;
        }
        .comments-section {
            background: white;
            border-radius: 24px;
            padding: 24px;
        }
        .comment-item {
            margin-top: 16px;
            padding: 12px;
            background: #f9fafb;
            border-radius: 20px;
            border: 1px solid #eef2ff;
            transition: 0.1s;
        }
        .comment-meta {
            display: flex;
            align-items: center;
            gap: 12px;
            flex-wrap: wrap;
            margin-bottom: 8px;
        }
        .comment-meta strong {
            color: #0f172a;
        }
        .time {
            font-size: 0.7rem;
            color: #94a3b8;
        }
        .comment-content {
            font-size: 0.9rem;
            margin: 8px 0 10px 0;
            word-break: break-word;
        }
        button {
            background: #eef2ff;
            padding: 4px 12px;
            border-radius: 30px;
            font-size: 0.7rem;
            cursor: pointer;
            font-weight: 500;
            border: solid 1px;
        }
        button:hover {
            background: #e2e8f0;
        }
        .comment-children {
            margin-top: 8px;
        }
        .reply-form-container {
            margin-top: 12px;
        }
        .inline-reply-form {
            background: white;
            padding: 12px;
            border-radius: 20px;
            border: 1px solid #cbd5e1;
        }
        .inline-reply-form textarea {
            width: 100%;
            border-radius: 14px;
            border: 1px solid #cbd5e1;
            padding: 8px;
            font-family: inherit;
            resize: vertical;
        }
        .inline-reply-form input {
            margin: 8px 0;
            padding: 6px 10px;
            border-radius: 40px;
            border: 1px solid #cbd5e1;
        }
        .inline-reply-form button {
            margin-right: 8px;
        }
        .root-reply {
            margin-top: 28px;
            background: #f1f5f9;
            padding: 20px;
            border-radius: 24px;
        }
        .collapsed .comment-children {
            display: none;
        }
        .error-msg {
            background: #fee2e2;
            color: #991b1b;
            padding: 10px;
            border-radius: 16px;
            margin-bottom: 20px;
        }
    </style>
</head>
<body>
    <div class="post-container">
        <div class="post-title"><?php echo htmlspecialchars($tieZiTitle, ENT_QUOTES); ?></div>
        <div class="post-meta">
            <?php echo $creator_head ;?>
            作者:<?php echo htmlspecialchars($user ?? '匿名用户', ENT_QUOTES); ?> 
            发表于 <?php echo htmlspecialchars($_GET['created_at'], ENT_QUOTES); ?>
            <?php if ($_SESSION["uid"] == $_GET["user_id"]): ?>
                <button onclick="editTop()">编辑</button>
                <button onclick="deleteTieZi()">删除</button>
            <?php endif;?>
        </div>
        <div class="post-content"><?php echo $tieZiContent; ?></div>
    </div>

    <div class="comments-section">
        <h3>💬 树形回复(可折叠缩进)</h3>
        <?php if (isset($error)): ?>
            <div class="error-msg"><?php echo htmlspecialchars($error); ?></div>
        <?php endif; ?>
        
        <div id="commentsTree">
            <?php echo $commentsHtml ?: '<div style="text-align:center; padding: 30px;">✨ 暂无回复,来抢沙发吧~</div>'; ?>
        </div>

        <!-- 回复楼主的表单 -->
        <div class="root-reply">
            <form method="post" id="replyLouZhu">
                <input type="hidden" name="action" value="replyLouZhu">
                <input type="hidden" name="post_id" value=<?php echo $_GET["post_id"];?>>
                <textarea name="content" rows="3" cols="100px" placeholder="发表你的看法..." required></textarea><br>
                <button type="submit">发布回复</button>
            </form>
        </div>
    </div>
    <script>
         // 折叠/展开功能
         document.querySelectorAll('.collapse-btn').forEach(btn => {
            btn.addEventListener('click', function(e) {
                e.stopPropagation();
                const targetId = this.dataset.target;
                const targetDiv = document.getElementById(targetId);
                if (targetDiv) {
                    targetDiv.classList.toggle('collapsed');
                    this.textContent = targetDiv.classList.contains('collapsed') ? '展开' : '折叠';
                }
            });
        });

        // 回复按钮:显示对应的内嵌回复表单
        document.querySelectorAll('.reply-btn').forEach(btn => {
            btn.addEventListener('click', function(e) {
                e.stopPropagation();
                const parentId = this.dataset.parentId;
                const formContainer = document.getElementById(`reply-form-${parentId}`);
                if (formContainer) {
                    // 隐藏其他打开的表单
                    document.querySelectorAll('.reply-form-container').forEach(container => {
                        if (container.id !== `reply-form-${parentId}`) container.style.display = 'none';
                    });
                    formContainer.style.display = formContainer.style.display === 'none' ? 'block' : 'none';
                }
            });
        });

        // 取消回复
        document.querySelectorAll('.cancel-reply').forEach(btn => {
            btn.addEventListener('click', function() {
                this.closest('.reply-form-container').style.display = 'none';
            });
        });
        function editTop()
        {
            var d = dialog({
                title: "提示",
                content: `
                    <input id="editTopTitleInput" type="text" name="title" placeholder="标题" value=""><br/>
                    <textarea id="editTopContent" name="content" placeholder="内容" cols="100px"></textarea>
                `,
                ok: function()
                {
                    var data = new FormData()
                    data.append("action", "modifyTieZiRoot")
                    data.append("group_id", "<?php echo htmlspecialchars($_GET['id'], ENT_QUOTES, 'UTF-8'); ?>")
                    data.append("post_id", "<?php echo htmlspecialchars($_GET['post_id'], ENT_QUOTES, 'UTF-8'); ?>")
                    data.append("title", document.getElementById("editTopTitleInput").value)
                    data.append("content", document.getElementById("editTopContent").value)
                    const request = new XMLHttpRequest();
                    request.open('POST', "../php/group.php");
                    request.onload = function()
                    {
                        if (request.status === 200)
                        {
                            var json = JSON.parse(request.responseText)
                            if (json.status == "success")
                            {
                                var tip = dialog({
                                    title: "提示",
                                    content: json.message,
                                    ok: function()
                                    {
                                        window.location.reload()
                                    }
                                })
                                tip.show()
                            }
                            else{
                                var tip = dialog({
                                    title: "提示",
                                    content: json.message,
                                })
                                tip.show()
                            }
                        }

                    }
                    request.send(data);
                }
            })
            d.show()
        }
        // 辅助函数防XSS
        function escapeHtml(str) {
            if (!str) return '';
            return str.replace(/[&<>]/g, function(m) {
                if (m === '&') return '&amp;';
                if (m === '<') return '&lt;';
                if (m === '>') return '&gt;';
                return m;
            });
        }
        function editLayered(commentId, currentContent)
        {
            var d = dialog({
                title: "提示",
                content: `
                    <textarea id="editLayeredReplayContent" name="content" placeholder="内容" cols="100px">${escapeHtml(currentContent)}</textarea>
                `,
                ok: function()
                {
                    var data = new FormData()
                    data.append("action", "editLayered")
                    data.append("comment_id", commentId)
                    // data.append("post_id", "<?php echo htmlspecialchars($_GET['post_id'], ENT_QUOTES, 'UTF-8'); ?>")
                    data.append("content", document.getElementById("editLayeredReplayContent").value)
                    const request = new XMLHttpRequest();
                    request.open("POST", "../php/group.php")
                    request.onload = function()
                    {
                        if (request.status === 200)
                        {
                            var json = JSON.parse(request.responseText)
                            if (json.status === "success")
                            {
                                var tip = dialog({
                                    title: "提示",
                                    content: json.message,
                                    ok: function()
                                    {
                                        window.location.reload()
                                    }
                                })
                                tip.show()
                            }
                            else{
                                var tip = dialog({
                                    title: "提示",
                                    content: json.message,
                                    ok: function()
                                    {
                                        window.location.reload()
                                    }
                                })
                                tip.show()
                            }
                        }
                        else
                        {
                            alert("操作失败")
                        }
                    }
                    request.send(data)
                }
            })
            d.show()
        }
        function deleteLayered(commentId, depth)
        {
            var d = dialog({
                title: "提示",
                content: "确定删除该回复吗?其所有子回复也将被删除。",
                ok: function()
                {
                    var data = new FormData()
                    data.append("action", "deleteLayered")
                    data.append("comment_id", commentId)
                    // data.append("post_id", "<?php echo htmlspecialchars($_GET['post_id'], ENT_QUOTES, 'UTF-8'); ?>")
                    // data.append("parent_id", "<?php echo htmlspecialchars($comment['id'], ENT_QUOTES, 'UTF-8'); ?>")
                    // data.append("depth", "<?php echo htmlspecialchars($comment['depth'], ENT_QUOTES, 'UTF-8'); ?>")
                    const request = new XMLHttpRequest()
                    request.open('POST', "../php/group.php")
                    request.onload = function()
                    {
                        if (request.status === 200)
                        {
                            console.log(request.responseText)
                            var json = JSON.parse(request.responseText)
                            if (json.status === "success")
                            {
                                var tip = dialog({
                                    title: "提示", 
                                    content: json.message,
                                    onclose: function()
                                    {
                                        window.location.reload()
                                    }
                                })
                                tip.show()
                            }
                            else{
                                var tip = dialog({
                                    title: "提示",
                                    content: json.message,
                                    onclose: function()
                                    {
                                        window.location.reload()
                                    }
                                })
                                tip.show()
                            }
                        }
                    }
                    request.send(data)
                }
            })
            d.show()
        }
        function deleteTieZi()
        {
            var d = dialog({
                title: "提示",
                content: "确定要删除吗?",
                ok: function()
                {
                    var data = new FormData()
                    data.append("action", "deleteTieZi")
                    data.append("post_id", "<?php echo htmlspecialchars($_GET['post_id'], ENT_QUOTES, 'UTF-8'); ?>")
                    data.append("user_id", "<?php echo htmlspecialchars($_GET['user_id'], ENT_QUOTES, 'UTF-8'); ?>")
                    data.append("group_id", "<?php echo htmlspecialchars($_GET['group_id'], ENT_QUOTES, 'UTF-8'); ?>")
                    const request = new XMLHttpRequest()
                    request.open('POST', "../php/group.php")
                    request.onload = function()
                    {
                        if (request.status === 200)
                        {
                            const json = JSON.parse(request.responseText)
                            if (json.status == "success")
                            {
                                var tip = dialog({
                                    title: "提示",
                                    content: "删除成功",
                                    onclose: function()
                                    {
                                        window.location.href = "groupViewer.php"
                                    }
                                })
                                tip.show()
                            }
                            else{
                                var tip = dialog({
                                    title: "提示",
                                    content: json.message
                                })
                                tip.show()
                            }
                        }
                    }
                    request.send(data)
                }
            })
            d.show()
        }
        const replyLouZhuForm = document.getElementById('replyLouZhu');
        replyLouZhuForm.addEventListener("submit", function(e)
        {
            e.preventDefault()
            var formData = new FormData(this)
            const submitBtn = this.querySelector('button[type="submit"]')
            submitBtn.disabled = true
            submitBtn.textContent = '提交中...'
            const request = new XMLHttpRequest()
            request.open('POST', "../php/group.php")
            request.onload = function()
            {
                if (request.status === 200)
                {
                    const json = JSON.parse(request.responseText)
                    if (json.status == "success")
                    {
                        var tip = dialog({
                            title: "提示", 
                            content: "回复成功",
                            onclose: function()
                            {
                                window.location.reload()
                            }
                        })
                        tip.show()
                    }
                    else
                    {
                        var tip = dialog({
                            title: "提示", 
                            content: json.message,
                        })
                        tip.show()
                    }
                }
            }
            request.send(formData)
        })
        const replyForms = document.getElementsByClassName("inline-reply-form")
        for (var i = 0; i < replyForms.length; i++)
        {
            const replyForm = replyForms[i]
            replyForm.addEventListener("submit", function(e)
            {
                e.preventDefault()
                var formData = new FormData(this)
                const submitBtn = this.querySelector('button[type="submit"]')
                submitBtn.disabled = true
                submitBtn.textContent = '提交中...'
                const request = new XMLHttpRequest()
                request.open('POST', "../php/group.php")
                request.onload = function()
                {
                    if (request.status === 200)
                    {
                        console.log(request.responseText)
                        const json = JSON.parse(request.responseText)
                        if (json.status == "success")
                        {
                            var tip = dialog({
                                title: "提示", 
                                content: "回复成功",
                                onclose: function()
                                {
                                    window.location.reload()
                                }
                            })
                            tip.show()
                        }
                        else
                        {
                            var tip = dialog({
                                title: "提示", 
                                content: json.message,
                            })
                            tip.show()
                        }
                    }
                }
                request.send(formData)
            })
        }
    </script>
</body>
</html>

 

posted on 2026-05-21 12:30  小沙盒工作室  阅读(12)  评论(0)    收藏  举报