fix: allow optional digital RMB code and inline payment password setup
This commit is contained in:
@@ -44,14 +44,14 @@ func (r *profileRepository) UpdateTradePassword(_ context.Context, _, _ int64, _
|
|||||||
func TestProfileMapsMemberCategory(t *testing.T) {
|
func TestProfileMapsMemberCategory(t *testing.T) {
|
||||||
repository := &profileRepository{overview: &store.MemberOverview{
|
repository := &profileRepository{overview: &store.MemberOverview{
|
||||||
MemberID: 7, MemberNo: "M0007", Username: "robot", MemberStatus: 1, MemberCategory: 2,
|
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}
|
service := &Service{repository: repository, ready: true}
|
||||||
profile, err := service.Profile(context.Background(), 0, 7)
|
profile, err := service.Profile(context.Background(), 0, 7)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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)
|
t.Fatalf("unexpected profile states: %+v", profile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ type Profile struct {
|
|||||||
Phone string `json:"phone"`
|
Phone string `json:"phone"`
|
||||||
AvatarURL string `json:"avatar_url"`
|
AvatarURL string `json:"avatar_url"`
|
||||||
InviteCode string `json:"invite_code"`
|
InviteCode string `json:"invite_code"`
|
||||||
|
HasTradePassword bool `json:"has_trade_password"`
|
||||||
Level ProfileLevel `json:"level"`
|
Level ProfileLevel `json:"level"`
|
||||||
MemberStatus string `json:"member_status"`
|
MemberStatus string `json:"member_status"`
|
||||||
MemberCategory string `json:"member_category"`
|
MemberCategory string `json:"member_category"`
|
||||||
@@ -268,6 +269,7 @@ func (s *Service) Profile(ctx context.Context, tenantID, memberID int64) (*Profi
|
|||||||
return &Profile{
|
return &Profile{
|
||||||
MemberID: row.MemberID, MemberNo: row.MemberNo, Username: row.Username,
|
MemberID: row.MemberID, MemberNo: row.MemberNo, Username: row.Username,
|
||||||
Phone: row.Mobile, AvatarURL: row.AvatarURL, InviteCode: row.InviteCode,
|
Phone: row.Mobile, AvatarURL: row.AvatarURL, InviteCode: row.InviteCode,
|
||||||
|
HasTradePassword: row.HasTradePassword != 0,
|
||||||
Level: ProfileLevel{ID: row.LevelID, Code: row.LevelCode, Name: row.LevelName,
|
Level: ProfileLevel{ID: row.LevelID, Code: row.LevelCode, Name: row.LevelName,
|
||||||
IconURL: row.LevelIconURL, CommissionRate: row.CommissionRate},
|
IconURL: row.LevelIconURL, CommissionRate: row.CommissionRate},
|
||||||
MemberStatus: memberStatusName(row.MemberStatus), MemberCategory: memberCategoryName(row.MemberCategory),
|
MemberStatus: memberStatusName(row.MemberStatus), MemberCategory: memberCategoryName(row.MemberCategory),
|
||||||
|
|||||||
@@ -301,18 +301,23 @@ 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)
|
digitalRMBCodeCipher, digitalRMBCodeHash, digitalRMBCodeMask := "", "", ""
|
||||||
|
if input.DigitalRMBCode != "" {
|
||||||
|
digitalRMBCodeCipher, err = s.encrypt(input.DigitalRMBCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
digitalRMBCodeHash = s.lookupHash(input.DigitalRMBCode)
|
||||||
|
digitalRMBCodeMask = maskAccount(input.DigitalRMBCode)
|
||||||
|
}
|
||||||
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),
|
DigitalRMBCodeCipher: digitalRMBCodeCipher, DigitalRMBCodeHash: digitalRMBCodeHash,
|
||||||
DigitalRMBCodeMask: maskAccount(input.DigitalRMBCode),
|
DigitalRMBCodeMask: digitalRMBCodeMask,
|
||||||
WechatQRAttachmentID: input.WechatQRAttachmentID,
|
WechatQRAttachmentID: input.WechatQRAttachmentID,
|
||||||
AlipayQRAttachmentID: input.AlipayQRAttachmentID,
|
AlipayQRAttachmentID: input.AlipayQRAttachmentID,
|
||||||
})
|
})
|
||||||
@@ -759,7 +764,7 @@ func normalizePaymentAccount(input *PaymentAccountInput) error {
|
|||||||
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.DigitalRMBCode != "" && (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
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"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"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeRepository struct {
|
type fakeRepository struct {
|
||||||
@@ -23,6 +24,16 @@ type fakeRepository struct {
|
|||||||
createdRecharge store.RechargeCreation
|
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) {
|
func (f *fakeRepository) CurrentSiteConfig(context.Context, int64) (json.RawMessage, error) {
|
||||||
return f.config, nil
|
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) {
|
func TestMaskAccountShortValueDoesNotLeak(t *testing.T) {
|
||||||
if got := maskAccount("abcde"); got != "a***e" {
|
if got := maskAccount("abcde"); got != "a***e" {
|
||||||
t.Fatalf("unexpected mask %q", got)
|
t.Fatalf("unexpected mask %q", got)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type MemberOverview struct {
|
|||||||
Username string `db:"username"`
|
Username string `db:"username"`
|
||||||
Mobile string `db:"mobile"`
|
Mobile string `db:"mobile"`
|
||||||
InviteCode string `db:"invite_code"`
|
InviteCode string `db:"invite_code"`
|
||||||
|
HasTradePassword int `db:"has_trade_password"`
|
||||||
MemberStatus int `db:"member_status"`
|
MemberStatus int `db:"member_status"`
|
||||||
MemberCategory int `db:"member_category"`
|
MemberCategory int `db:"member_category"`
|
||||||
CreditScore int `db:"credit_score"`
|
CreditScore int `db:"credit_score"`
|
||||||
@@ -49,7 +50,8 @@ func (s *Store) MemberOverview(ctx context.Context, tenantID, memberID int64) (*
|
|||||||
}
|
}
|
||||||
var row MemberOverview
|
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,
|
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(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.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,
|
COALESCE(l.name,'') AS level_name,COALESCE(l.icon_url,'') AS level_icon_url,
|
||||||
|
|||||||
@@ -2744,6 +2744,16 @@ body {
|
|||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
.m-withdraw-password-notice {
|
||||||
|
margin: 16px 0;
|
||||||
|
color: var(--m-muted);
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.m-withdraw-confirm .m-withdraw-setup-btn,
|
||||||
|
.m-withdraw-confirm .m-withdraw-back-btn {
|
||||||
|
margin: 8px 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.m-finance-records {
|
.m-finance-records {
|
||||||
padding-inline: 0;
|
padding-inline: 0;
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
v-model.trim="bank.digital_rmb_code"
|
v-model.trim="bank.digital_rmb_code"
|
||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
maxlength="32"
|
maxlength="32"
|
||||||
placeholder="请输入数字编码"
|
placeholder="请输入数字编码(选填)"
|
||||||
/></label>
|
/></label>
|
||||||
<section class="m-qr-field">
|
<section class="m-qr-field">
|
||||||
<h3>微信二维码</h3>
|
<h3>微信二维码</h3>
|
||||||
@@ -276,16 +276,26 @@
|
|||||||
}
|
}
|
||||||
async function saveBank() {
|
async function saveBank() {
|
||||||
error.value = '';
|
error.value = '';
|
||||||
if (
|
const identityNo = bank.identity_no.replace(/[ -]/g, '');
|
||||||
!bank.holder_name ||
|
const accountNo = bank.account_no.replace(/[ -]/g, '');
|
||||||
!/^[1-9]\d{14}(?:\d{2}[\dXx])?$/.test(bank.identity_no.replace(/[ -]/g, '')) ||
|
const digitalRMBCode = bank.digital_rmb_code.replace(/[ -]/g, '');
|
||||||
!/^[0-9 -]{8,36}$/.test(bank.account_no) ||
|
const validationError =
|
||||||
!bank.bank_name ||
|
bank.holder_name.length < 2
|
||||||
!/^\d{6,32}$/.test(bank.digital_rmb_code.replace(/[ -]/g, '')) ||
|
? '请输入至少 2 个字符的姓名'
|
||||||
!bank.wechat_qr_attachment_id ||
|
: !/^[1-9]\d{14}(?:\d{2}[\dXx])?$/.test(identityNo)
|
||||||
!bank.alipay_qr_attachment_id
|
? '请输入正确的 15 位或 18 位身份证号'
|
||||||
) {
|
: !/^\d{8,32}$/.test(accountNo)
|
||||||
error.value = '请完整填写绑卡信息并上传微信、支付宝二维码';
|
? '请输入 8 至 32 位银行卡号'
|
||||||
|
: bank.bank_name.length < 2
|
||||||
|
? '请输入开户行'
|
||||||
|
: digitalRMBCode && !/^\d{6,32}$/.test(digitalRMBCode)
|
||||||
|
? '数字人民币编码须为 6 至 32 位数字,也可留空'
|
||||||
|
: !bank.wechat_qr_attachment_id || !bank.alipay_qr_attachment_id
|
||||||
|
? '请上传微信和支付宝二维码'
|
||||||
|
: '';
|
||||||
|
if (validationError) {
|
||||||
|
error.value = validationError;
|
||||||
|
window.$message?.error(validationError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
|
|||||||
@@ -47,12 +47,39 @@
|
|||||||
:trap-focus="false"
|
:trap-focus="false"
|
||||||
@after-enter="focusTradePassword"
|
@after-enter="focusTradePassword"
|
||||||
>
|
>
|
||||||
<form class="m-withdraw-confirm m-card" @submit.prevent="submitWithdrawal">
|
<form v-if="showTradePasswordSetup" class="m-withdraw-confirm m-card" @submit.prevent="setTradePassword">
|
||||||
|
<h2>设置支付密码</h2>
|
||||||
|
<p class="m-withdraw-password-notice">设置完成后,请返回确认申请。</p>
|
||||||
|
<label>
|
||||||
|
<span>新支付密码</span>
|
||||||
|
<input
|
||||||
|
v-model="setupForm.newTradePassword"
|
||||||
|
type="password"
|
||||||
|
inputmode="numeric"
|
||||||
|
maxlength="6"
|
||||||
|
autocomplete="new-password"
|
||||||
|
placeholder="请输入 6 位数字"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>登录密码确认</span>
|
||||||
|
<input
|
||||||
|
v-model="setupForm.loginPassword"
|
||||||
|
type="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
placeholder="请输入登录密码"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<p v-if="setupError" class="m-form-error">{{ setupError }}</p>
|
||||||
|
<button class="m-primary-btn" :disabled="submitting">{{ submitting ? '设置中...' : '确认设置' }}</button>
|
||||||
|
<button class="m-outline-btn m-withdraw-back-btn" type="button" @click="showTradePasswordSetup = false">返回确认申请</button>
|
||||||
|
</form>
|
||||||
|
<form v-else class="m-withdraw-confirm m-card" @submit.prevent="submitWithdrawal">
|
||||||
<h2>确认取出</h2>
|
<h2>确认取出</h2>
|
||||||
<div><span>申请金额</span><b>{{ money(quote?.request_amount) }}</b></div>
|
<div><span>申请金额</span><b>{{ money(quote?.request_amount) }}</b></div>
|
||||||
<div><span>手续费</span><b>{{ money(quote?.fee_amount) }}</b></div>
|
<div><span>手续费</span><b>{{ money(quote?.fee_amount) }}</b></div>
|
||||||
<div><span>预计到账</span><strong>{{ money(quote?.actual_amount) }}</strong></div>
|
<div><span>预计到账</span><strong>{{ money(quote?.actual_amount) }}</strong></div>
|
||||||
<label>
|
<label v-if="hasTradePassword">
|
||||||
<span>支付密码</span>
|
<span>支付密码</span>
|
||||||
<input
|
<input
|
||||||
ref="tradePasswordInput"
|
ref="tradePasswordInput"
|
||||||
@@ -65,8 +92,10 @@
|
|||||||
placeholder="请输入 6 位支付密码"
|
placeholder="请输入 6 位支付密码"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<p v-else class="m-withdraw-password-notice">尚未设置支付密码,请先完成设置。</p>
|
||||||
|
<button v-if="!hasTradePassword" class="m-outline-btn m-withdraw-setup-btn" type="button" @click="openTradePasswordSetup">去设置支付密码</button>
|
||||||
<p v-if="actionError" class="m-form-error">{{ actionError }}</p>
|
<p v-if="actionError" class="m-form-error">{{ actionError }}</p>
|
||||||
<button class="m-primary-btn" :disabled="submitting">确认申请</button>
|
<button class="m-primary-btn" :disabled="submitting || !hasTradePassword">确认申请</button>
|
||||||
</form>
|
</form>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
</section>
|
</section>
|
||||||
@@ -78,7 +107,7 @@
|
|||||||
import { ChevronForwardOutline } from '@vicons/ionicons5';
|
import { ChevronForwardOutline } from '@vicons/ionicons5';
|
||||||
import PageHeader from '../components/PageHeader.vue';
|
import PageHeader from '../components/PageHeader.vue';
|
||||||
import AsyncState from '../components/AsyncState.vue';
|
import AsyncState from '../components/AsyncState.vue';
|
||||||
import { errorText, idempotencyKey, money, request } from '../api';
|
import { MobileApiError, errorText, idempotencyKey, money, request } from '../api';
|
||||||
import { mobileAssets } from '../assets';
|
import { mobileAssets } from '../assets';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -90,9 +119,13 @@
|
|||||||
const accounts = ref<any[]>([]);
|
const accounts = ref<any[]>([]);
|
||||||
const quote = ref<any>();
|
const quote = ref<any>();
|
||||||
const showConfirm = ref(false);
|
const showConfirm = ref(false);
|
||||||
|
const showTradePasswordSetup = ref(false);
|
||||||
|
const setupError = ref('');
|
||||||
const withdrawalKey = ref('');
|
const withdrawalKey = ref('');
|
||||||
const tradePasswordInput = ref<HTMLInputElement>();
|
const tradePasswordInput = ref<HTMLInputElement>();
|
||||||
const form = reactive({ paymentAccountId: 0, amountYuan: '', tradePassword: '' });
|
const form = reactive({ paymentAccountId: 0, amountYuan: '', tradePassword: '' });
|
||||||
|
const setupForm = reactive({ newTradePassword: '', loginPassword: '' });
|
||||||
|
const hasTradePassword = computed(() => me.value?.has_trade_password === true);
|
||||||
const selectedAccount = computed(() =>
|
const selectedAccount = computed(() =>
|
||||||
accounts.value.find((item) => item.id === form.paymentAccountId)
|
accounts.value.find((item) => item.id === form.paymentAccountId)
|
||||||
);
|
);
|
||||||
@@ -112,7 +145,44 @@
|
|||||||
}
|
}
|
||||||
async function focusTradePassword() {
|
async function focusTradePassword() {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
tradePasswordInput.value?.focus();
|
if (hasTradePassword.value && !showTradePasswordSetup.value) tradePasswordInput.value?.focus();
|
||||||
|
}
|
||||||
|
function openTradePasswordSetup() {
|
||||||
|
setupError.value = '';
|
||||||
|
actionError.value = '';
|
||||||
|
setupForm.newTradePassword = '';
|
||||||
|
setupForm.loginPassword = '';
|
||||||
|
showTradePasswordSetup.value = true;
|
||||||
|
}
|
||||||
|
async function setTradePassword() {
|
||||||
|
if (submitting.value) return;
|
||||||
|
setupError.value = '';
|
||||||
|
if (!/^\d{6}$/.test(setupForm.newTradePassword) || setupForm.loginPassword.length < 6) {
|
||||||
|
setupError.value = '请输入 6 位数字支付密码及登录密码';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
await request({
|
||||||
|
url: '/v1/me/trade-password',
|
||||||
|
method: 'PUT',
|
||||||
|
data: {
|
||||||
|
new_trade_password: setupForm.newTradePassword,
|
||||||
|
login_password: setupForm.loginPassword,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
me.value = { ...me.value, has_trade_password: true };
|
||||||
|
setupForm.newTradePassword = '';
|
||||||
|
setupForm.loginPassword = '';
|
||||||
|
form.tradePassword = '';
|
||||||
|
showTradePasswordSetup.value = false;
|
||||||
|
window.$message?.success('支付密码设置成功,请确认申请');
|
||||||
|
await focusTradePassword();
|
||||||
|
} catch (err) {
|
||||||
|
setupError.value = errorText(err);
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@@ -152,6 +222,7 @@
|
|||||||
});
|
});
|
||||||
withdrawalKey.value = idempotencyKey();
|
withdrawalKey.value = idempotencyKey();
|
||||||
form.tradePassword = '';
|
form.tradePassword = '';
|
||||||
|
showTradePasswordSetup.value = false;
|
||||||
showConfirm.value = true;
|
showConfirm.value = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
actionError.value = errorText(err);
|
actionError.value = errorText(err);
|
||||||
@@ -161,6 +232,10 @@
|
|||||||
}
|
}
|
||||||
async function submitWithdrawal() {
|
async function submitWithdrawal() {
|
||||||
if (!quote.value || submitting.value) return;
|
if (!quote.value || submitting.value) return;
|
||||||
|
if (!hasTradePassword.value) {
|
||||||
|
actionError.value = '请先设置支付密码';
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!/^\d{6}$/.test(form.tradePassword)) {
|
if (!/^\d{6}$/.test(form.tradePassword)) {
|
||||||
actionError.value = '请输入 6 位支付密码';
|
actionError.value = '请输入 6 位支付密码';
|
||||||
return;
|
return;
|
||||||
@@ -184,6 +259,9 @@
|
|||||||
await router.replace('/app/profile/records?tab=withdrawal');
|
await router.replace('/app/profile/records?tab=withdrawal');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
actionError.value = errorText(err);
|
actionError.value = errorText(err);
|
||||||
|
if (err instanceof MobileApiError && err.reason === 'TRADE_PASSWORD_NOT_SET') {
|
||||||
|
me.value = { ...me.value, has_trade_password: false };
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false;
|
submitting.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user