fix: allow optional digital RMB code and inline payment password setup

This commit is contained in:
richjohnrambo
2026-10-15 23:03:59 +08:00
parent 6e223e537b
commit 66121f48e9
8 changed files with 184 additions and 36 deletions
+2 -2
View File
@@ -44,14 +44,14 @@ func (r *profileRepository) UpdateTradePassword(_ context.Context, _, _ int64, _
func TestProfileMapsMemberCategory(t *testing.T) {
repository := &profileRepository{overview: &store.MemberOverview{
MemberID: 7, MemberNo: "M0007", Username: "robot", MemberStatus: 1, MemberCategory: 2,
Mobile: "13800138000", Currency: "CNY", WalletStatus: 1,
Mobile: "13800138000", Currency: "CNY", WalletStatus: 1, HasTradePassword: 1,
}}
service := &Service{repository: repository, ready: true}
profile, err := service.Profile(context.Background(), 0, 7)
if err != nil {
t.Fatal(err)
}
if profile.Phone != "13800138000" || profile.MemberCategory != "fake" || profile.MemberStatus != "active" || profile.Wallet.Status != "active" {
if profile.Phone != "13800138000" || profile.MemberCategory != "fake" || profile.MemberStatus != "active" || profile.Wallet.Status != "active" || !profile.HasTradePassword {
t.Fatalf("unexpected profile states: %+v", profile)
}
}
+13 -11
View File
@@ -78,17 +78,18 @@ type Result struct {
}
type Profile struct {
MemberID int64 `json:"member_id"`
MemberNo string `json:"member_no"`
Username string `json:"username"`
Phone string `json:"phone"`
AvatarURL string `json:"avatar_url"`
InviteCode string `json:"invite_code"`
Level ProfileLevel `json:"level"`
MemberStatus string `json:"member_status"`
MemberCategory string `json:"member_category"`
CreditScore int `json:"credit_score"`
Wallet ProfileWallet `json:"wallet"`
MemberID int64 `json:"member_id"`
MemberNo string `json:"member_no"`
Username string `json:"username"`
Phone string `json:"phone"`
AvatarURL string `json:"avatar_url"`
InviteCode string `json:"invite_code"`
HasTradePassword bool `json:"has_trade_password"`
Level ProfileLevel `json:"level"`
MemberStatus string `json:"member_status"`
MemberCategory string `json:"member_category"`
CreditScore int `json:"credit_score"`
Wallet ProfileWallet `json:"wallet"`
}
type ProfileLevel struct {
@@ -268,6 +269,7 @@ func (s *Service) Profile(ctx context.Context, tenantID, memberID int64) (*Profi
return &Profile{
MemberID: row.MemberID, MemberNo: row.MemberNo, Username: row.Username,
Phone: row.Mobile, AvatarURL: row.AvatarURL, InviteCode: row.InviteCode,
HasTradePassword: row.HasTradePassword != 0,
Level: ProfileLevel{ID: row.LevelID, Code: row.LevelCode, Name: row.LevelName,
IconURL: row.LevelIconURL, CommissionRate: row.CommissionRate},
MemberStatus: memberStatusName(row.MemberStatus), MemberCategory: memberCategoryName(row.MemberCategory),
+11 -6
View File
@@ -301,9 +301,14 @@ 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
digitalRMBCodeCipher, digitalRMBCodeHash, digitalRMBCodeMask := "", "", ""
if input.DigitalRMBCode != "" {
digitalRMBCodeCipher, err = s.encrypt(input.DigitalRMBCode)
if err != nil {
return nil, err
}
digitalRMBCodeHash = s.lookupHash(input.DigitalRMBCode)
digitalRMBCodeMask = maskAccount(input.DigitalRMBCode)
}
row, err := s.repository.CreatePaymentAccount(ctx, store.PaymentAccountCreation{
TenantID: tenantID, MemberID: memberID, AccountType: 1, BankCode: "BANK", BankName: input.BankName,
@@ -311,8 +316,8 @@ func (s *Service) CreatePaymentAccount(ctx context.Context, tenantID, memberID i
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),
DigitalRMBCodeCipher: digitalRMBCodeCipher, DigitalRMBCodeHash: digitalRMBCodeHash,
DigitalRMBCodeMask: digitalRMBCodeMask,
WechatQRAttachmentID: input.WechatQRAttachmentID,
AlipayQRAttachmentID: input.AlipayQRAttachmentID,
})
@@ -759,7 +764,7 @@ func normalizePaymentAccount(input *PaymentAccountInput) error {
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.DigitalRMBCode != "" && (len(input.DigitalRMBCode) < 6 || len(input.DigitalRMBCode) > 32 || !allDigits(input.DigitalRMBCode))) ||
input.WechatQRAttachmentID <= 0 || input.AlipayQRAttachmentID <= 0 ||
input.WechatQRAttachmentID == input.AlipayQRAttachmentID {
return ErrAccountFieldsInvalid
+41
View File
@@ -9,6 +9,7 @@ import (
"git.118im.com/im_team/olons_server/internal/config"
"git.118im.com/im_team/olons_server/internal/store"
"github.com/redis/go-redis/v9"
)
type fakeRepository struct {
@@ -23,6 +24,16 @@ type fakeRepository struct {
createdRecharge store.RechargeCreation
}
type paymentAccountRepository struct {
fakeRepository
created store.PaymentAccountCreation
}
func (f *paymentAccountRepository) CreatePaymentAccount(_ context.Context, input store.PaymentAccountCreation) (*store.PaymentAccount, error) {
f.created = input
return &store.PaymentAccount{ID: 1, AccountType: input.AccountType}, nil
}
func (f *fakeRepository) CurrentSiteConfig(context.Context, int64) (json.RawMessage, error) {
return f.config, nil
}
@@ -115,6 +126,36 @@ func TestNormalizePaymentAccount(t *testing.T) {
}
}
func TestNormalizePaymentAccountAllowsEmptyDigitalRMBCode(t *testing.T) {
input := PaymentAccountInput{BankName: "中国工商银行", HolderName: "张三",
IdentityNo: "11010119900101123X", AccountNo: "6222000000001234",
WechatQRAttachmentID: 8, AlipayQRAttachmentID: 9}
if err := normalizePaymentAccount(&input); err != nil {
t.Fatalf("empty digital RMB code should be optional: %v", err)
}
input.DigitalRMBCode = "1234"
if err := normalizePaymentAccount(&input); !errors.Is(err, ErrAccountFieldsInvalid) {
t.Fatalf("short non-empty digital RMB code should be rejected: %v", err)
}
}
func TestCreatePaymentAccountDoesNotStoreEmptyDigitalRMBCode(t *testing.T) {
repository := &paymentAccountRepository{}
service := New(repository, &redis.Client{}, config.AuthConfig{
FinanceMasterKey: "test-finance-master-key-with-at-least-32-characters",
})
_, err := service.CreatePaymentAccount(context.Background(), 0, 1, PaymentAccountInput{
BankName: "中国工商银行", HolderName: "张三", IdentityNo: "11010119900101123X",
AccountNo: "6222000000001234", WechatQRAttachmentID: 8, AlipayQRAttachmentID: 9,
})
if err != nil {
t.Fatal(err)
}
if repository.created.DigitalRMBCodeCipher != "" || repository.created.DigitalRMBCodeHash != "" || repository.created.DigitalRMBCodeMask != "" {
t.Fatalf("empty digital RMB code must not be encrypted, hashed or masked: %#v", repository.created)
}
}
func TestMaskAccountShortValueDoesNotLeak(t *testing.T) {
if got := maskAccount("abcde"); got != "a***e" {
t.Fatalf("unexpected mask %q", got)
+3 -1
View File
@@ -13,6 +13,7 @@ type MemberOverview struct {
Username string `db:"username"`
Mobile string `db:"mobile"`
InviteCode string `db:"invite_code"`
HasTradePassword int `db:"has_trade_password"`
MemberStatus int `db:"member_status"`
MemberCategory int `db:"member_category"`
CreditScore int `db:"credit_score"`
@@ -49,7 +50,8 @@ func (s *Store) MemberOverview(ctx context.Context, tenantID, memberID int64) (*
}
var row MemberOverview
err := s.db.GetContext(ctx, &row, `SELECT m.tenant_id,m.id AS member_id,m.member_no,m.username,m.mobile,
m.invite_code,m.member_status,m.risk_status AS member_category,m.credit_score,
m.invite_code,CASE WHEN m.trade_password_hash <> '' THEN 1 ELSE 0 END AS has_trade_password,
m.member_status,m.risk_status AS member_category,m.credit_score,
COALESCE(p.avatar_attachment_id,0) AS avatar_attachment_id,COALESCE(a.file_url,'') AS avatar_url,
COALESCE(l.id,0) AS level_id,COALESCE(l.level_code,'') AS level_code,
COALESCE(l.name,'') AS level_name,COALESCE(l.icon_url,'') AS level_icon_url,