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

631 lines
23 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.

# 技术设计文档发帖和回复时间间隔Post Reply Interval
## Overview
发帖和回复时间间隔功能允许管理员按认证等级分别配置发帖和回复的最小时间间隔(秒),后端在 PublishPosts 和 PublishPostComments 接口中校验间隔,前端在操作成功后启动本地倒计时并禁用操作按钮。核心变更包括:
1. **数据库层**:新建 `T_PostReplyIntervals` 时间间隔配置表,关联 `T_CertificationTypes`
2. **后端 API 层**:在 `PostsService.PublishPosts``PostCommentsService.PublishPostComments` 中增加间隔校验逻辑;新增间隔配置管理接口(管理端);修改 `GetAppConfig` 返回当前用户的间隔配置
3. **管理端**:新增时间间隔配置页面,以列表形式展示和编辑各认证等级的间隔配置
4. **前端小程序**:发帖页和帖子详情页增加倒计时逻辑,倒计时期间禁用操作按钮并显示剩余秒数
## Architecture
```mermaid
graph TB
subgraph "小程序端"
PostPage[发帖页 post-page.vue] --> |"发布帖子"| PublishPostsAPI
DetailPage[帖子详情页 post-details-page.vue] --> |"发表评论"| PublishCommentsAPI
PostPage --> |"获取间隔配置"| GetAppConfigAPI
DetailPage --> |"获取间隔配置"| GetAppConfigAPI
end
subgraph "后台管理端 ZR.Vue"
AdminPage[时间间隔配置页] --> |"查询/修改间隔配置"| AdminAPI
end
subgraph "服务端 LiveForum.WebApi"
PublishPostsAPI[PostsController.PublishPosts] --> PostsService
PublishCommentsAPI[PostCommentsController.PublishPostComments] --> CommentsService[PostCommentsService]
GetAppConfigAPI[ConfigController.GetAppConfig] --> ConfigService
PostsService --> IntervalCheck{间隔校验}
CommentsService --> IntervalCheck
IntervalCheck --> IntervalsRepo[(T_PostReplyIntervals)]
IntervalCheck --> PostsRepo[(T_Posts)]
IntervalCheck --> CommentsRepo[(T_Comments)]
IntervalCheck --> UsersRepo[(T_Users)]
ConfigService --> IntervalsRepo
end
subgraph "管理端 ZrAdminNetCore"
AdminAPI[T_PostReplyIntervalsController] --> AdminService[T_PostReplyIntervalsService]
AdminService --> IntervalsRepo
end
```
**核心流程:**
1. **管理员配置间隔**:管理端列表页展示所有认证等级 → 编辑 PostInterval / ReplyInterval → 调用管理端 API 保存到 T_PostReplyIntervals
2. **发帖间隔校验**:用户发帖 → PostsService 查询用户认证等级 → 查询对应 PostInterval → 查询用户最近一次发帖时间 → 计算时间差 → 不满足则拒绝并返回剩余秒数
3. **回复间隔校验**:用户回复 → PostCommentsService 查询用户认证等级 → 查询对应 ReplyInterval → 查询用户最近一次回复时间 → 计算时间差 → 不满足则拒绝并返回剩余秒数
4. **前端倒计时**:操作成功后 → 根据 GetAppConfig 返回的间隔值启动本地倒计时 → 禁用按钮并显示剩余秒数 → 倒计时结束恢复按钮
## Components and Interfaces
### 1. 数据库变更
#### T_PostReplyIntervals 表(新建)
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| Id | INT | 自增主键 | 主键 |
| CertificationTypeId | INT | - | 关联 T_CertificationTypes.Id唯一索引 |
| PostInterval | INT | 0 | 发帖最小间隔0 表示不限制 |
| ReplyInterval | INT | 0 | 回复最小间隔0 表示不限制 |
| CreatedAt | DATETIME2 | getdate() | 创建时间 |
| UpdatedAt | DATETIME2 | getdate() | 更新时间 |
SQL 脚本:
```sql
CREATE TABLE T_PostReplyIntervals (
Id INT IDENTITY(1,1) PRIMARY KEY,
CertificationTypeId INT NOT NULL,
PostInterval INT NOT NULL DEFAULT 0,
ReplyInterval INT NOT NULL DEFAULT 0,
CreatedAt DATETIME2 NOT NULL DEFAULT GETDATE(),
UpdatedAt DATETIME2 NOT NULL DEFAULT GETDATE(),
CONSTRAINT UQ_PostReplyIntervals_CertTypeId UNIQUE (CertificationTypeId),
CONSTRAINT FK_PostReplyIntervals_CertType FOREIGN KEY (CertificationTypeId) REFERENCES T_CertificationTypes(Id)
);
```
### 2. 后端实体与 DTO
#### 2.1 T_PostReplyIntervals 实体(新建)
```csharp
// LiveForum.Model/Model/T_PostReplyIntervals.cs
namespace LiveForum.Model
{
[JsonObject(MemberSerialization.OptIn), Table(DisableSyncStructure = true)]
public partial class T_PostReplyIntervals
{
[JsonProperty, Column(IsPrimary = true, IsIdentity = true)]
public int Id { get; set; }
/// <summary>
/// 认证类型ID关联 T_CertificationTypes.Id
/// </summary>
[JsonProperty]
public int CertificationTypeId { get; set; }
/// <summary>
/// 发帖最小间隔0 表示不限制
/// </summary>
[JsonProperty]
public int PostInterval { get; set; } = 0;
/// <summary>
/// 回复最小间隔0 表示不限制
/// </summary>
[JsonProperty]
public int ReplyInterval { get; set; } = 0;
[JsonProperty, Column(DbType = "datetime2", InsertValueSql = "getdate()")]
public DateTime CreatedAt { get; set; }
[JsonProperty, Column(DbType = "datetime2", InsertValueSql = "getdate()")]
public DateTime UpdatedAt { get; set; }
}
}
```
### 3. 后端服务层变更
#### 3.1 间隔校验辅助方法
`PostsService``PostCommentsService` 中分别增加间隔校验逻辑。为避免重复代码,抽取公共的间隔查询方法到一个新的 `PostReplyIntervalService`
```csharp
// IPostReplyIntervalService.cs
public interface IPostReplyIntervalService
{
/// <summary>
/// 获取用户的发帖间隔配置(秒)
/// </summary>
Task<int> GetPostIntervalAsync(long userId);
/// <summary>
/// 获取用户的回复间隔配置(秒)
/// </summary>
Task<int> GetReplyIntervalAsync(long userId);
/// <summary>
/// 获取用户最近一次发帖时间
/// </summary>
Task<DateTime?> GetLastPostTimeAsync(long userId);
/// <summary>
/// 获取用户最近一次回复时间
/// </summary>
Task<DateTime?> GetLastCommentTimeAsync(long userId);
/// <summary>
/// 校验发帖间隔,返回 null 表示通过,否则返回剩余秒数
/// </summary>
Task<int?> CheckPostIntervalAsync(long userId);
/// <summary>
/// 校验回复间隔,返回 null 表示通过,否则返回剩余秒数
/// </summary>
Task<int?> CheckReplyIntervalAsync(long userId);
}
```
```csharp
// PostReplyIntervalService.cs
public class PostReplyIntervalService : IPostReplyIntervalService
{
private readonly IBaseRepository<T_PostReplyIntervals> _intervalsRepository;
private readonly IBaseRepository<T_Users> _usersRepository;
private readonly IBaseRepository<T_Posts> _postsRepository;
private readonly IBaseRepository<T_Comments> _commentsRepository;
// 默认间隔值(未配置时使用)
private const int DEFAULT_INTERVAL = 0;
public async Task<int> GetPostIntervalAsync(long userId)
{
var user = await _usersRepository.Select
.Where(x => x.Id == userId)
.FirstAsync();
if (user?.CertifiedType == null || user.CertifiedType <= 0)
return DEFAULT_INTERVAL;
var config = await _intervalsRepository.Select
.Where(x => x.CertificationTypeId == user.CertifiedType.Value)
.FirstAsync();
return config?.PostInterval ?? DEFAULT_INTERVAL;
}
public async Task<int> GetReplyIntervalAsync(long userId)
{
// 同 GetPostIntervalAsync返回 config?.ReplyInterval
// ...
}
public async Task<DateTime?> GetLastPostTimeAsync(long userId)
{
var lastPost = await _postsRepository.Select
.Where(x => x.UserId == userId && !x.IsDeleted)
.OrderByDescending(x => x.CreatedAt)
.FirstAsync();
return lastPost?.CreatedAt;
}
public async Task<DateTime?> GetLastCommentTimeAsync(long userId)
{
var lastComment = await _commentsRepository.Select
.Where(x => x.UserId == userId && !x.IsDeleted)
.OrderByDescending(x => x.CreatedAt)
.FirstAsync();
return lastComment?.CreatedAt;
}
public async Task<int?> CheckPostIntervalAsync(long userId)
{
var interval = await GetPostIntervalAsync(userId);
if (interval <= 0) return null; // 不限制
var lastTime = await GetLastPostTimeAsync(userId);
if (lastTime == null) return null; // 从未发帖
var elapsed = (DateTime.Now - lastTime.Value).TotalSeconds;
if (elapsed >= interval) return null; // 已满足间隔
return (int)Math.Ceiling(interval - elapsed); // 返回剩余秒数
}
public async Task<int?> CheckReplyIntervalAsync(long userId)
{
var interval = await GetReplyIntervalAsync(userId);
if (interval <= 0) return null;
var lastTime = await GetLastCommentTimeAsync(userId);
if (lastTime == null) return null;
var elapsed = (DateTime.Now - lastTime.Value).TotalSeconds;
if (elapsed >= interval) return null;
return (int)Math.Ceiling(interval - elapsed);
}
}
```
#### 3.2 修改 PostsService.PublishPosts
在现有的认证检查之后、敏感词过滤之前,增加发帖间隔校验:
```csharp
// 在 userInfo 认证检查之后添加
var remainingSeconds = await _postReplyIntervalService.CheckPostIntervalAsync(currentUserId);
if (remainingSeconds.HasValue)
{
return new BaseResponse<PublishPostsRespDto>(
ResponseCode.Error,
$"发帖过于频繁,请等待 {remainingSeconds.Value} 秒后再试");
}
```
#### 3.3 修改 PostCommentsService.PublishPostComments
在帖子存在性检查和回复权限检查之后,增加回复间隔校验:
```csharp
// 在 AllowReply 检查之后添加
var remainingSeconds = await _postReplyIntervalService.CheckReplyIntervalAsync(currentUserId);
if (remainingSeconds.HasValue)
{
return new BaseResponse<PublishPostCommentsRespDto>(
ResponseCode.Error,
$"回复过于频繁,请等待 {remainingSeconds.Value} 秒后再试");
}
```
#### 3.4 修改 ConfigService.GetAppConfig
在 GetAppConfig 方法中增加当前用户的间隔配置返回。由于 GetAppConfig 当前不需要认证(无 `[Authorize]`),需要判断用户是否已登录:
```csharp
// 尝试获取当前用户的间隔配置
// 如果用户未登录,返回默认值 0
int postInterval = 0;
int replyInterval = 0;
try
{
var userId = _userInfoModel?.UserId;
if (userId != null && userId > 0)
{
postInterval = await _postReplyIntervalService.GetPostIntervalAsync((long)userId);
replyInterval = await _postReplyIntervalService.GetReplyIntervalAsync((long)userId);
}
}
catch { /* 未登录时忽略 */ }
resultDict["postInterval"] = postInterval;
resultDict["replyInterval"] = replyInterval;
```
> 设计决策:将间隔配置放在 GetAppConfig 中返回,而非单独接口,因为前端在启动时已调用 GetAppConfig 获取全局配置,减少额外请求。注意 GetAppConfig 有 10 分钟缓存,需要按用户区分缓存或移除缓存(因为不同认证等级间隔不同)。建议对间隔配置部分不使用缓存,或改为前端在发帖/回复成功后从响应中获取间隔值。
> 替代方案:在 PublishPosts 和 PublishPostComments 的成功响应中直接返回间隔秒数,前端据此启动倒计时。这样避免了 GetAppConfig 缓存问题。**采用此方案。**
#### 3.5 修改 PublishPostsRespDto 和 PublishPostCommentsRespDto
在成功响应中增加间隔配置字段:
```csharp
// PublishPostsRespDto 新增
/// <summary>
/// 发帖间隔(秒),前端据此启动倒计时
/// </summary>
public int PostInterval { get; set; }
// PublishPostCommentsRespDto 新增
/// <summary>
/// 回复间隔(秒),前端据此启动倒计时
/// </summary>
public int ReplyInterval { get; set; }
```
在 PostsService.PublishPosts 成功返回时赋值:
```csharp
var postInterval = await _postReplyIntervalService.GetPostIntervalAsync(currentUserId);
var result = new PublishPostsRespDto
{
PostId = post.Id,
Title = post.Title,
Status = (byte)post.Status,
PublishTime = post.PublishTime,
PostInterval = postInterval
};
```
在 PostCommentsService.PublishPostComments 成功返回时赋值:
```csharp
var replyInterval = await _postReplyIntervalService.GetReplyIntervalAsync(currentUserId);
var result = new PublishPostCommentsRespDto
{
CommentId = comment.Id,
PostId = comment.PostId,
Content = comment.Content,
CreatedAt = comment.CreatedAt,
ReplyInterval = replyInterval
};
```
### 4. 管理端接口ZrAdminNetCore
#### 4.1 管理端实体与 DTO
```csharp
// ZR.LiveForum.Model/Liveforum/T_PostReplyIntervals.cs
// 与 LiveForum.Model 中的实体结构一致
// ZR.LiveForum.Model/Liveforum/Dto/T_PostReplyIntervalsDto.cs
public class T_PostReplyIntervalsDto
{
public int Id { get; set; }
public int CertificationTypeId { get; set; }
public string CertificationTypeName { get; set; } // 认证等级名称(关联查询)
public int PostInterval { get; set; }
public int ReplyInterval { get; set; }
}
public class T_PostReplyIntervalsQueryDto : PagerInfo
{
// 无额外查询条件,查询全部
}
```
#### 4.2 管理端 Service
```csharp
// T_PostReplyIntervalsService.cs
// 遵循现有 T_CertificationTypesService 的模式
[AppService(ServiceType = typeof(IT_PostReplyIntervalsService), ServiceLifetime = LifeTime.Transient)]
public class T_PostReplyIntervalsService : BaseService<T_PostReplyIntervals>, IT_PostReplyIntervalsService
{
public PagedInfo<T_PostReplyIntervalsDto> GetList(T_PostReplyIntervalsQueryDto parm);
public T_PostReplyIntervals GetInfo(int id);
public T_PostReplyIntervals AddOrUpdate(T_PostReplyIntervals model);
}
```
#### 4.3 管理端 Controller
```csharp
// T_PostReplyIntervalsController.cs
[Route("liveforum/tpostreplyintervals")]
public class T_PostReplyIntervalsController : BaseController
{
/// <summary>
/// 查询时间间隔配置列表(含认证等级名称)
/// </summary>
[HttpGet("list")]
[ActionPermissionFilter(Permission = "tpostreplyintervals:list")]
public IActionResult QueryList([FromQuery] T_PostReplyIntervalsQueryDto parm);
/// <summary>
/// 新增或修改时间间隔配置
/// </summary>
[HttpPut]
[ActionPermissionFilter(Permission = "tpostreplyintervals:edit")]
[Log(Title = "时间间隔配置", BusinessType = BusinessType.UPDATE)]
public IActionResult Update([FromBody] T_PostReplyIntervalsDto parm);
}
```
> 设计决策:列表接口需要关联查询 T_CertificationTypes 获取认证等级名称。如果某认证等级尚未配置间隔记录列表中仍应展示该等级PostInterval 和 ReplyInterval 显示为 0管理员编辑保存时自动创建记录。
### 5. 前端变更
#### 5.1 发帖页post-page.vue
**倒计时逻辑:**
```javascript
data() {
return {
// ... 现有字段 ...
postCountdown: 0, // 发帖倒计时剩余秒数
countdownTimer: null // 倒计时定时器
}
},
methods: {
// 发布成功后启动倒计时
startPostCountdown(seconds) {
if (seconds <= 0) return;
this.postCountdown = seconds;
this.countdownTimer = setInterval(() => {
this.postCountdown--;
if (this.postCountdown <= 0) {
clearInterval(this.countdownTimer);
this.countdownTimer = null;
}
}, 1000);
},
handlePublish() {
// ... 现有校验逻辑 ...
// 倒计时期间禁止发帖
if (this.postCountdown > 0) {
uni.showToast({ title: `请等待 ${this.postCountdown} 秒后再发帖`, icon: 'none' });
return;
}
appServer.PublishPosts(this.title, this.content, images, this.allowReply)
.then((data) => {
if (data.code == 0) {
// 启动倒计时
this.startPostCountdown(data.data.postInterval);
// ... 现有成功逻辑 ...
}
})
.catch((err) => {
// 如果后端返回间隔错误,解析剩余秒数并更新倒计时
// ...
});
}
}
```
**发布按钮状态:**
```html
<view class="center" @click="!isSubmitting && postCountdown <= 0 && handlePublish()"
:style="{ opacity: (isSubmitting || postCountdown > 0) ? 0.6 : 1 }">
{{ postCountdown > 0 ? `请等待 ${postCountdown} 秒` : (isSubmitting ? '发布中...' : '立即发布') }}
</view>
```
#### 5.2 帖子详情页post-details-page.vue
**回复倒计时逻辑:**
```javascript
data() {
return {
// ... 现有字段 ...
replyCountdown: 0,
replyCountdownTimer: null
}
},
methods: {
startReplyCountdown(seconds) {
if (seconds <= 0) return;
this.replyCountdown = seconds;
this.replyCountdownTimer = setInterval(() => {
this.replyCountdown--;
if (this.replyCountdown <= 0) {
clearInterval(this.replyCountdownTimer);
this.replyCountdownTimer = null;
}
}, 1000);
}
}
```
**评论输入区域状态:**
- 倒计时期间:显示「请等待 X 秒后再回复」提示文字,提交按钮禁用
- 倒计时结束:恢复正常可输入可提交状态
## Data Models
### 后端新增实体
```csharp
// T_PostReplyIntervals.cs完整定义见 Components 2.1 节)
public partial class T_PostReplyIntervals
{
public int Id { get; set; }
public int CertificationTypeId { get; set; }
public int PostInterval { get; set; } = 0;
public int ReplyInterval { get; set; } = 0;
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
```
### 后端 DTO 变更
```csharp
// PublishPostsRespDto 新增字段
public int PostInterval { get; set; }
// PublishPostCommentsRespDto 新增字段
public int ReplyInterval { get; set; }
```
### 前端数据模型
```javascript
// post-page.vue data 新增
{
postCountdown: 0, // 发帖倒计时
countdownTimer: null // 定时器引用
}
// post-details-page.vue data 新增
{
replyCountdown: 0, // 回复倒计时
replyCountdownTimer: null // 定时器引用
}
```
## 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: 间隔配置保存 Round-Trip
*For any* 认证等级 ID 和任意非负整数的 PostInterval、ReplyInterval 值,保存间隔配置后再查询该认证等级的配置,返回的 PostInterval 和 ReplyInterval 应与保存时的值一致。
**Validates: Requirements 1.3**
### Property 2: 未配置认证等级默认返回 0
*For any* 未在 T_PostReplyIntervals 表中配置记录的认证等级 ID或用户未关联任何认证等级查询其 PostInterval 和 ReplyInterval 均应返回 0。
**Validates: Requirements 1.5, 2.5, 3.5, 6.5**
### Property 3: 发帖间隔校验正确性
*For any* 用户、任意 PostInterval 配置值和任意最近发帖时间,当 PostInterval > 0 且当前时间与最近发帖时间的差值elapsed小于 PostInterval 时,校验应拒绝并返回剩余秒数 = ceil(PostInterval - elapsed);当 PostInterval = 0 或 elapsed >= PostInterval 或从未发帖时,校验应通过。
**Validates: Requirements 2.3, 2.4**
### Property 4: 回复间隔校验正确性
*For any* 用户、任意 ReplyInterval 配置值和任意最近回复时间,当 ReplyInterval > 0 且当前时间与最近回复时间的差值elapsed小于 ReplyInterval 时,校验应拒绝并返回剩余秒数 = ceil(ReplyInterval - elapsed);当 ReplyInterval = 0 或 elapsed >= ReplyInterval 或从未回复时,校验应通过。
**Validates: Requirements 3.3, 3.4**
### Property 5: 成功响应包含正确的间隔值
*For any* 用户和其关联的认证等级,发帖成功响应中的 PostInterval 字段应等于该认证等级配置的 PostInterval 值;回复成功响应中的 ReplyInterval 字段应等于该认证等级配置的 ReplyInterval 值。
**Validates: Requirements 6.4**
## Error Handling
| 场景 | 错误码 | 错误信息 |
|------|--------|----------|
| 发帖间隔未满足 | Error | 发帖过于频繁,请等待 {remainingSeconds} 秒后再试 |
| 回复间隔未满足 | Error | 回复过于频繁,请等待 {remainingSeconds} 秒后再试 |
| 管理端保存间隔值为负数 | Error | 时间间隔不能为负数 |
| 管理端保存的认证等级不存在 | Error | 认证等级不存在 |
## Testing Strategy
### 单元测试
- `PostReplyIntervalService.GetPostIntervalAsync`:用户有认证等级且已配置间隔 → 返回配置值
- `PostReplyIntervalService.GetPostIntervalAsync`:用户无认证等级 → 返回 0
- `PostReplyIntervalService.CheckPostIntervalAsync`:用户从未发帖 → 返回 null通过
- `PostReplyIntervalService.CheckReplyIntervalAsync`:用户从未回复 → 返回 null通过
- 管理端保存负数间隔值 → 拒绝或归零
- 管理端列表接口返回所有认证等级(含未配置的)
### 属性测试
使用 **FsCheck.Xunit**(项目已引入)进行属性测试:
- 每个属性测试配置最少 100 次迭代
- 每个测试以注释标注对应的设计属性编号
- 标注格式:**Feature: post-reply-interval, Property {number}: {property_text}**
属性测试覆盖:
1. **Property 1** - 生成随机认证等级 ID 和随机非负整数的 PostInterval/ReplyInterval验证保存→查询的 round-trip
2. **Property 2** - 生成随机不存在的认证等级 ID 或 CertifiedType 为 null/0 的用户,验证返回值均为 0
3. **Property 3** - 生成随机 PostInterval含 0、随机最近发帖时间验证 CheckPostIntervalAsync 的返回值符合预期(拒绝/通过及剩余秒数计算)
4. **Property 4** - 生成随机 ReplyInterval含 0、随机最近回复时间验证 CheckReplyIntervalAsync 的返回值符合预期
5. **Property 5** - 生成随机用户和认证等级配置,验证发帖/回复成功响应中的间隔字段与配置值一致
### 前端测试
- 发帖成功后倒计时启动,按钮禁用并显示剩余秒数
- 倒计时结束后按钮恢复可用
- 回复成功后倒计时启动,评论输入区域显示等待提示
- 后端返回间隔错误时弹出提示并更新本地倒计时