feat: show own withdrawal account details and enable avatar upload
This commit is contained in:
@@ -42,6 +42,7 @@ type Repository interface {
|
||||
CurrentSiteConfig(context.Context, int64) (json.RawMessage, error)
|
||||
AccountSecurity(context.Context, int64, int64) (*store.AccountSecurity, error)
|
||||
PaymentAccounts(context.Context, int64, int64) ([]store.PaymentAccount, error)
|
||||
PaymentAccountDetails(context.Context, int64, int64) ([]store.PaymentAccount, error)
|
||||
CreatePaymentAccount(context.Context, store.PaymentAccountCreation) (*store.PaymentAccount, error)
|
||||
PaymentAccountForWithdrawal(context.Context, int64, int64, int64) (*store.PaymentAccount, error)
|
||||
ActiveWithdrawalChannel(context.Context, int64, []int) (*store.WithdrawalChannel, error)
|
||||
@@ -92,6 +93,19 @@ type PaymentAccount struct {
|
||||
UpdatedAt string `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
type PaymentAccountDetail struct {
|
||||
ID int64 `json:"id"`
|
||||
BankName string `json:"bank_name"`
|
||||
HolderName string `json:"holder_name"`
|
||||
IdentityNo string `json:"identity_no"`
|
||||
AccountNo string `json:"account_no"`
|
||||
DigitalRMBCode string `json:"digital_rmb_code"`
|
||||
WechatQRURL string `json:"wechat_qr_url"`
|
||||
AlipayQRURL string `json:"alipay_qr_url"`
|
||||
VerifyStatus string `json:"verify_status"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
type QuoteInput struct {
|
||||
PaymentAccountID int64 `json:"payment_account_id"`
|
||||
Amount int64 `json:"amount"`
|
||||
@@ -282,6 +296,48 @@ func (s *Service) PaymentAccounts(ctx context.Context, tenantID, memberID int64)
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Service) PaymentAccountDetails(ctx context.Context, tenantID, memberID int64) ([]PaymentAccountDetail, error) {
|
||||
if !s.ready || s.repository == nil {
|
||||
return nil, ErrUnavailable
|
||||
}
|
||||
rows, err := s.repository.PaymentAccountDetails(ctx, tenantID, memberID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]PaymentAccountDetail, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
holderName, err := s.decrypt(row.HolderCipher)
|
||||
if err != nil {
|
||||
return nil, store.ErrPaymentAccountInvalid
|
||||
}
|
||||
accountNo, err := s.decrypt(row.AccountNoCipher)
|
||||
if err != nil {
|
||||
return nil, store.ErrPaymentAccountInvalid
|
||||
}
|
||||
identityNo := ""
|
||||
if row.IdentityNoCipher != "" {
|
||||
identityNo, err = s.decrypt(row.IdentityNoCipher)
|
||||
if err != nil {
|
||||
return nil, store.ErrPaymentAccountInvalid
|
||||
}
|
||||
}
|
||||
digitalRMBCode := ""
|
||||
if row.DigitalRMBCodeCipher != "" {
|
||||
digitalRMBCode, err = s.decrypt(row.DigitalRMBCodeCipher)
|
||||
if err != nil {
|
||||
return nil, store.ErrPaymentAccountInvalid
|
||||
}
|
||||
}
|
||||
items = append(items, PaymentAccountDetail{
|
||||
ID: row.ID, BankName: row.BankName, HolderName: holderName,
|
||||
IdentityNo: identityNo, AccountNo: accountNo, DigitalRMBCode: digitalRMBCode,
|
||||
WechatQRURL: row.WechatQRURL, AlipayQRURL: row.AlipayQRURL,
|
||||
VerifyStatus: verifyStatusName(row.VerifyStatus), IsDefault: row.IsDefault == 1,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Service) CreatePaymentAccount(ctx context.Context, tenantID, memberID int64, input PaymentAccountInput) (*PaymentAccount, error) {
|
||||
if !s.ready {
|
||||
return nil, ErrUnavailable
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
type fakeRepository struct {
|
||||
config json.RawMessage
|
||||
account *store.PaymentAccount
|
||||
details []store.PaymentAccount
|
||||
channel *store.WithdrawalChannel
|
||||
wallet *store.WithdrawalWallet
|
||||
orders []store.WithdrawalOrder
|
||||
@@ -43,6 +44,9 @@ func (f *fakeRepository) AccountSecurity(context.Context, int64, int64) (*store.
|
||||
func (f *fakeRepository) PaymentAccounts(context.Context, int64, int64) ([]store.PaymentAccount, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakeRepository) PaymentAccountDetails(context.Context, int64, int64) ([]store.PaymentAccount, error) {
|
||||
return f.details, nil
|
||||
}
|
||||
func (f *fakeRepository) CreatePaymentAccount(context.Context, store.PaymentAccountCreation) (*store.PaymentAccount, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
@@ -139,6 +143,31 @@ func TestNormalizePaymentAccountAllowsEmptyDigitalRMBCode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaymentAccountDetailsReturnsOriginalFields(t *testing.T) {
|
||||
repository := &fakeRepository{}
|
||||
service := New(repository, &redis.Client{}, config.AuthConfig{
|
||||
FinanceMasterKey: "test-finance-master-key-with-at-least-32-characters",
|
||||
})
|
||||
holder, _ := service.encrypt("张三")
|
||||
identity, _ := service.encrypt("11010119900101123X")
|
||||
account, _ := service.encrypt("6222000000001234")
|
||||
digital, _ := service.encrypt("123456")
|
||||
repository.details = []store.PaymentAccount{{
|
||||
ID: 7, BankName: "中国工商银行", HolderCipher: holder, IdentityNoCipher: identity,
|
||||
AccountNoCipher: account, DigitalRMBCodeCipher: digital,
|
||||
WechatQRURL: "/wechat.png", AlipayQRURL: "/alipay.png", VerifyStatus: 2,
|
||||
}}
|
||||
items, err := service.PaymentAccountDetails(context.Background(), 1, 2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(items) != 1 || items[0].HolderName != "张三" || items[0].IdentityNo != "11010119900101123X" ||
|
||||
items[0].AccountNo != "6222000000001234" || items[0].DigitalRMBCode != "123456" ||
|
||||
items[0].WechatQRURL != "/wechat.png" || items[0].AlipayQRURL != "/alipay.png" {
|
||||
t.Fatalf("unexpected account details: %#v", items)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatePaymentAccountDoesNotStoreEmptyDigitalRMBCode(t *testing.T) {
|
||||
repository := &paymentAccountRepository{}
|
||||
service := New(repository, &redis.Client{}, config.AuthConfig{
|
||||
|
||||
@@ -55,6 +55,7 @@ func (a *API) Register(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/v1/manual-products", a.manualProducts)
|
||||
mux.HandleFunc("/v1/manual-products/", a.manualProduct)
|
||||
mux.HandleFunc("/v1/payment-accounts", a.paymentAccounts)
|
||||
mux.HandleFunc("/v1/payment-accounts/details", a.paymentAccountDetails)
|
||||
mux.HandleFunc("/v1/attachments/images", a.uploadImage)
|
||||
mux.HandleFunc("/v1/wallet/ledgers", a.walletLedgers)
|
||||
mux.HandleFunc("/v1/recharge/channels", a.rechargeChannels)
|
||||
|
||||
@@ -41,6 +41,24 @@ func (a *API) paymentAccounts(w http.ResponseWriter, r *http.Request) {
|
||||
writeSuccess(w, r, result)
|
||||
}
|
||||
|
||||
func (a *API) paymentAccountDetails(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
methodNotAllowed(w, r, http.MethodGet)
|
||||
return
|
||||
}
|
||||
principal, ok := a.authenticateFinanceMember(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
result, err := a.finance.PaymentAccountDetails(r.Context(), principal.TenantID, principal.MemberID)
|
||||
if err != nil {
|
||||
writeFinanceError(w, r, err)
|
||||
return
|
||||
}
|
||||
writeSuccess(w, r, result)
|
||||
}
|
||||
|
||||
func (a *API) walletLedgers(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
methodNotAllowed(w, r, http.MethodGet)
|
||||
|
||||
@@ -30,6 +30,8 @@ type PaymentAccount struct {
|
||||
QRAttachmentID int64 `db:"qr_attachment_id"`
|
||||
WechatQRAttachmentID int64 `db:"wechat_qr_attachment_id"`
|
||||
AlipayQRAttachmentID int64 `db:"alipay_qr_attachment_id"`
|
||||
WechatQRURL string `db:"wechat_qr_url"`
|
||||
AlipayQRURL string `db:"alipay_qr_url"`
|
||||
VerifyStatus int `db:"verify_status"`
|
||||
IsDefault int `db:"is_default"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
@@ -154,6 +156,29 @@ func (s *Store) PaymentAccounts(ctx context.Context, tenantID, memberID int64) (
|
||||
return items, err
|
||||
}
|
||||
|
||||
// PaymentAccountDetails returns encrypted fields only for the authenticated account owner.
|
||||
func (s *Store) PaymentAccountDetails(ctx context.Context, tenantID, memberID int64) ([]PaymentAccount, error) {
|
||||
items := make([]PaymentAccount, 0)
|
||||
err := s.db.SelectContext(ctx, &items, `SELECT a.id,a.account_type,a.bank_code,a.bank_name,
|
||||
COALESCE(a.holder_cipher,'') AS holder_cipher,
|
||||
COALESCE(a.identity_no_cipher,'') AS identity_no_cipher,
|
||||
COALESCE(a.account_no_cipher,'') AS account_no_cipher,
|
||||
COALESCE(a.digital_rmb_code_cipher,'') AS digital_rmb_code_cipher,
|
||||
COALESCE(a.wechat_qr_attachment_id,0) AS wechat_qr_attachment_id,
|
||||
COALESCE(a.alipay_qr_attachment_id,0) AS alipay_qr_attachment_id,
|
||||
COALESCE(wechat.file_url,'') AS wechat_qr_url,
|
||||
COALESCE(alipay.file_url,'') AS alipay_qr_url,
|
||||
a.verify_status,a.is_default
|
||||
FROM hg_app_member_payment_account a
|
||||
LEFT JOIN hg_sys_attachment wechat ON wechat.id=a.wechat_qr_attachment_id
|
||||
AND wechat.member_id=a.member_id AND wechat.app_id='olons_server' AND wechat.status=1
|
||||
LEFT JOIN hg_sys_attachment alipay ON alipay.id=a.alipay_qr_attachment_id
|
||||
AND alipay.member_id=a.member_id AND alipay.app_id='olons_server' AND alipay.status=1
|
||||
WHERE a.tenant_id=? AND a.member_id=? AND a.deleted_at IS NULL
|
||||
ORDER BY a.is_default ASC,a.id DESC`, tenantID, memberID)
|
||||
return items, err
|
||||
}
|
||||
|
||||
func (s *Store) CreatePaymentAccount(ctx context.Context, input PaymentAccountCreation) (*PaymentAccount, error) {
|
||||
tx, err := s.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user