feat: improve member payment and order flows
This commit is contained in:
@@ -12,6 +12,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"git.118im.com/im_team/olons_server/internal/config"
|
"git.118im.com/im_team/olons_server/internal/config"
|
||||||
"git.118im.com/im_team/olons_server/internal/store"
|
"git.118im.com/im_team/olons_server/internal/store"
|
||||||
@@ -36,7 +38,6 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
usernamePattern = regexp.MustCompile(`^[a-z0-9]{1,10}$`)
|
|
||||||
phonePattern = regexp.MustCompile(`^1[3-9][0-9]{9}$`)
|
phonePattern = regexp.MustCompile(`^1[3-9][0-9]{9}$`)
|
||||||
referralPattern = regexp.MustCompile(`^[A-Za-z0-9]{1,32}$`)
|
referralPattern = regexp.MustCompile(`^[A-Za-z0-9]{1,32}$`)
|
||||||
tradePasswordPattern = regexp.MustCompile(`^[0-9]{6}$`)
|
tradePasswordPattern = regexp.MustCompile(`^[0-9]{6}$`)
|
||||||
@@ -142,10 +143,10 @@ func (s *Service) Register(ctx context.Context, input RegisterInput) (*Result, e
|
|||||||
if !s.ready {
|
if !s.ready {
|
||||||
return nil, ErrUnavailable
|
return nil, ErrUnavailable
|
||||||
}
|
}
|
||||||
input.Username = strings.ToLower(strings.TrimSpace(input.Username))
|
input.Username = strings.TrimSpace(input.Username)
|
||||||
input.Phone = strings.TrimSpace(input.Phone)
|
input.Phone = strings.TrimSpace(input.Phone)
|
||||||
input.ReferralCode = strings.TrimSpace(input.ReferralCode)
|
input.ReferralCode = strings.TrimSpace(input.ReferralCode)
|
||||||
if !usernamePattern.MatchString(input.Username) {
|
if !validMemberName(input.Username) {
|
||||||
return nil, ErrUsernameInvalid
|
return nil, ErrUsernameInvalid
|
||||||
}
|
}
|
||||||
if !phonePattern.MatchString(input.Phone) {
|
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)
|
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) {
|
func (s *Service) Login(ctx context.Context, input LoginInput) (*Result, error) {
|
||||||
if !s.ready {
|
if !s.ready {
|
||||||
return nil, ErrUnavailable
|
return nil, ErrUnavailable
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func TestRegisterValidation(t *testing.T) {
|
|||||||
input RegisterInput
|
input RegisterInput
|
||||||
want error
|
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: "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: "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},
|
{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) {
|
func TestLoginRateKeyDoesNotExposePhone(t *testing.T) {
|
||||||
s := &Service{}
|
s := &Service{}
|
||||||
key := s.loginRateKey("13800138000")
|
key := s.loginRateKey("13800138000")
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ type PaymentAccountInput struct {
|
|||||||
HolderName string `json:"holder_name"`
|
HolderName string `json:"holder_name"`
|
||||||
IdentityNo string `json:"identity_no"`
|
IdentityNo string `json:"identity_no"`
|
||||||
AccountNo string `json:"account_no"`
|
AccountNo string `json:"account_no"`
|
||||||
|
DigitalRMBCode string `json:"digital_rmb_code"`
|
||||||
WechatQRAttachmentID int64 `json:"wechat_qr_attachment_id"`
|
WechatQRAttachmentID int64 `json:"wechat_qr_attachment_id"`
|
||||||
AlipayQRAttachmentID int64 `json:"alipay_qr_attachment_id"`
|
AlipayQRAttachmentID int64 `json:"alipay_qr_attachment_id"`
|
||||||
}
|
}
|
||||||
@@ -82,6 +83,7 @@ type PaymentAccount struct {
|
|||||||
HolderMask string `json:"holder_mask"`
|
HolderMask string `json:"holder_mask"`
|
||||||
IdentityNoMask string `json:"identity_no_mask"`
|
IdentityNoMask string `json:"identity_no_mask"`
|
||||||
AccountNoMask string `json:"account_no_mask"`
|
AccountNoMask string `json:"account_no_mask"`
|
||||||
|
DigitalRMBCodeMask string `json:"digital_rmb_code_mask"`
|
||||||
WechatQRAttachmentID int64 `json:"wechat_qr_attachment_id"`
|
WechatQRAttachmentID int64 `json:"wechat_qr_attachment_id"`
|
||||||
AlipayQRAttachmentID int64 `json:"alipay_qr_attachment_id"`
|
AlipayQRAttachmentID int64 `json:"alipay_qr_attachment_id"`
|
||||||
VerifyStatus string `json:"verify_status"`
|
VerifyStatus string `json:"verify_status"`
|
||||||
@@ -299,12 +301,18 @@ func (s *Service) CreatePaymentAccount(ctx context.Context, tenantID, memberID i
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
digitalRMBCodeCipher, err := s.encrypt(input.DigitalRMBCode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
row, err := s.repository.CreatePaymentAccount(ctx, store.PaymentAccountCreation{
|
row, err := s.repository.CreatePaymentAccount(ctx, store.PaymentAccountCreation{
|
||||||
TenantID: tenantID, MemberID: memberID, AccountType: 1, BankCode: "BANK", BankName: input.BankName,
|
TenantID: tenantID, MemberID: memberID, AccountType: 1, BankCode: "BANK", BankName: input.BankName,
|
||||||
HolderCipher: holderCipher, HolderMask: maskName(input.HolderName),
|
HolderCipher: holderCipher, HolderMask: maskName(input.HolderName),
|
||||||
IdentityNoCipher: identityNoCipher, IdentityNoHash: s.lookupHash(input.IdentityNo),
|
IdentityNoCipher: identityNoCipher, IdentityNoHash: s.lookupHash(input.IdentityNo),
|
||||||
IdentityNoMask: maskIdentityNo(input.IdentityNo), AccountNoCipher: accountCipher,
|
IdentityNoMask: maskIdentityNo(input.IdentityNo), AccountNoCipher: accountCipher,
|
||||||
AccountNoHash: s.lookupHash(input.AccountNo), AccountNoMask: maskAccount(input.AccountNo),
|
AccountNoHash: s.lookupHash(input.AccountNo), AccountNoMask: maskAccount(input.AccountNo),
|
||||||
|
DigitalRMBCodeCipher: digitalRMBCodeCipher, DigitalRMBCodeHash: s.lookupHash(input.DigitalRMBCode),
|
||||||
|
DigitalRMBCodeMask: maskAccount(input.DigitalRMBCode),
|
||||||
WechatQRAttachmentID: input.WechatQRAttachmentID,
|
WechatQRAttachmentID: input.WechatQRAttachmentID,
|
||||||
AlipayQRAttachmentID: input.AlipayQRAttachmentID,
|
AlipayQRAttachmentID: input.AlipayQRAttachmentID,
|
||||||
})
|
})
|
||||||
@@ -360,10 +368,18 @@ func (s *Service) CreateWithdrawal(ctx context.Context, tenantID, memberID int64
|
|||||||
return nil, store.ErrPaymentAccountInvalid
|
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{
|
snapshot, err := json.Marshal(map[string]any{
|
||||||
"account_type": accountTypeName(account.AccountType), "bank_code": account.BankCode,
|
"account_type": accountTypeName(account.AccountType), "bank_code": account.BankCode,
|
||||||
"bank_name": account.BankName, "holder_name": holderName,
|
"bank_name": account.BankName, "holder_name": holderName,
|
||||||
"identity_no": identityNo, "account_no": accountNo,
|
"identity_no": identityNo, "account_no": accountNo,
|
||||||
|
"digital_rmb_code": digitalRMBCode,
|
||||||
"wechat_qr_attachment_id": account.WechatQRAttachmentID,
|
"wechat_qr_attachment_id": account.WechatQRAttachmentID,
|
||||||
"alipay_qr_attachment_id": account.AlipayQRAttachmentID,
|
"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,
|
ExpectedBankName: account.BankName, ExpectedHolderCipher: account.HolderCipher,
|
||||||
ExpectedIdentityNoCipher: account.IdentityNoCipher, ExpectedIdentityNoHash: account.IdentityNoHash,
|
ExpectedIdentityNoCipher: account.IdentityNoCipher, ExpectedIdentityNoHash: account.IdentityNoHash,
|
||||||
ExpectedAccountNoCipher: account.AccountNoCipher, ExpectedAccountNoHash: account.AccountNoHash,
|
ExpectedAccountNoCipher: account.AccountNoCipher, ExpectedAccountNoHash: account.AccountNoHash,
|
||||||
|
ExpectedDigitalRMBCodeCipher: account.DigitalRMBCodeCipher,
|
||||||
|
ExpectedDigitalRMBCodeHash: account.DigitalRMBCodeHash,
|
||||||
ExpectedWechatQRAttachmentID: account.WechatQRAttachmentID,
|
ExpectedWechatQRAttachmentID: account.WechatQRAttachmentID,
|
||||||
ExpectedAlipayQRAttachmentID: account.AlipayQRAttachmentID,
|
ExpectedAlipayQRAttachmentID: account.AlipayQRAttachmentID,
|
||||||
Channel: *channel,
|
Channel: *channel,
|
||||||
@@ -737,9 +755,11 @@ func normalizePaymentAccount(input *PaymentAccountInput) error {
|
|||||||
input.HolderName = strings.TrimSpace(input.HolderName)
|
input.HolderName = strings.TrimSpace(input.HolderName)
|
||||||
input.IdentityNo = strings.ToUpper(strings.NewReplacer(" ", "", "-", "").Replace(input.IdentityNo))
|
input.IdentityNo = strings.ToUpper(strings.NewReplacer(" ", "", "-", "").Replace(input.IdentityNo))
|
||||||
input.AccountNo = strings.NewReplacer(" ", "", "-", "").Replace(strings.TrimSpace(input.AccountNo))
|
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) ||
|
if !validHumanName(input.HolderName) || !identityNoPattern.MatchString(input.IdentityNo) ||
|
||||||
utf8.RuneCountInString(input.BankName) < 2 || utf8.RuneCountInString(input.BankName) > 100 ||
|
utf8.RuneCountInString(input.BankName) < 2 || utf8.RuneCountInString(input.BankName) > 100 ||
|
||||||
len(input.AccountNo) < 8 || len(input.AccountNo) > 32 || !allDigits(input.AccountNo) ||
|
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 <= 0 || input.AlipayQRAttachmentID <= 0 ||
|
||||||
input.WechatQRAttachmentID == input.AlipayQRAttachmentID {
|
input.WechatQRAttachmentID == input.AlipayQRAttachmentID {
|
||||||
return ErrAccountFieldsInvalid
|
return ErrAccountFieldsInvalid
|
||||||
@@ -750,7 +770,8 @@ func normalizePaymentAccount(input *PaymentAccountInput) error {
|
|||||||
func paymentAccountFromStore(row store.PaymentAccount) PaymentAccount {
|
func paymentAccountFromStore(row store.PaymentAccount) PaymentAccount {
|
||||||
return PaymentAccount{ID: row.ID, AccountType: accountTypeName(row.AccountType), BankCode: row.BankCode,
|
return PaymentAccount{ID: row.ID, AccountType: accountTypeName(row.AccountType), BankCode: row.BankCode,
|
||||||
BankName: row.BankName, HolderMask: row.HolderMask, IdentityNoMask: row.IdentityNoMask,
|
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,
|
AlipayQRAttachmentID: row.AlipayQRAttachmentID,
|
||||||
VerifyStatus: verifyStatusName(row.VerifyStatus), IsDefault: row.IsDefault == 1,
|
VerifyStatus: verifyStatusName(row.VerifyStatus), IsDefault: row.IsDefault == 1,
|
||||||
CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}
|
CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}
|
||||||
|
|||||||
@@ -103,11 +103,13 @@ func TestQuoteFailsClosed(t *testing.T) {
|
|||||||
func TestNormalizePaymentAccount(t *testing.T) {
|
func TestNormalizePaymentAccount(t *testing.T) {
|
||||||
input := PaymentAccountInput{BankName: "中国工商银行", HolderName: "张三",
|
input := PaymentAccountInput{BankName: "中国工商银行", HolderName: "张三",
|
||||||
IdentityNo: "11010119900101123X", AccountNo: "6222 0000-0000 1234",
|
IdentityNo: "11010119900101123X", AccountNo: "6222 0000-0000 1234",
|
||||||
|
DigitalRMBCode: "1234 5678",
|
||||||
WechatQRAttachmentID: 8, AlipayQRAttachmentID: 9}
|
WechatQRAttachmentID: 8, AlipayQRAttachmentID: 9}
|
||||||
if err := normalizePaymentAccount(&input); err != nil {
|
if err := normalizePaymentAccount(&input); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if input.IdentityNo != "11010119900101123X" || input.AccountNo != "6222000000001234" ||
|
if input.IdentityNo != "11010119900101123X" || input.AccountNo != "6222000000001234" ||
|
||||||
|
input.DigitalRMBCode != "12345678" ||
|
||||||
maskAccount(input.AccountNo) != "6222********1234" || maskIdentityNo(input.IdentityNo) != "1101**********123X" {
|
maskAccount(input.AccountNo) != "6222********1234" || maskIdentityNo(input.IdentityNo) != "1101**********123X" {
|
||||||
t.Fatalf("unexpected normalized input: %#v", input)
|
t.Fatalf("unexpected normalized input: %#v", input)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ type PaymentAccount struct {
|
|||||||
AccountNoCipher string `db:"account_no_cipher"`
|
AccountNoCipher string `db:"account_no_cipher"`
|
||||||
AccountNoHash string `db:"account_no_hash"`
|
AccountNoHash string `db:"account_no_hash"`
|
||||||
AccountNoMask string `db:"account_no_mask"`
|
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"`
|
QRAttachmentID int64 `db:"qr_attachment_id"`
|
||||||
WechatQRAttachmentID int64 `db:"wechat_qr_attachment_id"`
|
WechatQRAttachmentID int64 `db:"wechat_qr_attachment_id"`
|
||||||
AlipayQRAttachmentID int64 `db:"alipay_qr_attachment_id"`
|
AlipayQRAttachmentID int64 `db:"alipay_qr_attachment_id"`
|
||||||
@@ -47,6 +50,9 @@ type PaymentAccountCreation struct {
|
|||||||
AccountNoCipher string
|
AccountNoCipher string
|
||||||
AccountNoHash string
|
AccountNoHash string
|
||||||
AccountNoMask string
|
AccountNoMask string
|
||||||
|
DigitalRMBCodeCipher string
|
||||||
|
DigitalRMBCodeHash string
|
||||||
|
DigitalRMBCodeMask string
|
||||||
WechatQRAttachmentID int64
|
WechatQRAttachmentID int64
|
||||||
AlipayQRAttachmentID int64
|
AlipayQRAttachmentID int64
|
||||||
}
|
}
|
||||||
@@ -86,6 +92,8 @@ type WithdrawalCreation struct {
|
|||||||
ExpectedIdentityNoHash string
|
ExpectedIdentityNoHash string
|
||||||
ExpectedAccountNoCipher string
|
ExpectedAccountNoCipher string
|
||||||
ExpectedAccountNoHash string
|
ExpectedAccountNoHash string
|
||||||
|
ExpectedDigitalRMBCodeCipher string
|
||||||
|
ExpectedDigitalRMBCodeHash string
|
||||||
ExpectedWechatQRAttachmentID int64
|
ExpectedWechatQRAttachmentID int64
|
||||||
ExpectedAlipayQRAttachmentID int64
|
ExpectedAlipayQRAttachmentID int64
|
||||||
Channel WithdrawalChannel
|
Channel WithdrawalChannel
|
||||||
@@ -136,6 +144,7 @@ func (s *Store) PaymentAccounts(ctx context.Context, tenantID, memberID int64) (
|
|||||||
items := make([]PaymentAccount, 0)
|
items := make([]PaymentAccount, 0)
|
||||||
err := s.db.SelectContext(ctx, &items, `SELECT id,account_type,bank_code,bank_name,holder_mask,
|
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(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(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,
|
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,
|
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
|
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,
|
(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,
|
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,
|
wechat_qr_attachment_id,alipay_qr_attachment_id,verify_status,
|
||||||
is_default,created_by,updated_by)
|
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.BankCode, input.BankName, input.HolderCipher, input.HolderMask,
|
||||||
input.IdentityNoCipher, input.IdentityNoHash, input.IdentityNoMask,
|
input.IdentityNoCipher, input.IdentityNoHash, input.IdentityNoMask,
|
||||||
input.AccountNoCipher, input.AccountNoHash, input.AccountNoMask,
|
input.AccountNoCipher, input.AccountNoHash, input.AccountNoMask,
|
||||||
|
input.DigitalRMBCodeCipher, input.DigitalRMBCodeHash, input.DigitalRMBCodeMask,
|
||||||
input.WechatQRAttachmentID, input.AlipayQRAttachmentID,
|
input.WechatQRAttachmentID, input.AlipayQRAttachmentID,
|
||||||
isDefault, input.MemberID, input.MemberID)
|
isDefault, input.MemberID, input.MemberID)
|
||||||
if err != nil {
|
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,
|
return &PaymentAccount{ID: id, AccountType: input.AccountType, BankCode: input.BankCode,
|
||||||
BankName: input.BankName, HolderMask: input.HolderMask, IdentityNoMask: input.IdentityNoMask,
|
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,
|
AlipayQRAttachmentID: input.AlipayQRAttachmentID,
|
||||||
VerifyStatus: 2, IsDefault: isDefault}, nil
|
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,
|
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_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(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(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,
|
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,
|
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,
|
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_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(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(wechat_qr_attachment_id,0) AS wechat_qr_attachment_id,
|
||||||
COALESCE(alipay_qr_attachment_id,0) AS alipay_qr_attachment_id,
|
COALESCE(alipay_qr_attachment_id,0) AS alipay_qr_attachment_id,
|
||||||
verify_status,is_default,'' AS created_at,'' AS updated_at
|
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.BankName != input.ExpectedBankName || account.HolderCipher != input.ExpectedHolderCipher ||
|
||||||
account.IdentityNoCipher != input.ExpectedIdentityNoCipher || account.IdentityNoHash != input.ExpectedIdentityNoHash ||
|
account.IdentityNoCipher != input.ExpectedIdentityNoCipher || account.IdentityNoHash != input.ExpectedIdentityNoHash ||
|
||||||
account.AccountNoCipher != input.ExpectedAccountNoCipher || account.AccountNoHash != input.ExpectedAccountNoHash ||
|
account.AccountNoCipher != input.ExpectedAccountNoCipher || account.AccountNoHash != input.ExpectedAccountNoHash ||
|
||||||
|
account.DigitalRMBCodeCipher != input.ExpectedDigitalRMBCodeCipher ||
|
||||||
|
account.DigitalRMBCodeHash != input.ExpectedDigitalRMBCodeHash ||
|
||||||
account.WechatQRAttachmentID != input.ExpectedWechatQRAttachmentID ||
|
account.WechatQRAttachmentID != input.ExpectedWechatQRAttachmentID ||
|
||||||
account.AlipayQRAttachmentID != input.ExpectedAlipayQRAttachmentID {
|
account.AlipayQRAttachmentID != input.ExpectedAlipayQRAttachmentID {
|
||||||
return nil, ErrConcurrentUpdate
|
return nil, ErrConcurrentUpdate
|
||||||
|
|||||||
@@ -1036,10 +1036,27 @@ body {
|
|||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
font-size: 16px;
|
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-panel button:disabled,
|
||||||
.m-order-submit-actions button:disabled {
|
.m-order-submit-actions button:disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
.m-order-submit-panel > button:disabled {
|
||||||
|
color: #999;
|
||||||
|
background: #e9e9e9;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
.m-order-submit-confirm {
|
.m-order-submit-confirm {
|
||||||
width: min(90vw, 430px);
|
width: min(90vw, 430px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -3362,6 +3379,13 @@ body {
|
|||||||
background: #ffb21f;
|
background: #ffb21f;
|
||||||
color: #fff;
|
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 */
|
/* Olons 2 manual task mall */
|
||||||
.m-task-mall {
|
.m-task-mall {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
v-model.trim="form.username"
|
v-model.trim="form.username"
|
||||||
autocomplete="username"
|
autocomplete="username"
|
||||||
maxlength="10"
|
maxlength="10"
|
||||||
placeholder="请输入账户名"
|
placeholder="请输入名字"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="m-input-row">
|
<label class="m-input-row">
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const mode = ref<'login' | 'register'>('login');
|
const mode = ref<'login' | 'register'>('login');
|
||||||
const accepted = ref(false);
|
const accepted = ref(true);
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const form = reactive({ username: '', phone: '', password: '', referral_code: '' });
|
const form = reactive({ username: '', phone: '', password: '', referral_code: '' });
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
form.phone = '';
|
form.phone = '';
|
||||||
form.password = '';
|
form.password = '';
|
||||||
form.referral_code = '';
|
form.referral_code = '';
|
||||||
accepted.value = false;
|
accepted.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -113,8 +113,8 @@
|
|||||||
if (!/^1\d{10}$/.test(form.phone)) return void (error.value = '请输入正确的手机号码');
|
if (!/^1\d{10}$/.test(form.phone)) return void (error.value = '请输入正确的手机号码');
|
||||||
if (form.password.length < 6) return void (error.value = '密码至少为 6 位');
|
if (form.password.length < 6) return void (error.value = '密码至少为 6 位');
|
||||||
if (!accepted.value) return void (error.value = '请先阅读并同意用户协议和隐私政策');
|
if (!accepted.value) return void (error.value = '请先阅读并同意用户协议和隐私政策');
|
||||||
if (mode.value === 'register' && !/^[a-z0-9]{1,10}$/.test(form.username)) {
|
if (mode.value === 'register' && !/^[\p{L}\p{M}·]{1,10}$/u.test(form.username)) {
|
||||||
return void (error.value = '账户名须为 1 至 10 位小写字母或数字');
|
return void (error.value = '名字须为 1 至 10 个字符,支持汉字');
|
||||||
}
|
}
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -53,10 +53,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="isAutoMatching" class="m-match-progress"><i :style="{ width: `${matchProgress}%` }"></i></div>
|
<div v-if="isAutoMatching" class="m-match-progress"><i :style="{ width: `${matchProgress}%` }"></i></div>
|
||||||
<strong v-if="isAutoMatching" class="m-match-percent">{{ matchProgress }}%</strong>
|
<strong v-if="isAutoMatching" class="m-match-percent">{{ matchProgress }}%</strong>
|
||||||
<ol class="m-match-steps">
|
<ol class="m-match-steps m-match-steps--single">
|
||||||
<li v-for="(step, index) in matchSteps" :key="step" :class="{ active: index <= matchStep }">
|
<li class="active"><span>✓</span>正在匹配任务</li>
|
||||||
<span>✓</span>{{ step }}
|
|
||||||
</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
<button class="m-result-secondary" @click="openOrders">稍后去记录查看</button>
|
<button class="m-result-secondary" @click="openOrders">稍后去记录查看</button>
|
||||||
</template>
|
</template>
|
||||||
@@ -139,8 +137,6 @@
|
|||||||
const end = String(eligibility.value?.business_end_at || '21:00').slice(0, 5);
|
const end = String(eligibility.value?.business_end_at || '21:00').slice(0, 5);
|
||||||
return `${start}-${end}`;
|
return `${start}-${end}`;
|
||||||
});
|
});
|
||||||
const matchStep = computed(() => (matchProgress.value < 35 ? 0 : matchProgress.value < 60 ? 1 : matchProgress.value < 85 ? 2 : 3));
|
|
||||||
const matchSteps = ['正在匹配任务', '获取商品信息', '确认任务内容', '发放任务结果'];
|
|
||||||
const isAutoMatching = computed(() => order.value?.dispatch_mode !== 'manual');
|
const isAutoMatching = computed(() => order.value?.dispatch_mode !== 'manual');
|
||||||
const matchingHint = computed(() => {
|
const matchingHint = computed(() => {
|
||||||
if (order.value?.dispatch_mode === 'manual') return '订单已领取,正在等待后台匹配...';
|
if (order.value?.dispatch_mode === 'manual') return '订单已领取,正在等待后台匹配...';
|
||||||
|
|||||||
@@ -59,6 +59,14 @@
|
|||||||
<label
|
<label
|
||||||
><span>开户行</span><input v-model.trim="bank.bank_name" placeholder="请输入开户行"
|
><span>开户行</span><input v-model.trim="bank.bank_name" placeholder="请输入开户行"
|
||||||
/></label>
|
/></label>
|
||||||
|
<label
|
||||||
|
><span>数字人民币</span
|
||||||
|
><input
|
||||||
|
v-model.trim="bank.digital_rmb_code"
|
||||||
|
inputmode="numeric"
|
||||||
|
maxlength="32"
|
||||||
|
placeholder="请输入数字编码"
|
||||||
|
/></label>
|
||||||
<section class="m-qr-field">
|
<section class="m-qr-field">
|
||||||
<h3>微信二维码</h3>
|
<h3>微信二维码</h3>
|
||||||
<label class="m-qr-upload">
|
<label class="m-qr-upload">
|
||||||
@@ -244,6 +252,7 @@
|
|||||||
holder_name: '',
|
holder_name: '',
|
||||||
identity_no: '',
|
identity_no: '',
|
||||||
account_no: '',
|
account_no: '',
|
||||||
|
digital_rmb_code: '',
|
||||||
wechat_qr_attachment_id: 0,
|
wechat_qr_attachment_id: 0,
|
||||||
alipay_qr_attachment_id: 0,
|
alipay_qr_attachment_id: 0,
|
||||||
});
|
});
|
||||||
@@ -272,6 +281,7 @@
|
|||||||
!/^[1-9]\d{14}(?:\d{2}[\dXx])?$/.test(bank.identity_no.replace(/[ -]/g, '')) ||
|
!/^[1-9]\d{14}(?:\d{2}[\dXx])?$/.test(bank.identity_no.replace(/[ -]/g, '')) ||
|
||||||
!/^[0-9 -]{8,36}$/.test(bank.account_no) ||
|
!/^[0-9 -]{8,36}$/.test(bank.account_no) ||
|
||||||
!bank.bank_name ||
|
!bank.bank_name ||
|
||||||
|
!/^\d{6,32}$/.test(bank.digital_rmb_code.replace(/[ -]/g, '')) ||
|
||||||
!bank.wechat_qr_attachment_id ||
|
!bank.wechat_qr_attachment_id ||
|
||||||
!bank.alipay_qr_attachment_id
|
!bank.alipay_qr_attachment_id
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -105,10 +105,22 @@
|
|||||||
</section>
|
</section>
|
||||||
<section v-if="canSubmit" class="m-order-submit-panel">
|
<section v-if="canSubmit" class="m-order-submit-panel">
|
||||||
<div>
|
<div>
|
||||||
<strong>大额订单已匹配</strong>
|
<strong>{{ balanceInsufficient ? '账户余额不足' : '大额订单已匹配' }}</strong>
|
||||||
<p>请确认订单信息。提交时将校验可用余额,余额不足需先完成储入。</p>
|
<p v-if="balanceInsufficient">
|
||||||
|
当前可用余额 {{ money(availableAmount, detail.currency) }},订单需支付
|
||||||
|
{{ money(requiredAmount, detail.currency) }},还差
|
||||||
|
{{ money(balanceShortfall, detail.currency) }}。请先储入后再提交。
|
||||||
|
</p>
|
||||||
|
<p v-else>当前余额充足,请确认订单信息后提交。</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" :disabled="submitting" @click="showSubmitConfirm = true">提交确认</button>
|
<router-link v-if="balanceInsufficient" class="m-order-recharge-link" to="/app/recharge">
|
||||||
|
去储入
|
||||||
|
</router-link>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:disabled="submitting || balanceInsufficient"
|
||||||
|
@click="showSubmitConfirm = true"
|
||||||
|
>{{ balanceInsufficient ? '余额不足,无法提交' : '提交确认' }}</button>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</AsyncState>
|
</AsyncState>
|
||||||
@@ -178,6 +190,14 @@
|
|||||||
const canSubmit = computed(
|
const canSubmit = computed(
|
||||||
() => detail.value?.status === 'pending' && detail.value?.allowed_actions?.includes('submit')
|
() => detail.value?.status === 'pending' && detail.value?.allowed_actions?.includes('submit')
|
||||||
);
|
);
|
||||||
|
const availableAmount = computed(() => Number(stats.value?.available_amount || 0));
|
||||||
|
const requiredAmount = computed(() => Number(detail.value?.total_amount || 0));
|
||||||
|
const balanceShortfall = computed(() =>
|
||||||
|
Math.max(0, requiredAmount.value - availableAmount.value)
|
||||||
|
);
|
||||||
|
const balanceInsufficient = computed(
|
||||||
|
() => canSubmit.value && availableAmount.value < requiredAmount.value
|
||||||
|
);
|
||||||
async function loadList() {
|
async function loadList() {
|
||||||
listController?.abort();
|
listController?.abort();
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
@@ -207,7 +227,12 @@
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
try {
|
try {
|
||||||
detail.value = await request({ url: `/v1/task-orders/${route.params.id}` });
|
const [orderDetail, stat] = await Promise.all([
|
||||||
|
request<any>({ url: `/v1/task-orders/${route.params.id}` }),
|
||||||
|
request<any>({ url: '/v1/tasks/stats' }),
|
||||||
|
]);
|
||||||
|
detail.value = orderDetail;
|
||||||
|
stats.value = stat;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = errorText(err);
|
error.value = errorText(err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -216,6 +241,11 @@
|
|||||||
}
|
}
|
||||||
async function submitOrder() {
|
async function submitOrder() {
|
||||||
if (!detail.value?.id || submitting.value) return;
|
if (!detail.value?.id || submitting.value) return;
|
||||||
|
if (balanceInsufficient.value) {
|
||||||
|
showSubmitConfirm.value = false;
|
||||||
|
window.$message.warning('账户余额不足,请先完成储入');
|
||||||
|
return;
|
||||||
|
}
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
await request({
|
await request({
|
||||||
|
|||||||
@@ -187,6 +187,26 @@ const router = createRouter({
|
|||||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const staleChunkReloadKey = 'olons:stale-chunk-reload-at';
|
||||||
|
const staleChunkErrorPattern =
|
||||||
|
/Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module/i;
|
||||||
|
|
||||||
|
function reloadAfterStaleChunk(error?: unknown) {
|
||||||
|
if (error && !staleChunkErrorPattern.test(String(error))) return;
|
||||||
|
const now = Date.now();
|
||||||
|
const lastReloadAt = Number(window.sessionStorage.getItem(staleChunkReloadKey) || 0);
|
||||||
|
if (now - lastReloadAt < 10_000) return;
|
||||||
|
window.sessionStorage.setItem(staleChunkReloadKey, String(now));
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('vite:preloadError', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
reloadAfterStaleChunk();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.onError((error) => reloadAfterStaleChunk(error));
|
||||||
|
|
||||||
router.afterEach((route) => {
|
router.afterEach((route) => {
|
||||||
document.title = route.meta.title ? `${String(route.meta.title)} - Olons` : 'Olons';
|
document.title = route.meta.title ? `${String(route.meta.title)} - Olons` : 'Olons';
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user