fix: edit existing payment account in binding form
This commit is contained in:
@@ -28,6 +28,8 @@
|
|||||||
- `GET /v1/manual-products/{product_id}`
|
- `GET /v1/manual-products/{product_id}`
|
||||||
- `GET /v1/payment-accounts`
|
- `GET /v1/payment-accounts`
|
||||||
- `POST /v1/payment-accounts`
|
- `POST /v1/payment-accounts`
|
||||||
|
- `PUT /v1/payment-accounts`
|
||||||
|
- `GET /v1/payment-accounts/details`
|
||||||
- `POST /v1/withdrawal-orders/quote`
|
- `POST /v1/withdrawal-orders/quote`
|
||||||
- `POST /v1/withdrawal-orders`
|
- `POST /v1/withdrawal-orders`
|
||||||
- `GET /v1/withdrawal-orders`
|
- `GET /v1/withdrawal-orders`
|
||||||
|
|||||||
@@ -867,6 +867,8 @@ GET /v1/wallet/ledgers?page=1&page_size=20&biz_type=task&direction=income
|
|||||||
```http
|
```http
|
||||||
GET /v1/payment-accounts
|
GET /v1/payment-accounts
|
||||||
POST /v1/payment-accounts
|
POST /v1/payment-accounts
|
||||||
|
PUT /v1/payment-accounts
|
||||||
|
GET /v1/payment-accounts/details
|
||||||
POST /v1/attachments/images
|
POST /v1/attachments/images
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -883,6 +885,8 @@ POST /v1/attachments/images
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
在“取出信息”页面,`GET /v1/payment-accounts/details` 返回当前会员自己的原始绑定字段及二维码附件 ID;页面将首条默认账户回填到同一表单。修改时调用 `PUT /v1/payment-accounts`,请求体沿用新增字段并增加已有账户的 `id`。更新按租户、会员和账户 ID 校验归属,原记录 ID 保持不变;未更换二维码时可沿用已绑定的有效附件 ID。历史资料无法解密时不回填脱敏字符串,用户须重新填写敏感字段。
|
||||||
|
|
||||||
绑卡页面按原型固定为银行卡账户,字段规则如下:
|
绑卡页面按原型固定为银行卡账户,字段规则如下:
|
||||||
|
|
||||||
| 字段 | 是否必填 | 说明 |
|
| 字段 | 是否必填 | 说明 |
|
||||||
|
|||||||
+53
-24
@@ -44,6 +44,7 @@ type Repository interface {
|
|||||||
PaymentAccounts(context.Context, int64, int64) ([]store.PaymentAccount, error)
|
PaymentAccounts(context.Context, int64, int64) ([]store.PaymentAccount, error)
|
||||||
PaymentAccountDetails(context.Context, int64, int64) ([]store.PaymentAccount, error)
|
PaymentAccountDetails(context.Context, int64, int64) ([]store.PaymentAccount, error)
|
||||||
CreatePaymentAccount(context.Context, store.PaymentAccountCreation) (*store.PaymentAccount, error)
|
CreatePaymentAccount(context.Context, store.PaymentAccountCreation) (*store.PaymentAccount, error)
|
||||||
|
UpdatePaymentAccount(context.Context, int64, store.PaymentAccountCreation) (*store.PaymentAccount, error)
|
||||||
PaymentAccountForWithdrawal(context.Context, int64, int64, int64) (*store.PaymentAccount, error)
|
PaymentAccountForWithdrawal(context.Context, int64, int64, int64) (*store.PaymentAccount, error)
|
||||||
ActiveWithdrawalChannel(context.Context, int64, []int) (*store.WithdrawalChannel, error)
|
ActiveWithdrawalChannel(context.Context, int64, []int) (*store.WithdrawalChannel, error)
|
||||||
WithdrawalWallet(context.Context, int64, int64) (*store.WithdrawalWallet, error)
|
WithdrawalWallet(context.Context, int64, int64) (*store.WithdrawalWallet, error)
|
||||||
@@ -67,6 +68,7 @@ type Service struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PaymentAccountInput struct {
|
type PaymentAccountInput struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
BankName string `json:"bank_name"`
|
BankName string `json:"bank_name"`
|
||||||
HolderName string `json:"holder_name"`
|
HolderName string `json:"holder_name"`
|
||||||
IdentityNo string `json:"identity_no"`
|
IdentityNo string `json:"identity_no"`
|
||||||
@@ -94,17 +96,19 @@ type PaymentAccount struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PaymentAccountDetail struct {
|
type PaymentAccountDetail struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
BankName string `json:"bank_name"`
|
BankName string `json:"bank_name"`
|
||||||
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"`
|
DigitalRMBCode string `json:"digital_rmb_code"`
|
||||||
WechatQRURL string `json:"wechat_qr_url"`
|
WechatQRURL string `json:"wechat_qr_url"`
|
||||||
AlipayQRURL string `json:"alipay_qr_url"`
|
AlipayQRURL string `json:"alipay_qr_url"`
|
||||||
VerifyStatus string `json:"verify_status"`
|
WechatQRAttachmentID int64 `json:"wechat_qr_attachment_id"`
|
||||||
IsDefault bool `json:"is_default"`
|
AlipayQRAttachmentID int64 `json:"alipay_qr_attachment_id"`
|
||||||
Masked bool `json:"masked"`
|
VerifyStatus string `json:"verify_status"`
|
||||||
|
IsDefault bool `json:"is_default"`
|
||||||
|
Masked bool `json:"masked"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type QuoteInput struct {
|
type QuoteInput struct {
|
||||||
@@ -335,6 +339,7 @@ func (s *Service) PaymentAccountDetails(ctx context.Context, tenantID, memberID
|
|||||||
ID: row.ID, BankName: row.BankName, HolderName: holderName,
|
ID: row.ID, BankName: row.BankName, HolderName: holderName,
|
||||||
IdentityNo: identityNo, AccountNo: accountNo, DigitalRMBCode: digitalRMBCode,
|
IdentityNo: identityNo, AccountNo: accountNo, DigitalRMBCode: digitalRMBCode,
|
||||||
WechatQRURL: row.WechatQRURL, AlipayQRURL: row.AlipayQRURL,
|
WechatQRURL: row.WechatQRURL, AlipayQRURL: row.AlipayQRURL,
|
||||||
|
WechatQRAttachmentID: row.WechatQRAttachmentID, AlipayQRAttachmentID: row.AlipayQRAttachmentID,
|
||||||
VerifyStatus: verifyStatusName(row.VerifyStatus), IsDefault: row.IsDefault == 1, Masked: masked,
|
VerifyStatus: verifyStatusName(row.VerifyStatus), IsDefault: row.IsDefault == 1, Masked: masked,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -342,34 +347,63 @@ func (s *Service) PaymentAccountDetails(ctx context.Context, tenantID, memberID
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CreatePaymentAccount(ctx context.Context, tenantID, memberID int64, input PaymentAccountInput) (*PaymentAccount, error) {
|
func (s *Service) CreatePaymentAccount(ctx context.Context, tenantID, memberID int64, input PaymentAccountInput) (*PaymentAccount, error) {
|
||||||
|
data, err := s.paymentAccountCreation(tenantID, memberID, input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
row, err := s.repository.CreatePaymentAccount(ctx, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := paymentAccountFromStore(*row)
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UpdatePaymentAccount(ctx context.Context, tenantID, memberID int64, input PaymentAccountInput) (*PaymentAccount, error) {
|
||||||
|
if input.ID <= 0 {
|
||||||
|
return nil, ErrAccountFieldsInvalid
|
||||||
|
}
|
||||||
|
data, err := s.paymentAccountCreation(tenantID, memberID, input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
row, err := s.repository.UpdatePaymentAccount(ctx, input.ID, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := paymentAccountFromStore(*row)
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) paymentAccountCreation(tenantID, memberID int64, input PaymentAccountInput) (store.PaymentAccountCreation, error) {
|
||||||
if !s.ready {
|
if !s.ready {
|
||||||
return nil, ErrUnavailable
|
return store.PaymentAccountCreation{}, ErrUnavailable
|
||||||
}
|
}
|
||||||
if err := normalizePaymentAccount(&input); err != nil {
|
if err := normalizePaymentAccount(&input); err != nil {
|
||||||
return nil, err
|
return store.PaymentAccountCreation{}, err
|
||||||
}
|
}
|
||||||
holderCipher, err := s.encrypt(input.HolderName)
|
holderCipher, err := s.encrypt(input.HolderName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return store.PaymentAccountCreation{}, err
|
||||||
}
|
}
|
||||||
identityNoCipher, err := s.encrypt(input.IdentityNo)
|
identityNoCipher, err := s.encrypt(input.IdentityNo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return store.PaymentAccountCreation{}, err
|
||||||
}
|
}
|
||||||
accountCipher, err := s.encrypt(input.AccountNo)
|
accountCipher, err := s.encrypt(input.AccountNo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return store.PaymentAccountCreation{}, err
|
||||||
}
|
}
|
||||||
digitalRMBCodeCipher, digitalRMBCodeHash, digitalRMBCodeMask := "", "", ""
|
digitalRMBCodeCipher, digitalRMBCodeHash, digitalRMBCodeMask := "", "", ""
|
||||||
if input.DigitalRMBCode != "" {
|
if input.DigitalRMBCode != "" {
|
||||||
digitalRMBCodeCipher, err = s.encrypt(input.DigitalRMBCode)
|
digitalRMBCodeCipher, err = s.encrypt(input.DigitalRMBCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return store.PaymentAccountCreation{}, err
|
||||||
}
|
}
|
||||||
digitalRMBCodeHash = s.lookupHash(input.DigitalRMBCode)
|
digitalRMBCodeHash = s.lookupHash(input.DigitalRMBCode)
|
||||||
digitalRMBCodeMask = maskAccount(input.DigitalRMBCode)
|
digitalRMBCodeMask = maskAccount(input.DigitalRMBCode)
|
||||||
}
|
}
|
||||||
row, err := s.repository.CreatePaymentAccount(ctx, store.PaymentAccountCreation{
|
return 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),
|
||||||
@@ -379,12 +413,7 @@ func (s *Service) CreatePaymentAccount(ctx context.Context, tenantID, memberID i
|
|||||||
DigitalRMBCodeMask: digitalRMBCodeMask,
|
DigitalRMBCodeMask: digitalRMBCodeMask,
|
||||||
WechatQRAttachmentID: input.WechatQRAttachmentID,
|
WechatQRAttachmentID: input.WechatQRAttachmentID,
|
||||||
AlipayQRAttachmentID: input.AlipayQRAttachmentID,
|
AlipayQRAttachmentID: input.AlipayQRAttachmentID,
|
||||||
})
|
}, nil
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result := paymentAccountFromStore(*row)
|
|
||||||
return &result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Quote(ctx context.Context, tenantID, memberID int64, input QuoteInput) (*Quote, error) {
|
func (s *Service) Quote(ctx context.Context, tenantID, memberID int64, input QuoteInput) (*Quote, error) {
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ type fakeRepository struct {
|
|||||||
|
|
||||||
type paymentAccountRepository struct {
|
type paymentAccountRepository struct {
|
||||||
fakeRepository
|
fakeRepository
|
||||||
created store.PaymentAccountCreation
|
created store.PaymentAccountCreation
|
||||||
|
updated store.PaymentAccountCreation
|
||||||
|
updatedID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *paymentAccountRepository) CreatePaymentAccount(_ context.Context, input store.PaymentAccountCreation) (*store.PaymentAccount, error) {
|
func (f *paymentAccountRepository) CreatePaymentAccount(_ context.Context, input store.PaymentAccountCreation) (*store.PaymentAccount, error) {
|
||||||
@@ -35,6 +37,12 @@ func (f *paymentAccountRepository) CreatePaymentAccount(_ context.Context, input
|
|||||||
return &store.PaymentAccount{ID: 1, AccountType: input.AccountType}, nil
|
return &store.PaymentAccount{ID: 1, AccountType: input.AccountType}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *paymentAccountRepository) UpdatePaymentAccount(_ context.Context, id int64, input store.PaymentAccountCreation) (*store.PaymentAccount, error) {
|
||||||
|
f.updatedID = id
|
||||||
|
f.updated = input
|
||||||
|
return &store.PaymentAccount{ID: id, AccountType: input.AccountType, BankName: input.BankName}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f *fakeRepository) CurrentSiteConfig(context.Context, int64) (json.RawMessage, error) {
|
func (f *fakeRepository) CurrentSiteConfig(context.Context, int64) (json.RawMessage, error) {
|
||||||
return f.config, nil
|
return f.config, nil
|
||||||
}
|
}
|
||||||
@@ -50,6 +58,9 @@ func (f *fakeRepository) PaymentAccountDetails(context.Context, int64, int64) ([
|
|||||||
func (f *fakeRepository) CreatePaymentAccount(context.Context, store.PaymentAccountCreation) (*store.PaymentAccount, error) {
|
func (f *fakeRepository) CreatePaymentAccount(context.Context, store.PaymentAccountCreation) (*store.PaymentAccount, error) {
|
||||||
return nil, errors.New("not implemented")
|
return nil, errors.New("not implemented")
|
||||||
}
|
}
|
||||||
|
func (f *fakeRepository) UpdatePaymentAccount(context.Context, int64, store.PaymentAccountCreation) (*store.PaymentAccount, error) {
|
||||||
|
return nil, errors.New("not implemented")
|
||||||
|
}
|
||||||
func (f *fakeRepository) PaymentAccountForWithdrawal(context.Context, int64, int64, int64) (*store.PaymentAccount, error) {
|
func (f *fakeRepository) PaymentAccountForWithdrawal(context.Context, int64, int64, int64) (*store.PaymentAccount, error) {
|
||||||
return f.account, nil
|
return f.account, nil
|
||||||
}
|
}
|
||||||
@@ -155,7 +166,8 @@ func TestPaymentAccountDetailsReturnsOriginalFields(t *testing.T) {
|
|||||||
repository.details = []store.PaymentAccount{{
|
repository.details = []store.PaymentAccount{{
|
||||||
ID: 7, BankName: "中国工商银行", HolderCipher: holder, IdentityNoCipher: identity,
|
ID: 7, BankName: "中国工商银行", HolderCipher: holder, IdentityNoCipher: identity,
|
||||||
AccountNoCipher: account, DigitalRMBCodeCipher: digital,
|
AccountNoCipher: account, DigitalRMBCodeCipher: digital,
|
||||||
WechatQRURL: "/wechat.png", AlipayQRURL: "/alipay.png", VerifyStatus: 2,
|
WechatQRURL: "/wechat.png", AlipayQRURL: "/alipay.png",
|
||||||
|
WechatQRAttachmentID: 8, AlipayQRAttachmentID: 9, VerifyStatus: 2,
|
||||||
}}
|
}}
|
||||||
items, err := service.PaymentAccountDetails(context.Background(), 1, 2)
|
items, err := service.PaymentAccountDetails(context.Background(), 1, 2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -163,11 +175,34 @@ func TestPaymentAccountDetailsReturnsOriginalFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
if len(items) != 1 || items[0].HolderName != "张三" || items[0].IdentityNo != "11010119900101123X" ||
|
if len(items) != 1 || items[0].HolderName != "张三" || items[0].IdentityNo != "11010119900101123X" ||
|
||||||
items[0].AccountNo != "6222000000001234" || items[0].DigitalRMBCode != "123456" ||
|
items[0].AccountNo != "6222000000001234" || items[0].DigitalRMBCode != "123456" ||
|
||||||
items[0].WechatQRURL != "/wechat.png" || items[0].AlipayQRURL != "/alipay.png" {
|
items[0].WechatQRURL != "/wechat.png" || items[0].AlipayQRURL != "/alipay.png" ||
|
||||||
|
items[0].WechatQRAttachmentID != 8 || items[0].AlipayQRAttachmentID != 9 {
|
||||||
t.Fatalf("unexpected account details: %#v", items)
|
t.Fatalf("unexpected account details: %#v", items)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUpdatePaymentAccountEncryptsEditedFields(t *testing.T) {
|
||||||
|
repository := &paymentAccountRepository{}
|
||||||
|
service := New(repository, &redis.Client{}, config.AuthConfig{
|
||||||
|
FinanceMasterKey: "test-finance-master-key-with-at-least-32-characters",
|
||||||
|
})
|
||||||
|
result, err := service.UpdatePaymentAccount(context.Background(), 1, 2, PaymentAccountInput{
|
||||||
|
ID: 7, BankName: "中国工商银行", HolderName: "李四", IdentityNo: "11010119900101123X",
|
||||||
|
AccountNo: "6222000000001234", WechatQRAttachmentID: 8, AlipayQRAttachmentID: 9,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if result.ID != 7 || repository.updatedID != 7 || repository.updated.TenantID != 1 || repository.updated.MemberID != 2 ||
|
||||||
|
repository.updated.WechatQRAttachmentID != 8 || repository.updated.AlipayQRAttachmentID != 9 ||
|
||||||
|
repository.updated.DigitalRMBCodeCipher != "" {
|
||||||
|
t.Fatalf("unexpected updated account: result=%#v data=%#v", result, repository.updated)
|
||||||
|
}
|
||||||
|
if holder, err := service.decrypt(repository.updated.HolderCipher); err != nil || holder != "李四" {
|
||||||
|
t.Fatalf("edited holder was not encrypted correctly: %q, %v", holder, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPaymentAccountDetailsFallsBackToMasksWhenHistoricalKeyIsUnavailable(t *testing.T) {
|
func TestPaymentAccountDetailsFallsBackToMasksWhenHistoricalKeyIsUnavailable(t *testing.T) {
|
||||||
repository := &fakeRepository{details: []store.PaymentAccount{{
|
repository := &fakeRepository{details: []store.PaymentAccount{{
|
||||||
ID: 8, BankName: "中国银行", HolderCipher: "legacy-holder", HolderMask: "张*",
|
ID: 8, BankName: "中国银行", HolderCipher: "legacy-holder", HolderMask: "张*",
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (a *API) paymentAccounts(w http.ResponseWriter, r *http.Request) {
|
func (a *API) paymentAccounts(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet && r.Method != http.MethodPost {
|
if r.Method != http.MethodGet && r.Method != http.MethodPost && r.Method != http.MethodPut {
|
||||||
methodNotAllowed(w, r, http.MethodGet, http.MethodPost)
|
methodNotAllowed(w, r, http.MethodGet, http.MethodPost, http.MethodPut)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
principal, ok := a.authenticateFinanceMember(w, r)
|
principal, ok := a.authenticateFinanceMember(w, r)
|
||||||
@@ -33,7 +33,13 @@ func (a *API) paymentAccounts(w http.ResponseWriter, r *http.Request) {
|
|||||||
if !decodeBody(w, r, &body) {
|
if !decodeBody(w, r, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result, err := a.finance.CreatePaymentAccount(r.Context(), principal.TenantID, principal.MemberID, body)
|
var result *finance.PaymentAccount
|
||||||
|
var err error
|
||||||
|
if r.Method == http.MethodPut {
|
||||||
|
result, err = a.finance.UpdatePaymentAccount(r.Context(), principal.TenantID, principal.MemberID, body)
|
||||||
|
} else {
|
||||||
|
result, err = a.finance.CreatePaymentAccount(r.Context(), principal.TenantID, principal.MemberID, body)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeFinanceError(w, r, err)
|
writeFinanceError(w, r, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -251,6 +251,59 @@ func (s *Store) CreatePaymentAccount(ctx context.Context, input PaymentAccountCr
|
|||||||
VerifyStatus: 2, IsDefault: isDefault}, nil
|
VerifyStatus: 2, IsDefault: isDefault}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpdatePaymentAccount(ctx context.Context, accountID int64, input PaymentAccountCreation) (*PaymentAccount, error) {
|
||||||
|
tx, err := s.db.BeginTxx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
var isDefault int
|
||||||
|
err = tx.GetContext(ctx, &isDefault, `SELECT is_default FROM hg_app_member_payment_account
|
||||||
|
WHERE id=? AND tenant_id=? AND member_id=? AND deleted_at IS NULL LIMIT 1 FOR UPDATE`,
|
||||||
|
accountID, input.TenantID, input.MemberID)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, ErrPaymentAccountInvalid
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var attachmentCount int
|
||||||
|
err = tx.GetContext(ctx, &attachmentCount, `SELECT COUNT(*) FROM hg_sys_attachment
|
||||||
|
WHERE id IN (?,?) AND app_id='olons_server' AND member_id=? AND status=1 AND mime_type LIKE 'image/%'`,
|
||||||
|
input.WechatQRAttachmentID, input.AlipayQRAttachmentID, input.MemberID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if attachmentCount != 2 {
|
||||||
|
return nil, ErrAttachmentInvalid
|
||||||
|
}
|
||||||
|
_, err = tx.ExecContext(ctx, `UPDATE hg_app_member_payment_account SET
|
||||||
|
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=2,updated_by=?,updated_at=NOW() WHERE id=? AND tenant_id=? AND member_id=? AND deleted_at IS NULL`,
|
||||||
|
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, input.MemberID,
|
||||||
|
accountID, input.TenantID, input.MemberID)
|
||||||
|
if err != nil {
|
||||||
|
if isDuplicate(err) {
|
||||||
|
return nil, ErrPaymentAccountExists
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &PaymentAccount{ID: accountID, AccountType: input.AccountType, BankCode: input.BankCode,
|
||||||
|
BankName: input.BankName, HolderMask: input.HolderMask, IdentityNoMask: input.IdentityNoMask,
|
||||||
|
AccountNoMask: input.AccountNoMask, DigitalRMBCodeMask: input.DigitalRMBCodeMask,
|
||||||
|
WechatQRAttachmentID: input.WechatQRAttachmentID,
|
||||||
|
AlipayQRAttachmentID: input.AlipayQRAttachmentID,
|
||||||
|
VerifyStatus: 2, IsDefault: isDefault}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func nullableString(value string) any {
|
func nullableString(value string) any {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1839,36 +1839,10 @@ body {
|
|||||||
.m-profile-info > .m-form-error {
|
.m-profile-info > .m-form-error {
|
||||||
margin: -10px 6px 0;
|
margin: -10px 6px 0;
|
||||||
}
|
}
|
||||||
.m-bound-accounts {
|
.m-bound-account-hint {
|
||||||
display: grid;
|
margin: 16px 20px;
|
||||||
gap: 10px;
|
|
||||||
margin: 18px 0;
|
|
||||||
}
|
|
||||||
.m-bound-accounts h2,
|
|
||||||
.m-bank-form-title {
|
|
||||||
margin: 0 20px 4px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.m-bound-account {
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
margin: 0 12px;
|
|
||||||
padding: 16px;
|
|
||||||
color: var(--m-ink);
|
|
||||||
}
|
|
||||||
.m-bound-account > div {
|
|
||||||
display: grid;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
.m-bound-account span {
|
|
||||||
color: var(--m-muted);
|
color: var(--m-muted);
|
||||||
}
|
font-size: 14px;
|
||||||
.m-bound-account strong {
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.m-bank-form-title {
|
|
||||||
padding: 12px 0 0;
|
|
||||||
}
|
}
|
||||||
.m-profile-edit {
|
.m-profile-edit {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -50,20 +50,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</AsyncState>
|
</AsyncState>
|
||||||
|
|
||||||
<div v-else-if="formType === 'bank'">
|
<AsyncState v-else-if="formType === 'bank'" :loading="bankLoading" :error="bankLoadError" @retry="loadPaymentAccounts">
|
||||||
<section v-if="paymentAccounts.length" class="m-bound-accounts">
|
|
||||||
<h2>已绑定的取出信息</h2>
|
|
||||||
<div v-for="account in paymentAccounts" :key="account.id" class="m-bound-account m-card">
|
|
||||||
<p v-if="account.masked" class="m-bound-account-hint">历史资料仅展示脱敏信息,重新绑定后可查看完整资料</p>
|
|
||||||
<div><span>姓名</span><strong>{{ account.holder_name }}</strong></div>
|
|
||||||
<div><span>身份证号</span><strong>{{ account.identity_no || '未填写' }}</strong></div>
|
|
||||||
<div><span>银行名</span><strong>{{ account.bank_name }}</strong></div>
|
|
||||||
<div><span>银行账户</span><strong>{{ account.account_no }}</strong></div>
|
|
||||||
<div v-if="account.digital_rmb_code"><span>人民币编码</span><strong>{{ account.digital_rmb_code }}</strong></div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<form class="m-plain-form m-bank-form" @submit.prevent="saveBank">
|
<form class="m-plain-form m-bank-form" @submit.prevent="saveBank">
|
||||||
<h2 v-if="paymentAccounts.length" class="m-bank-form-title">添加银行卡</h2>
|
<p v-if="existingAccount?.masked" class="m-bound-account-hint">历史资料无法完整读取,请重新填写姓名、身份证号和银行卡号后保存</p>
|
||||||
<label
|
<label
|
||||||
><span>姓名</span><input v-model.trim="bank.holder_name" placeholder="请输入姓名"
|
><span>姓名</span><input v-model.trim="bank.holder_name" placeholder="请输入姓名"
|
||||||
/></label>
|
/></label>
|
||||||
@@ -92,7 +81,7 @@
|
|||||||
<label class="m-qr-upload">
|
<label class="m-qr-upload">
|
||||||
<input type="file" accept="image/jpeg,image/png,image/webp" @change="uploadQRCode('wechat', $event)" />
|
<input type="file" accept="image/jpeg,image/png,image/webp" @change="uploadQRCode('wechat', $event)" />
|
||||||
<img v-if="qr.wechat.url" :src="safeImage(qr.wechat.url)" alt="微信二维码" />
|
<img v-if="qr.wechat.url" :src="safeImage(qr.wechat.url)" alt="微信二维码" />
|
||||||
<span v-else><n-icon :component="CameraOutline" /><b>{{ qr.wechat.uploading ? '上传中...' : '点击上传' }}</b></span>
|
<span v-else><n-icon :component="CameraOutline" /><b>{{ qr.wechat.uploading ? '上传中...' : bank.wechat_qr_attachment_id ? '已上传,点击更换' : '点击上传' }}</b></span>
|
||||||
</label>
|
</label>
|
||||||
</section>
|
</section>
|
||||||
<section class="m-qr-field">
|
<section class="m-qr-field">
|
||||||
@@ -100,15 +89,15 @@
|
|||||||
<label class="m-qr-upload">
|
<label class="m-qr-upload">
|
||||||
<input type="file" accept="image/jpeg,image/png,image/webp" @change="uploadQRCode('alipay', $event)" />
|
<input type="file" accept="image/jpeg,image/png,image/webp" @change="uploadQRCode('alipay', $event)" />
|
||||||
<img v-if="qr.alipay.url" :src="safeImage(qr.alipay.url)" alt="支付宝二维码" />
|
<img v-if="qr.alipay.url" :src="safeImage(qr.alipay.url)" alt="支付宝二维码" />
|
||||||
<span v-else><n-icon :component="CameraOutline" /><b>{{ qr.alipay.uploading ? '上传中...' : '点击上传' }}</b></span>
|
<span v-else><n-icon :component="CameraOutline" /><b>{{ qr.alipay.uploading ? '上传中...' : bank.alipay_qr_attachment_id ? '已上传,点击更换' : '点击上传' }}</b></span>
|
||||||
</label>
|
</label>
|
||||||
</section>
|
</section>
|
||||||
<p v-if="error" class="m-form-error">{{ error }}</p
|
<p v-if="error" class="m-form-error">{{ error }}</p
|
||||||
><button class="m-primary-btn" :disabled="submitting">{{
|
><button class="m-primary-btn" :disabled="submitting || qr.wechat.uploading || qr.alipay.uploading">{{
|
||||||
submitting ? '提交中...' : '确认绑定'
|
submitting ? '提交中...' : existingAccount ? '保存修改' : '确认绑定'
|
||||||
}}</button>
|
}}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</AsyncState>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
v-else-if="formType === 'tradePassword'"
|
v-else-if="formType === 'tradePassword'"
|
||||||
@@ -265,6 +254,8 @@
|
|||||||
String(route.meta.title || '')
|
String(route.meta.title || '')
|
||||||
);
|
);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const bankLoading = ref(false);
|
||||||
|
const bankLoadError = ref('');
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const avatarError = ref('');
|
const avatarError = ref('');
|
||||||
@@ -279,9 +270,11 @@
|
|||||||
digital_rmb_code: string;
|
digital_rmb_code: string;
|
||||||
wechat_qr_url: string;
|
wechat_qr_url: string;
|
||||||
alipay_qr_url: string;
|
alipay_qr_url: string;
|
||||||
|
wechat_qr_attachment_id: number;
|
||||||
|
alipay_qr_attachment_id: number;
|
||||||
masked: boolean;
|
masked: boolean;
|
||||||
}
|
}
|
||||||
const paymentAccounts = ref<PaymentAccountDetail[]>([]);
|
const existingAccount = ref<PaymentAccountDetail | null>(null);
|
||||||
let paymentAccountsLoadVersion = 0;
|
let paymentAccountsLoadVersion = 0;
|
||||||
const content = ref<any>();
|
const content = ref<any>();
|
||||||
const resetPassword = ref('');
|
const resetPassword = ref('');
|
||||||
@@ -346,13 +339,33 @@
|
|||||||
}
|
}
|
||||||
async function loadPaymentAccounts() {
|
async function loadPaymentAccounts() {
|
||||||
const version = ++paymentAccountsLoadVersion;
|
const version = ++paymentAccountsLoadVersion;
|
||||||
|
bankLoading.value = true;
|
||||||
|
bankLoadError.value = '';
|
||||||
error.value = '';
|
error.value = '';
|
||||||
paymentAccounts.value = [];
|
existingAccount.value = null;
|
||||||
|
Object.assign(bank, { bank_name: '', holder_name: '', identity_no: '', account_no: '', digital_rmb_code: '',
|
||||||
|
wechat_qr_attachment_id: 0, alipay_qr_attachment_id: 0 });
|
||||||
|
qr.wechat.url = '';
|
||||||
|
qr.alipay.url = '';
|
||||||
try {
|
try {
|
||||||
const accounts = await request<PaymentAccountDetail[]>({ url: '/v1/payment-accounts/details' });
|
const accounts = await request<PaymentAccountDetail[]>({ url: '/v1/payment-accounts/details' });
|
||||||
if (version === paymentAccountsLoadVersion && formType.value === 'bank') paymentAccounts.value = accounts;
|
if (version !== paymentAccountsLoadVersion || formType.value !== 'bank') return;
|
||||||
|
const account = accounts[0];
|
||||||
|
if (!account) return;
|
||||||
|
existingAccount.value = account;
|
||||||
|
bank.bank_name = account.bank_name;
|
||||||
|
bank.wechat_qr_attachment_id = account.wechat_qr_attachment_id;
|
||||||
|
bank.alipay_qr_attachment_id = account.alipay_qr_attachment_id;
|
||||||
|
if (!account.masked) {
|
||||||
|
bank.holder_name = account.holder_name;
|
||||||
|
bank.identity_no = account.identity_no;
|
||||||
|
bank.account_no = account.account_no;
|
||||||
|
bank.digital_rmb_code = account.digital_rmb_code;
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (version === paymentAccountsLoadVersion && formType.value === 'bank') error.value = errorText(err);
|
if (version === paymentAccountsLoadVersion && formType.value === 'bank') bankLoadError.value = errorText(err);
|
||||||
|
} finally {
|
||||||
|
if (version === paymentAccountsLoadVersion) bankLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function saveBank() {
|
async function saveBank() {
|
||||||
@@ -381,8 +394,9 @@
|
|||||||
}
|
}
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
await request({ url: '/v1/payment-accounts', method: 'POST', data: bank });
|
await request({ url: '/v1/payment-accounts', method: existingAccount.value ? 'PUT' : 'POST',
|
||||||
window.$message?.success('银行卡绑定成功');
|
data: { ...bank, id: existingAccount.value?.id } });
|
||||||
|
window.$message?.success(existingAccount.value ? '绑定信息已更新' : '银行卡绑定成功');
|
||||||
await router.replace(route.query.from === 'profile' ? '/app/profile/info' : '/app/profile/withdrawal');
|
await router.replace(route.query.from === 'profile' ? '/app/profile/info' : '/app/profile/withdrawal');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = errorText(err);
|
error.value = errorText(err);
|
||||||
|
|||||||
Reference in New Issue
Block a user