16 KiB
技术设计文档:管理员删除帖子(Admin Delete Post)
Overview
管理员删除帖子功能扩展现有的帖子详情页和删除接口,使认证等级为「管理员」的用户能够删除任意帖子。核心变更包括:
- 前端帖子详情页:扩展「……」按钮的显示条件(不仅限于帖子作者,管理员也可见),根据用户身份动态构建 action sheet 菜单项,新增删除确认弹窗
- 后端 DeletePosts 接口:扩展权限校验逻辑,支持管理员删除非自己的帖子
- 后端 GetPostDetail 接口:返回帖子是否已删除的状态标识,支持前端拦截已删除帖子
- 后端 PublishPostComments 接口:增加帖子已删除状态的校验,拒绝对已删除帖子的评论
本功能不涉及数据库表结构变更,T_Posts 表已有 IsDeleted 和 DeletedAt 字段。
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
核心流程:
- 管理员删除流程:管理员在帖子详情页点击「……」→ action sheet 显示「删除帖子」→ 点击后弹出确认弹窗 → 确认删除 → 调用 DeletePosts 接口 → 后端校验管理员身份 → 软删除 → 前端返回上级页面并提示「帖子已删除」
- 已删除帖子访问拦截:用户点击已删除帖子 → GetPostDetail 返回已删除标识 → 前端阻止进入详情页并提示「帖子已删除」
- 已删除帖子回复拦截:用户在已删除帖子内提交回复 → 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 表已有 IsDeleted(bool)和 DeletedAt(DateTime?)字段,无需新增。
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}
属性测试覆盖:
- Property 1 - 生成随机 isAdmin/isMine 布尔值组合,验证菜单项构建函数输出正确
- Property 2 - 生成随机管理员用户和随机帖子,验证管理员删除后帖子 IsDeleted=true
- Property 3 - 生成随机非管理员非作者用户,验证删除请求被拒绝且帖子状态不变
- Property 4 - 生成随机帖子并软删除,验证 GetPostDetail 返回 PostDeleted 错误码
- Property 5 - 生成随机帖子并软删除,生成随机评论内容,验证 PublishPostComments 返回 PostDeleted 错误码
前端测试
- 管理员在非自己帖子详情页看到「……」按钮
- 管理员 action sheet 包含「删除帖子」选项
- 删除确认弹窗显示正确文案和按钮
- 取消按钮关闭弹窗不执行删除
- 已删除帖子访问时显示提示并返回