feat: 接入工资提现接口

This commit is contained in:
zhx 2026-07-02 11:01:56 +08:00
parent f343bab95f
commit 2175b7a886
10 changed files with 282 additions and 81 deletions

View File

@ -1280,6 +1280,23 @@ var default_api = 'https://api.global-interaction.com/';
body: payload || {}, body: payload || {},
}); });
}, },
getWithdrawAddress: function () {
return request('/api/v1/salary-wallet/withdraw-address', {
method: 'GET',
});
},
saveWithdrawAddress: function (payload) {
return request('/api/v1/salary-wallet/withdraw-address', {
method: 'POST',
body: payload || {},
});
},
withdraw: function (payload) {
return request('/api/v1/salary-wallet/withdraw', {
method: 'POST',
body: payload || {},
});
},
}; };
var hostAPI = { var hostAPI = {

View File

@ -273,6 +273,7 @@
"withdraw_exchange.trc20_address": "عنوان TRC20", "withdraw_exchange.trc20_address": "عنوان TRC20",
"withdraw_exchange.save_address": "حفظ العنوان", "withdraw_exchange.save_address": "حفظ العنوان",
"withdraw_exchange.saving": "جار الحفظ...", "withdraw_exchange.saving": "جار الحفظ...",
"withdraw_exchange.edit_address": "تعديل العنوان",
"withdraw_exchange.usdt_only": "USDT فقط", "withdraw_exchange.usdt_only": "USDT فقط",
"withdraw_exchange.withdraw_now": "اسحب الآن", "withdraw_exchange.withdraw_now": "اسحب الآن",
"withdraw_exchange.exchange_now": "بدّل الآن", "withdraw_exchange.exchange_now": "بدّل الآن",

View File

@ -274,6 +274,7 @@
"withdraw_exchange.trc20_address": "TRC20 address", "withdraw_exchange.trc20_address": "TRC20 address",
"withdraw_exchange.save_address": "Save address", "withdraw_exchange.save_address": "Save address",
"withdraw_exchange.saving": "Saving...", "withdraw_exchange.saving": "Saving...",
"withdraw_exchange.edit_address": "Edit address",
"withdraw_exchange.usdt_only": "USDT only", "withdraw_exchange.usdt_only": "USDT only",
"withdraw_exchange.withdraw_now": "Withdraw now", "withdraw_exchange.withdraw_now": "Withdraw now",
"withdraw_exchange.exchange_now": "Exchange now", "withdraw_exchange.exchange_now": "Exchange now",

View File

@ -273,6 +273,7 @@
"withdraw_exchange.trc20_address": "Dirección TRC20", "withdraw_exchange.trc20_address": "Dirección TRC20",
"withdraw_exchange.save_address": "Guardar dirección", "withdraw_exchange.save_address": "Guardar dirección",
"withdraw_exchange.saving": "Guardando...", "withdraw_exchange.saving": "Guardando...",
"withdraw_exchange.edit_address": "Editar dirección",
"withdraw_exchange.usdt_only": "Solo USDT", "withdraw_exchange.usdt_only": "Solo USDT",
"withdraw_exchange.withdraw_now": "Retirar ahora", "withdraw_exchange.withdraw_now": "Retirar ahora",
"withdraw_exchange.exchange_now": "Intercambiar ahora", "withdraw_exchange.exchange_now": "Intercambiar ahora",

View File

@ -240,6 +240,7 @@
"withdraw_exchange.trc20_address": "Alamat TRC20", "withdraw_exchange.trc20_address": "Alamat TRC20",
"withdraw_exchange.save_address": "Simpan alamat", "withdraw_exchange.save_address": "Simpan alamat",
"withdraw_exchange.saving": "Menyimpan...", "withdraw_exchange.saving": "Menyimpan...",
"withdraw_exchange.edit_address": "Edit alamat",
"withdraw_exchange.usdt_only": "Hanya USDT", "withdraw_exchange.usdt_only": "Hanya USDT",
"withdraw_exchange.withdraw_now": "Tarik sekarang", "withdraw_exchange.withdraw_now": "Tarik sekarang",
"withdraw_exchange.exchange_now": "Tukar sekarang", "withdraw_exchange.exchange_now": "Tukar sekarang",

View File

@ -240,6 +240,7 @@
"withdraw_exchange.trc20_address": "TRC20 adresi", "withdraw_exchange.trc20_address": "TRC20 adresi",
"withdraw_exchange.save_address": "Adresi kaydet", "withdraw_exchange.save_address": "Adresi kaydet",
"withdraw_exchange.saving": "Kaydediliyor...", "withdraw_exchange.saving": "Kaydediliyor...",
"withdraw_exchange.edit_address": "Adresi düzenle",
"withdraw_exchange.usdt_only": "Sadece USDT", "withdraw_exchange.usdt_only": "Sadece USDT",
"withdraw_exchange.withdraw_now": "Şimdi çek", "withdraw_exchange.withdraw_now": "Şimdi çek",
"withdraw_exchange.exchange_now": "Şimdi değiştir", "withdraw_exchange.exchange_now": "Şimdi değiştir",

View File

@ -241,6 +241,7 @@
"withdraw_exchange.trc20_address": "TRC20 地址", "withdraw_exchange.trc20_address": "TRC20 地址",
"withdraw_exchange.save_address": "保存地址", "withdraw_exchange.save_address": "保存地址",
"withdraw_exchange.saving": "保存中...", "withdraw_exchange.saving": "保存中...",
"withdraw_exchange.edit_address": "编辑地址",
"withdraw_exchange.usdt_only": "仅支持 USDT", "withdraw_exchange.usdt_only": "仅支持 USDT",
"withdraw_exchange.withdraw_now": "立即提现", "withdraw_exchange.withdraw_now": "立即提现",
"withdraw_exchange.exchange_now": "立即兑换", "withdraw_exchange.exchange_now": "立即兑换",

View File

@ -304,14 +304,22 @@
</div> </div>
<div class="bound-copy"> <div class="bound-copy">
<div class="bound-network">USDT TRC20</div> <div class="bound-network">USDT TRC20</div>
<div class="bound-address" id="boundAddress">-</div>
<div
class="bound-meta"
data-i18n="withdraw_exchange.usdt_only"
>
USDT only
</div>
</div> </div>
<button
class="edit-address-button"
id="editAddressButton"
type="button"
aria-label="Edit address"
data-i18n-aria="withdraw_exchange.edit_address"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 20h9" />
<path
d="M16.5 3.5a2.1 2.1 0 0 1 3 3L8 18l-4 1 1-4Z"
/>
</svg>
</button>
<div class="bound-address" id="boundAddress">-</div>
</div> </div>
</section> </section>
@ -388,7 +396,10 @@
aria-labelledby="contactTitle" aria-labelledby="contactTitle"
> >
<div class="contact-head"> <div class="contact-head">
<h2 id="contactTitle" data-i18n="contact_card.sheet_title"> <h2
id="contactTitle"
data-i18n="contact_card.sheet_title"
>
WhatsApp Contact WhatsApp Contact
</h2> </h2>
<button <button
@ -439,7 +450,7 @@
<script src="../../common/jsbridge.js"></script> <script src="../../common/jsbridge.js"></script>
<script src="../../common/i18n.js?v=20260602"></script> <script src="../../common/i18n.js?v=20260602"></script>
<script src="../../common/params.js?v=20260604"></script> <script src="../../common/params.js?v=20260604"></script>
<script src="../../common/api.js?v=20260604-salary-wallet"></script> <script src="../../common/api.js?v=20260701-salary-withdraw"></script>
<script src="./script.js?v=20260701-history-tap"></script> <script src="./script.js?v=20260701-withdraw-api"></script>
</body> </body>
</html> </html>

View File

@ -14,6 +14,7 @@
selectedPayee: null, selectedPayee: null,
isSearching: false, isSearching: false,
isSaving: false, isSaving: false,
isEditingAddress: false,
isSavingContact: false, isSavingContact: false,
isSubmitting: false, isSubmitting: false,
contactInfo: '', contactInfo: '',
@ -107,7 +108,10 @@
} }
function normalizeIdentity(value) { function normalizeIdentity(value) {
value = String(value || '').trim().toUpperCase().replace(/-/g, '_'); value = String(value || '')
.trim()
.toUpperCase()
.replace(/-/g, '_');
if (value === 'ADMIN') return 'BD_LEADER'; if (value === 'ADMIN') return 'BD_LEADER';
if (['HOST', 'AGENCY', 'BD', 'BD_LEADER'].indexOf(value) >= 0) if (['HOST', 'AGENCY', 'BD', 'BD_LEADER'].indexOf(value) >= 0)
return value; return value;
@ -132,7 +136,13 @@
} }
function commandId(prefix) { function commandId(prefix) {
return prefix + '-' + Date.now() + '-' + Math.random().toString(16).slice(2); return (
prefix +
'-' +
Date.now() +
'-' +
Math.random().toString(16).slice(2)
);
} }
function money(value) { function money(value) {
@ -161,6 +171,13 @@
return Number(value || 0) / 100; return Number(value || 0) / 100;
} }
function salaryBalanceAfterToNumber(value) {
if (value && typeof value === 'object') {
return usdMinorToNumber(value.available_amount);
}
return usdMinorToNumber(value);
}
function findTransferRate(value) { function findTransferRate(value) {
var minor = Math.round(Number(value || 0) * 100); var minor = Math.round(Number(value || 0) * 100);
return (state.transferRateTiers || []).find(function (tier) { return (state.transferRateTiers || []).find(function (tier) {
@ -268,6 +285,36 @@
).trim(); ).trim();
} }
function withdrawAddressFrom(value) {
if (typeof value === 'string') return value.trim();
if (!value || typeof value !== 'object') return '';
if (typeof value.withdraw_address === 'string')
return value.withdraw_address.trim();
var source =
value.withdraw_address || value.user || value.profile || value;
return String(
firstValue(source, [
'cardNo',
'withdraw_usdt_trc20_address',
'withdrawUsdtTrc20Address',
'usdt_trc20_address',
'usdtTrc20Address',
'payment_address',
'paymentAddress',
'address',
]) || ''
).trim();
}
function paymentInfoFrom(value) {
var address = withdrawAddressFrom(value);
if (!address) return null;
return {
id: 'usdt_trc20',
cardNo: address,
};
}
function timeLabelFromMS(value) { function timeLabelFromMS(value) {
var number = Number(value || 0); var number = Number(value || 0);
if (!Number.isFinite(number) || number <= 0) return ''; if (!Number.isFinite(number) || number <= 0) return '';
@ -333,7 +380,9 @@
amount: Math.abs(usdMinorToNumber(delta)), amount: Math.abs(usdMinorToNumber(delta)),
balance: usdMinorToNumber(balance), balance: usdMinorToNumber(balance),
type: delta < 0 ? 1 : 0, type: delta < 0 ? 1 : 0,
date: timeLabelFromMS(firstValue(item, ['created_at_ms', 'createdAtMs'])), date: timeLabelFromMS(
firstValue(item, ['created_at_ms', 'createdAtMs'])
),
}; };
} }
@ -359,12 +408,6 @@
return /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(String(value || '').trim()); return /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(String(value || '').trim());
} }
function maskAddress(value) {
var text = String(value || '').trim();
if (text.length <= 12) return text || '-';
return text.slice(0, 6) + '...' + text.slice(-6);
}
function activeSubmitLabel() { function activeSubmitLabel() {
if (currentMode === 'withdraw') if (currentMode === 'withdraw')
return t('withdraw_exchange.withdraw_now', 'Withdraw now'); return t('withdraw_exchange.withdraw_now', 'Withdraw now');
@ -398,11 +441,11 @@
function renderPaymentInfo() { function renderPaymentInfo() {
var payment = state.paymentInfo; var payment = state.paymentInfo;
$('emptyAddressState').hidden = Boolean(payment); var hasPayment = Boolean(payment);
$('addressForm').hidden = Boolean(payment); $('emptyAddressState').hidden = hasPayment || state.isEditingAddress;
$('boundAddressCard').hidden = !payment; $('addressForm').hidden = hasPayment && !state.isEditingAddress;
if (payment) $('boundAddressCard').hidden = !hasPayment || state.isEditingAddress;
$('boundAddress').textContent = maskAddress(payment.cardNo); if (payment) $('boundAddress').textContent = payment.cardNo || '-';
updateSubmit(); updateSubmit();
} }
@ -554,14 +597,23 @@
: api() && api().history : api() && api().history
? api().history(identity, 30).then(normalizeHistoryItems) ? api().history(identity, 30).then(normalizeHistoryItems)
: Promise.resolve([]); : Promise.resolve([]);
historyRequest.then(function (items) { historyRequest
state.historyItems = items || []; .then(function (items) {
renderHistory(); state.historyItems = items || [];
}).catch(function (err) { renderHistory();
state.historyItems = []; })
renderHistory(); .catch(function (err) {
toast(err && err.message ? err.message : t('withdraw_exchange.load_failed', 'Load failed. Try again later.')); state.historyItems = [];
}); renderHistory();
toast(
err && err.message
? err.message
: t(
'withdraw_exchange.load_failed',
'Load failed. Try again later.'
)
);
});
} }
function closeHistory() { function closeHistory() {
@ -629,9 +681,7 @@
toast(t('contact_card.saved', 'Saved.')); toast(t('contact_card.saved', 'Saved.'));
}) })
.catch(function () { .catch(function () {
setContactStatus( setContactStatus(t('contact_card.save_failed', 'Save failed.'));
t('contact_card.save_failed', 'Save failed.')
);
}) })
.finally(function () { .finally(function () {
setContactSaving(false); setContactSaving(false);
@ -655,19 +705,55 @@
'withdraw_exchange.saving', 'withdraw_exchange.saving',
'Saving...' 'Saving...'
); );
mockRequest('saveAddress', { cardNo: address }).then( var saveRequest = isMock
function (payment) { ? mockRequest('saveAddress', { cardNo: address })
state.paymentInfo = payment; : api() && api().saveWithdrawAddress
? api()
.saveWithdrawAddress({
payment_address: address,
})
.then(paymentInfoFrom)
: Promise.reject(
new Error(
t('withdraw_exchange.submit_failed', 'Submit failed.')
)
);
saveRequest
.then(function (payment) {
state.paymentInfo = payment || {
id: 'usdt_trc20',
cardNo: address,
};
state.isEditingAddress = false;
$('trc20Address').value = '';
renderPaymentInfo();
toast(t('withdraw_exchange.address_saved', 'Address saved'));
})
.catch(function (err) {
toast(
err && err.message
? err.message
: t('withdraw_exchange.submit_failed', 'Submit failed.')
);
})
.finally(function () {
state.isSaving = false; state.isSaving = false;
$('saveAddressButton').disabled = false; $('saveAddressButton').disabled = false;
$('saveAddressButton').textContent = t( $('saveAddressButton').textContent = t(
'withdraw_exchange.save_address', 'withdraw_exchange.save_address',
'Save address' 'Save address'
); );
renderPaymentInfo(); });
toast(t('withdraw_exchange.address_saved', 'Address saved')); }
}
); function editAddress() {
if (!state.paymentInfo) return;
state.isEditingAddress = true;
$('trc20Address').value = state.paymentInfo.cardNo || '';
renderPaymentInfo();
window.setTimeout(function () {
$('trc20Address').focus();
}, 0);
} }
function searchPayee() { function searchPayee() {
@ -688,7 +774,12 @@
state.selectedPayee = null; state.selectedPayee = null;
renderPayee(); renderPayee();
if (!isMock && !api()) { if (!isMock && !api()) {
toast(t('withdraw_exchange.search_failed', 'Search failed. Try again later.')); toast(
t(
'withdraw_exchange.search_failed',
'Search failed. Try again later.'
)
);
return; return;
} }
state.isSearching = true; state.isSearching = true;
@ -699,16 +790,21 @@
); );
var request = isMock var request = isMock
? mockRequest('searchPayee', { account: account }) ? mockRequest('searchPayee', { account: account })
: api().searchCoinSeller(account, identity).then(function (data) { : api()
var seller = data && data.seller; .searchCoinSeller(account, identity)
if (!seller) return null; .then(function (data) {
return { var seller = data && data.seller;
id: seller.user_id || seller.userId, if (!seller) return null;
name: seller.username || seller.display_user_id || account, return {
account: seller.display_user_id || account, id: seller.user_id || seller.userId,
avatar: seller.avatar || '', name:
}; seller.username ||
}); seller.display_user_id ||
account,
account: seller.display_user_id || account,
avatar: seller.avatar || '',
};
});
request request
.then(function (payee) { .then(function (payee) {
state.isSearching = false; state.isSearching = false;
@ -718,7 +814,9 @@
'Search' 'Search'
); );
if (!payee) { if (!payee) {
toast(t('withdraw_exchange.payee_not_found', 'User not found')); toast(
t('withdraw_exchange.payee_not_found', 'User not found')
);
return; return;
} }
state.selectedPayee = payee; state.selectedPayee = payee;
@ -731,7 +829,14 @@
'withdraw_exchange.search', 'withdraw_exchange.search',
'Search' 'Search'
); );
toast(err && err.message ? err.message : t('withdraw_exchange.payee_not_found', 'User not found')); toast(
err && err.message
? err.message
: t(
'withdraw_exchange.payee_not_found',
'User not found'
)
);
}); });
} }
@ -780,15 +885,6 @@
openContactModal(); openContactModal();
return; return;
} }
if (!isMock && currentMode === 'withdraw') {
toast(
t(
'withdraw_exchange.withdraw_not_available',
'Withdraw is not available yet.'
)
);
return;
}
state.isSubmitting = true; state.isSubmitting = true;
updateSubmit(); updateSubmit();
submitRequest() submitRequest()
@ -802,8 +898,14 @@
} }
if (isMock) { if (isMock) {
state.balance = mock.balance; state.balance = mock.balance;
} else if (currentMode === 'withdraw') {
state.balance = salaryBalanceAfterToNumber(
result.salary_balance_after
);
} else if (currentMode === 'exchange') { } else if (currentMode === 'exchange') {
state.balance = usdMinorToNumber(result.salary_balance_after); state.balance = salaryBalanceAfterToNumber(
result.salary_balance_after
);
} else if (currentMode === 'transfer') { } else if (currentMode === 'transfer') {
state.balance = usdMinorToNumber( state.balance = usdMinorToNumber(
result.source_salary_balance_after result.source_salary_balance_after
@ -823,7 +925,11 @@
.catch(function (err) { .catch(function (err) {
state.isSubmitting = false; state.isSubmitting = false;
updateSubmit(); updateSubmit();
toast(err && err.message ? err.message : t('withdraw_exchange.submit_failed', 'Submit failed.')); toast(
err && err.message
? err.message
: t('withdraw_exchange.submit_failed', 'Submit failed.')
);
}); });
} }
@ -839,7 +945,9 @@
} }
if (!api()) { if (!api()) {
return Promise.reject( return Promise.reject(
new Error(t('withdraw_exchange.submit_failed', 'Submit failed.')) new Error(
t('withdraw_exchange.submit_failed', 'Submit failed.')
)
); );
} }
if (currentMode === 'exchange') { if (currentMode === 'exchange') {
@ -850,6 +958,14 @@
reason: 'salary exchange', reason: 'salary exchange',
}); });
} }
if (currentMode === 'withdraw') {
return api().withdraw({
command_id: commandId('salary-withdraw'),
identity: identity,
amount_usd: amountText(value),
payment_address: state.paymentInfo && state.paymentInfo.cardNo,
});
}
return api().transferToCoinSeller({ return api().transferToCoinSeller({
command_id: commandId('salary-transfer'), command_id: commandId('salary-transfer'),
identity: identity, identity: identity,
@ -920,6 +1036,7 @@
}); });
bindTap('searchPayeeButton', searchPayee); bindTap('searchPayeeButton', searchPayee);
bindTap('saveAddressButton', saveAddress); bindTap('saveAddressButton', saveAddress);
bindTap('editAddressButton', editAddress);
bindTap('submitButton', submit); bindTap('submitButton', submit);
bindTap('salaryHistoryButton', openHistory); bindTap('salaryHistoryButton', openHistory);
bindTap('salaryHistoryBackdrop', closeHistory); bindTap('salaryHistoryBackdrop', closeHistory);
@ -982,7 +1099,12 @@
return; return;
} }
if (!api()) { if (!api()) {
toast(t('withdraw_exchange.load_failed', 'Load failed. Try again later.')); toast(
t(
'withdraw_exchange.load_failed',
'Load failed. Try again later.'
)
);
return; return;
} }
api() api()
@ -991,16 +1113,26 @@
identity = normalizeIdentity(data.identity || identity); identity = normalizeIdentity(data.identity || identity);
salaryAssetType = data.salary_asset_type || salaryAssetType; salaryAssetType = data.salary_asset_type || salaryAssetType;
EXCHANGE_RATE = Number(data.exchange_rate || EXCHANGE_RATE); EXCHANGE_RATE = Number(data.exchange_rate || EXCHANGE_RATE);
var payment = paymentInfoFrom(data);
if (payment) state.paymentInfo = payment;
state.balance = usdMinorToNumber( state.balance = usdMinorToNumber(
data.salary && data.salary.available_amount data.salary && data.salary.available_amount
); );
state.transferRateTiers = data.transfer_rate_tiers || []; state.transferRateTiers = data.transfer_rate_tiers || [];
renderStaticDynamicText(); renderStaticDynamicText();
renderBalance(); renderBalance();
renderPaymentInfo();
updateSubmit(); updateSubmit();
}) })
.catch(function (err) { .catch(function (err) {
toast(err && err.message ? err.message : t('withdraw_exchange.load_failed', 'Load failed. Try again later.')); toast(
err && err.message
? err.message
: t(
'withdraw_exchange.load_failed',
'Load failed. Try again later.'
)
);
}); });
} }
@ -1013,7 +1145,18 @@
return; return;
} }
state.paymentInfo = null; state.paymentInfo = null;
state.isEditingAddress = false;
renderPaymentInfo(); renderPaymentInfo();
if (!api() || !api().getWithdrawAddress) return;
api()
.getWithdrawAddress()
.then(function (data) {
state.paymentInfo = paymentInfoFrom(data);
renderPaymentInfo();
})
.catch(function () {
renderPaymentInfo();
});
} }
function loadContactInfo() { function loadContactInfo() {

View File

@ -556,10 +556,12 @@ input {
} }
.bound-address-card { .bound-address-card {
display: flex; display: grid;
grid-template-columns: 56px minmax(0, 1fr) 38px;
align-items: center; align-items: center;
gap: 14px; column-gap: 14px;
min-height: 88px; row-gap: 11px;
min-height: 86px;
margin-top: 16px; margin-top: 16px;
padding: 14px; padding: 14px;
border-radius: 8px; border-radius: 8px;
@ -592,18 +594,40 @@ input {
} }
.bound-address { .bound-address {
margin-top: 5px; display: block;
overflow-wrap: anywhere; grid-column: 1 / -1;
color: rgba(255, 255, 255, 0.82); color: rgba(255, 255, 255, 0.86);
font-size: 14px; font-size: 13px;
font-weight: 850; font-weight: 850;
line-height: 1.25;
overflow-wrap: anywhere;
white-space: normal;
word-break: break-word;
} }
.bound-meta { .edit-address-button {
margin-top: 4px; display: flex;
color: rgba(255, 255, 255, 0.62); align-items: center;
font-size: 12px; justify-content: center;
font-weight: 750; width: 38px;
height: 38px;
flex: 0 0 38px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.13);
color: #fff;
}
.edit-address-button svg {
width: 20px;
height: 20px;
}
.edit-address-button path {
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2;
} }
.primary-button, .primary-button,