live-forum/.kiro/specs/admin-delete-post/design.md
2026-03-24 11:27:37 +08:00

16 KiB
Raw Blame History

技术设计文档管理员删除帖子Admin Delete Post

Overview

管理员删除帖子功能扩展现有的帖子详情页和删除接口,使认证等级为「管理员」的用户能够删除任意帖子。核心变更包括:

  1. 前端帖子详情页:扩展「……」按钮的显示条件(不仅限于帖子作者,管理员也可见),根据用户身份动态构建 action sheet 菜单项,新增删除确认弹窗
  2. 后端 DeletePosts 接口:扩展权限校验逻辑,支持管理员删除非自己的帖子
  3. 后端 GetPostDetail 接口:返回帖子是否已删除的状态标识,支持前端拦截已删除帖子
  4. 后端 PublishPostComments 接口:增加帖子已删除状态的校验,拒绝对已删除帖子的评论

本功能不涉及数据库表结构变更T_Posts 表已有 IsDeletedDeletedAt 字段。

Architecture

graph TB
    subgraph "小程序端"
        DetailPage[帖子详情页 post-details-page.vue]
        DetailPage --> |"管理员点击删除帖子"| DeleteAPI
        DetailPage --> |"获取帖子详情"| GetDetailAPI
        DetailPage --> |"发表评论"| CommentAPI
    end

    subgraph "服务端 LiveForum.WebApi"
        DeleteAPI[PostsController.DeletePosts] --> PostsService
        GetDetailAPI[PostsController.GetPostDetail] --> PostsService
        CommentAPI[PostCommentsController.PublishPostComments] --> CommentsService[PostCommentsService]
        PostsService --> UsersRepo[(T_Users)]
        PostsService --> PostsRepo[(T_Posts)]
        CommentsService --> PostsRepo
    end

核心流程:

  1. 管理员删除流程:管理员在帖子详情页点击「……」→ action sheet 显示「删除帖子」→ 点击后弹出确认弹窗 → 确认删除 → 调用 DeletePosts 接口 → 后端校验管理员身份 → 软删除 → 前端返回上级页面并提示「帖子已删除」
  2. 已删除帖子访问拦截:用户点击已删除帖子 → GetPostDetail 返回已删除标识 → 前端阻止进入详情页并提示「帖子已删除」
  3. 已删除帖子回复拦截:用户在已删除帖子内提交回复 → PublishPostComments 返回错误 → 前端返回上级页面并提示「帖子已删除」

Components and Interfaces

1. 后端接口变更

1.1 修改 DeletePosts 接口PostsService

当前逻辑仅允许帖子作者删除(x.UserId == currentUserId),需扩展为:

public async Task<BaseResponseBool> DeletePosts(DeletePostsReq request)
{
    var currentUserId = (long)_userInfoModel.UserId;

    // 1. 获取帖子信息(不限制 UserId
    var post = await _postsRepository.Select
        .Where(x => x.Id == request.PostId && !x.IsDeleted)
        .FirstAsync();

    if (post == null)
    {
        return new BaseResponseBool { Code = ResponseCode.Error, Message = "帖子不存在" };
    }

    // 2. 权限校验:帖子作者 或 管理员
    if (post.UserId != currentUserId)
    {
        // 非作者,检查是否为管理员
        var currentUser = await _usersRepository.Select
            .Where(x => x.Id == currentUserId)
            .FirstAsync();

        var isAdmin = currentUser?.CertifiedType != null 
            && currentUser.CertifiedType > 0
            && await IsAdminCertificationType(currentUser.CertifiedType.Value);

        if (!isAdmin)
        {
            return new BaseResponseBool { Code = ResponseCode.Error, Message = "权限不足,无法删除该帖子" };
        }
    }

    // 3. 软删除
    post.IsDeleted = true;
    post.DeletedAt = DateTime.Now;
    await _postsRepository.UpdateAsync(post);
    await ClearPostImageCacheAsync(post.Id);

    return new BaseResponseBool { Code = ResponseCode.Success, Data = true };
}

