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

480 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 技术设计文档管理员删除帖子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 包含「删除帖子」选项
- 删除确认弹窗显示正确文案和按钮
- 取消按钮关闭弹窗不执行删除
- 已删除帖子访问时显示提示并返回