import request from '@/utils/request' import { saveAs } from 'file-saver' import { blobValidate } from '@/utils/bms' import { ElLoading } from 'element-plus' import errorCode from '@/utils/errorCode' /** Quote trial for billing center */ export function quoteBilling(data) { return request({ url: '/price/billing/quote', method: 'post', data }) } /** Products from customer's bound effective package (trial dropdown) */ export function listQuoteBoundProducts(customerCode, bizDate) { return request({ url: '/price/billing/quote/products', method: 'get', params: { customerCode, bizDate } }) } /** Historical quote list */ export function listQuote(query) { return request({ url: '/price/billing/quote/list', method: 'get', params: query }) } /** Historical quote detail */ export function getQuote(quoteId) { return request({ url: '/price/billing/quote/' + quoteId, method: 'get' }) } /** Sync order receivable bill */ export function billOrder(data) { return request({ url: '/price/billing/bill', method: 'post', data }) } /** Receivable bill list */ export function listBill(query) { return request({ url: '/price/billing/bill/list', method: 'get', params: query }) } export function listOrderBill(query) { return request({ url: '/price/billing/orderBill/list', method: 'get', params: query }) } /** Order-bill product check: package sales products missing from catalog */ export function checkOrderBillProducts(data) { return request({ url: '/price/billing/orderBill/checkProducts', method: 'post', data, timeout: 180000 }) } /** Drill-down order bills for one product under product-check filters */ export function listOrderBillsByProduct(data) { return request({ url: '/price/billing/orderBill/checkProducts/orders', method: 'post', data, timeout: 120000 }) } /** Order-list local rebill (prc_order_bill only, no Oracle). Returns { taskId }. */ export function submitLocalRebill(data) { return request({ url: '/price/billing/orderBill/rebill', method: 'post', data, timeout: 120000 }) } export function getLocalRebillTask(taskId) { return request({ url: '/price/billing/orderBill/rebill/task/' + taskId, method: 'get' }) } export function cancelLocalRebill(taskId) { return request({ url: '/price/billing/orderBill/rebill/task/' + taskId + '/cancel', method: 'post' }) } /** One in-flight poll per taskId — avoids stacked loops that flood XHR and freeze the page. */ const localRebillPollControllers = new Map() /** * Poll local rebill task until terminal status. * @param {string} taskId * @param {(task: object) => void} [onProgress] */ export async function pollLocalRebillTask(taskId, onProgress) { const id = String(taskId || '') if (!id) throw new Error('missing local rebill taskId') const prev = localRebillPollControllers.get(id) if (prev) prev.aborted = true const ctrl = { aborted: false } localRebillPollControllers.set(id, ctrl) const started = Date.now() const maxMs = 30 * 60 * 1000 let lastSig = '' try { while (Date.now() - started < maxMs) { if (ctrl.aborted) { const err = new Error('local rebill poll superseded') err.superseded = true throw err } // Hidden tab: slow down; no need to hammer Redis while user is away. const hidden = typeof document !== 'undefined' && document.visibilityState === 'hidden' const res = await getLocalRebillTask(id) const task = res.data || {} const st = String(task.status || '').toUpperCase() const sig = [st, task.percent, task.success, task.fail, task.total].join('|') if (typeof onProgress === 'function' && sig !== lastSig) { lastSig = sig onProgress(task) } if (st === 'SUCCESS') return { task } if (st === 'CANCELLED') return { task, cancelled: true } if (st === 'FAIL') { const err = new Error(task.error || task.message || 'local rebill failed') err.task = task throw err } // 1s visible / 2.5s hidden — dense 400ms polls were freezing the list page. await new Promise(resolve => setTimeout(resolve, hidden ? 2500 : 1000)) } throw new Error('local rebill timeout') } finally { if (localRebillPollControllers.get(id) === ctrl) { localRebillPollControllers.delete(id) } } } /** * Async local rebill; poll until done. * @param {object} data LocalRebillRequest * @param {(task: object) => void} [onProgress] * @param {{ getTaskId?: (taskId: string) => void }} [options] */ export async function localRebillAsync(data, onProgress, options) { const submitRes = await submitLocalRebill(data) const d = submitRes && submitRes.data const taskId = d && (d.taskId || d) if (!taskId) throw new Error('missing local rebill taskId') if (options && typeof options.getTaskId === 'function') options.getTaskId(String(taskId)) return pollLocalRebillTask(String(taskId), onProgress) } /** Receivable bill detail */ export function getBill(billId) { return request({ url: '/price/billing/bill/' + billId, method: 'get' }) } /** ES billing history for one bill/order */ export function listBillHistory(billId, query) { return request({ url: '/price/billing/bill/' + encodeURIComponent(billId) + '/history', method: 'get', params: query }) } /** Refund receivable bill (billType=REFUND) */ export function refundBill(billId) { return request({ url: '/price/billing/bill/' + encodeURIComponent(billId) + '/refund', method: 'post', timeout: 120000 }) } /** Order no options for remote search (bill list multi search) */ export function listOrderNoOptions(keyword) { return request({ url: '/price/billing/bill/orderNos', method: 'get', params: { keyword } }) } /** 运单号下拉远程搜索选项 */ export function listTrackingNoOptions(keyword) { return request({ url: '/price/billing/bill/trackingNos', method: 'get', params: { keyword } }) } /** 转单号下拉远程搜索选项 */ export function listTransferNoOptions(keyword) { return request({ url: '/price/billing/bill/transferNos', method: 'get', params: { keyword } }) } /** 计费详情:增加应收费用明细 */ export function addIncomeBillFee(data) { return request({ url: '/price/billing/bill/fee', method: 'post', data }) } /** 批量增加费用:按筛选预览匹配订单 */ export function previewBatchAddIncomeFee(data) { return request({ url: '/price/billing/bill/fee/batch/preview', method: 'post', data }) } /** 批量增加费用:提交异步任务 */ export function submitBatchAddIncomeFee(data) { return request({ url: '/price/billing/bill/fee/batch', method: 'post', data }) } export function getBatchAddIncomeFeeTask(taskId) { return request({ url: '/price/billing/bill/fee/batch/task/' + taskId, method: 'get' }) } async function pollBatchAddIncomeFeeTask(taskId, onProgress) { const started = Date.now() const maxMs = 30 * 60 * 1000 while (Date.now() - started < maxMs) { const res = await getBatchAddIncomeFeeTask(taskId) const task = res.data || {} if (typeof onProgress === 'function') onProgress(task) if (task.status === 'SUCCESS') return { task } if (task.status === 'FAIL') { const err = new Error(task.error || task.message || 'batch add fee failed') err.task = task throw err } await new Promise(resolve => setTimeout(resolve, 800)) } throw new Error('batch add fee timeout') } /** * Async batch add fee; poll until done. * @param {object} data BatchAddIncomeBillFeeRequest * @param {(task: object) => void} [onProgress] */ export async function batchAddIncomeFeeAsync(data, onProgress) { const submitRes = await submitBatchAddIncomeFee(data) const d = submitRes && submitRes.data const taskId = d && (d.taskId || d) if (!taskId) throw new Error('missing batch fee taskId') return pollBatchAddIncomeFeeTask(String(taskId), onProgress) } /** 计费详情:编辑手动应收费用 */ export function updateIncomeBillFee(data) { return request({ url: '/price/billing/bill/fee', method: 'put', data }) } /** 计费详情:删除手动应收费用 */ export function deleteIncomeBillFee(feeId) { return request({ url: '/price/billing/bill/fee/' + feeId, method: 'delete' }) } /** 计费列表:批量更新订单额度欠费状态(校验可用额度) */ export function updateCreditArrears(billIds) { return request({ url: '/price/billing/bill/creditArrears', method: 'put', data: billIds }) } /** 计费列表:单票修改计费重(写 charge_weight,保留 weight)并重新计费 */ export function updateChargeWeight(data) { return request({ url: '/price/billing/bill/chargeWeight', method: 'put', data }) } export function getChargeWeightImportTask(taskId) { return request({ url: '/price/billing/bill/chargeWeight/import/task/' + taskId, method: 'get' }) } function pickChargeWeightImportTaskId(res) { const d = res && res.data if (!d) return '' if (typeof d === 'string' || typeof d === 'number') return String(d) return String(d.taskId || d.id || '') } async function pollChargeWeightImportTask(taskId, onProgress) { const started = Date.now() const maxMs = 30 * 60 * 1000 while (Date.now() - started < maxMs) { const res = await getChargeWeightImportTask(taskId) const task = res.data || {} if (typeof onProgress === 'function') onProgress(task) if (task.status === 'SUCCESS') return { task } if (task.status === 'FAIL') { const err = new Error(task.error || task.message || 'charge weight import failed') err.task = task throw err } await new Promise(resolve => setTimeout(resolve, 800)) } throw new Error('charge weight import timeout') } /** * Async import charge weight + rebill; poll until done. * @param {File|Blob} file * @param {(task: object) => void} [onProgress] */ export async function importChargeWeightAsync(file, onProgress) { const formData = new FormData() formData.append('file', file) const submitRes = await request({ url: '/price/billing/bill/chargeWeight/importData', method: 'post', data: formData, headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false }, // Backend parses Excel + validates rows before returning taskId timeout: 10 * 60 * 1000 }) const taskId = pickChargeWeightImportTaskId(submitRes) if (!taskId) throw new Error('missing charge weight import taskId') return pollChargeWeightImportTask(taskId, onProgress) } /** Download charge-weight import template (optionally fill by list filters). */ export async function downloadChargeWeightTemplate(data, filename) { const loading = ElLoading.service({ text: '正在下载模板,请稍候', background: 'rgba(0, 0, 0, 0.7)' }) try { const blobData = await request({ url: '/price/billing/bill/chargeWeight/importTemplate', method: 'post', data: data || {}, responseType: 'blob', timeout: 120000 }) if (blobValidate(blobData)) { saveAs(new Blob([blobData]), filename || ('批量修改计费重导入模板_' + Date.now() + '.xlsx')) return } const resText = await blobData.text() const rspObj = JSON.parse(resText) const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'] || '下载模板失败' throw new Error(errMsg) } finally { loading.close() } } /** 批量修改计费重导入附件记录 */ export function listChargeWeightImportAttachments(query) { return request({ url: '/price/billing/bill/chargeWeight/import/attachments', method: 'get', params: query }) } /** Submit async recalc (PRODUCT / REDLINE) */ export function submitRecalcBillPrice(mode, data) { const path = mode === 'REDLINE' ? 'recalcRedline' : 'recalcProduct' return request({ url: '/price/billing/bill/' + path, method: 'post', data }) } export function getRecalcBillPriceTask(taskId) { return request({ url: '/price/billing/bill/recalcPrice/task/' + taskId, method: 'get' }) } /** * Async recalc product/redline amount; poll until done. * @param {'PRODUCT'|'REDLINE'} mode * @param {object} data * @param {(task: object) => void} [onProgress] */ export async function recalcBillPriceAsync(mode, data, onProgress) { const submitRes = await submitRecalcBillPrice(mode, data) const d = submitRes && submitRes.data const taskId = d && (d.taskId || d) if (!taskId) throw new Error('missing recalc taskId') const started = Date.now() const maxMs = 30 * 60 * 1000 while (Date.now() - started < maxMs) { const res = await getRecalcBillPriceTask(String(taskId)) const task = res.data || {} if (typeof onProgress === 'function') onProgress(task) if (task.status === 'SUCCESS') return { task } if (task.status === 'FAIL') { const err = new Error(task.error || task.message || 'recalc price failed') err.task = task throw err } await new Promise(resolve => setTimeout(resolve, 800)) } throw new Error('recalc price timeout') }