管理员身份判断方法:通过查询 T_CertificationTypes 表,判断用户的 CertifiedType 对应的认证名称是否为「管理员认证」。

private async Task<bool> IsAdminCertificationType(int certificationTypeId)
{
    var certType = await _certificationTypesRepository.Select
        .Where(x => x.Id == certificationTypeId && x.IsActive)
        .FirstAsync();
    return certType?.Name == "管理员认证";
}

设计决策:使用认证名称而非硬编码 ID 判断管理员身份,因为 T_CertificationTypes 是配置表ID 可能因环境不同而变化。

1.2 修改 GetPostDetail 接口PostsService

当前逻辑在帖子已删除时返回「帖子不存在」,需改为返回已删除状态标识:

// 修改查询条件:移除 !x.IsDeleted 过滤
var post = await _postsRepository.Select
    .Where(x => x.Id == request.PostId)
    .FirstAsync();

if (post == null)
{
    return new BaseResponse<PostDetailDto>(ResponseCode.Error, "帖子不存在");
}

// 如果帖子已删除,返回特定错误码
if (post.IsDeleted)
{
    return new BaseResponse<PostDetailDto>(ResponseCode.PostDeleted, "帖子已删除");
}

需在 ResponseCode 中新增 PostDeleted 错误码。

1.3 修改 PublishPostComments 接口PostCommentsService

当前逻辑已检查 !x.IsDeleted,帖子已删除时返回「帖子不存在」。需修改错误信息以区分:

var post = await _postsRepository.Select
    .Where(x => x.Id == request.PostId)
    .FirstAsync();

if (post == null)
{
    return new BaseResponse<PublishPostCommentsRespDto>(ResponseCode.Error, "帖子不存在");
}

if (post.IsDeleted)
{
    return new BaseResponse<PublishPostCommentsRespDto>(ResponseCode.PostDeleted, "帖子已删除");
}

1.4 PostDetailDto 变更

新增 IsAdmin 字段,标识当前用户是否为管理员,供前端判断是否显示「删除帖子」菜单项:

/// <summary>
/// 当前用户是否为管理员
/// </summary>
public bool IsAdmin { get; set; }

GetPostDetail 方法中赋值:

var currentUser = await _usersRepository.Select
    .Where(x => x.Id == currentUserId)
    .FirstAsync();

var isAdmin = currentUser?.CertifiedType != null 
    && currentUser.CertifiedType > 0
    && await IsAdminCertificationType(currentUser.CertifiedType.Value);

// 构建返回数据
var result = new PostDetailDto
{
    // ... 现有字段 ...
    IsAdmin = isAdmin
};

2. 前端变更

2.1 帖子详情页post-details-page.vue

「……」按钮显示条件扩展:

当前:v-if="detailsData.isMine" 时显示「……」按钮 修改为:v-if="detailsData.isMine || detailsData.isAdmin" 时显示「……」按钮

对于非自己帖子的管理员(!isMine && isAdmin),需要在 v-else 分支中也添加「……」按钮。

Action Sheet 菜单动态构建:

用户身份 菜单项
帖子作者(非管理员) 帖子内回复设置、举报、取消
帖子作者 + 管理员 删除帖子、帖子内回复设置、举报、取消
管理员(非作者) 删除帖子、举报、取消
普通用户(非作者) 不显示「……」按钮(仅显示举报图标)
showMoreMenu() {
    let itemList = [];
    let actions = [];

    if (this.detailsData.isAdmin) {
        itemList.push('删除帖子');
        actions.push('deletePost');
    }
    if (this.detailsData.isMine) {
        itemList.push('帖子内回复设置');
        actions.push('replySetting');
    }
    itemList.push('举报');
    actions.push('report');

    uni.showActionSheet({
        itemList: itemList,
        success: (res) => {
            const action = actions[res.tapIndex];
            if (action === 'deletePost') {
                this.showAdminDeleteConfirm = true;
            } else if (action === 'replySetting') {
                this.showReplySettingPopup = true;
            } else if (action === 'report') {
                this.clickReport(this.detailsData.postId, 1);
            }
        }
    });
}

