Files
olons_server/internal/store/accounts.go
T

176 lines
5.3 KiB
Go

package store
import (
"context"
"database/sql"
"errors"
"strconv"
"strings"
"time"
"github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
const (
memberStatusActive = 1
memberCategoryReal = 1
)
type AccountRegistration struct {
TenantID int64
MemberNo string
Username string
Mobile string
PasswordHash string
ReferralCode string
InviteCode string
}
type Account struct {
TenantID int64 `db:"tenant_id"`
MemberID int64 `db:"member_id"`
MemberNo string `db:"member_no"`
Username string `db:"username"`
Mobile string `db:"mobile"`
PasswordHash string `db:"password_hash"`
InviteCode string `db:"invite_code"`
MemberStatus int `db:"member_status"`
MemberCategory int `db:"member_category"`
}
func (s *Store) RegisterAccount(ctx context.Context, input AccountRegistration) (*Account, error) {
if s == nil || s.db == nil {
return nil, errors.New("store is unavailable")
}
tx, err := s.db.BeginTxx(ctx, nil)
if err != nil {
return nil, err
}
defer tx.Rollback()
parentID, teamLevel, tree, err := referralParent(ctx, tx, input.TenantID, input.ReferralCode)
if err != nil {
return nil, err
}
levelID, err := defaultMemberLevel(ctx, tx, input.TenantID)
if err != nil {
return nil, err
}
result, err := tx.ExecContext(ctx, `INSERT INTO hg_app_member
(tenant_id,member_no,username,mobile,password_hash,
level_id,parent_id,level,tree,invite_code,member_status,risk_status,auto_dispatch,
credit_score,is_test_account,created_by,updated_by)
VALUES(?,?,?,?,?,?,?,?,?,?,1,1,1,100,2,0,0)`,
input.TenantID, input.MemberNo, input.Username, input.Mobile, input.PasswordHash,
levelID, parentID, teamLevel, tree, input.InviteCode)
if err != nil {
if isDuplicate(err) {
if strings.Contains(err.Error(), "uk_app_member_invite_code") {
return nil, ErrInviteCodeExists
}
return nil, ErrAccountExists
}
return nil, err
}
memberID, err := result.LastInsertId()
if err != nil {
return nil, err
}
if _, err = tx.ExecContext(ctx, `INSERT INTO hg_app_member_profile
(tenant_id,member_id,created_by,updated_by) VALUES(?,?,0,0)`, input.TenantID, memberID); err != nil {
return nil, err
}
if _, err = tx.ExecContext(ctx, `INSERT INTO hg_wallet_account
(tenant_id,member_id,currency,available_amount,frozen_amount,total_income,total_expense,account_status,version)
VALUES(?,?,'CNY',0,0,0,0,1,0)`, input.TenantID, memberID); err != nil {
return nil, err
}
if err = tx.Commit(); err != nil {
return nil, err
}
return &Account{TenantID: input.TenantID, MemberID: memberID, MemberNo: input.MemberNo,
Username: input.Username, Mobile: input.Mobile,
InviteCode: input.InviteCode, MemberStatus: memberStatusActive, MemberCategory: memberCategoryReal}, nil
}
func (s *Store) AccountByMobile(ctx context.Context, tenantID int64, mobile string) (*Account, error) {
if s == nil || s.db == nil {
return nil, errors.New("store is unavailable")
}
return accountByMobile(ctx, s.db, tenantID, mobile)
}
func (s *Store) RecordLogin(ctx context.Context, tenantID, memberID int64) error {
_, err := s.db.ExecContext(ctx, `UPDATE hg_app_member SET last_login_at=?,updated_at=?
WHERE tenant_id=? AND id=? AND deleted_at IS NULL`, time.Now().UTC(), time.Now().UTC(), tenantID, memberID)
return err
}
type queryer interface {
GetContext(context.Context, any, string, ...any) error
}
func accountByMobile(ctx context.Context, db queryer, tenantID int64, mobile string) (*Account, error) {
var row Account
err := db.GetContext(ctx, &row, `SELECT tenant_id, id AS member_id, member_no, username, mobile,
password_hash, invite_code, member_status, risk_status AS member_category
FROM hg_app_member WHERE tenant_id=? AND mobile=? AND deleted_at IS NULL LIMIT 1`, tenantID, mobile)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrInvalidCredentials
}
if err != nil {
return nil, err
}
if row.MemberStatus != memberStatusActive {
return nil, ErrInvalidCredentials
}
return &row, nil
}
func referralParent(ctx context.Context, tx *sqlx.Tx, tenantID int64, code string) (int64, int, string, error) {
code = strings.TrimSpace(code)
if code == "" {
return 0, 0, "", ErrReferralInvalid
}
var parent struct {
ID int64 `db:"id"`
Level int `db:"level"`
Tree string `db:"tree"`
}
err := tx.GetContext(ctx, &parent, `SELECT id,level,tree FROM hg_app_member
WHERE tenant_id=? AND invite_code=? AND member_status=1
AND deleted_at IS NULL LIMIT 1 FOR UPDATE`, tenantID, code)
if errors.Is(err, sql.ErrNoRows) {
return 0, 0, "", ErrReferralInvalid
}
if err != nil {
return 0, 0, "", err
}
tree := strings.Trim(strings.TrimSpace(parent.Tree)+","+formatInt64(parent.ID), ",")
if len(tree) > 512 {
return 0, 0, "", ErrReferralInvalid
}
return parent.ID, parent.Level + 1, tree, nil
}
func defaultMemberLevel(ctx context.Context, tx *sqlx.Tx, tenantID int64) (int64, error) {
var id int64
err := tx.GetContext(ctx, &id, `SELECT id FROM hg_app_member_level
WHERE tenant_id=? AND status=1 AND deleted_at IS NULL ORDER BY sort ASC,id ASC LIMIT 1`, tenantID)
if errors.Is(err, sql.ErrNoRows) {
return 0, nil
}
return id, err
}
func isDuplicate(err error) bool {
var mysqlErr *mysql.MySQLError
return errors.As(err, &mysqlErr) && mysqlErr.Number == 1062
}
func formatInt64(value int64) string {
return strconv.FormatInt(value, 10)
}