owpengram-server/internal/store/postgres/groupcall.go

675 lines
23 KiB
Go
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.

package postgres
import (
"context"
"errors"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"telesrv/internal/domain"
"telesrv/internal/store/postgres/sqlcgen"
)
// GroupCallStore 是 store.GroupCallStore 的 PostgreSQL 实现(迁移 0092
// 所有参与者维度变更在事务内对 group_calls 行 FOR UPDATE 后 version+1
// 行锁天然串行化并发变更,保证 version 单调无跳号。
type GroupCallStore struct {
db sqlcgen.DBTX
}
// NewGroupCallStore 基于 pgx 连接池创建 GroupCallStore。
func NewGroupCallStore(db sqlcgen.DBTX) *GroupCallStore {
return &GroupCallStore{db: db}
}
const groupCallColumns = `call_id, access_hash, channel_id, creator_user_id, state, title, join_muted,
version, participants_count, created_at, discarded_at, duration, started_msg_id`
const groupCallParticipantColumns = `call_id, user_id, ssrc, join_date, active_date, muted, muted_by_admin,
volume_by_admin, raise_hand_rating, video_json, presentation_json, left_call, last_check_date`
func scanGroupCall(row rowScanner) (domain.GroupCall, error) {
var c domain.GroupCall
var state string
if err := row.Scan(
&c.ID, &c.AccessHash, &c.ChannelID, &c.CreatorUserID, &state, &c.Title, &c.JoinMuted,
&c.Version, &c.ParticipantsCount, &c.CreatedAt, &c.DiscardedAt, &c.Duration, &c.StartedMsgID,
); err != nil {
return domain.GroupCall{}, err
}
c.State = domain.GroupCallState(state)
return c, nil
}
func scanGroupCallParticipant(row rowScanner) (domain.GroupCallParticipant, error) {
var p domain.GroupCallParticipant
if err := row.Scan(
&p.CallID, &p.UserID, &p.SSRC, &p.JoinDate, &p.ActiveDate, &p.Muted, &p.MutedByAdmin,
&p.VolumeByAdmin, &p.RaiseHandRating, &p.VideoJSON, &p.PresentationJSON, &p.Left, &p.LastCheckDate,
); err != nil {
return domain.GroupCallParticipant{}, err
}
return p, nil
}
func (s *GroupCallStore) begin(ctx context.Context, op string) (pgx.Tx, error) {
beginner, ok := s.db.(txBeginner)
if !ok {
return nil, fmt.Errorf("%s: db does not support transactions", op)
}
tx, err := beginner.Begin(ctx)
if err != nil {
return nil, fmt.Errorf("begin %s: %w", op, err)
}
return tx, nil
}
// lockGroupCallTx 锁定 call 行(串行化参与者变更的唯一入口)。
func lockGroupCallTx(ctx context.Context, tx pgx.Tx, callID int64) (domain.GroupCall, error) {
call, err := scanGroupCall(tx.QueryRow(ctx,
`SELECT `+groupCallColumns+` FROM group_calls WHERE call_id = $1 FOR UPDATE`, callID))
if errors.Is(err, pgx.ErrNoRows) {
return domain.GroupCall{}, domain.ErrGroupCallInvalid
}
if err != nil {
return domain.GroupCall{}, fmt.Errorf("lock group call: %w", err)
}
return call, nil
}
func bumpGroupCallVersionTx(ctx context.Context, tx pgx.Tx, callID int64, countDelta int) (domain.GroupCall, error) {
call, err := scanGroupCall(tx.QueryRow(ctx, `
UPDATE group_calls
SET version = version + 1,
participants_count = GREATEST(0, participants_count + $2)
WHERE call_id = $1
RETURNING `+groupCallColumns, callID, countDelta))
if err != nil {
return domain.GroupCall{}, fmt.Errorf("bump group call version: %w", err)
}
return call, nil
}
func (s *GroupCallStore) CreateGroupCall(ctx context.Context, call domain.GroupCall) (domain.GroupCall, error) {
if call.ID == 0 || call.ChannelID == 0 || call.AccessHash == 0 {
return domain.GroupCall{}, domain.ErrGroupCallInvalid
}
if call.Version <= 0 {
call.Version = 1
}
_, err := s.db.Exec(ctx, `
INSERT INTO group_calls (call_id, access_hash, channel_id, creator_user_id, state, title, join_muted, version, participants_count, created_at)
VALUES ($1, $2, $3, $4, 'active', $5, $6, $7, 0, $8)`,
call.ID, call.AccessHash, call.ChannelID, call.CreatorUserID, call.Title, call.JoinMuted, call.Version, call.CreatedAt)
if err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
if pgErr.ConstraintName == "group_calls_active_channel_uniq" {
return domain.GroupCall{}, domain.ErrGroupCallAlreadyStarted
}
return domain.GroupCall{}, domain.ErrGroupCallInvalid
}
return domain.GroupCall{}, fmt.Errorf("insert group call: %w", err)
}
call.State = domain.GroupCallStateActive
call.ParticipantsCount = 0
return call, nil
}
func (s *GroupCallStore) GetGroupCall(ctx context.Context, callID int64) (domain.GroupCall, bool, error) {
call, err := scanGroupCall(s.db.QueryRow(ctx,
`SELECT `+groupCallColumns+` FROM group_calls WHERE call_id = $1`, callID))
if errors.Is(err, pgx.ErrNoRows) {
return domain.GroupCall{}, false, nil
}
if err != nil {
return domain.GroupCall{}, false, fmt.Errorf("get group call: %w", err)
}
return call, true, nil
}
func (s *GroupCallStore) JoinGroupCall(ctx context.Context, req domain.JoinGroupCallRequest) (domain.GroupCallMutation, error) {
if req.SSRC == 0 {
return domain.GroupCallMutation{}, domain.ErrGroupCallInvalid
}
tx, err := s.begin(ctx, "join group call")
if err != nil {
return domain.GroupCallMutation{}, err
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
call, err := lockGroupCallTx(ctx, tx, req.CallID)
if err != nil {
return domain.GroupCallMutation{}, err
}
if !call.Active() {
return domain.GroupCallMutation{}, domain.ErrGroupCallDiscarded
}
existing, err := scanGroupCallParticipant(tx.QueryRow(ctx,
`SELECT `+groupCallParticipantColumns+` FROM group_call_participants WHERE call_id = $1 AND user_id = $2`,
req.CallID, req.UserID))
wasActive := err == nil && !existing.Left
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return domain.GroupCallMutation{}, fmt.Errorf("load group call participant: %w", err)
}
p := domain.GroupCallParticipant{
CallID: req.CallID,
UserID: req.UserID,
SSRC: req.SSRC,
JoinDate: req.Now,
ActiveDate: req.Now,
LastCheckDate: req.Now,
}
if wasActive {
// 同人换 ssrc 的 rejoin 保留原 join_date列表排序稳定
p.JoinDate = existing.JoinDate
}
if call.JoinMuted && !req.IsAdmin {
p.Muted = true
p.MutedByAdmin = true
}
p.VideoJSON = append([]byte(nil), req.VideoJSON...)
// video_json 整体替换、presentation_json 清空rejoin 后客户端会重发
// joinGroupCallPresentation旧屏幕登记必须作废
if _, err := tx.Exec(ctx, `
INSERT INTO group_call_participants (call_id, user_id, ssrc, join_date, active_date, muted, muted_by_admin, volume_by_admin, raise_hand_rating, video_json, left_call, last_check_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, 0, 0, $8, FALSE, $9)
ON CONFLICT (call_id, user_id) DO UPDATE SET
ssrc = EXCLUDED.ssrc,
join_date = EXCLUDED.join_date,
active_date = EXCLUDED.active_date,
muted = EXCLUDED.muted,
muted_by_admin = EXCLUDED.muted_by_admin,
volume_by_admin = 0,
raise_hand_rating = 0,
video_json = EXCLUDED.video_json,
presentation_json = NULL,
left_call = FALSE,
last_check_date = EXCLUDED.last_check_date`,
req.CallID, req.UserID, req.SSRC, p.JoinDate, p.ActiveDate, p.Muted, p.MutedByAdmin, nullableJSON(p.VideoJSON), p.LastCheckDate); err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
return domain.GroupCallMutation{}, domain.ErrGroupCallSSRCDuplicate
}
return domain.GroupCallMutation{}, fmt.Errorf("upsert group call participant: %w", err)
}
countDelta := 0
if !wasActive {
countDelta = 1
}
call, err = bumpGroupCallVersionTx(ctx, tx, req.CallID, countDelta)
if err != nil {
return domain.GroupCallMutation{}, err
}
if err := tx.Commit(ctx); err != nil {
return domain.GroupCallMutation{}, fmt.Errorf("commit join group call: %w", err)
}
committed = true
return domain.GroupCallMutation{Call: call, Participant: p}, nil
}
func (s *GroupCallStore) LeaveGroupCall(ctx context.Context, callID, userID int64, now int) (domain.GroupCallMutation, error) {
tx, err := s.begin(ctx, "leave group call")
if err != nil {
return domain.GroupCallMutation{}, err
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
if _, err := lockGroupCallTx(ctx, tx, callID); err != nil {
return domain.GroupCallMutation{}, err
}
p, err := scanGroupCallParticipant(tx.QueryRow(ctx, `
UPDATE group_call_participants
SET left_call = TRUE, active_date = $3
WHERE call_id = $1 AND user_id = $2 AND NOT left_call
RETURNING `+groupCallParticipantColumns, callID, userID, now))
if errors.Is(err, pgx.ErrNoRows) {
return domain.GroupCallMutation{}, domain.ErrGroupCallNotJoined
}
if err != nil {
return domain.GroupCallMutation{}, fmt.Errorf("leave group call participant: %w", err)
}
call, err := bumpGroupCallVersionTx(ctx, tx, callID, -1)
if err != nil {
return domain.GroupCallMutation{}, err
}
if err := tx.Commit(ctx); err != nil {
return domain.GroupCallMutation{}, fmt.Errorf("commit leave group call: %w", err)
}
committed = true
return domain.GroupCallMutation{Call: call, Participant: p}, nil
}
func (s *GroupCallStore) DiscardGroupCall(ctx context.Context, callID int64, now int) (domain.GroupCall, []domain.GroupCallParticipant, error) {
tx, err := s.begin(ctx, "discard group call")
if err != nil {
return domain.GroupCall{}, nil, err
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
call, err := lockGroupCallTx(ctx, tx, callID)
if err != nil {
return domain.GroupCall{}, nil, err
}
if !call.Active() {
return domain.GroupCall{}, nil, domain.ErrGroupCallDiscarded
}
rows, err := tx.Query(ctx, `
UPDATE group_call_participants
SET left_call = TRUE, active_date = $2
WHERE call_id = $1 AND NOT left_call
RETURNING `+groupCallParticipantColumns, callID, now)
if err != nil {
return domain.GroupCall{}, nil, fmt.Errorf("clear group call participants: %w", err)
}
var active []domain.GroupCallParticipant
for rows.Next() {
p, err := scanGroupCallParticipant(rows)
if err != nil {
rows.Close()
return domain.GroupCall{}, nil, err
}
active = append(active, p)
}
rows.Close()
if err := rows.Err(); err != nil {
return domain.GroupCall{}, nil, err
}
call, err = scanGroupCall(tx.QueryRow(ctx, `
UPDATE group_calls
SET state = 'discarded', discarded_at = $2, duration = GREATEST(0, $2 - created_at),
participants_count = 0, version = version + 1
WHERE call_id = $1
RETURNING `+groupCallColumns, callID, now))
if err != nil {
return domain.GroupCall{}, nil, fmt.Errorf("discard group call: %w", err)
}
if err := tx.Commit(ctx); err != nil {
return domain.GroupCall{}, nil, fmt.Errorf("commit discard group call: %w", err)
}
committed = true
return call, active, nil
}
func (s *GroupCallStore) TouchParticipant(ctx context.Context, callID, userID int64, now int) ([]int64, bool, error) {
call, found, err := s.GetGroupCall(ctx, callID)
if err != nil {
return nil, false, err
}
if !found {
return nil, false, domain.ErrGroupCallInvalid
}
if !call.Active() {
return nil, false, nil
}
var ssrc int64
err = s.db.QueryRow(ctx, `
UPDATE group_call_participants
SET last_check_date = $3, active_date = $3
WHERE call_id = $1 AND user_id = $2 AND NOT left_call
RETURNING ssrc`, callID, userID, now).Scan(&ssrc)
if errors.Is(err, pgx.ErrNoRows) {
return nil, false, nil
}
if err != nil {
return nil, false, fmt.Errorf("touch group call participant: %w", err)
}
return []int64{ssrc}, true, nil
}
func (s *GroupCallStore) GetParticipant(ctx context.Context, callID, userID int64) (domain.GroupCallParticipant, bool, error) {
p, err := scanGroupCallParticipant(s.db.QueryRow(ctx,
`SELECT `+groupCallParticipantColumns+` FROM group_call_participants WHERE call_id = $1 AND user_id = $2`,
callID, userID))
if errors.Is(err, pgx.ErrNoRows) {
return domain.GroupCallParticipant{}, false, nil
}
if err != nil {
return domain.GroupCallParticipant{}, false, fmt.Errorf("get group call participant: %w", err)
}
return p, true, nil
}
func (s *GroupCallStore) ListParticipants(ctx context.Context, callID int64, offset string, limit int) (domain.GroupCallParticipantPage, error) {
if limit <= 0 || limit > 200 {
limit = 200
}
call, found, err := s.GetGroupCall(ctx, callID)
if err != nil {
return domain.GroupCallParticipantPage{}, err
}
if !found {
return domain.GroupCallParticipantPage{}, domain.ErrGroupCallInvalid
}
page := domain.GroupCallParticipantPage{Version: call.Version}
if err := s.db.QueryRow(ctx,
`SELECT COUNT(*) FROM group_call_participants WHERE call_id = $1 AND NOT left_call`, callID,
).Scan(&page.Count); err != nil {
return domain.GroupCallParticipantPage{}, fmt.Errorf("count group call participants: %w", err)
}
offDate, offUser, hasOffset := parseGroupCallOffset(offset)
query := `SELECT ` + groupCallParticipantColumns + ` FROM group_call_participants
WHERE call_id = $1 AND NOT left_call`
args := []any{callID}
if hasOffset {
query += ` AND (join_date, user_id) > ($2, $3)`
args = append(args, offDate, offUser)
}
query += fmt.Sprintf(` ORDER BY join_date ASC, user_id ASC LIMIT %d`, limit)
rows, err := s.db.Query(ctx, query, args...)
if err != nil {
return domain.GroupCallParticipantPage{}, fmt.Errorf("list group call participants: %w", err)
}
defer rows.Close()
for rows.Next() {
p, err := scanGroupCallParticipant(rows)
if err != nil {
return domain.GroupCallParticipantPage{}, err
}
page.Participants = append(page.Participants, p)
}
if err := rows.Err(); err != nil {
return domain.GroupCallParticipantPage{}, err
}
if n := len(page.Participants); n == limit && n < page.Count {
last := page.Participants[n-1]
page.NextOffset = formatGroupCallOffset(last.JoinDate, last.UserID)
}
return page, nil
}
func (s *GroupCallStore) UpdateParticipant(ctx context.Context, callID, userID int64, update domain.GroupCallParticipantUpdate) (domain.GroupCallMutation, bool, error) {
tx, err := s.begin(ctx, "update group call participant")
if err != nil {
return domain.GroupCallMutation{}, false, err
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback(ctx)
}
}()
call, err := lockGroupCallTx(ctx, tx, callID)
if err != nil {
return domain.GroupCallMutation{}, false, err
}
if !call.Active() {
return domain.GroupCallMutation{}, false, domain.ErrGroupCallDiscarded
}
p, err := scanGroupCallParticipant(tx.QueryRow(ctx,
`SELECT `+groupCallParticipantColumns+` FROM group_call_participants WHERE call_id = $1 AND user_id = $2`,
callID, userID))
if errors.Is(err, pgx.ErrNoRows) {
return domain.GroupCallMutation{}, false, domain.ErrGroupCallNotJoined
}
if err != nil {
return domain.GroupCallMutation{}, false, fmt.Errorf("load group call participant: %w", err)
}
if p.Left {
return domain.GroupCallMutation{}, false, domain.ErrGroupCallNotJoined
}
changed := applyGroupCallParticipantUpdateRow(&p, update)
if !changed {
return domain.GroupCallMutation{Call: call, Participant: p}, false, nil
}
if update.Now > 0 {
p.ActiveDate = update.Now
}
if _, err := tx.Exec(ctx, `
UPDATE group_call_participants
SET muted = $3, muted_by_admin = $4, volume_by_admin = $5, raise_hand_rating = $6,
video_json = $7, presentation_json = $8, active_date = $9
WHERE call_id = $1 AND user_id = $2`,
callID, userID, p.Muted, p.MutedByAdmin, p.VolumeByAdmin, p.RaiseHandRating,
nullableJSON(p.VideoJSON), nullableJSON(p.PresentationJSON), p.ActiveDate); err != nil {
return domain.GroupCallMutation{}, false, fmt.Errorf("update group call participant: %w", err)
}
call, err = bumpGroupCallVersionTx(ctx, tx, callID, 0)
if err != nil {
return domain.GroupCallMutation{}, false, err
}
if err := tx.Commit(ctx); err != nil {
return domain.GroupCallMutation{}, false, fmt.Errorf("commit update group call participant: %w", err)
}
committed = true
return domain.GroupCallMutation{Call: call, Participant: p}, true, nil
}
func (s *GroupCallStore) SetGroupCallTitle(ctx context.Context, callID int64, title string) (domain.GroupCall, bool, error) {
call, err := scanGroupCall(s.db.QueryRow(ctx, `
UPDATE group_calls SET title = $2 WHERE call_id = $1 RETURNING `+groupCallColumns, callID, title))
if errors.Is(err, pgx.ErrNoRows) {
return domain.GroupCall{}, false, domain.ErrGroupCallInvalid
}
if err != nil {
return domain.GroupCall{}, false, fmt.Errorf("set group call title: %w", err)
}
return call, true, nil
}
func (s *GroupCallStore) SetGroupCallJoinMuted(ctx context.Context, callID int64, joinMuted bool) (domain.GroupCall, bool, error) {
call, err := scanGroupCall(s.db.QueryRow(ctx, `
UPDATE group_calls SET join_muted = $2 WHERE call_id = $1 RETURNING `+groupCallColumns, callID, joinMuted))
if errors.Is(err, pgx.ErrNoRows) {
return domain.GroupCall{}, false, domain.ErrGroupCallInvalid
}
if err != nil {
return domain.GroupCall{}, false, fmt.Errorf("set group call join muted: %w", err)
}
return call, true, nil
}
func (s *GroupCallStore) SetStartedMessageID(ctx context.Context, callID int64, msgID int) error {
if _, err := s.db.Exec(ctx,
`UPDATE group_calls SET started_msg_id = $2 WHERE call_id = $1`, callID, msgID); err != nil {
return fmt.Errorf("set group call started msg: %w", err)
}
return nil
}
func (s *GroupCallStore) SweepStaleParticipants(ctx context.Context, checkOlderThan, now, limit int) ([]domain.GroupCallMutation, error) {
if limit <= 0 {
limit = 100
}
rows, err := s.db.Query(ctx, `
SELECT p.call_id, p.user_id
FROM group_call_participants p
JOIN group_calls c ON c.call_id = p.call_id AND c.state = 'active'
WHERE NOT p.left_call AND p.last_check_date < $1
ORDER BY p.call_id, p.user_id
LIMIT $2`, checkOlderThan, limit)
if err != nil {
return nil, fmt.Errorf("list stale group call participants: %w", err)
}
type key struct{ callID, userID int64 }
var stale []key
for rows.Next() {
var k key
if err := rows.Scan(&k.callID, &k.userID); err != nil {
rows.Close()
return nil, err
}
stale = append(stale, k)
}
rows.Close()
if err := rows.Err(); err != nil {
return nil, err
}
var out []domain.GroupCallMutation
for _, k := range stale {
mut, err := s.LeaveGroupCall(ctx, k.callID, k.userID, now)
if errors.Is(err, domain.ErrGroupCallNotJoined) || errors.Is(err, domain.ErrGroupCallInvalid) {
continue // 并发 leave/discard 竞态,无害
}
if err != nil {
return out, err
}
out = append(out, mut)
}
return out, nil
}
func (s *GroupCallStore) ResetAllParticipants(ctx context.Context, now int) ([]domain.GroupCall, error) {
rows, err := s.db.Query(ctx, `
SELECT DISTINCT c.call_id
FROM group_calls c
JOIN group_call_participants p ON p.call_id = c.call_id AND NOT p.left_call
WHERE c.state = 'active'`)
if err != nil {
return nil, fmt.Errorf("list active group calls: %w", err)
}
var callIDs []int64
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
rows.Close()
return nil, err
}
callIDs = append(callIDs, id)
}
rows.Close()
if err := rows.Err(); err != nil {
return nil, err
}
var out []domain.GroupCall
for _, callID := range callIDs {
tx, err := s.begin(ctx, "reset group call participants")
if err != nil {
return out, err
}
if _, err := lockGroupCallTx(ctx, tx, callID); err != nil {
_ = tx.Rollback(ctx)
continue
}
if _, err := tx.Exec(ctx, `
UPDATE group_call_participants SET left_call = TRUE, active_date = $2
WHERE call_id = $1 AND NOT left_call`, callID, now); err != nil {
_ = tx.Rollback(ctx)
return out, fmt.Errorf("reset group call participants: %w", err)
}
call, err := scanGroupCall(tx.QueryRow(ctx, `
UPDATE group_calls SET participants_count = 0, version = version + 1
WHERE call_id = $1 RETURNING `+groupCallColumns, callID))
if err != nil {
_ = tx.Rollback(ctx)
return out, fmt.Errorf("reset group call version: %w", err)
}
if err := tx.Commit(ctx); err != nil {
return out, fmt.Errorf("commit reset group call: %w", err)
}
out = append(out, call)
}
return out, nil
}
// applyGroupCallParticipantUpdateRow 与 memory 实现共享同一字段语义contract test 钉死)。
func applyGroupCallParticipantUpdateRow(p *domain.GroupCallParticipant, u domain.GroupCallParticipantUpdate) bool {
changed := false
if u.Muted != nil && p.Muted != *u.Muted {
p.Muted = *u.Muted
changed = true
}
if u.MutedByAdmin != nil && p.MutedByAdmin != *u.MutedByAdmin {
p.MutedByAdmin = *u.MutedByAdmin
changed = true
}
if u.VolumeByAdmin != nil && p.VolumeByAdmin != *u.VolumeByAdmin {
p.VolumeByAdmin = *u.VolumeByAdmin
changed = true
}
if u.RaiseHandRating != nil && p.RaiseHandRating != *u.RaiseHandRating {
p.RaiseHandRating = *u.RaiseHandRating
changed = true
}
if u.VideoJSON != nil && string(p.VideoJSON) != string(*u.VideoJSON) {
p.VideoJSON = append([]byte(nil), *u.VideoJSON...)
changed = true
}
if u.PresentationJSON != nil && string(p.PresentationJSON) != string(*u.PresentationJSON) {
p.PresentationJSON = append([]byte(nil), *u.PresentationJSON...)
changed = true
}
return changed
}
func nullableJSON(b []byte) any {
if len(b) == 0 {
return nil
}
return b
}
func parseGroupCallOffset(offset string) (joinDate int, userID int64, ok bool) {
if offset == "" {
return 0, 0, false
}
var d int
var u int64
if _, err := fmt.Sscanf(offset, "j:%d:%d", &d, &u); err != nil {
return 0, 0, false
}
return d, u, true
}
func formatGroupCallOffset(joinDate int, userID int64) string {
return fmt.Sprintf("j:%d:%d", joinDate, userID)
}
func (s *GroupCallStore) NextRaiseHandRating(ctx context.Context, callID int64) (int64, error) {
// 全局单调序号用 sequencegroup_calls.version 同步推进保证跨进程单调。
var rating int64
if err := s.db.QueryRow(ctx, `
UPDATE group_calls SET version = version + 1 WHERE call_id = $1 RETURNING version`, callID).Scan(&rating); err != nil {
return 0, fmt.Errorf("next raise hand rating: %w", err)
}
return rating, nil
}
func (s *GroupCallStore) SetParticipantOverride(ctx context.Context, callID, setterUserID, targetUserID int64, override domain.GroupCallParticipantOverride, clear bool) error {
if clear {
if _, err := s.db.Exec(ctx,
`DELETE FROM group_call_participant_overrides WHERE call_id = $1 AND setter_user_id = $2 AND target_user_id = $3`,
callID, setterUserID, targetUserID); err != nil {
return fmt.Errorf("clear participant override: %w", err)
}
return nil
}
if _, err := s.db.Exec(ctx, `
INSERT INTO group_call_participant_overrides (call_id, setter_user_id, target_user_id, muted_by_you, volume)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (call_id, setter_user_id, target_user_id) DO UPDATE SET
muted_by_you = EXCLUDED.muted_by_you,
volume = EXCLUDED.volume`,
callID, setterUserID, targetUserID, override.MutedByYou, override.Volume); err != nil {
return fmt.Errorf("upsert participant override: %w", err)
}
return nil
}
func (s *GroupCallStore) GetParticipantOverride(ctx context.Context, callID, setterUserID, targetUserID int64) (domain.GroupCallParticipantOverride, bool, error) {
var ov domain.GroupCallParticipantOverride
err := s.db.QueryRow(ctx,
`SELECT muted_by_you, volume FROM group_call_participant_overrides WHERE call_id = $1 AND setter_user_id = $2 AND target_user_id = $3`,
callID, setterUserID, targetUserID).Scan(&ov.MutedByYou, &ov.Volume)
if errors.Is(err, pgx.ErrNoRows) {
return domain.GroupCallParticipantOverride{}, false, nil
}
if err != nil {
return domain.GroupCallParticipantOverride{}, false, fmt.Errorf("get participant override: %w", err)
}
return ov, true, nil
}