feat: improve member payment and order flows
This commit is contained in:
@@ -12,6 +12,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.118im.com/im_team/olons_server/internal/config"
|
||||
"git.118im.com/im_team/olons_server/internal/store"
|
||||
@@ -36,7 +38,6 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
usernamePattern = regexp.MustCompile(`^[a-z0-9]{1,10}$`)
|
||||
phonePattern = regexp.MustCompile(`^1[3-9][0-9]{9}$`)
|
||||
referralPattern = regexp.MustCompile(`^[A-Za-z0-9]{1,32}$`)
|
||||
tradePasswordPattern = regexp.MustCompile(`^[0-9]{6}$`)
|
||||
@@ -142,10 +143,10 @@ func (s *Service) Register(ctx context.Context, input RegisterInput) (*Result, e
|
||||
if !s.ready {
|
||||
return nil, ErrUnavailable
|
||||
}
|
||||
input.Username = strings.ToLower(strings.TrimSpace(input.Username))
|
||||
input.Username = strings.TrimSpace(input.Username)
|
||||
input.Phone = strings.TrimSpace(input.Phone)
|
||||
input.ReferralCode = strings.TrimSpace(input.ReferralCode)
|
||||
if !usernamePattern.MatchString(input.Username) {
|
||||
if !validMemberName(input.Username) {
|
||||
return nil, ErrUsernameInvalid
|
||||
}
|
||||
if !phonePattern.MatchString(input.Phone) {
|
||||
@@ -191,6 +192,19 @@ func (s *Service) Register(ctx context.Context, input RegisterInput) (*Result, e
|
||||
return s.issueSession(ctx, account, input.DeviceID)
|
||||
}
|
||||
|
||||
func validMemberName(value string) bool {
|
||||
count := utf8.RuneCountInString(value)
|
||||
if count < 1 || count > 10 {
|
||||
return false
|
||||
}
|
||||
for _, r := range value {
|
||||
if !unicode.IsLetter(r) && !unicode.IsMark(r) && r != '·' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Service) Login(ctx context.Context, input LoginInput) (*Result, error) {
|
||||
if !s.ready {
|
||||
return nil, ErrUnavailable
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestRegisterValidation(t *testing.T) {
|
||||
input RegisterInput
|
||||
want error
|
||||
}{
|
||||
{name: "username", input: RegisterInput{Username: "中文", Phone: "13800138000", Password: "secret1", PrivacyAccepted: true}, want: ErrUsernameInvalid},
|
||||
{name: "username", input: RegisterInput{Username: "张 三", Phone: "13800138000", Password: "secret1", PrivacyAccepted: true}, want: ErrUsernameInvalid},
|
||||
{name: "phone", input: RegisterInput{Username: "alice", Phone: "12800138000", Password: "secret1", PrivacyAccepted: true}, want: ErrPhoneInvalid},
|
||||
{name: "password", input: RegisterInput{Username: "alice", Phone: "13800138000", Password: "123", PrivacyAccepted: true}, want: ErrPasswordInvalid},
|
||||
{name: "referral", input: RegisterInput{Username: "alice", Phone: "13800138000", Password: "secret1", ReferralCode: "bad!", PrivacyAccepted: true}, want: ErrReferralInvalid},
|
||||
@@ -59,6 +59,19 @@ func TestRegisterValidation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidMemberNameSupportsChinese(t *testing.T) {
|
||||
for _, name := range []string{"杨情", "欧阳娜娜", "Alicia", "阿依古丽·买买提"} {
|
||||
if !validMemberName(name) {
|
||||
t.Errorf("validMemberName(%q) = false, want true", name)
|
||||
}
|
||||
}
|
||||
for _, name := range []string{"", "张 三", "user01", "这是一个超过十个字符长度的名字"} {
|
||||
if validMemberName(name) {
|
||||
t.Errorf("validMemberName(%q) = true, want false", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginRateKeyDoesNotExposePhone(t *testing.T) {
|
||||
s := &Service{}
|
||||
key := s.loginRateKey("13800138000")
|
||||
|
||||
@@ -70,6 +70,7 @@ type PaymentAccountInput struct {
|
||||
HolderName string `json:"holder_name"`
|
||||
IdentityNo string `json:"identity_no"`
|
||||
AccountNo string `json:"account_no"`
|
||||
DigitalRMBCode string `json:"digital_rmb_code"`
|
||||
WechatQRAttachmentID int64 `json:"wechat_qr_attachment_id"`
|
||||
AlipayQRAttachmentID int64 `json:"alipay_qr_attachment_id"`
|
||||
}
|
||||
@@ -82,6 +83,7 @@ type PaymentAccount struct {
|
||||
HolderMask string `json:"holder_mask"`
|
||||
IdentityNoMask string `json:"identity_no_mask"`
|
||||
AccountNoMask string `json:"account_no_mask"`
|
||||
DigitalRMBCodeMask string `json:"digital_rmb_code_mask"`
|
||||
WechatQRAttachmentID int64 `json:"wechat_qr_attachment_id"`
|
||||
AlipayQRAttachmentID int64 `json:"alipay_qr_attachment_id"`
|
||||
VerifyStatus string `json:"verify_status"`
|
||||
@@ -299,12 +301,18 @@ func (s *Service) CreatePaymentAccount(ctx context.Context, tenantID, memberID i
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
digitalRMBCodeCipher, err := s.encrypt(input.DigitalRMBCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row, err := s.repository.CreatePaymentAccount(ctx, store.PaymentAccountCreation{
|
||||
TenantID: tenantID, MemberID: memberID, AccountType: 1, BankCode: "BANK", BankName: input.BankName,
|
||||
HolderCipher: holderCipher, HolderMask: maskName(input.HolderName),
|
||||
IdentityNoCipher: identityNoCipher, IdentityNoHash: s.lookupHash(input.IdentityNo),
|
||||
IdentityNoMask: maskIdentityNo(input.IdentityNo), AccountNoCipher: accountCipher,
|
||||
AccountNoHash: s.lookupHash(input.AccountNo), AccountNoMask: maskAccount(input.AccountNo),
|
||||
DigitalRMBCodeCipher: digitalRMBCodeCipher, DigitalRMBCodeHash: s.lookupHash(input.DigitalRMBCode),
|
||||
DigitalRMBCodeMask: maskAccount(input.DigitalRMBCode),
|
||||
WechatQRAttachmentID: input.WechatQRAttachmentID,
|
||||
AlipayQRAttachmentID: input.AlipayQRAttachmentID,
|
||||
})
|
||||
@@ -360,10 +368,18 @@ func (s *Service) CreateWithdrawal(ctx context.Context, tenantID, memberID int64
|
||||
return nil, store.ErrPaymentAccountInvalid
|
||||
}
|
||||
}
|
||||
digitalRMBCode := ""
|
||||
if account.DigitalRMBCodeCipher != "" {
|
||||
digitalRMBCode, err = s.decrypt(account.DigitalRMBCodeCipher)
|
||||
if err != nil {
|
||||
return nil, store.ErrPaymentAccountInvalid
|
||||
}
|
||||
}
|
||||
snapshot, err := json.Marshal(map[string]any{
|
||||
"account_type": accountTypeName(account.AccountType), "bank_code": account.BankCode,
|
||||
"bank_name": account.BankName, "holder_name": holderName,
|
||||
"identity_no": identityNo, "account_no": accountNo,
|
||||
"digital_rmb_code": digitalRMBCode,
|
||||
"wechat_qr_attachment_id": account.WechatQRAttachmentID,
|
||||
"alipay_qr_attachment_id": account.AlipayQRAttachmentID,
|
||||
})
|
||||
@@ -382,6 +398,8 @@ func (s *Service) CreateWithdrawal(ctx context.Context, tenantID, memberID int64
|
||||
ExpectedBankName: account.BankName, ExpectedHolderCipher: account.HolderCipher,
|
||||
ExpectedIdentityNoCipher: account.IdentityNoCipher, ExpectedIdentityNoHash: account.IdentityNoHash,
|
||||
ExpectedAccountNoCipher: account.AccountNoCipher, ExpectedAccountNoHash: account.AccountNoHash,
|
||||
ExpectedDigitalRMBCodeCipher: account.DigitalRMBCodeCipher,
|
||||
ExpectedDigitalRMBCodeHash: account.DigitalRMBCodeHash,
|
||||
ExpectedWechatQRAttachmentID: account.WechatQRAttachmentID,
|
||||
ExpectedAlipayQRAttachmentID: account.AlipayQRAttachmentID,
|
||||
Channel: *channel,
|
||||
@@ -737,9 +755,11 @@ func normalizePaymentAccount(input *PaymentAccountInput) error {
|
||||
input.HolderName = strings.TrimSpace(input.HolderName)
|
||||
input.IdentityNo = strings.ToUpper(strings.NewReplacer(" ", "", "-", "").Replace(input.IdentityNo))
|
||||
input.AccountNo = strings.NewReplacer(" ", "", "-", "").Replace(strings.TrimSpace(input.AccountNo))
|
||||
input.DigitalRMBCode = strings.NewReplacer(" ", "", "-", "").Replace(strings.TrimSpace(input.DigitalRMBCode))
|
||||
if !validHumanName(input.HolderName) || !identityNoPattern.MatchString(input.IdentityNo) ||
|
||||
utf8.RuneCountInString(input.BankName) < 2 || utf8.RuneCountInString(input.BankName) > 100 ||
|
||||
len(input.AccountNo) < 8 || len(input.AccountNo) > 32 || !allDigits(input.AccountNo) ||
|
||||
len(input.DigitalRMBCode) < 6 || len(input.DigitalRMBCode) > 32 || !allDigits(input.DigitalRMBCode) ||
|
||||
input.WechatQRAttachmentID <= 0 || input.AlipayQRAttachmentID <= 0 ||
|
||||
input.WechatQRAttachmentID == input.AlipayQRAttachmentID {
|
||||
return ErrAccountFieldsInvalid
|
||||
@@ -750,7 +770,8 @@ func normalizePaymentAccount(input *PaymentAccountInput) error {
|
||||
func paymentAccountFromStore(row store.PaymentAccount) PaymentAccount {
|
||||
return PaymentAccount{ID: row.ID, AccountType: accountTypeName(row.AccountType), BankCode: row.BankCode,
|
||||
BankName: row.BankName, HolderMask: row.HolderMask, IdentityNoMask: row.IdentityNoMask,
|
||||
AccountNoMask: row.AccountNoMask, WechatQRAttachmentID: row.WechatQRAttachmentID,
|
||||
AccountNoMask: row.AccountNoMask, DigitalRMBCodeMask: row.DigitalRMBCodeMask,
|
||||
WechatQRAttachmentID: row.WechatQRAttachmentID,
|
||||
AlipayQRAttachmentID: row.AlipayQRAttachmentID,
|
||||
VerifyStatus: verifyStatusName(row.VerifyStatus), IsDefault: row.IsDefault == 1,
|
||||
CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}
|
||||
|
||||
@@ -103,11 +103,13 @@ func TestQuoteFailsClosed(t *testing.T) {
|
||||
func TestNormalizePaymentAccount(t *testing.T) {
|
||||
input := PaymentAccountInput{BankName: "中国工商银行", HolderName: "张三",
|
||||
IdentityNo: "11010119900101123X", AccountNo: "6222 0000-0000 1234",
|
||||
DigitalRMBCode: "1234 5678",
|
||||
WechatQRAttachmentID: 8, AlipayQRAttachmentID: 9}
|
||||
if err := normalizePaymentAccount(&input); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if input.IdentityNo != "11010119900101123X" || input.AccountNo != "6222000000001234" ||
|
||||
input.DigitalRMBCode != "12345678" ||
|
||||
maskAccount(input.AccountNo) != "6222********1234" || maskIdentityNo(input.IdentityNo) != "1101**********123X" {
|
||||
t.Fatalf("unexpected normalized input: %#v", input)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ type PaymentAccount struct {
|
||||
AccountNoCipher string `db:"account_no_cipher"`
|
||||
AccountNoHash string `db:"account_no_hash"`
|
||||
AccountNoMask string `db:"account_no_mask"`
|
||||
DigitalRMBCodeCipher string `db:"digital_rmb_code_cipher"`
|
||||
DigitalRMBCodeHash string `db:"digital_rmb_code_hash"`
|
||||
DigitalRMBCodeMask string `db:"digital_rmb_code_mask"`
|
||||
QRAttachmentID int64 `db:"qr_attachment_id"`
|
||||
WechatQRAttachmentID int64 `db:"wechat_qr_attachment_id"`
|
||||
AlipayQRAttachmentID int64 `db:"alipay_qr_attachment_id"`
|
||||
@@ -47,6 +50,9 @@ type PaymentAccountCreation struct {
|
||||
AccountNoCipher string
|
||||
AccountNoHash string
|
||||
AccountNoMask string
|
||||
DigitalRMBCodeCipher string
|
||||
DigitalRMBCodeHash string
|
||||
DigitalRMBCodeMask string
|
||||
WechatQRAttachmentID int64
|
||||
AlipayQRAttachmentID int64
|
||||
}
|
||||
@@ -86,6 +92,8 @@ type WithdrawalCreation struct {
|
||||
ExpectedIdentityNoHash string
|
||||
ExpectedAccountNoCipher string
|
||||
ExpectedAccountNoHash string
|
||||
ExpectedDigitalRMBCodeCipher string
|
||||
ExpectedDigitalRMBCodeHash string
|
||||
ExpectedWechatQRAttachmentID int64
|
||||
ExpectedAlipayQRAttachmentID int64
|
||||
Channel WithdrawalChannel
|
||||
@@ -136,6 +144,7 @@ func (s *Store) PaymentAccounts(ctx context.Context, tenantID, memberID int64) (
|
||||
items := make([]PaymentAccount, 0)
|
||||
err := s.db.SelectContext(ctx, &items, `SELECT id,account_type,bank_code,bank_name,holder_mask,
|
||||
COALESCE(identity_no_mask,'') AS identity_no_mask,account_no_mask,
|
||||
COALESCE(digital_rmb_code_mask,'') AS digital_rmb_code_mask,
|
||||
COALESCE(wechat_qr_attachment_id,0) AS wechat_qr_attachment_id,
|
||||
COALESCE(alipay_qr_attachment_id,0) AS alipay_qr_attachment_id,verify_status,is_default,
|
||||
DATE_FORMAT(created_at,'%Y-%m-%dT%H:%i:%sZ') AS created_at,
|
||||
@@ -182,12 +191,14 @@ func (s *Store) CreatePaymentAccount(ctx context.Context, input PaymentAccountCr
|
||||
result, err := tx.ExecContext(ctx, `INSERT INTO hg_app_member_payment_account
|
||||
(tenant_id,member_id,account_type,bank_code,bank_name,holder_cipher,holder_mask,
|
||||
identity_no_cipher,identity_no_hash,identity_no_mask,account_no_cipher,account_no_hash,account_no_mask,
|
||||
digital_rmb_code_cipher,digital_rmb_code_hash,digital_rmb_code_mask,
|
||||
wechat_qr_attachment_id,alipay_qr_attachment_id,verify_status,
|
||||
is_default,created_by,updated_by)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,2,?,?,?)`, input.TenantID, input.MemberID, input.AccountType,
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,2,?,?,?)`, input.TenantID, input.MemberID, input.AccountType,
|
||||
input.BankCode, input.BankName, input.HolderCipher, input.HolderMask,
|
||||
input.IdentityNoCipher, input.IdentityNoHash, input.IdentityNoMask,
|
||||
input.AccountNoCipher, input.AccountNoHash, input.AccountNoMask,
|
||||
input.DigitalRMBCodeCipher, input.DigitalRMBCodeHash, input.DigitalRMBCodeMask,
|
||||
input.WechatQRAttachmentID, input.AlipayQRAttachmentID,
|
||||
isDefault, input.MemberID, input.MemberID)
|
||||
if err != nil {
|
||||
@@ -205,7 +216,8 @@ func (s *Store) CreatePaymentAccount(ctx context.Context, input PaymentAccountCr
|
||||
}
|
||||
return &PaymentAccount{ID: id, AccountType: input.AccountType, BankCode: input.BankCode,
|
||||
BankName: input.BankName, HolderMask: input.HolderMask, IdentityNoMask: input.IdentityNoMask,
|
||||
AccountNoMask: input.AccountNoMask, WechatQRAttachmentID: input.WechatQRAttachmentID,
|
||||
AccountNoMask: input.AccountNoMask, DigitalRMBCodeMask: input.DigitalRMBCodeMask,
|
||||
WechatQRAttachmentID: input.WechatQRAttachmentID,
|
||||
AlipayQRAttachmentID: input.AlipayQRAttachmentID,
|
||||
VerifyStatus: 2, IsDefault: isDefault}, nil
|
||||
}
|
||||
@@ -222,6 +234,9 @@ func (s *Store) PaymentAccountForWithdrawal(ctx context.Context, tenantID, membe
|
||||
err := s.db.GetContext(ctx, &row, `SELECT id,account_type,bank_code,bank_name,holder_cipher,holder_mask,
|
||||
COALESCE(identity_no_cipher,'') AS identity_no_cipher,COALESCE(identity_no_hash,'') AS identity_no_hash,
|
||||
COALESCE(identity_no_mask,'') AS identity_no_mask,account_no_cipher,account_no_hash,account_no_mask,
|
||||
COALESCE(digital_rmb_code_cipher,'') AS digital_rmb_code_cipher,
|
||||
COALESCE(digital_rmb_code_hash,'') AS digital_rmb_code_hash,
|
||||
COALESCE(digital_rmb_code_mask,'') AS digital_rmb_code_mask,
|
||||
COALESCE(wechat_qr_attachment_id,0) AS wechat_qr_attachment_id,
|
||||
COALESCE(alipay_qr_attachment_id,0) AS alipay_qr_attachment_id,verify_status,is_default,
|
||||
DATE_FORMAT(created_at,'%Y-%m-%dT%H:%i:%sZ') AS created_at,
|
||||
@@ -304,6 +319,9 @@ func (s *Store) CreateWithdrawal(ctx context.Context, input WithdrawalCreation)
|
||||
err = tx.GetContext(ctx, &account, `SELECT id,account_type,bank_code,bank_name,holder_cipher,holder_mask,
|
||||
COALESCE(identity_no_cipher,'') AS identity_no_cipher,COALESCE(identity_no_hash,'') AS identity_no_hash,
|
||||
COALESCE(identity_no_mask,'') AS identity_no_mask,account_no_cipher,account_no_hash,account_no_mask,
|
||||
COALESCE(digital_rmb_code_cipher,'') AS digital_rmb_code_cipher,
|
||||
COALESCE(digital_rmb_code_hash,'') AS digital_rmb_code_hash,
|
||||
COALESCE(digital_rmb_code_mask,'') AS digital_rmb_code_mask,
|
||||
COALESCE(wechat_qr_attachment_id,0) AS wechat_qr_attachment_id,
|
||||
COALESCE(alipay_qr_attachment_id,0) AS alipay_qr_attachment_id,
|
||||
verify_status,is_default,'' AS created_at,'' AS updated_at
|
||||
@@ -319,6 +337,8 @@ func (s *Store) CreateWithdrawal(ctx context.Context, input WithdrawalCreation)
|
||||
account.BankName != input.ExpectedBankName || account.HolderCipher != input.ExpectedHolderCipher ||
|
||||
account.IdentityNoCipher != input.ExpectedIdentityNoCipher || account.IdentityNoHash != input.ExpectedIdentityNoHash ||
|
||||
account.AccountNoCipher != input.ExpectedAccountNoCipher || account.AccountNoHash != input.ExpectedAccountNoHash ||
|
||||
account.DigitalRMBCodeCipher != input.ExpectedDigitalRMBCodeCipher ||
|
||||
account.DigitalRMBCodeHash != input.ExpectedDigitalRMBCodeHash ||
|
||||
account.WechatQRAttachmentID != input.ExpectedWechatQRAttachmentID ||
|
||||
account.AlipayQRAttachmentID != input.ExpectedAlipayQRAttachmentID {
|
||||
return nil, ErrConcurrentUpdate
|
||||
|
||||
Reference in New Issue
Block a user