465 lines
13 KiB
JavaScript
465 lines
13 KiB
JavaScript
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')
|
|
}
|