fix: align customer payment encryption and protect original values
Deploy olons-admin production / deploy (push) Successful in 9m19s

This commit is contained in:
richjohnrambo
2026-10-22 22:33:42 +08:00
parent 70ef3d9d04
commit 3d69d68a5d
8 changed files with 371 additions and 46 deletions
+13 -8
View File
@@ -458,6 +458,7 @@ func (s *sSysAppMember) PaymentList(ctx context.Context, in *sysin.AppMemberPaym
if row.HolderCipher != "" { if row.HolderCipher != "" {
plain, decryptErr := encrypt.DecryptField(ctx, row.HolderCipher) plain, decryptErr := encrypt.DecryptField(ctx, row.HolderCipher)
if decryptErr != nil { if decryptErr != nil {
row.SensitiveUnavailable = true
g.Log().Warningf(ctx, "收款人姓名解密失败,回退为脱敏值 memberId:%d accountId:%d err:%+v", row.MemberId, row.Id, decryptErr) g.Log().Warningf(ctx, "收款人姓名解密失败,回退为脱敏值 memberId:%d accountId:%d err:%+v", row.MemberId, row.Id, decryptErr)
row.Holder = row.HolderMask row.Holder = row.HolderMask
} else { } else {
@@ -469,6 +470,7 @@ func (s *sSysAppMember) PaymentList(ctx context.Context, in *sysin.AppMemberPaym
if row.IdentityNoCipher != "" { if row.IdentityNoCipher != "" {
plain, decryptErr := encrypt.DecryptField(ctx, row.IdentityNoCipher) plain, decryptErr := encrypt.DecryptField(ctx, row.IdentityNoCipher)
if decryptErr != nil { if decryptErr != nil {
row.SensitiveUnavailable = true
g.Log().Warningf(ctx, "收款人证件号解密失败,回退为脱敏值 memberId:%d accountId:%d err:%+v", row.MemberId, row.Id, decryptErr) g.Log().Warningf(ctx, "收款人证件号解密失败,回退为脱敏值 memberId:%d accountId:%d err:%+v", row.MemberId, row.Id, decryptErr)
row.IdentityNo = row.IdentityNoMask row.IdentityNo = row.IdentityNoMask
} else { } else {
@@ -480,6 +482,7 @@ func (s *sSysAppMember) PaymentList(ctx context.Context, in *sysin.AppMemberPaym
if row.MobileCipher != "" { if row.MobileCipher != "" {
plain, decryptErr := encrypt.DecryptField(ctx, row.MobileCipher) plain, decryptErr := encrypt.DecryptField(ctx, row.MobileCipher)
if decryptErr != nil { if decryptErr != nil {
row.SensitiveUnavailable = true
g.Log().Warningf(ctx, "收款手机号解密失败,回退为脱敏值 memberId:%d accountId:%d err:%+v", row.MemberId, row.Id, decryptErr) g.Log().Warningf(ctx, "收款手机号解密失败,回退为脱敏值 memberId:%d accountId:%d err:%+v", row.MemberId, row.Id, decryptErr)
row.Mobile = row.MobileMask row.Mobile = row.MobileMask
} else { } else {
@@ -491,6 +494,7 @@ func (s *sSysAppMember) PaymentList(ctx context.Context, in *sysin.AppMemberPaym
if row.AccountNoCipher != "" { if row.AccountNoCipher != "" {
plain, decryptErr := encrypt.DecryptField(ctx, row.AccountNoCipher) plain, decryptErr := encrypt.DecryptField(ctx, row.AccountNoCipher)
if decryptErr != nil { if decryptErr != nil {
row.SensitiveUnavailable = true
g.Log().Warningf(ctx, "收款账号解密失败,回退为脱敏值 memberId:%d accountId:%d err:%+v", row.MemberId, row.Id, decryptErr) g.Log().Warningf(ctx, "收款账号解密失败,回退为脱敏值 memberId:%d accountId:%d err:%+v", row.MemberId, row.Id, decryptErr)
row.AccountNo = row.AccountNoMask row.AccountNo = row.AccountNoMask
} else { } else {
@@ -502,6 +506,7 @@ func (s *sSysAppMember) PaymentList(ctx context.Context, in *sysin.AppMemberPaym
if row.DigitalRmbCodeCipher != "" { if row.DigitalRmbCodeCipher != "" {
plain, decryptErr := encrypt.DecryptField(ctx, row.DigitalRmbCodeCipher) plain, decryptErr := encrypt.DecryptField(ctx, row.DigitalRmbCodeCipher)
if decryptErr != nil { if decryptErr != nil {
row.SensitiveUnavailable = true
g.Log().Warningf(ctx, "数字钱包编码解密失败,回退为脱敏值 memberId:%d accountId:%d err:%+v", row.MemberId, row.Id, decryptErr) g.Log().Warningf(ctx, "数字钱包编码解密失败,回退为脱敏值 memberId:%d accountId:%d err:%+v", row.MemberId, row.Id, decryptErr)
row.DigitalRMBCode = row.DigitalRMBCodeMask row.DigitalRMBCode = row.DigitalRMBCodeMask
} else { } else {
@@ -534,7 +539,7 @@ func (s *sSysAppMember) PaymentEdit(ctx context.Context, in *sysin.AppMemberPaym
data[c.AlipayQrAttachmentId] = in.AlipayQrAttachmentId data[c.AlipayQrAttachmentId] = in.AlipayQrAttachmentId
} }
if in.Holder != "" { if in.Holder != "" {
v, e := encrypt.EncryptField(ctx, in.Holder) v, e := encrypt.EncryptPaymentField(ctx, in.Holder)
if e != nil { if e != nil {
return e return e
} }
@@ -542,7 +547,7 @@ func (s *sSysAppMember) PaymentEdit(ctx context.Context, in *sysin.AppMemberPaym
data[c.HolderMask] = encrypt.MaskName(in.Holder) data[c.HolderMask] = encrypt.MaskName(in.Holder)
} }
if in.Mobile != "" { if in.Mobile != "" {
v, e := encrypt.EncryptField(ctx, in.Mobile) v, e := encrypt.EncryptPaymentField(ctx, in.Mobile)
if e != nil { if e != nil {
return e return e
} }
@@ -550,11 +555,11 @@ func (s *sSysAppMember) PaymentEdit(ctx context.Context, in *sysin.AppMemberPaym
data[c.MobileMask] = encrypt.MaskMobile(in.Mobile) data[c.MobileMask] = encrypt.MaskMobile(in.Mobile)
} }
if in.IdentityNo != "" { if in.IdentityNo != "" {
v, e := encrypt.EncryptField(ctx, in.IdentityNo) v, e := encrypt.EncryptPaymentField(ctx, in.IdentityNo)
if e != nil { if e != nil {
return e return e
} }
h, e := encrypt.HashField(ctx, in.IdentityNo) h, e := encrypt.HashPaymentField(ctx, in.IdentityNo)
if e != nil { if e != nil {
return e return e
} }
@@ -563,11 +568,11 @@ func (s *sSysAppMember) PaymentEdit(ctx context.Context, in *sysin.AppMemberPaym
data[c.IdentityNoMask] = encrypt.MaskAccount(in.IdentityNo) data[c.IdentityNoMask] = encrypt.MaskAccount(in.IdentityNo)
} }
if in.AccountNo != "" { if in.AccountNo != "" {
v, e := encrypt.EncryptField(ctx, in.AccountNo) v, e := encrypt.EncryptPaymentField(ctx, in.AccountNo)
if e != nil { if e != nil {
return e return e
} }
h, e := encrypt.HashField(ctx, in.AccountNo) h, e := encrypt.HashPaymentField(ctx, in.AccountNo)
if e != nil { if e != nil {
return e return e
} }
@@ -576,11 +581,11 @@ func (s *sSysAppMember) PaymentEdit(ctx context.Context, in *sysin.AppMemberPaym
data[c.AccountNoMask] = encrypt.MaskAccount(in.AccountNo) data[c.AccountNoMask] = encrypt.MaskAccount(in.AccountNo)
} }
if in.DigitalRMBCode != "" { if in.DigitalRMBCode != "" {
v, e := encrypt.EncryptField(ctx, in.DigitalRMBCode) v, e := encrypt.EncryptPaymentField(ctx, in.DigitalRMBCode)
if e != nil { if e != nil {
return e return e
} }
h, e := encrypt.HashField(ctx, in.DigitalRMBCode) h, e := encrypt.HashPaymentField(ctx, in.DigitalRMBCode)
if e != nil { if e != nil {
return e return e
} }
@@ -95,6 +95,9 @@ type AppMemberEditInp struct {
} }
func (in *AppMemberEditInp) Filter(ctx context.Context) error { func (in *AppMemberEditInp) Filter(ctx context.Context) error {
if err := rejectMaskedSensitiveFields(in.RealName, in.IdNo); err != nil {
return err
}
if in.Id == 0 { if in.Id == 0 {
return gerror.New("后台不支持创建会员,请由会员在C端完成注册") return gerror.New("后台不支持创建会员,请由会员在C端完成注册")
} }
@@ -156,6 +159,7 @@ type AppMemberPaymentListInp struct {
func (in *AppMemberPaymentListInp) Filter(ctx context.Context) error { return nil } func (in *AppMemberPaymentListInp) Filter(ctx context.Context) error { return nil }
type AppMemberPaymentModel struct { type AppMemberPaymentModel struct {
SensitiveUnavailable bool `json:"sensitiveUnavailable"`
Id uint64 `json:"id"` Id uint64 `json:"id"`
MemberId uint64 `json:"memberId"` MemberId uint64 `json:"memberId"`
AccountType int `json:"accountType"` AccountType int `json:"accountType"`
@@ -207,6 +211,9 @@ type AppMemberPaymentEditInp struct {
} }
func (in *AppMemberPaymentEditInp) Filter(ctx context.Context) error { func (in *AppMemberPaymentEditInp) Filter(ctx context.Context) error {
if err := rejectMaskedSensitiveFields(in.Holder, in.IdentityNo, in.Mobile, in.AccountNo, in.DigitalRMBCode); err != nil {
return err
}
if !dict.HasOptionKey(consts.PaymentAccountTypeOptions, in.AccountType) { if !dict.HasOptionKey(consts.PaymentAccountTypeOptions, in.AccountType) {
return gerror.New("收款账户类型不合法") return gerror.New("收款账户类型不合法")
} }
@@ -260,12 +267,24 @@ type AppMemberAddressEditInp struct {
} }
func (in *AppMemberAddressEditInp) Filter(ctx context.Context) error { func (in *AppMemberAddressEditInp) Filter(ctx context.Context) error {
if err := rejectMaskedSensitiveFields(in.Receiver, in.Mobile, in.Address); err != nil {
return err
}
if in.Id == 0 && (strings.TrimSpace(in.Receiver) == "" || strings.TrimSpace(in.Mobile) == "" || strings.TrimSpace(in.Address) == "") { if in.Id == 0 && (strings.TrimSpace(in.Receiver) == "" || strings.TrimSpace(in.Mobile) == "" || strings.TrimSpace(in.Address) == "") {
return gerror.New("新增地址时收货人、手机号和详细地址不能为空") return gerror.New("新增地址时收货人、手机号和详细地址不能为空")
} }
return nil return nil
} }
func rejectMaskedSensitiveFields(values ...string) error {
for _, value := range values {
if strings.Contains(value, "*") {
return gerror.New("不能保存带星号的脱敏资料,请填写完整信息或留空保留原值")
}
}
return nil
}
type AppMemberAddressDeleteInp struct { type AppMemberAddressDeleteInp struct {
Id uint64 `json:"id" v:"required|min:1#地址ID不能为空|地址ID不能为空"` Id uint64 `json:"id" v:"required|min:1#地址ID不能为空|地址ID不能为空"`
} }
@@ -0,0 +1,58 @@
package sysin
import (
"context"
"encoding/json"
"strings"
"testing"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/database/gdb"
)
func TestPaymentCipherFieldsScanWithoutJSONExposure(t *testing.T) {
const ciphertext = "test-ciphertext-not-a-real-bank-account"
record := gdb.Record{}
for _, field := range []string{"holder_cipher", "identity_no_cipher", "mobile_cipher", "account_no_cipher", "digital_rmb_code_cipher"} {
record[field] = gvar.New(ciphertext)
}
var row AppMemberPaymentModel
if err := record.Struct(&row); err != nil {
t.Fatal(err)
}
for name, actual := range map[string]string{"holder": row.HolderCipher, "identity": row.IdentityNoCipher, "mobile": row.MobileCipher, "account": row.AccountNoCipher, "digital": row.DigitalRmbCodeCipher} {
if actual != ciphertext {
t.Errorf("%s ciphertext was not loaded by database scan", name)
}
}
data, err := json.Marshal(row)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(data), ciphertext) {
t.Fatal("internal ciphertext must not be exposed in API JSON")
}
}
func TestPaymentEditRejectsMaskedValues(t *testing.T) {
for _, field := range []string{"holder", "account", "identity", "mobile", "digital"} {
t.Run(field, func(t *testing.T) {
in := &AppMemberPaymentEditInp{Id: 1, MemberId: 1, AccountType: 1}
switch field {
case "holder":
in.Holder = "王*"
case "account":
in.AccountNo = "6222********5937"
case "identity":
in.IdentityNo = "1234********5678"
case "mobile":
in.Mobile = "138****1234"
case "digital":
in.DigitalRMBCode = "0021********8708"
}
if err := in.Filter(context.Background()); err == nil {
t.Fatal("masked values must never overwrite originals")
}
})
}
}
@@ -195,6 +195,11 @@ token:
# 业务敏感字段保护。生产环境必须替换为独立的高强度密钥,并通过密钥管理系统注入。 # 业务敏感字段保护。生产环境必须替换为独立的高强度密钥,并通过密钥管理系统注入。
security: security:
dataKey: "change-me-before-production" dataKey: "change-me-before-production"
# 与 C 端收款资料使用相同配置;历史主密钥只用于读取,不修改原密文。
financeMasterKey: ""
financeLegacyMasterKeys: []
financeDataEncryptionKey: ""
financeLookupHmacKey: ""
# 消息队列 # 消息队列
+19 -17
View File
@@ -54,31 +54,33 @@ func DecryptField(ctx context.Context, encoded string) (string, error) {
if encoded == "" { if encoded == "" {
return "", nil return "", nil
} }
key, err := fieldKey(ctx) raw, err := decodeFieldPayload(encoded)
if err != nil { if err != nil {
return "", err return "", err
} }
raw, err := base64.RawURLEncoding.DecodeString(encoded) if len(raw) < 12+16 {
if err != nil {
return "", gerror.Wrap(err, "敏感字段密文格式错误")
}
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
if len(raw) < gcm.NonceSize() {
return "", gerror.New("敏感字段密文不完整") return "", gerror.New("敏感字段密文不完整")
} }
plain, err := gcm.Open(nil, raw[:gcm.NonceSize()], raw[gcm.NonceSize():], nil) keys := financeFieldReadKeys(ctx)
if err != nil { if key, keyErr := fieldKey(ctx); keyErr == nil {
return "", gerror.Wrap(err, "敏感字段解密失败") keys = append(keys, key)
} }
for _, key := range keys {
block, keyErr := aes.NewCipher(key)
if keyErr != nil {
continue
}
gcm, keyErr := cipher.NewGCM(block)
if keyErr != nil {
continue
}
plain, decryptErr := gcm.Open(nil, raw[:gcm.NonceSize()], raw[gcm.NonceSize():], nil)
if decryptErr == nil {
return string(plain), nil return string(plain), nil
} }
}
return "", gerror.New("敏感字段解密失败,请核对当前及历史密钥配置")
}
// HashField creates a deterministic, non-reversible lookup key with a secret pepper. // HashField creates a deterministic, non-reversible lookup key with a secret pepper.
func HashField(ctx context.Context, value string) (string, error) { func HashField(ctx context.Context, value string) (string, error) {
+111
View File
@@ -0,0 +1,111 @@
package encrypt
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"io"
"strings"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
)
// financeFieldKey uses the same key derivation as the customer-facing finance service.
// An empty configuration retains the existing HotGo format for older installations.
func financeFieldKey(ctx context.Context, purpose string) ([]byte, error) {
master := strings.TrimSpace(g.Cfg().MustGet(ctx, "security.financeMasterKey", "").String())
if master != "" {
if len([]byte(master)) < 32 {
return nil, gerror.New("收款资料密钥配置不正确")
}
return deriveFinanceFieldKey(master, purpose), nil
}
name := "security.financeDataEncryptionKey"
if purpose == "lookup-hmac" {
name = "security.financeLookupHmacKey"
}
encoded := strings.TrimSpace(g.Cfg().MustGet(ctx, name, "").String())
if encoded == "" {
return nil, nil
}
key, err := decodeFieldPayload(encoded)
if err != nil || (purpose == "data-encryption" && len(key) != 32) || (purpose == "lookup-hmac" && len(key) < 32) {
return nil, gerror.New("收款资料密钥配置不正确")
}
return key, nil
}
func deriveFinanceFieldKey(master, purpose string) []byte {
mac := hmac.New(sha256.New, []byte(master))
_, _ = mac.Write([]byte("olons-finance:" + purpose + ":v1"))
return mac.Sum(nil)
}
func financeFieldReadKeys(ctx context.Context) [][]byte {
keys := make([][]byte, 0)
if key, err := financeFieldKey(ctx, "data-encryption"); err == nil && len(key) > 0 {
keys = append(keys, key)
}
for _, master := range g.Cfg().MustGet(ctx, "security.financeLegacyMasterKeys", []string{}).Strings() {
if master = strings.TrimSpace(master); len([]byte(master)) >= 32 {
keys = append(keys, deriveFinanceFieldKey(master, "data-encryption"))
}
}
return keys
}
func decodeFieldPayload(value string) ([]byte, error) {
value = strings.TrimSpace(value)
for _, encoding := range []*base64.Encoding{base64.RawURLEncoding, base64.RawStdEncoding, base64.URLEncoding, base64.StdEncoding} {
if raw, err := encoding.DecodeString(value); err == nil {
return raw, nil
}
}
return nil, gerror.New("敏感字段密文格式错误")
}
// EncryptPaymentField keeps new admin edits readable by the customer-facing service.
func EncryptPaymentField(ctx context.Context, plaintext string) (string, error) {
if plaintext == "" {
return "", nil
}
key, err := financeFieldKey(ctx, "data-encryption")
if err != nil {
return "", err
}
if len(key) == 0 {
return EncryptField(ctx, plaintext)
}
block, err := aes.NewCipher(key)
if err != nil {
return "", gerror.Wrap(err, "初始化收款资料加密失败")
}
aead, err := cipher.NewGCM(block)
if err != nil {
return "", gerror.Wrap(err, "初始化收款资料加密失败")
}
nonce := make([]byte, aead.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return "", gerror.Wrap(err, "生成收款资料随机数失败")
}
return base64.RawStdEncoding.EncodeToString(aead.Seal(nonce, nonce, []byte(plaintext), nil)), nil
}
func HashPaymentField(ctx context.Context, value string) (string, error) {
key, err := financeFieldKey(ctx, "lookup-hmac")
if err != nil {
return "", err
}
if len(key) == 0 {
return HashField(ctx, value)
}
mac := hmac.New(sha256.New, key)
_, _ = mac.Write([]byte(strings.TrimSpace(value)))
return hex.EncodeToString(mac.Sum(nil)), nil
}
@@ -0,0 +1,113 @@
package encrypt
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"testing"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcfg"
)
func TestDecryptFieldAcceptsCustomerPaymentKeys(t *testing.T) {
ctx := context.Background()
current := "current-finance-master-key-with-at-least-32-characters"
previous := "previous-finance-master-key-with-at-least-32-characters"
setFieldTestConfig(t, map[string]interface{}{"dataKey": "admin-legacy-key", "financeMasterKey": current, "financeLegacyMasterKeys": []string{previous}})
for _, master := range []string{current, previous} {
encoded := customerPaymentFixture(t, master, "6222000000001234")
plain, err := DecryptField(ctx, encoded)
if err != nil || plain != "6222000000001234" {
t.Fatalf("customer payment must decrypt with current or previous key: %v", err)
}
}
}
func TestPaymentWritesRemainReadableByCustomerService(t *testing.T) {
ctx := context.Background()
master := "current-finance-master-key-with-at-least-32-characters"
setFieldTestConfig(t, map[string]interface{}{"dataKey": "admin-legacy-key", "financeMasterKey": master})
const value = "6222000000001234"
encoded, err := EncryptPaymentField(ctx, value)
if err != nil {
t.Fatal(err)
}
payload, err := base64.RawStdEncoding.DecodeString(encoded)
if err != nil {
t.Fatal("payment edit must use the customer's standard Base64 format")
}
mac := hmac.New(sha256.New, []byte(master))
_, _ = mac.Write([]byte("olons-finance:data-encryption:v1"))
block, _ := aes.NewCipher(mac.Sum(nil))
aead, _ := cipher.NewGCM(block)
plain, err := aead.Open(nil, payload[:aead.NonceSize()], payload[aead.NonceSize():], nil)
if err != nil || string(plain) != value {
t.Fatalf("customer service cannot read admin edit: %v", err)
}
actualHash, err := HashPaymentField(ctx, value)
if err != nil {
t.Fatal(err)
}
mac = hmac.New(sha256.New, []byte(master))
_, _ = mac.Write([]byte("olons-finance:lookup-hmac:v1"))
lookup := hmac.New(sha256.New, mac.Sum(nil))
_, _ = lookup.Write([]byte(value))
if actualHash != hex.EncodeToString(lookup.Sum(nil)) {
t.Fatal("admin lookup hash must match customer service")
}
}
func TestDecryptFieldKeepsLegacyHotGoDataReadable(t *testing.T) {
ctx := context.Background()
setFieldTestConfig(t, map[string]interface{}{"dataKey": "admin-legacy-key", "financeMasterKey": "current-finance-master-key-with-at-least-32-characters"})
encoded, err := EncryptField(ctx, "旧资料")
if err != nil {
t.Fatal(err)
}
plain, err := DecryptField(ctx, encoded)
if err != nil || plain != "旧资料" {
t.Fatalf("legacy HotGo record unreadable: %v", err)
}
for _, invalid := range []string{"DEMO-NOT-CIPHERTEXT", "AAAA", customerPaymentFixture(t, "unknown-master-key-with-at-least-32-characters", "姓名")} {
if _, err := DecryptField(ctx, invalid); err == nil {
t.Fatal("invalid or unknown-key records must not be reported as plaintext")
}
}
}
func setFieldTestConfig(t *testing.T, security map[string]interface{}) {
t.Helper()
data, err := json.Marshal(map[string]interface{}{"security": security})
if err != nil {
t.Fatal(err)
}
adapter, err := gcfg.NewAdapterContent(string(data))
if err != nil {
t.Fatal(err)
}
previous := g.Cfg().GetAdapter()
g.Cfg().SetAdapter(adapter)
t.Cleanup(func() { g.Cfg().SetAdapter(previous) })
}
func customerPaymentFixture(t *testing.T, master, value string) string {
t.Helper()
mac := hmac.New(sha256.New, []byte(master))
_, _ = mac.Write([]byte("olons-finance:data-encryption:v1"))
block, err := aes.NewCipher(mac.Sum(nil))
if err != nil {
t.Fatal(err)
}
aead, err := cipher.NewGCM(block)
if err != nil {
t.Fatal(err)
}
nonce := []byte{251, 255, 239, 255, 251, 255, 239, 255, 251, 255, 239, 255}
return base64.RawStdEncoding.EncodeToString(aead.Seal(nonce, nonce, []byte(value), nil))
}
+32 -20
View File
@@ -178,8 +178,10 @@
<n-spin :show="relatedLoading"> <n-spin :show="relatedLoading">
<n-form label-placement="left" label-width="110"> <n-form label-placement="left" label-width="110">
<template v-if="relatedType === 'payment'"> <template v-if="relatedType === 'payment'">
<n-alert v-if="paymentForm.id" type="warning" :show-icon="false"> <n-alert v-if="paymentForm.id" :type="paymentForm.sensitiveUnavailable ? 'error' : 'warning'" :show-icon="false">
敏感资料已明文展示;请仅用于当前业务处理。留空保存不会覆盖原敏感字段或收款码。 {{ paymentForm.sensitiveUnavailable
? '部分历史资料暂无法解密。脱敏值只作占位,不会写回原数据;请留空保留原值。'
: '资料已明文展示。留空保存不会覆盖原字段或收款码。' }}
</n-alert> </n-alert>
<n-form-item label="开户人" <n-form-item label="开户人"
><n-input v-model:value="paymentForm.holder" :placeholder="paymentForm.holderMask || '留空不修改'" ><n-input v-model:value="paymentForm.holder" :placeholder="paymentForm.holderMask || '留空不修改'"
@@ -734,16 +736,17 @@
reset(paymentForm, { reset(paymentForm, {
memberId, memberId,
id: bank?.id || 0, id: bank?.id || 0,
holder: bank?.holder || bank?.holderMask || '', holder: editableSensitive(bank?.holder),
sensitiveUnavailable: bank?.sensitiveUnavailable || false,
holderMask: bank?.holderMask || '', holderMask: bank?.holderMask || '',
identityNo: bank?.identityNo || bank?.identityNoMask || '', identityNo: editableSensitive(bank?.identityNo),
identityNoMask: bank?.identityNoMask || '', identityNoMask: bank?.identityNoMask || '',
bankName: bank?.bankName || '', bankName: bank?.bankName || '',
mobile: bank?.mobile || bank?.mobileMask || '', mobile: editableSensitive(bank?.mobile),
mobileMask: bank?.mobileMask || '', mobileMask: bank?.mobileMask || '',
bankAccount: bank?.accountNo || bank?.accountNoMask || '', bankAccount: editableSensitive(bank?.accountNo),
accountNoMask: bank?.accountNoMask || '', accountNoMask: bank?.accountNoMask || '',
digitalWallet: bank?.digitalRmbCode || bank?.digitalRmbCodeMask || '', digitalWallet: editableSensitive(bank?.digitalRmbCode),
digitalRmbCodeMask: bank?.digitalRmbCodeMask || '', digitalRmbCodeMask: bank?.digitalRmbCodeMask || '',
alipayQrCode: bank?.alipayQrUrl || '', alipayQrCode: bank?.alipayQrUrl || '',
wechatQrCode: bank?.wechatQrUrl || '', wechatQrCode: bank?.wechatQrUrl || '',
@@ -756,14 +759,14 @@
reset(addressForm, { reset(addressForm, {
id: row?.id || 0, id: row?.id || 0,
memberId, memberId,
receiver: row?.receiver || row?.receiverMask || '', receiver: editableSensitive(row?.receiver),
mobile: row?.mobile || row?.mobileMask || '', mobile: editableSensitive(row?.mobile),
region, region,
originalRegion: region, originalRegion: region,
province: row?.province || '', province: row?.province || '',
city: row?.city || '', city: row?.city || '',
district: row?.district || '', district: row?.district || '',
address: '', address: editableSensitive(row?.address),
isDefault: 1, isDefault: 1,
}); });
} }
@@ -778,6 +781,9 @@
: await AddressList({ memberId: detail.value.id }); : await AddressList({ memberId: detail.value.id });
relatedRows.value = r.list || []; relatedRows.value = r.list || [];
} }
function editableSensitive(value: unknown) {
return typeof value === 'string' && !value.includes('*') ? value : '';
}
function startRelatedAdd() { function startRelatedAdd() {
showRelatedForm.value = true; showRelatedForm.value = true;
if (relatedType.value === 'payment') if (relatedType.value === 'payment')
@@ -788,7 +794,8 @@
bankName: '', bankName: '',
holder: '', holder: '',
mobile: '', mobile: '',
accountNo: '', bankAccount: '',
digitalWallet: '',
verifyStatus: 1, verifyStatus: 1,
isDefault: 2, isDefault: 2,
}); });
@@ -801,7 +808,7 @@
province: '', province: '',
city: '', city: '',
district: '', district: '',
address: row?.address || row?.addressMask || '', address: '',
isDefault: 2, isDefault: 2,
}); });
} }
@@ -855,11 +862,16 @@
memberId: row.memberId, memberId: row.memberId,
accountType: row.accountType, accountType: row.accountType,
bankName: row.bankName, bankName: row.bankName,
holder: row.holder || row.holderMask || '', holder: editableSensitive(row.holder),
identityNo: row.identityNo || row.identityNoMask || '', sensitiveUnavailable: row.sensitiveUnavailable || false,
mobile: row.mobile || row.mobileMask || '', identityNo: editableSensitive(row.identityNo),
accountNo: row.accountNo || row.accountNoMask || '', mobile: editableSensitive(row.mobile),
digitalRmbCode: row.digitalRmbCode || row.digitalRmbCodeMask || '', bankAccount: editableSensitive(row.accountNo),
digitalWallet: editableSensitive(row.digitalRmbCode),
wechatQrCode: row.wechatQrUrl || '',
alipayQrCode: row.alipayQrUrl || '',
wechatQrAttachmentId: row.wechatQrAttachmentId || 0,
alipayQrAttachmentId: row.alipayQrAttachmentId || 0,
verifyStatus: row.verifyStatus, verifyStatus: row.verifyStatus,
isDefault: row.isDefault, isDefault: row.isDefault,
}); });
@@ -867,12 +879,12 @@
reset(addressForm, { reset(addressForm, {
id: row.id, id: row.id,
memberId: row.memberId, memberId: row.memberId,
receiver: row.receiver || row.receiverMask || '', receiver: editableSensitive(row.receiver),
mobile: row.mobile || row.mobileMask || '', mobile: editableSensitive(row.mobile),
province: row.province, province: row.province,
city: row.city, city: row.city,
district: row.district, district: row.district,
address: row.address || row.addressMask || '', address: editableSensitive(row.address),
isDefault: row.isDefault, isDefault: row.isDefault,
}); });
} }