管理员删除确认弹窗Delete_Confirm_Dialog

居中 modal 弹窗,设计稿样式:

  • 提示文案:「确定删除该帖子吗?」
  • 左侧按钮:「取消」(普通样式)
  • 右侧按钮:「删除」(蓝色高亮)
<!-- 管理员删除确认弹窗 -->
<up-popup v-model:show="showAdminDeleteConfirm" mode="center" :round="10">
    <view class="admin-delete-dialog">
        <text class="admin-delete-title">确定删除该帖子吗?</text>
        <view class="admin-delete-actions">
            <view class="admin-delete-btn admin-delete-cancel" @click="showAdminDeleteConfirm = false">
                <text>取消</text>
            </view>
            <view class="admin-delete-btn admin-delete-confirm" @click="adminDeletePost()">
                <text style="color: #FFFFFF;">删除</text>
            </view>
        </view>
    </view>
</up-popup>

样式:

.admin-delete-dialog {
    width: 590rpx;
    display: flex;
    flex-direction: column;
    align-items: center;
}
.admin-delete-title {
    font-size: 30rpx;
    margin-top: 60rpx;
}
.admin-delete-actions {
    display: flex;
    flex-direction: row;
    margin-top: 50rpx;
    margin-bottom: 40rpx;
}
.admin-delete-btn {
    width: 200rpx;
    height: 72rpx;
    border-radius: 8rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 28rpx;
}
.admin-delete-cancel {
    background-color: #F0F0F0;
    color: #333333;
    margin-right: 30rpx;
}
.admin-delete-confirm {
    background-color: #4A90D9;
    color: #FFFFFF;
}

管理员删除方法:

async adminDeletePost() {
    if (this.isSubmitting) return;
    this.isSubmitting = true;
    this.showAdminDeleteConfirm = false;

    try {
        var appServer = new AppServer();
        const data = await appServer.DeletePosts(this.detailsData.postId);
        if (data.code == 0) {
            uni.showToast({ title: '帖子已删除', icon: 'none' });
            setTimeout(() => {
                uni.navigateBack({ delta: 1 });
            }, 500);
        } else {
            uni.showToast({ title: data.message || '删除失败', icon: 'none' });
        }
    } catch (error) {
        uni.showToast({ title: '网络异常', icon: 'none' });
    } finally {
        this.isSubmitting = false;
    }
}

注意:管理员删除成功后不调用 notifyPrevPage() 刷新上级页面列表(需求 3.3 要求不强制刷新)。

2.2 已删除帖子访问拦截

修改 getPostDetail 方法,处理 PostDeleted 错误码:

getPostDetail(PostId) {
    var appServer = new AppServer();
    appServer.GetPostDetail(PostId).then(data => {
        if (data.code == 0) {
            this.detailsData = data.data;
        } else if (data.code == /* PostDeleted code */) {
            uni.showToast({ title: '帖子已删除', icon: 'none' });
            setTimeout(() => {
                uni.navigateBack({ delta: 1 });
            }, 500);
            return;
        }
        this.detailLoaded = true;
        this.checkAndHideLoading();
    });
}

2.3 已删除帖子回复拦截

修改 publishPostComments 方法,处理 PostDeleted 错误码:

// 在 publishPostComments 和 publishPostDetailsComments 中
if (data.code == /* PostDeleted code */) {
    uni.showToast({ title: '帖子已删除', icon: 'none' });
    setTimeout(() => {
        uni.navigateBack({ delta: 1 });
    }, 500);
    return;
}

3. ResponseCode 扩展

ResponseCode 枚举中新增:

/// <summary>
/// 帖子已删除
/// </summary>
PostDeleted = 1001

Data Models

后端变更

// PostDetailDto 新增字段
public bool IsAdmin { get; set; }

// ResponseCode 新增枚举值
PostDeleted = 1001

