diff --git a/internal/accounts/service.go b/internal/accounts/service.go index 6525cd2..6cdf726 100644 --- a/internal/accounts/service.go +++ b/internal/accounts/service.go @@ -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 diff --git a/internal/accounts/service_test.go b/internal/accounts/service_test.go index 77aaf10..476c945 100644 --- a/internal/accounts/service_test.go +++ b/internal/accounts/service_test.go @@ -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") diff --git a/internal/finance/service.go b/internal/finance/service.go index e5fdeea..6918620 100644 --- a/internal/finance/service.go +++ b/internal/finance/service.go @@ -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} diff --git a/internal/finance/service_test.go b/internal/finance/service_test.go index 87681d5..156ba25 100644 --- a/internal/finance/service_test.go +++ b/internal/finance/service_test.go @@ -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) } diff --git a/internal/store/withdrawals.go b/internal/store/withdrawals.go index 6dfa5a4..3c36af9 100644 --- a/internal/store/withdrawals.go +++ b/internal/store/withdrawals.go @@ -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 diff --git a/web/src/mobile/styles.css b/web/src/mobile/styles.css index f28d4ed..5959b18 100644 --- a/web/src/mobile/styles.css +++ b/web/src/mobile/styles.css @@ -1036,10 +1036,27 @@ body { border-radius: 24px; font-size: 16px; } +.m-order-recharge-link { + min-height: 46px; + margin-bottom: 10px; + border-radius: 24px; + color: #fff; + background: linear-gradient(90deg, #ffa32b, #f15b20); + display: grid; + place-items: center; + font-size: 16px; + font-weight: 600; + text-decoration: none; +} .m-order-submit-panel button:disabled, .m-order-submit-actions button:disabled { opacity: 0.6; } +.m-order-submit-panel > button:disabled { + color: #999; + background: #e9e9e9; + cursor: not-allowed; +} .m-order-submit-confirm { width: min(90vw, 430px); overflow: hidden; @@ -3362,6 +3379,13 @@ body { background: #ffb21f; color: #fff; } +.m-match-steps--single { + margin-top: 4px; + margin-bottom: 28px; +} +.m-match-steps--single li { + justify-content: center; +} /* Olons 2 manual task mall */ .m-task-mall { diff --git a/web/src/mobile/views/AuthView.vue b/web/src/mobile/views/AuthView.vue index e53c9ed..0158f13 100644 --- a/web/src/mobile/views/AuthView.vue +++ b/web/src/mobile/views/AuthView.vue @@ -21,7 +21,7 @@ v-model.trim="form.username" autocomplete="username" maxlength="10" - placeholder="请输入账户名" + placeholder="请输入名字" />