480 lines
16 KiB
Markdown
480 lines
16 KiB
Markdown
# 技术设计文档:管理员删除帖子(Admin Delete Post)
|
||
|
||
## Overview
|
||
|
||
管理员删除帖子功能扩展现有的帖子详情页和删除接口,使认证等级为「管理员」的用户能够删除任意帖子。核心变更包括:
|
||
|
||
1. **前端帖子详情页**:扩展「……」按钮的显示条件(不仅限于帖子作者,管理员也可见),根据用户身份动态构建 action sheet 菜单项,新增删除确认弹窗
|
||
2. **后端 DeletePosts 接口**:扩展权限校验逻辑,支持管理员删除非自己的帖子
|
||
3. **后端 GetPostDetail 接口**:返回帖子是否已删除的状态标识,支持前端拦截已删除帖子
|
||
4. **后端 PublishPostComments 接口**:增加帖子已删除状态的校验,拒绝对已删除帖子的评论
|
||
|
||
本功能不涉及数据库表结构变更,T_Posts 表已有 `IsDeleted` 和 `DeletedAt` 字段。
|
||
|
||
## Architecture
|
||
|
||
```mermaid
|
||
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`),需扩展为:
|
||
|
||
```csharp
|
||
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` 对应的认证名称是否为「管理员认证」。
|
||
|
||
```csharp
|
||
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)
|
||
|
||
当前逻辑在帖子已删除时返回「帖子不存在」,需改为返回已删除状态标识:
|
||
|
||
```csharp
|
||
// 修改查询条件:移除 !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`,帖子已删除时返回「帖子不存在」。需修改错误信息以区分:
|
||
|
||
```csharp
|
||
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` 字段,标识当前用户是否为管理员,供前端判断是否显示「删除帖子」菜单项:
|
||
|
||
```csharp
|
||
/// <summary>
|
||
/// 当前用户是否为管理员
|
||
/// </summary>
|
||
public bool IsAdmin { get; set; }
|
||
```
|
||
|
||
在 `GetPostDetail` 方法中赋值:
|
||
|
||
```csharp
|
||
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 菜单动态构建:**
|
||
|
||
| 用户身份 | 菜单项 |
|
||
|---------|--------|
|
||
| 帖子作者(非管理员) | 帖子内回复设置、举报、取消 |
|
||
| 帖子作者 + 管理员 | 删除帖子、帖子内回复设置、举报、取消 |
|
||
| 管理员(非作者) | 删除帖子、举报、取消 |
|
||
| 普通用户(非作者) | 不显示「……」按钮(仅显示举报图标) |
|
||
|
||
```javascript
|
||
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 弹窗,设计稿样式:
|
||
- 提示文案:「确定删除该帖子吗?」
|
||
- 左侧按钮:「取消」(普通样式)
|
||
- 右侧按钮:「删除」(蓝色高亮)
|
||
|
||
```html
|
||
<!-- 管理员删除确认弹窗 -->
|
||
<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>
|
||
```
|
||
|
||
样式:
|
||
```css
|
||
.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;
|
||
}
|
||
```
|
||
|
||
**管理员删除方法:**
|
||
|
||
```javascript
|
||
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` 错误码:
|
||
|
||
```javascript
|
||
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` 错误码:
|
||
|
||
```javascript
|
||
// 在 publishPostComments 和 publishPostDetailsComments 中
|
||
if (data.code == /* PostDeleted code */) {
|
||
uni.showToast({ title: '帖子已删除', icon: 'none' });
|
||
setTimeout(() => {
|
||
uni.navigateBack({ delta: 1 });
|
||
}, 500);
|
||
return;
|
||
}
|
||
```
|
||
|
||
### 3. ResponseCode 扩展
|
||
|
||
在 `ResponseCode` 枚举中新增:
|
||
|
||
```csharp
|
||
/// <summary>
|
||
/// 帖子已删除
|
||
/// </summary>
|
||
PostDeleted = 1001
|
||
```
|
||
|
||
## Data Models
|
||
|
||
### 后端变更
|
||
|
||
```csharp
|
||
// PostDetailDto 新增字段
|
||
public bool IsAdmin { get; set; }
|
||
|
||
// ResponseCode 新增枚举值
|
||
PostDeleted = 1001
|
||
```
|
||
|
||
### 前端数据模型
|
||
|
||
```javascript
|
||
// 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}**
|
||
|
||
属性测试覆盖:
|
||
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 包含「删除帖子」选项
|
||
- 删除确认弹窗显示正确文案和按钮
|
||
- 取消按钮关闭弹窗不执行删除
|
||
- 已删除帖子访问时显示提示并返回
|