234 lines
6.4 KiB
Go
234 lines
6.4 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgerrcode"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
|
|
"telesrv/internal/domain"
|
|
"telesrv/internal/store/postgres/sqlcgen"
|
|
)
|
|
|
|
// UserStore 用 PostgreSQL 实现 store.UserStore。
|
|
type UserStore struct {
|
|
q *sqlcgen.Queries
|
|
}
|
|
|
|
// NewUserStore 基于 pgx 连接池(或事务)创建 UserStore。
|
|
func NewUserStore(db sqlcgen.DBTX) *UserStore {
|
|
return &UserStore{q: sqlcgen.New(db)}
|
|
}
|
|
|
|
func (s *UserStore) ByID(ctx context.Context, id int64) (domain.User, bool, error) {
|
|
row, err := s.q.GetUserByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.User{}, false, nil
|
|
}
|
|
return domain.User{}, false, fmt.Errorf("get user by id: %w", err)
|
|
}
|
|
return userFromModel(row), true, nil
|
|
}
|
|
|
|
func (s *UserStore) ByIDs(ctx context.Context, ids []int64) ([]domain.User, error) {
|
|
if len(ids) == 0 {
|
|
return nil, nil
|
|
}
|
|
rows, err := s.q.GetUsersByIDs(ctx, ids)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get users by ids: %w", err)
|
|
}
|
|
out := make([]domain.User, 0, len(rows))
|
|
for _, row := range rows {
|
|
out = append(out, userFromModel(row))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *UserStore) ByPhone(ctx context.Context, phone string) (domain.User, bool, error) {
|
|
row, err := s.q.GetUserByPhone(ctx, phone)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.User{}, false, nil
|
|
}
|
|
return domain.User{}, false, fmt.Errorf("get user by phone: %w", err)
|
|
}
|
|
return userFromModel(row), true, nil
|
|
}
|
|
|
|
func (s *UserStore) ByPhones(ctx context.Context, phones []string) ([]domain.User, error) {
|
|
if len(phones) == 0 {
|
|
return nil, nil
|
|
}
|
|
rows, err := s.q.GetUsersByPhones(ctx, phones)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get users by phones: %w", err)
|
|
}
|
|
out := make([]domain.User, 0, len(rows))
|
|
for _, row := range rows {
|
|
out = append(out, userFromModel(row))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *UserStore) ByUsername(ctx context.Context, username string) (domain.User, bool, error) {
|
|
username = strings.TrimSpace(strings.TrimPrefix(username, "@"))
|
|
if username == "" {
|
|
return domain.User{}, false, nil
|
|
}
|
|
row, err := s.q.GetUserByUsername(ctx, username)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.User{}, false, nil
|
|
}
|
|
return domain.User{}, false, fmt.Errorf("get user by username: %w", err)
|
|
}
|
|
return userFromModel(row), true, nil
|
|
}
|
|
|
|
func (s *UserStore) Search(ctx context.Context, currentUserID int64, query, phoneQuery string, limit int) (domain.UserSearchResult, error) {
|
|
query = strings.ToLower(strings.TrimSpace(query))
|
|
if currentUserID == 0 || query == "" {
|
|
return domain.UserSearchResult{}, nil
|
|
}
|
|
if limit <= 0 || limit > 50 {
|
|
limit = 50
|
|
}
|
|
rows, err := s.q.SearchUsers(ctx, sqlcgen.SearchUsersParams{
|
|
CurrentUserID: currentUserID,
|
|
QueryLower: query,
|
|
QueryLike: escapeLike(query),
|
|
PhoneQuery: phoneQuery,
|
|
LimitCount: int32(limit),
|
|
})
|
|
if err != nil {
|
|
return domain.UserSearchResult{}, fmt.Errorf("search users: %w", err)
|
|
}
|
|
out := domain.UserSearchResult{
|
|
MyResults: make([]domain.User, 0, len(rows)),
|
|
Results: make([]domain.User, 0, len(rows)),
|
|
}
|
|
for _, row := range rows {
|
|
u := domain.User{
|
|
ID: row.ID,
|
|
AccessHash: row.AccessHash,
|
|
Phone: row.Phone,
|
|
FirstName: row.FirstName,
|
|
LastName: row.LastName,
|
|
About: row.About,
|
|
Username: row.Username,
|
|
CountryCode: row.CountryCode,
|
|
Verified: row.Verified,
|
|
Support: row.Support,
|
|
LastSeenAt: int(row.LastSeenAt),
|
|
Contact: row.Contact,
|
|
Mutual: row.Mutual,
|
|
}
|
|
if row.Contact {
|
|
out.MyResults = append(out.MyResults, u)
|
|
} else {
|
|
out.Results = append(out.Results, u)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *UserStore) UpdateProfile(ctx context.Context, userID int64, firstName, lastName, about string) (domain.User, error) {
|
|
row, err := s.q.UpdateUserProfile(ctx, sqlcgen.UpdateUserProfileParams{
|
|
ID: userID,
|
|
FirstName: firstName,
|
|
LastName: lastName,
|
|
About: about,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.User{}, domain.ErrFirstNameInvalid
|
|
}
|
|
return domain.User{}, fmt.Errorf("update user profile: %w", err)
|
|
}
|
|
return userFromModel(row), nil
|
|
}
|
|
|
|
func (s *UserStore) UpdateUsername(ctx context.Context, userID int64, username string) (domain.User, error) {
|
|
username = strings.TrimSpace(strings.TrimPrefix(username, "@"))
|
|
row, err := s.q.UpdateUserUsername(ctx, sqlcgen.UpdateUserUsernameParams{
|
|
ID: userID,
|
|
Username: username,
|
|
})
|
|
if err != nil {
|
|
var pgErr *pgconn.PgError
|
|
if errors.As(err, &pgErr) && pgErr.Code == pgerrcode.UniqueViolation && pgErr.ConstraintName == "users_username_lower_unique_idx" {
|
|
return domain.User{}, domain.ErrUsernameOccupied
|
|
}
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.User{}, domain.ErrUsernameNotOccupied
|
|
}
|
|
return domain.User{}, fmt.Errorf("update user username: %w", err)
|
|
}
|
|
return userFromModel(row), nil
|
|
}
|
|
|
|
func (s *UserStore) UpdateLastSeen(ctx context.Context, userID int64, lastSeenAt int) error {
|
|
if lastSeenAt <= 0 {
|
|
return nil
|
|
}
|
|
if err := s.q.UpdateUserLastSeen(ctx, sqlcgen.UpdateUserLastSeenParams{
|
|
ID: userID,
|
|
LastSeenAt: int64(lastSeenAt),
|
|
}); err != nil {
|
|
return fmt.Errorf("update user last seen: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *UserStore) Create(ctx context.Context, u domain.User) (domain.User, error) {
|
|
row, err := s.q.CreateUser(ctx, sqlcgen.CreateUserParams{
|
|
AccessHash: u.AccessHash,
|
|
Phone: u.Phone,
|
|
FirstName: u.FirstName,
|
|
LastName: u.LastName,
|
|
Username: u.Username,
|
|
CountryCode: u.CountryCode,
|
|
})
|
|
if err != nil {
|
|
var pgErr *pgconn.PgError
|
|
if errors.As(err, &pgErr) && pgErr.Code == pgerrcode.UniqueViolation && pgErr.ConstraintName == "users_username_lower_unique_idx" {
|
|
return domain.User{}, domain.ErrUsernameOccupied
|
|
}
|
|
return domain.User{}, fmt.Errorf("create user: %w", err)
|
|
}
|
|
return userFromModel(row), nil
|
|
}
|
|
|
|
func escapeLike(s string) string {
|
|
var b strings.Builder
|
|
b.Grow(len(s))
|
|
for _, r := range s {
|
|
if r == '%' || r == '_' || r == '\\' {
|
|
b.WriteRune('\\')
|
|
}
|
|
b.WriteRune(r)
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func userFromModel(r sqlcgen.User) domain.User {
|
|
return domain.User{
|
|
ID: r.ID,
|
|
AccessHash: r.AccessHash,
|
|
Phone: r.Phone,
|
|
FirstName: r.FirstName,
|
|
LastName: r.LastName,
|
|
About: r.About,
|
|
Username: r.Username,
|
|
CountryCode: r.CountryCode,
|
|
Verified: r.Verified,
|
|
Support: r.Support,
|
|
LastSeenAt: int(r.LastSeenAt),
|
|
}
|
|
}
|