chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -0,0 +1,284 @@
package memory
import (
"context"
"sort"
"sync"
"time"
"telesrv/internal/domain"
)
// PollStore 是 store.PollStore 的内存实现,同时充当 MessageStore/ChannelStore 投票与
// 读路径 enrichment 的共享权威(postgres 侧对应 polls / poll_votes 两张表)。
// 校验与门控复用 domain.ValidatePollVote / ResolvePollResults,保证与 postgres 行为一致。
type PollStore struct {
mu sync.RWMutex
polls map[int64]domain.PollDefinition
votes map[int64]map[int64]domain.PollVote
}
// NewPollStore 创建内存 poll 权威存储。
func NewPollStore() *PollStore {
return &PollStore{
polls: make(map[int64]domain.PollDefinition),
votes: make(map[int64]map[int64]domain.PollVote),
}
}
func (s *PollStore) CreatePoll(_ context.Context, def domain.PollDefinition) error {
if def.ID == 0 || len(def.Options) == 0 {
return domain.ErrPollInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.polls[def.ID]; exists {
return domain.ErrPollInvalid
}
s.polls[def.ID] = clonePollDefinition(def)
return nil
}
func (s *PollStore) GetPollDefinition(_ context.Context, pollID int64) (domain.PollDefinition, bool, error) {
s.mu.RLock()
defer s.mu.RUnlock()
def, ok := s.polls[pollID]
if !ok {
return domain.PollDefinition{}, false, nil
}
return clonePollDefinition(def), true, nil
}
func (s *PollStore) ListPollVotes(_ context.Context, req domain.PollVotesListRequest) (domain.PollVotesList, error) {
if req.PollID == 0 || req.Limit <= 0 {
return domain.PollVotesList{}, domain.ErrPollInvalid
}
s.mu.RLock()
defer s.mu.RUnlock()
if _, ok := s.polls[req.PollID]; !ok {
return domain.PollVotesList{}, domain.ErrPollNotFound
}
rows := make([]domain.PollVote, 0, len(s.votes[req.PollID]))
for _, vote := range s.votes[req.PollID] {
if len(req.Option) > 0 && !voteHasOption(vote, req.Option) {
continue
}
rows = append(rows, clonePollVote(vote))
}
sort.Slice(rows, func(i, j int) bool {
if rows[i].Date != rows[j].Date {
return rows[i].Date > rows[j].Date
}
return rows[i].UserID > rows[j].UserID
})
out := domain.PollVotesList{Count: len(rows)}
start := 0
if req.OffsetDate > 0 || req.OffsetUserID > 0 {
for i, row := range rows {
if row.Date < req.OffsetDate || (row.Date == req.OffsetDate && row.UserID < req.OffsetUserID) {
start = i
break
}
start = i + 1
}
}
end := start + req.Limit
if end > len(rows) {
end = len(rows)
}
if start < end {
out.Votes = rows[start:end]
}
out.HasMore = end < len(rows)
return out, nil
}
// Vote 校验并落一票(options 为空 = 撤票)。校验逻辑全部来自 domain.ValidatePollVote。
func (s *PollStore) Vote(pollID, userID int64, options [][]byte, date int) error {
if pollID == 0 || userID == 0 {
return domain.ErrPollNotFound
}
if date == 0 {
date = int(time.Now().Unix())
}
s.mu.Lock()
defer s.mu.Unlock()
def, ok := s.polls[pollID]
if !ok {
return domain.ErrPollNotFound
}
var existing [][]byte
if vote, voted := s.votes[pollID][userID]; voted {
existing = vote.Options
}
if err := domain.ValidatePollVote(def, existing, options, date); err != nil {
return err
}
if len(options) == 0 {
delete(s.votes[pollID], userID)
return nil
}
if s.votes[pollID] == nil {
s.votes[pollID] = make(map[int64]domain.PollVote)
}
s.votes[pollID][userID] = clonePollVote(domain.PollVote{PollID: pollID, UserID: userID, Options: options, Date: date})
return nil
}
// Close 关闭 poll;仅创建者可关,重复关闭幂等。
func (s *PollStore) Close(pollID, byUserID int64) error {
if pollID == 0 || byUserID == 0 {
return domain.ErrPollNotFound
}
s.mu.Lock()
defer s.mu.Unlock()
def, ok := s.polls[pollID]
if !ok {
return domain.ErrPollNotFound
}
if def.CreatorUserID != byUserID {
return domain.ErrPollNotCreator
}
def.Closed = true
s.polls[pollID] = def
return nil
}
// EnrichPoll 把权威态 + viewer 视角聚合写回 media 定义快照;poll 不存在时保持快照原样。
func (s *PollStore) EnrichPoll(poll *domain.MessagePoll, viewerUserID int64, now int) {
if s == nil || poll == nil || poll.ID == 0 {
return
}
s.mu.RLock()
defer s.mu.RUnlock()
def, ok := s.polls[poll.ID]
if !ok {
return
}
agg := domain.PollAggregates{Counts: make(map[string]int)}
recent := make([]domain.PollVote, 0, len(s.votes[poll.ID]))
for userID, vote := range s.votes[poll.ID] {
agg.TotalVoters++
for _, option := range vote.Options {
agg.Counts[string(option)]++
}
if userID == viewerUserID {
agg.ViewerOptions = append([][]byte(nil), vote.Options...)
}
recent = append(recent, vote)
}
sort.Slice(recent, func(i, j int) bool {
if recent[i].Date != recent[j].Date {
return recent[i].Date > recent[j].Date
}
return recent[i].UserID > recent[j].UserID
})
for i, vote := range recent {
if i >= domain.MaxPollRecentVoters {
break
}
agg.RecentVoters = append(agg.RecentVoters, vote.UserID)
}
results := domain.ResolvePollResults(def, agg, viewerUserID, now)
domain.ApplyPollState(poll, def, results, now)
}
// EnrichPollForViewers 批量为一组 viewer 返回 per-viewer enrich 的 poll(fan-out 模板化):
// viewer-invariant 聚合(counts/total/recent)只遍历一次 + per-viewer ViewerOptions,每 viewer 用与
// 单 viewer EnrichPoll 完全相同的 ResolvePollResults/ApplyPollState 合成(字节同源)。返回 map[viewer]
// 各自的 poll 克隆;def 不存在时返回空 map(与 EnrichPoll 的 no-op 一致)。
func (s *PollStore) EnrichPollForViewers(basePoll *domain.MessagePoll, viewers []int64, now int) map[int64]*domain.MessagePoll {
out := make(map[int64]*domain.MessagePoll, len(viewers))
if s == nil || basePoll == nil || basePoll.ID == 0 || len(viewers) == 0 {
return out
}
s.mu.RLock()
defer s.mu.RUnlock()
def, ok := s.polls[basePoll.ID]
if !ok {
return out
}
counts := make(map[string]int)
total := 0
optionsByUser := make(map[int64][][]byte, len(s.votes[basePoll.ID]))
recent := make([]domain.PollVote, 0, len(s.votes[basePoll.ID]))
for userID, vote := range s.votes[basePoll.ID] {
total++
for _, option := range vote.Options {
counts[string(option)]++
}
optionsByUser[userID] = append([][]byte(nil), vote.Options...)
recent = append(recent, vote)
}
sort.Slice(recent, func(i, j int) bool {
if recent[i].Date != recent[j].Date {
return recent[i].Date > recent[j].Date
}
return recent[i].UserID > recent[j].UserID
})
recentVoters := make([]int64, 0, domain.MaxPollRecentVoters)
for i, vote := range recent {
if i >= domain.MaxPollRecentVoters {
break
}
recentVoters = append(recentVoters, vote.UserID)
}
for _, viewer := range viewers {
agg := domain.PollAggregates{
Counts: counts,
TotalVoters: total,
RecentVoters: recentVoters,
ViewerOptions: optionsByUser[viewer],
}
results := domain.ResolvePollResults(def, agg, viewer, now)
pollCopy := *basePoll
domain.ApplyPollState(&pollCopy, def, results, now)
out[viewer] = &pollCopy
}
return out
}
// enrichPollMediaForViewer 克隆 media(避免共享指针把 viewer 态写进 store 本体)并 enrich。
func enrichPollMediaForViewer(polls *PollStore, media *domain.MessageMedia, viewerUserID int64, now int) *domain.MessageMedia {
if polls == nil || media == nil || media.Kind != domain.MessageMediaKindPoll || media.Poll == nil {
return media
}
cloned := *media
poll := *media.Poll
poll.Answers = append([]domain.MessagePollAnswer(nil), media.Poll.Answers...)
cloned.Poll = &poll
polls.EnrichPoll(cloned.Poll, viewerUserID, now)
return &cloned
}
func clonePollDefinition(def domain.PollDefinition) domain.PollDefinition {
def.Options = cloneOptionList(def.Options)
def.CorrectOptions = cloneOptionList(def.CorrectOptions)
def.SolutionEntities = append([]domain.MessageEntity(nil), def.SolutionEntities...)
return def
}
func clonePollVote(vote domain.PollVote) domain.PollVote {
vote.Options = cloneOptionList(vote.Options)
return vote
}
func cloneOptionList(options [][]byte) [][]byte {
if options == nil {
return nil
}
out := make([][]byte, 0, len(options))
for _, option := range options {
out = append(out, append([]byte(nil), option...))
}
return out
}
func voteHasOption(vote domain.PollVote, option []byte) bool {
for _, candidate := range vote.Options {
if string(candidate) == string(option) {
return true
}
}
return false
}