feat: 基础框架搭建完毕
Checks / verify (push) Failing after 2m37s

This commit is contained in:
2026-10-07 09:27:47 +08:00
parent ba6dfffab7
commit bffdb3858b
64 changed files with 326 additions and 332 deletions
+86
View File
@@ -0,0 +1,86 @@
package repository
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
"git.sundynix.cn/Blizzard/Veyra/internal/model"
)
type AccountRepository struct{ pool *pgxpool.Pool }
func NewAccountRepository(pool *pgxpool.Pool) *AccountRepository {
return &AccountRepository{pool: pool}
}
func (r *AccountRepository) Create(ctx context.Context, a model.Account, entry *model.CashEntry) (model.AccountSummary, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return model.AccountSummary{}, apperror.ErrUnavailable
}
defer tx.Rollback(context.Background())
// Lock the owner so account creation cannot race with disabling the user.
var status string
if err := tx.QueryRow(ctx, `SELECT status FROM veyra_user WHERE id=$1 AND role='USER' FOR UPDATE`, a.UserID).Scan(&status); err != nil {
return model.AccountSummary{}, mapError(err)
}
if status != "ACTIVE" {
return model.AccountSummary{}, apperror.ErrForbidden
}
err = tx.QueryRow(ctx, `INSERT INTO veyra_account(id,user_id,mode,name,currency,ledger_status) VALUES($1,$2,$3,$4,$5,$6) RETURNING created_at`, a.ID, a.UserID, a.Mode, a.Name, a.Currency, a.LedgerStatus).Scan(&a.CreatedAt)
if err != nil {
return model.AccountSummary{}, mapError(err)
}
var balance *string
if entry != nil {
if _, err := tx.Exec(ctx, `INSERT INTO veyra_cash_entry(id,account_id,entry_type,amount_minor) VALUES($1,$2,$3,$4)`, entry.ID, entry.AccountID, entry.EntryType, entry.AmountMinor); err != nil {
return model.AccountSummary{}, mapError(err)
}
value := model.FormatMoney(entry.AmountMinor)
balance = &value
}
if err := tx.Commit(ctx); err != nil {
return model.AccountSummary{}, apperror.ErrUnavailable
}
return model.AccountSummary{Account: a, CashBalance: balance}, nil
}
const summarySelect = `SELECT a.id,a.user_id,a.mode,a.name,a.currency,a.ledger_status,a.created_at,CASE WHEN a.ledger_status='COMPLETE' THEN (SELECT COALESCE(sum(e.amount_minor),0)::bigint FROM veyra_cash_entry e WHERE e.account_id=a.id) ELSE NULL END FROM veyra_account a`
func scanSummary(row pgx.Row) (model.AccountSummary, error) {
var s model.AccountSummary
var minor *int64
err := row.Scan(&s.ID, &s.UserID, &s.Mode, &s.Name, &s.Currency, &s.LedgerStatus, &s.CreatedAt, &minor)
if err != nil {
return s, mapError(err)
}
if minor != nil {
value := model.FormatMoney(*minor)
s.CashBalance = &value
}
return s, nil
}
func (r *AccountRepository) Get(ctx context.Context, userID, accountID string) (model.AccountSummary, error) {
return scanSummary(r.pool.QueryRow(ctx, summarySelect+` WHERE a.user_id=$1 AND a.id=$2`, userID, accountID))
}
func (r *AccountRepository) List(ctx context.Context, userID string) ([]model.AccountSummary, error) {
rows, err := r.pool.Query(ctx, summarySelect+` WHERE a.user_id=$1 ORDER BY a.created_at,a.id`, userID)
if err != nil {
return nil, apperror.ErrUnavailable
}
defer rows.Close()
list := make([]model.AccountSummary, 0)
for rows.Next() {
s, err := scanSummary(rows)
if err != nil {
return nil, err
}
list = append(list, s)
}
return list, mapError(rows.Err())
}
+24
View File
@@ -0,0 +1,24 @@
package repository
import (
"errors"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
)
func mapError(err error) error {
if err == nil {
return nil
}
if errors.Is(err, pgx.ErrNoRows) {
return apperror.ErrNotFound
}
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
return apperror.ErrConflict
}
return apperror.ErrUnavailable
}
+82
View File
@@ -0,0 +1,82 @@
package repository
import (
"context"
"errors"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
"git.sundynix.cn/Blizzard/Veyra/internal/model"
)
type IdentityRepository struct{ pool *pgxpool.Pool }
func NewIdentityRepository(pool *pgxpool.Pool) *IdentityRepository {
return &IdentityRepository{pool: pool}
}
func (r *IdentityRepository) CreateUser(ctx context.Context, user model.User, credential model.Credential) (model.User, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return model.User{}, apperror.ErrUnavailable
}
defer tx.Rollback(context.Background())
err = tx.QueryRow(ctx, `INSERT INTO veyra_user(id,display_name,status,role) VALUES($1,$2,$3,$4) RETURNING created_at,updated_at`, user.ID, user.DisplayName, user.Status, user.Role).Scan(&user.CreatedAt, &user.UpdatedAt)
if err != nil {
return model.User{}, mapError(err)
}
_, err = tx.Exec(ctx, `INSERT INTO veyra_credential(id,user_id,login_name,password_hash) VALUES($1,$2,$3,$4)`, credential.ID, credential.UserID, credential.LoginName, credential.PasswordHash)
if err != nil {
return model.User{}, mapError(err)
}
if err = tx.Commit(ctx); err != nil {
return model.User{}, apperror.ErrUnavailable
}
return user, nil
}
func (r *IdentityRepository) FindCredential(ctx context.Context, login string) (model.User, model.Credential, error) {
var u model.User
var c model.Credential
err := r.pool.QueryRow(ctx, `SELECT u.id,u.display_name,u.status,u.role,u.created_at,u.updated_at,c.id,c.user_id,c.login_name,c.password_hash,c.created_at FROM veyra_user u JOIN veyra_credential c ON c.user_id=u.id WHERE c.login_name=$1`, login).Scan(&u.ID, &u.DisplayName, &u.Status, &u.Role, &u.CreatedAt, &u.UpdatedAt, &c.ID, &c.UserID, &c.LoginName, &c.PasswordHash, &c.CreatedAt)
return u, c, mapError(err)
}
func (r *IdentityRepository) CreateSession(ctx context.Context, s model.Session) error {
_, err := r.pool.Exec(ctx, `INSERT INTO veyra_session(id,user_id,token_hash,audience,expires_at) VALUES($1,$2,$3,$4,$5)`, s.ID, s.UserID, s.TokenHash, s.Audience, s.ExpiresAt)
return mapError(err)
}
func (r *IdentityRepository) FindSessionUser(ctx context.Context, hash, audience string, now time.Time) (model.User, error) {
var u model.User
err := r.pool.QueryRow(ctx, `SELECT u.id,u.display_name,u.status,u.role,u.created_at,u.updated_at FROM veyra_session s JOIN veyra_user u ON u.id=s.user_id WHERE s.token_hash=$1 AND s.audience=$2 AND s.expires_at>$3 AND s.revoked_at IS NULL AND u.status='ACTIVE' AND (($2='admin' AND u.role='ADMIN') OR ($2='app' AND u.role='USER'))`, hash, audience, now).Scan(&u.ID, &u.DisplayName, &u.Status, &u.Role, &u.CreatedAt, &u.UpdatedAt)
if errors.Is(err, pgx.ErrNoRows) {
return model.User{}, apperror.ErrUnauthenticated
}
return u, mapError(err)
}
func (r *IdentityRepository) RevokeSession(ctx context.Context, hash, audience string, now time.Time) error {
_, err := r.pool.Exec(ctx, `UPDATE veyra_session SET revoked_at=$3 WHERE token_hash=$1 AND audience=$2 AND revoked_at IS NULL`, hash, audience, now)
return mapError(err)
}
func (r *IdentityRepository) ListUsers(ctx context.Context, limit int) ([]model.User, error) {
rows, err := r.pool.Query(ctx, `SELECT id,display_name,status,role,created_at,updated_at FROM veyra_user WHERE role='USER' ORDER BY created_at DESC,id DESC LIMIT $1`, limit)
if err != nil {
return nil, apperror.ErrUnavailable
}
defer rows.Close()
users := make([]model.User, 0)
for rows.Next() {
var u model.User
if err := rows.Scan(&u.ID, &u.DisplayName, &u.Status, &u.Role, &u.CreatedAt, &u.UpdatedAt); err != nil {
return nil, apperror.ErrUnavailable
}
users = append(users, u)
}
return users, mapError(rows.Err())
}