chore: baseline snapshot before moderation-strictness feature
Repo .git was damaged (refs/ missing, index truncated, object store near-empty). Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak, then full worktree committed as a rollback baseline.
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
// Copyright 2022 E99p1ant. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/thanhpk/randstr"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/tamakyi/TamaBox/internal/dbutil"
|
||||
)
|
||||
|
||||
var Conversations ConversationStore
|
||||
|
||||
type ConversationStore interface {
|
||||
Create(ctx context.Context, opts CreateConversationOptions) (*Conversation, error)
|
||||
GetByID(ctx context.Context, id uint) (*Conversation, error)
|
||||
GetByToken(ctx context.Context, token string) (*Conversation, error)
|
||||
GetByIDAndOwner(ctx context.Context, id, ownerID uint) (*Conversation, error)
|
||||
GetByIDAndAsker(ctx context.Context, id, askerID uint) (*Conversation, error)
|
||||
IncrementMessageCount(ctx context.Context, id uint) error
|
||||
IncrementMessageCountIfBelow(ctx context.Context, id uint, max int) (bool, error)
|
||||
DecrementMessageCount(ctx context.Context, id uint) error
|
||||
MaybeClose(ctx context.Context, id uint, max int) error
|
||||
Close(ctx context.Context, id uint) error
|
||||
// ListByOwner 返回某箱主拥有的会话。limit>0 时启用分页(取 limit 条、跳过 offset 条),
|
||||
// 用于私聊列表分页;导出场景传 limit=0 取全量,行为与改造前一致。
|
||||
ListByOwner(ctx context.Context, ownerID uint, limit, offset int) ([]*Conversation, error)
|
||||
// CountByOwner 统计某箱主拥有的会话总数(分页看板与"加载更多"判定用)。
|
||||
CountByOwner(ctx context.Context, ownerID uint) (int64, error)
|
||||
// ListByAsker 返回某提问者发起的会话;分页语义同 ListByOwner。
|
||||
ListByAsker(ctx context.Context, askerID uint, limit, offset int) ([]*Conversation, error)
|
||||
// CountByAsker 统计某提问者发起的会话总数。
|
||||
CountByAsker(ctx context.Context, askerID uint) (int64, error)
|
||||
MarkDeletedByOwner(ctx context.Context, id uint) error
|
||||
}
|
||||
|
||||
func NewConversationStore(db *gorm.DB) ConversationStore {
|
||||
return &conversations{db}
|
||||
}
|
||||
|
||||
type conversations struct {
|
||||
*gorm.DB
|
||||
}
|
||||
|
||||
type Conversation struct {
|
||||
dbutil.Model
|
||||
OwnerID uint `gorm:"index:idx_conversation_owner" json:"owner_id"`
|
||||
AskerID uint `gorm:"index:idx_conversation_asker" json:"asker_id"`
|
||||
Token string `gorm:"size:6;uniqueIndex" json:"token"`
|
||||
MaxMessages int `gorm:"default:20" json:"max_messages"`
|
||||
MessageCount int `gorm:"default:0" json:"message_count"`
|
||||
IsClosed bool `gorm:"default:false" json:"is_closed"`
|
||||
IsDeletedByOwner bool `gorm:"default:false" json:"is_deleted_by_owner"` // 新增
|
||||
}
|
||||
|
||||
func (c *Conversation) BeforeCreate(tx *gorm.DB) error {
|
||||
c.MessageCount = 0
|
||||
c.Token = randstr.String(6)
|
||||
return nil
|
||||
}
|
||||
|
||||
type CreateConversationOptions struct {
|
||||
OwnerID uint
|
||||
AskerID uint
|
||||
MaxMessages int
|
||||
}
|
||||
|
||||
func (db *conversations) Create(ctx context.Context, opts CreateConversationOptions) (*Conversation, error) {
|
||||
conversation := &Conversation{
|
||||
OwnerID: opts.OwnerID,
|
||||
AskerID: opts.AskerID,
|
||||
MaxMessages: opts.MaxMessages,
|
||||
}
|
||||
if err := db.WithContext(ctx).Create(conversation).Error; err != nil {
|
||||
return nil, errors.Wrap(err, "create conversation")
|
||||
}
|
||||
return conversation, nil
|
||||
}
|
||||
|
||||
func (db *conversations) GetByID(ctx context.Context, id uint) (*Conversation, error) {
|
||||
var conv Conversation
|
||||
if err := db.WithContext(ctx).First(&conv, id).Error; err != nil {
|
||||
return nil, errors.Wrap(err, "get conversation")
|
||||
}
|
||||
return &conv, nil
|
||||
}
|
||||
|
||||
func (db *conversations) GetByToken(ctx context.Context, token string) (*Conversation, error) {
|
||||
var conv Conversation
|
||||
if err := db.WithContext(ctx).Where("token = ?", token).First(&conv).Error; err != nil {
|
||||
return nil, errors.Wrap(err, "get conversation by token")
|
||||
}
|
||||
return &conv, nil
|
||||
}
|
||||
|
||||
func (db *conversations) GetByIDAndOwner(ctx context.Context, id, ownerID uint) (*Conversation, error) {
|
||||
var conv Conversation
|
||||
if err := db.WithContext(ctx).Where("id = ? AND owner_id = ?", id, ownerID).First(&conv).Error; err != nil {
|
||||
return nil, errors.Wrap(err, "get by owner")
|
||||
}
|
||||
return &conv, nil
|
||||
}
|
||||
|
||||
func (db *conversations) GetByIDAndAsker(ctx context.Context, id, askerID uint) (*Conversation, error) {
|
||||
var conv Conversation
|
||||
if err := db.WithContext(ctx).Where("id = ? AND asker_id = ?", id, askerID).First(&conv).Error; err != nil {
|
||||
return nil, errors.Wrap(err, "get by asker")
|
||||
}
|
||||
return &conv, nil
|
||||
}
|
||||
|
||||
func (db *conversations) IncrementMessageCount(ctx context.Context, id uint) error {
|
||||
return db.WithContext(ctx).Model(&Conversation{}).Where("id = ?", id).UpdateColumn("message_count", gorm.Expr("message_count + 1")).Error
|
||||
}
|
||||
|
||||
// IncrementMessageCountIfBelow 原子地检查并递增消息计数:
|
||||
// 仅当 message_count < max 且未关闭时才 +1,返回 true。
|
||||
// 并发场景下(同一对话两端同时发送)可防止消息数超出上限。
|
||||
func (db *conversations) IncrementMessageCountIfBelow(ctx context.Context, id uint, max int) (bool, error) {
|
||||
res := db.WithContext(ctx).Model(&Conversation{}).
|
||||
Where("id = ? AND message_count < ? AND is_closed = ?", id, max, false).
|
||||
UpdateColumn("message_count", gorm.Expr("message_count + 1"))
|
||||
if res.Error != nil {
|
||||
return false, errors.Wrap(res.Error, "increment message count if below")
|
||||
}
|
||||
return res.RowsAffected > 0, nil
|
||||
}
|
||||
|
||||
// DecrementMessageCount 回滚一次消息计数(计数已递增但消息落库失败时调用)。
|
||||
func (db *conversations) DecrementMessageCount(ctx context.Context, id uint) error {
|
||||
return db.WithContext(ctx).Model(&Conversation{}).
|
||||
Where("id = ? AND message_count > 0", id).
|
||||
UpdateColumn("message_count", gorm.Expr("message_count - 1")).Error
|
||||
}
|
||||
|
||||
// MaybeClose 当消息数达到上限时关闭对话(幂等,供发送消息后调用)。
|
||||
func (db *conversations) MaybeClose(ctx context.Context, id uint, max int) error {
|
||||
return db.WithContext(ctx).Model(&Conversation{}).
|
||||
Where("id = ? AND message_count >= ? AND is_closed = ?", id, max, false).
|
||||
Update("is_closed", true).Error
|
||||
}
|
||||
|
||||
func (db *conversations) Close(ctx context.Context, id uint) error {
|
||||
return db.WithContext(ctx).Model(&Conversation{}).Where("id = ?", id).Update("is_closed", true).Error
|
||||
}
|
||||
|
||||
func (db *conversations) ListByOwner(ctx context.Context, ownerID uint, limit, offset int) ([]*Conversation, error) {
|
||||
var convs []*Conversation
|
||||
q := db.WithContext(ctx).Where("owner_id = ? AND is_deleted_by_owner = ? AND token != ''", ownerID, false).
|
||||
Order("created_at DESC")
|
||||
if limit > 0 {
|
||||
q = q.Limit(limit)
|
||||
if offset > 0 {
|
||||
q = q.Offset(offset)
|
||||
}
|
||||
}
|
||||
if err := q.Find(&convs).Error; err != nil {
|
||||
return nil, errors.Wrap(err, "list by owner")
|
||||
}
|
||||
return convs, nil
|
||||
}
|
||||
|
||||
func (db *conversations) CountByOwner(ctx context.Context, ownerID uint) (int64, error) {
|
||||
var count int64
|
||||
if err := db.WithContext(ctx).Model(&Conversation{}).
|
||||
Where("owner_id = ? AND is_deleted_by_owner = ? AND token != ''", ownerID, false).
|
||||
Count(&count).Error; err != nil {
|
||||
return 0, errors.Wrap(err, "count by owner")
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (db *conversations) ListByAsker(ctx context.Context, askerID uint, limit, offset int) ([]*Conversation, error) {
|
||||
var convs []*Conversation
|
||||
q := db.WithContext(ctx).Where("asker_id = ? AND is_deleted_by_owner = ? AND token != ''", askerID, false).
|
||||
Order("created_at DESC")
|
||||
if limit > 0 {
|
||||
q = q.Limit(limit)
|
||||
if offset > 0 {
|
||||
q = q.Offset(offset)
|
||||
}
|
||||
}
|
||||
if err := q.Find(&convs).Error; err != nil {
|
||||
return nil, errors.Wrap(err, "list by asker")
|
||||
}
|
||||
return convs, nil
|
||||
}
|
||||
|
||||
func (db *conversations) CountByAsker(ctx context.Context, askerID uint) (int64, error) {
|
||||
var count int64
|
||||
if err := db.WithContext(ctx).Model(&Conversation{}).
|
||||
Where("asker_id = ? AND is_deleted_by_owner = ? AND token != ''", askerID, false).
|
||||
Count(&count).Error; err != nil {
|
||||
return 0, errors.Wrap(err, "count by asker")
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (db *conversations) MarkDeletedByOwner(ctx context.Context, id uint) error {
|
||||
var conv Conversation
|
||||
if err := db.WithContext(ctx).First(&conv, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// 已删除或不存在:幂等,不影响调用方。
|
||||
return nil
|
||||
}
|
||||
return errors.Wrap(err, "get conversation for delete")
|
||||
}
|
||||
if conv.IsDeletedByOwner {
|
||||
// 已经是删除状态:幂等返回,避免重复请求多次回退配额计数。
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := db.WithContext(ctx).Model(&Conversation{}).Where("id = ?", id).Update("is_deleted_by_owner", true).Error; err != nil {
|
||||
return errors.Wrap(err, "mark conversation deleted by owner")
|
||||
}
|
||||
|
||||
// 回退定量收集计数:删除私聊会话等价于少一条已收集内容,
|
||||
// 否则达上限后即便删掉旧会话,功能仍会显示 quota_reached 关闭,须手动重置计数。
|
||||
if conv.OwnerID != 0 {
|
||||
if err := FeatureControls.IncrementQuota(ctx, conv.OwnerID, FeatureChat, -1); err != nil {
|
||||
logrus.WithContext(ctx).WithError(err).Errorf("Failed to decrement chat quota for user %d", conv.OwnerID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user