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, mobile string) (*Account, error) { if s == nil || s.db == nil { return nil, errors.New("store is unavailable") } return accountByMobile(ctx, s.db, 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, 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 mobile=? AND deleted_at IS NULL LIMIT 1`, 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) }