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 || {},
});
},
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 = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -304,14 +304,22 @@
</div>
<div class="bound-copy">
<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>
<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>
</section>
@ -388,7 +396,10 @@
aria-labelledby="contactTitle"
>
<div class="contact-head">
<h2 id="contactTitle" data-i18n="contact_card.sheet_title">
<h2
id="contactTitle"
data-i18n="contact_card.sheet_title"
>
WhatsApp Contact
</h2>
<button
@ -439,7 +450,7 @@
<script src="../../common/jsbridge.js"></script>
<script src="../../common/i18n.js?v=20260602"></script>
<script src="../../common/params.js?v=20260604"></script>
<script src="../../common/api.js?v=20260604-salary-wallet"></script>
<script src="./script.js?v=20260701-history-tap"></script>
<script src="../../common/api.js?v=20260701-salary-withdraw"></script>
<script src="./script.js?v=20260701-withdraw-api"></script>
</body>
</html>

View File

@ -14,6 +14,7 @@
selectedPayee: null,
isSearching: false,
isSaving: false,
isEditingAddress: false,
isSavingContact: false,
isSubmitting: false,
contactInfo: '',
@ -107,7 +108,10 @@
}
function normalizeIdentity(value) {
value = String(value || '').trim().toUpperCase().replace(/-/g, '_');
value = String(value || '')
.trim()
.toUpperCase()
.replace(/-/g, '_');
if (value === 'ADMIN') return 'BD_LEADER';
if (['HOST', 'AGENCY', 'BD', 'BD_LEADER'].indexOf(value) >= 0)
return value;
@ -132,7 +136,13 @@
}
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) {
@ -161,6 +171,13 @@
return Number(value || 0) / 100;
}
function salaryBalanceAfterToNumber(value) {
if (value && typeof value === 'object') {
return usdMinorToNumber(value.available_amount);
}
return usdMinorToNumber(value);
}
function findTransferRate(value) {
var minor = Math.round(Number(value || 0) * 100);
return (state.transferRateTiers || []).find(function (tier) {
@ -268,6 +285,36 @@
).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) {
var number = Number(value || 0);
if (!Number.isFinite(number) || number <= 0) return '';
@ -333,7 +380,9 @@
amount: Math.abs(usdMinorToNumber(delta)),
balance: usdMinorToNumber(balance),
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());
}
function maskAddress(value) {
var text = String(value || '').trim();
if (text.length <= 12) return text || '-';
return text.slice(0, 6) + '...' + text.slice(-6);
}
function activeSubmitLabel() {
if (currentMode === 'withdraw')
return t('withdraw_exchange.withdraw_now', 'Withdraw now');
@ -398,11 +441,11 @@
function renderPaymentInfo() {
var payment = state.paymentInfo;
$('emptyAddressState').hidden = Boolean(payment);
$('addressForm').hidden = Boolean(payment);
$('boundAddressCard').hidden = !payment;
if (payment)
$('boundAddress').textContent = maskAddress(payment.cardNo);
var hasPayment = Boolean(payment);
$('emptyAddressState').hidden = hasPayment || state.isEditingAddress;
$('addressForm').hidden = hasPayment && !state.isEditingAddress;
$('boundAddressCard').hidden = !hasPayment || state.isEditingAddress;
if (payment) $('boundAddress').textContent = payment.cardNo || '-';
updateSubmit();
}
@ -554,14 +597,23 @@
: api() && api().history
? api().history(identity, 30).then(normalizeHistoryItems)
: Promise.resolve([]);
historyRequest.then(function (items) {
state.historyItems = items || [];
renderHistory();
}).catch(function (err) {
state.historyItems = [];
renderHistory();
toast(err && err.message ? err.message : t('withdraw_exchange.load_failed', 'Load failed. Try again later.'));
});
historyRequest
.then(function (items) {
state.historyItems = items || [];
renderHistory();
})
.catch(function (err) {
state.historyItems = [];
renderHistory();
toast(
err && err.message
? err.message
: t(
'withdraw_exchange.load_failed',
'Load failed. Try again later.'
)
);
});
}
function closeHistory() {
@ -629,9 +681,7 @@
toast(t('contact_card.saved', 'Saved.'));
})
.catch(function () {
setContactStatus(
t('contact_card.save_failed', 'Save failed.')
);
setContactStatus(t('contact_card.save_failed', 'Save failed.'));
})
.finally(function () {
setContactSaving(false);
@ -655,19 +705,55 @@
'withdraw_exchange.saving',
'Saving...'
);
mockRequest('saveAddress', { cardNo: address }).then(
function (payment) {
state.paymentInfo = payment;
var saveRequest = isMock
? mockRequest('saveAddress', { cardNo: address })
: 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;
$('saveAddressButton').disabled = false;
$('saveAddressButton').textContent = t(
'withdraw_exchange.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() {
@ -688,7 +774,12 @@
state.selectedPayee = null;
renderPayee();
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;
}
state.isSearching = true;
@ -699,16 +790,21 @@
);
var request = isMock
? mockRequest('searchPayee', { account: account })
: api().searchCoinSeller(account, identity).then(function (data) {
var seller = data && data.seller;
if (!seller) return null;
return {
id: seller.user_id || seller.userId,
name: seller.username || seller.display_user_id || account,
account: seller.display_user_id || account,
avatar: seller.avatar || '',
};
});
: api()
.searchCoinSeller(account, identity)
.then(function (data) {
var seller = data && data.seller;
if (!seller) return null;
return {
id: seller.user_id || seller.userId,
name:
seller.username ||
seller.display_user_id ||
account,
account: seller.display_user_id || account,
avatar: seller.avatar || '',
};
});
request
.then(function (payee) {
state.isSearching = false;
@ -718,7 +814,9 @@
'Search'
);
if (!payee) {
toast(t('withdraw_exchange.payee_not_found', 'User not found'));
toast(
t('withdraw_exchange.payee_not_found', 'User not found')
);
return;
}
state.selectedPayee = payee;
@ -731,7 +829,14 @@
'withdraw_exchange.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();
return;
}
if (!isMock && currentMode === 'withdraw') {
toast(
t(
'withdraw_exchange.withdraw_not_available',
'Withdraw is not available yet.'
)
);
return;
}
state.isSubmitting = true;
updateSubmit();
submitRequest()
@ -802,8 +898,14 @@
}
if (isMock) {
state.balance = mock.balance;
} else if (currentMode === 'withdraw') {
state.balance = salaryBalanceAfterToNumber(
result.salary_balance_after
);
} else if (currentMode === 'exchange') {
state.balance = usdMinorToNumber(result.salary_balance_after);
state.balance = salaryBalanceAfterToNumber(
result.salary_balance_after
);
} else if (currentMode === 'transfer') {
state.balance = usdMinorToNumber(
result.source_salary_balance_after
@ -823,7 +925,11 @@
.catch(function (err) {
state.isSubmitting = false;
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()) {
return Promise.reject(
new Error(t('withdraw_exchange.submit_failed', 'Submit failed.'))
new Error(
t('withdraw_exchange.submit_failed', 'Submit failed.')
)
);
}
if (currentMode === 'exchange') {
@ -850,6 +958,14 @@
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({
command_id: commandId('salary-transfer'),
identity: identity,
@ -920,6 +1036,7 @@
});
bindTap('searchPayeeButton', searchPayee);
bindTap('saveAddressButton', saveAddress);
bindTap('editAddressButton', editAddress);
bindTap('submitButton', submit);
bindTap('salaryHistoryButton', openHistory);
bindTap('salaryHistoryBackdrop', closeHistory);
@ -982,7 +1099,12 @@
return;
}
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;
}
api()
@ -991,16 +1113,26 @@
identity = normalizeIdentity(data.identity || identity);
salaryAssetType = data.salary_asset_type || salaryAssetType;
EXCHANGE_RATE = Number(data.exchange_rate || EXCHANGE_RATE);
var payment = paymentInfoFrom(data);
if (payment) state.paymentInfo = payment;
state.balance = usdMinorToNumber(
data.salary && data.salary.available_amount
);
state.transferRateTiers = data.transfer_rate_tiers || [];
renderStaticDynamicText();
renderBalance();
renderPaymentInfo();
updateSubmit();
})
.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;
}
state.paymentInfo = null;
state.isEditingAddress = false;
renderPaymentInfo();
if (!api() || !api().getWithdrawAddress) return;
api()
.getWithdrawAddress()
.then(function (data) {
state.paymentInfo = paymentInfoFrom(data);
renderPaymentInfo();
})
.catch(function () {
renderPaymentInfo();
});
}
function loadContactInfo() {

View File

@ -556,10 +556,12 @@ input {
}
.bound-address-card {
display: flex;
display: grid;
grid-template-columns: 56px minmax(0, 1fr) 38px;
align-items: center;
gap: 14px;
min-height: 88px;
column-gap: 14px;
row-gap: 11px;
min-height: 86px;
margin-top: 16px;
padding: 14px;
border-radius: 8px;
@ -592,18 +594,40 @@ input {
}
.bound-address {
margin-top: 5px;
overflow-wrap: anywhere;
color: rgba(255, 255, 255, 0.82);
font-size: 14px;
display: block;
grid-column: 1 / -1;
color: rgba(255, 255, 255, 0.86);
font-size: 13px;
font-weight: 850;
line-height: 1.25;
overflow-wrap: anywhere;
white-space: normal;
word-break: break-word;
}
.bound-meta {
margin-top: 4px;
color: rgba(255, 255, 255, 0.62);
font-size: 12px;
font-weight: 750;
.edit-address-button {
display: flex;
align-items: center;
justify-content: center;
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,