feat: 评论/帖子编辑历史与编辑标记,内容锁定

- 新增 smartDiff 差异计算与 EditHistoryModal 编辑历史弹窗,替换原 CommentEditHistoryModal
- 新增 PostEditedMark 编辑标记展示
- 新增内容锁定机制(content_lock)防止并发编辑冲突
- 审核与通知服务适配
This commit is contained in:
2026-09-25 23:41:45 +08:00
parent cab8230803
commit 5270fee2b7
40 changed files with 2521 additions and 350 deletions

View File

@@ -59,6 +59,12 @@ const (
// SettingKeyAnalyticsRetentionDays 访问明细保留天数;缺行=90
SettingKeyAnalyticsRetentionDays = "analytics_retention_days"
// 内容锁定与旧帖回复(全部缺行=0 关闭)
SettingKeyPostEditLockHours = "post_edit_lock_hours" // 帖子可编辑时长;0=不锁定
SettingKeyCommentEditLockHours = "comment_edit_lock_hours" // 评论可编辑时长;0=不锁定
SettingKeyNecroReplyAfterHours = "necro_reply_after_hours" // 超过该时长回复视为旧帖回复;0=关闭
SettingKeyNecroReplyPenalty = "necro_reply_penalty" // 旧帖回复扣除积分;0=仅提醒不扣分
// 积分增长规则(正整数奖励;0=关闭/不限,缺行=默认值)
SettingKeyPointsCheckinBase = "points_checkin_base" // 签到基础分;缺行=5
SettingKeyPointsStreakEveryDays = "points_streak_every_days" // 每满 N 天连续签到触发加成;0=关闭;缺行=7
@@ -100,6 +106,9 @@ const (
MinAnalyticsRetentionDays = 7
MaxAnalyticsRetentionDays = 365
// MaxContentLockHours 内容锁定/旧帖阈值小时数上限(一年)
MaxContentLockHours = 8760
DefaultPointsCheckinBase = 5
DefaultPointsStreakEveryDays = 7
MaxPointsRule = 100 // 各积分规则值/上限的统一上界
@@ -150,6 +159,12 @@ type PublicSiteSettings struct {
AttachmentMaxMB int `json:"attachment_max_mb"`
AttachmentMaxCount int `json:"attachment_max_count"`
ImageMaxMB int `json:"image_max_mb"`
// 内容锁定与旧帖回复(0=关闭;公开保持规则透明)
PostEditLockHours int `json:"post_edit_lock_hours"`
CommentEditLockHours int `json:"comment_edit_lock_hours"`
NecroReplyAfterHours int `json:"necro_reply_after_hours"`
NecroReplyPenalty int `json:"necro_reply_penalty"`
BgSiteURL string `json:"bg_site_url"`
BgSiteMode string `json:"bg_site_mode"`
BgAdminURL string `json:"bg_admin_url"`
@@ -438,6 +453,27 @@ func (s *SettingService) Public() (PublicSiteSettings, error) {
}
out.ImageMaxMB = imgMB
postLock, err := s.PostEditLockHours()
if err != nil {
return out, err
}
out.PostEditLockHours = postLock
commentLock, err := s.CommentEditLockHours()
if err != nil {
return out, err
}
out.CommentEditLockHours = commentLock
necroAfter, err := s.NecroReplyAfterHours()
if err != nil {
return out, err
}
out.NecroReplyAfterHours = necroAfter
necroPenalty, err := s.NecroReplyPenalty()
if err != nil {
return out, err
}
out.NecroReplyPenalty = necroPenalty
siteURL, err := s.BgSiteURL()
if err != nil {
return out, err
@@ -941,6 +977,91 @@ func (s *SettingService) ImageMaxBytes() (int64, error) {
return int64(mb) << 20, nil
}
// getLockHours 读取小时数配置(缺行/非法=0 关闭,超上限钳制)
func (s *SettingService) getLockHours(key string) (int, error) {
v, found, err := s.getValue(key)
if err != nil {
return 0, err
}
if !found {
return 0, nil
}
n, convErr := strconv.Atoi(strings.TrimSpace(v))
if convErr != nil || n < 0 {
return 0, nil
}
if n > MaxContentLockHours {
return MaxContentLockHours, nil
}
return n, nil
}
func (s *SettingService) setLockHours(key string, hours int) error {
if hours < 0 || hours > MaxContentLockHours {
return ErrInvalidSiteSetting
}
if hours == 0 {
return s.deleteKey(key)
}
return s.putValue(key, strconv.Itoa(hours))
}
// PostEditLockHours 帖子可编辑时长(小时)。缺行=0(不锁定)。
func (s *SettingService) PostEditLockHours() (int, error) {
return s.getLockHours(SettingKeyPostEditLockHours)
}
func (s *SettingService) SetPostEditLockHours(hours int) error {
return s.setLockHours(SettingKeyPostEditLockHours, hours)
}
// CommentEditLockHours 评论可编辑时长(小时)。缺行=0(不锁定)。
func (s *SettingService) CommentEditLockHours() (int, error) {
return s.getLockHours(SettingKeyCommentEditLockHours)
}
func (s *SettingService) SetCommentEditLockHours(hours int) error {
return s.setLockHours(SettingKeyCommentEditLockHours, hours)
}
// NecroReplyAfterHours 旧帖回复阈值(小时)。缺行=0(关闭)。
func (s *SettingService) NecroReplyAfterHours() (int, error) {
return s.getLockHours(SettingKeyNecroReplyAfterHours)
}
func (s *SettingService) SetNecroReplyAfterHours(hours int) error {
return s.setLockHours(SettingKeyNecroReplyAfterHours, hours)
}
// NecroReplyPenalty 旧帖回复扣分。缺行=0(仅提醒不扣分)。
func (s *SettingService) NecroReplyPenalty() (int, error) {
v, found, err := s.getValue(SettingKeyNecroReplyPenalty)
if err != nil {
return 0, err
}
if !found {
return 0, nil
}
n, convErr := strconv.Atoi(strings.TrimSpace(v))
if convErr != nil || n < 0 {
return 0, nil
}
if n > MaxPointsRule {
return MaxPointsRule, nil
}
return n, nil
}
func (s *SettingService) SetNecroReplyPenalty(n int) error {
if n < 0 || n > MaxPointsRule {
return ErrInvalidSiteSetting
}
if n == 0 {
return s.deleteKey(SettingKeyNecroReplyPenalty)
}
return s.putValue(SettingKeyNecroReplyPenalty, strconv.Itoa(n))
}
// AnalyticsEnabled 访问统计采集开关。缺行视为开启。
func (s *SettingService) AnalyticsEnabled() (bool, error) {
v, found, err := s.getValue(SettingKeyAnalyticsEnabled)