173 lines
5.2 KiB
Go
173 lines
5.2 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) {
|
|
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)
|
|
}
|