推进活动系统最小成品闭环与游客体验

This commit is contained in:
2026-04-07 19:05:18 +08:00
parent 1a6008449e
commit 6cd16f08dd
102 changed files with 16087 additions and 3556 deletions

View File

@@ -0,0 +1,217 @@
package postgres
import (
"context"
"errors"
"fmt"
"time"
"github.com/jackc/pgx/v5"
)
type OpsUser struct {
ID string
PublicID string
CountryCode string
Mobile string
DisplayName string
Status string
LastLoginAt *time.Time
}
type OpsRole struct {
ID string
RoleCode string
DisplayName string
RoleRank int
}
type CreateOpsUserParams struct {
PublicID string
CountryCode string
Mobile string
DisplayName string
Status string
}
type CreateOpsRefreshTokenParams struct {
OpsUserID string
DeviceKey string
TokenHash string
ExpiresAt time.Time
}
type OpsRefreshTokenRecord struct {
ID string
OpsUserID string
DeviceKey *string
ExpiresAt time.Time
IsRevoked bool
}
func (s *Store) CountOpsUsers(ctx context.Context) (int, error) {
row := s.pool.QueryRow(ctx, `SELECT COUNT(*) FROM ops_users WHERE status <> 'deleted'`)
var count int
if err := row.Scan(&count); err != nil {
return 0, fmt.Errorf("count ops users: %w", err)
}
return count, nil
}
func (s *Store) GetOpsUserByMobile(ctx context.Context, db queryRower, countryCode, mobile string) (*OpsUser, error) {
row := db.QueryRow(ctx, `
SELECT id, ops_user_public_id, country_code, mobile, display_name, status, last_login_at
FROM ops_users
WHERE country_code = $1 AND mobile = $2
LIMIT 1
`, countryCode, mobile)
return scanOpsUser(row)
}
func (s *Store) GetOpsUserByID(ctx context.Context, db queryRower, opsUserID string) (*OpsUser, error) {
row := db.QueryRow(ctx, `
SELECT id, ops_user_public_id, country_code, mobile, display_name, status, last_login_at
FROM ops_users
WHERE id = $1
`, opsUserID)
return scanOpsUser(row)
}
func (s *Store) CreateOpsUser(ctx context.Context, tx Tx, params CreateOpsUserParams) (*OpsUser, error) {
row := tx.QueryRow(ctx, `
INSERT INTO ops_users (ops_user_public_id, country_code, mobile, display_name, status)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, ops_user_public_id, country_code, mobile, display_name, status, last_login_at
`, params.PublicID, params.CountryCode, params.Mobile, params.DisplayName, params.Status)
return scanOpsUser(row)
}
func (s *Store) TouchOpsUserLogin(ctx context.Context, tx Tx, opsUserID string) error {
_, err := tx.Exec(ctx, `
UPDATE ops_users
SET last_login_at = NOW()
WHERE id = $1
`, opsUserID)
if err != nil {
return fmt.Errorf("touch ops user last login: %w", err)
}
return nil
}
func (s *Store) GetOpsRoleByCode(ctx context.Context, tx Tx, roleCode string) (*OpsRole, error) {
row := tx.QueryRow(ctx, `
SELECT id, role_code, display_name, role_rank
FROM ops_roles
WHERE role_code = $1
AND status = 'active'
LIMIT 1
`, roleCode)
return scanOpsRole(row)
}
func (s *Store) AssignOpsRole(ctx context.Context, tx Tx, opsUserID, roleID string) error {
_, err := tx.Exec(ctx, `
INSERT INTO ops_user_roles (ops_user_id, ops_role_id, status)
VALUES ($1, $2, 'active')
ON CONFLICT (ops_user_id, ops_role_id) DO NOTHING
`, opsUserID, roleID)
if err != nil {
return fmt.Errorf("assign ops role: %w", err)
}
return nil
}
func (s *Store) GetPrimaryOpsRole(ctx context.Context, db queryRower, opsUserID string) (*OpsRole, error) {
row := db.QueryRow(ctx, `
SELECT r.id, r.role_code, r.display_name, r.role_rank
FROM ops_user_roles ur
JOIN ops_roles r ON r.id = ur.ops_role_id
WHERE ur.ops_user_id = $1
AND ur.status = 'active'
AND r.status = 'active'
ORDER BY r.role_rank DESC, r.created_at ASC
LIMIT 1
`, opsUserID)
return scanOpsRole(row)
}
func (s *Store) CreateOpsRefreshToken(ctx context.Context, tx Tx, params CreateOpsRefreshTokenParams) (string, error) {
row := tx.QueryRow(ctx, `
INSERT INTO ops_refresh_tokens (ops_user_id, device_key, token_hash, expires_at)
VALUES ($1, NULLIF($2, ''), $3, $4)
RETURNING id
`, params.OpsUserID, params.DeviceKey, params.TokenHash, params.ExpiresAt)
var id string
if err := row.Scan(&id); err != nil {
return "", fmt.Errorf("create ops refresh token: %w", err)
}
return id, nil
}
func (s *Store) GetOpsRefreshTokenForUpdate(ctx context.Context, tx Tx, tokenHash string) (*OpsRefreshTokenRecord, error) {
row := tx.QueryRow(ctx, `
SELECT id, ops_user_id, device_key, expires_at, revoked_at IS NOT NULL
FROM ops_refresh_tokens
WHERE token_hash = $1
FOR UPDATE
`, tokenHash)
var record OpsRefreshTokenRecord
err := row.Scan(&record.ID, &record.OpsUserID, &record.DeviceKey, &record.ExpiresAt, &record.IsRevoked)
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("query ops refresh token for update: %w", err)
}
return &record, nil
}
func (s *Store) RotateOpsRefreshToken(ctx context.Context, tx Tx, oldTokenID, newTokenID string) error {
_, err := tx.Exec(ctx, `
UPDATE ops_refresh_tokens
SET revoked_at = NOW(), replaced_by_token_id = $2
WHERE id = $1
`, oldTokenID, newTokenID)
if err != nil {
return fmt.Errorf("rotate ops refresh token: %w", err)
}
return nil
}
func (s *Store) RevokeOpsRefreshToken(ctx context.Context, tokenHash string) error {
_, err := s.pool.Exec(ctx, `
UPDATE ops_refresh_tokens
SET revoked_at = COALESCE(revoked_at, NOW())
WHERE token_hash = $1
`, tokenHash)
if err != nil {
return fmt.Errorf("revoke ops refresh token: %w", err)
}
return nil
}
func scanOpsUser(row pgx.Row) (*OpsUser, error) {
var item OpsUser
err := row.Scan(&item.ID, &item.PublicID, &item.CountryCode, &item.Mobile, &item.DisplayName, &item.Status, &item.LastLoginAt)
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("scan ops user: %w", err)
}
return &item, nil
}
func scanOpsRole(row pgx.Row) (*OpsRole, error) {
var item OpsRole
err := row.Scan(&item.ID, &item.RoleCode, &item.DisplayName, &item.RoleRank)
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("scan ops role: %w", err)
}
return &item, nil
}