前端数据模型

// post-details-page.vue data 新增
{
    showAdminDeleteConfirm: false  // 管理员删除确认弹窗
}

// detailsData 新增字段(从接口获取)
{
    isAdmin: false  // 当前用户是否为管理员
}

无数据库表结构变更

T_Posts 表已有 IsDeletedboolDeletedAtDateTime?)字段,无需新增。

Correctness Properties

A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.

Property 1: 菜单项根据用户身份正确构建

For any isAdmin 和 isMine 的布尔值组合More_Menu 的菜单项列表应满足:当 isAdmin 为 true 时包含「删除帖子」,当 isAdmin 为 false 时不包含「删除帖子」;当 isMine 为 true 时包含「帖子内回复设置」,当 isMine 为 false 时不包含「帖子内回复设置」;所有组合下均包含「举报」。

Validates: Requirements 1.2, 1.3, 1.4

Property 2: 管理员可删除任意帖子

For any 管理员用户和任意帖子(包括非自己的帖子),调用 DeletePosts 接口后,该帖子的 IsDeleted 应为 true 且 DeletedAt 不为空。

Validates: Requirements 2.4, 6.1, 6.4

Property 3: 非管理员非作者删除被拒绝

For any 非管理员且非帖子作者的用户,调用 DeletePosts 接口应返回权限不足错误,且帖子的 IsDeleted 状态不发生变化。

Validates: Requirements 6.2, 6.3

Property 4: 已删除帖子详情返回已删除状态

For any 已被软删除的帖子,调用 GetPostDetail 接口应返回 PostDeleted 错误码,而非正常的帖子详情数据。

Validates: Requirements 4.1, 6.5

Property 5: 已删除帖子拒绝评论

For any 已被软删除的帖子和任意评论内容,调用 PublishPostComments 接口应返回 PostDeleted 错误码并拒绝评论。

Validates: Requirements 5.1, 6.6

Error Handling

场景 错误码 错误信息
非作者非管理员尝试删除帖子 Error 权限不足,无法删除该帖子
删除不存在的帖子 Error 帖子不存在
获取已删除帖子详情 PostDeleted (1001) 帖子已删除
对已删除帖子发表评论 PostDeleted (1001) 帖子已删除
网络异常(前端) - 网络异常

Testing Strategy

单元测试

  • DeletePosts 接口:帖子作者删除自己帖子(保持原有行为)
  • DeletePosts 接口:管理员删除他人帖子(新增场景)
  • DeletePosts 接口:普通用户删除他人帖子被拒绝
  • GetPostDetail 接口:已删除帖子返回 PostDeleted 错误码
  • PublishPostComments 接口:已删除帖子拒绝评论
  • 菜单项构建逻辑:各种 isAdmin/isMine 组合下的菜单项列表

属性测试

使用 FsCheck.Xunit(项目已引入)进行属性测试:

  • 每个属性测试配置最少 100 次迭代
  • 每个测试以注释标注对应的设计属性编号
  • 标注格式:Feature: admin-delete-post, Property {number}: {property_text}

属性测试覆盖:

  1. Property 1 - 生成随机 isAdmin/isMine 布尔值组合,验证菜单项构建函数输出正确
  2. Property 2 - 生成随机管理员用户和随机帖子,验证管理员删除后帖子 IsDeleted=true
  3. Property 3 - 生成随机非管理员非作者用户,验证删除请求被拒绝且帖子状态不变
  4. Property 4 - 生成随机帖子并软删除,验证 GetPostDetail 返回 PostDeleted 错误码
  5. Property 5 - 生成随机帖子并软删除,生成随机评论内容,验证 PublishPostComments 返回 PostDeleted 错误码

前端测试

  • 管理员在非自己帖子详情页看到「……」按钮
  • 管理员 action sheet 包含「删除帖子」选项
  • 删除确认弹窗显示正确文案和按钮
  • 取消按钮关闭弹窗不执行删除
  • 已删除帖子访问时显示提示并返回