增加查看计费历史记录
This commit is contained in:
@@ -22,9 +22,12 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.bms.common.annotation.Log;
|
||||
import com.bms.common.constant.HttpStatus;
|
||||
import com.bms.common.core.controller.BaseController;
|
||||
import com.bms.common.core.domain.AjaxResult;
|
||||
import com.bms.common.core.page.PageDomain;
|
||||
import com.bms.common.core.page.TableDataInfo;
|
||||
import com.bms.common.core.page.TableSupport;
|
||||
import com.bms.common.enums.BusinessType;
|
||||
import com.bms.common.utils.DateUtils;
|
||||
import com.bms.common.utils.MessageUtils;
|
||||
@@ -54,6 +57,11 @@ import com.bms.price.domain.vo.TmsPriceQuoteRequest;
|
||||
import com.bms.price.domain.vo.TmsPriceQuoteResult;
|
||||
import com.bms.price.enums.BillTaskSuccessStatus;
|
||||
import com.bms.price.enums.BillTaskType;
|
||||
import com.bms.price.search.BillSearchPage;
|
||||
import com.bms.price.search.BillSyncSnapshot;
|
||||
import com.bms.price.search.EsBizLogSupport;
|
||||
import com.bms.price.search.EsLogType;
|
||||
import com.bms.price.search.IBillSearchStore;
|
||||
import com.bms.price.service.IPrcBillingTaskService;
|
||||
import com.bms.price.service.IPrcOrderBillService;
|
||||
import com.bms.price.service.ITmsPriceQuoteService;
|
||||
@@ -84,6 +92,9 @@ public class BillingController extends BaseController
|
||||
@Autowired
|
||||
private OrderBillProductCheckService orderBillProductCheckService;
|
||||
|
||||
@Autowired
|
||||
private IBillSearchStore billSearchStore;
|
||||
|
||||
/**
|
||||
* Historical quote list.
|
||||
*/
|
||||
@@ -355,6 +366,97 @@ public class BillingController extends BaseController
|
||||
return success(prcOrderBillService.getBill(billId));
|
||||
}
|
||||
|
||||
/**
|
||||
* ES BILL logs for this order (billing / upstream sync / fail snapshots).
|
||||
* bizId prefers transferNo then orderNo (same as write path).
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:billing:bill') or @ss.hasPermi('price:orderBill:list')")
|
||||
@GetMapping("/bill/{billId}/history")
|
||||
public TableDataInfo billHistory(@PathVariable("billId") String billId)
|
||||
{
|
||||
if (StringUtils.isEmpty(billId))
|
||||
{
|
||||
return getDataTable(new ArrayList<BillSyncSnapshot>());
|
||||
}
|
||||
TmsPriceQuoteResult bill = prcOrderBillService.getBill(billId.trim());
|
||||
if (bill == null)
|
||||
{
|
||||
TableDataInfo empty = new TableDataInfo();
|
||||
empty.setCode(HttpStatus.SUCCESS);
|
||||
empty.setMsg("查询成功");
|
||||
empty.setRows(new ArrayList<BillSyncSnapshot>());
|
||||
empty.setTotal(0L);
|
||||
return empty;
|
||||
}
|
||||
PageDomain pageDomain = TableSupport.buildPageRequest();
|
||||
int pageNum = pageDomain.getPageNum() == null ? 1 : pageDomain.getPageNum().intValue();
|
||||
int pageSize = pageDomain.getPageSize() == null ? 10 : pageDomain.getPageSize().intValue();
|
||||
String primary = EsBizLogSupport.billBizId(bill.getTransferNo(), bill.getOrderNo());
|
||||
BillSearchPage page = billSearchStore.search(EsLogType.BILL, primary, null, pageNum, pageSize);
|
||||
// Also pull by orderNo when transferNo was used as bizId, so older orderNo-keyed rows appear.
|
||||
if (StringUtils.isNotEmpty(bill.getTransferNo()) && StringUtils.isNotEmpty(bill.getOrderNo())
|
||||
&& !bill.getTransferNo().trim().equalsIgnoreCase(bill.getOrderNo().trim())
|
||||
&& pageNum == 1)
|
||||
{
|
||||
BillSearchPage byOrder = billSearchStore.search(EsLogType.BILL, bill.getOrderNo().trim(), null, 1,
|
||||
Math.max(pageSize, 50));
|
||||
page = mergeBillHistory(page, byOrder, pageSize);
|
||||
}
|
||||
TableDataInfo rsp = new TableDataInfo();
|
||||
rsp.setCode(HttpStatus.SUCCESS);
|
||||
rsp.setMsg("查询成功");
|
||||
rsp.setRows(page.getRows());
|
||||
rsp.setTotal(page.getTotal());
|
||||
return rsp;
|
||||
}
|
||||
|
||||
private BillSearchPage mergeBillHistory(BillSearchPage primary, BillSearchPage secondary, int pageSize)
|
||||
{
|
||||
BillSearchPage out = new BillSearchPage();
|
||||
Map<String, BillSyncSnapshot> byId = new HashMap<String, BillSyncSnapshot>();
|
||||
if (primary != null && primary.getRows() != null)
|
||||
{
|
||||
for (BillSyncSnapshot row : primary.getRows())
|
||||
{
|
||||
if (row != null && StringUtils.isNotEmpty(row.getDocId()))
|
||||
{
|
||||
byId.put(row.getDocId(), row);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (secondary != null && secondary.getRows() != null)
|
||||
{
|
||||
for (BillSyncSnapshot row : secondary.getRows())
|
||||
{
|
||||
if (row != null && StringUtils.isNotEmpty(row.getDocId()) && !byId.containsKey(row.getDocId()))
|
||||
{
|
||||
byId.put(row.getDocId(), row);
|
||||
}
|
||||
}
|
||||
}
|
||||
List<BillSyncSnapshot> merged = new ArrayList<BillSyncSnapshot>(byId.values());
|
||||
merged.sort((a, b) -> {
|
||||
String ta = a == null || a.getUpdateTime() == null ? "" : a.getUpdateTime();
|
||||
String tb = b == null || b.getUpdateTime() == null ? "" : b.getUpdateTime();
|
||||
return tb.compareTo(ta);
|
||||
});
|
||||
long total = Math.max(primary == null ? 0L : primary.getTotal(), merged.size());
|
||||
if (secondary != null)
|
||||
{
|
||||
total = Math.max(total, secondary.getTotal());
|
||||
}
|
||||
out.setTotal(total);
|
||||
if (merged.size() > pageSize)
|
||||
{
|
||||
out.setRows(new ArrayList<BillSyncSnapshot>(merged.subList(0, pageSize)));
|
||||
}
|
||||
else
|
||||
{
|
||||
out.setRows(merged);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refund one receivable bill via billing task billType=REFUND.
|
||||
*/
|
||||
|
||||
@@ -187,6 +187,15 @@ export function getBill(billId) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 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({
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:model-value="modelValue"
|
||||
:title="title"
|
||||
size="860px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
>
|
||||
<div class="hist-wrap" v-loading="loading">
|
||||
<el-table :data="rows" border height="calc(100vh - 220px)" :empty-text="emptyText">
|
||||
<el-table-column :label="colTime" width="170" align="center" prop="updateTime" />
|
||||
<el-table-column :label="colAction" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="actionTag(row)">{{ actionLabel(row) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colStatus" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="statusTag(row.status)">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colSet" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ priceSetOf(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colVer" width="90" align="center">
|
||||
<template #default="{ row }">{{ versionOf(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colPsd" width="100" align="center">
|
||||
<template #default="{ row }">{{ psdOf(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colAmount" width="110" align="right">
|
||||
<template #default="{ row }">{{ amountOf(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colOps" width="80" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openDetail(row)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pager">
|
||||
<el-pagination
|
||||
v-model:current-page="query.pageNum"
|
||||
v-model:page-size="query.pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="reload"
|
||||
@current-change="reload"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-drawer v-model="detailOpen" size="720px" :title="detailTitle" append-to-body destroy-on-close>
|
||||
<div v-if="!active">
|
||||
<el-empty :description="emptyDetail" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-descriptions :column="2" border size="small" class="mb12">
|
||||
<el-descriptions-item :label="colTime">{{ active.updateTime || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="colAction">{{ actionLabel(active) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="colStatus">{{ statusLabel(active.status) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="colSet">{{ priceSetOf(active) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="colVer">{{ versionOf(active) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="colPsd">{{ psdOf(active) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="colAmount">{{ amountOf(active) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="lbMatch">{{ matchMsgOf(active) }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="active.errorMessage" :label="lbError" :span="2">
|
||||
<span class="err">{{ active.errorMessage }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="json-panel">
|
||||
<div class="json-panel__head">请求报文</div>
|
||||
<pre class="code-pre">{{ formatJson(active.requestJson) }}</pre>
|
||||
</div>
|
||||
<div class="json-panel">
|
||||
<div class="json-panel__head">返回报文</div>
|
||||
<pre class="code-pre">{{ formatJson(active.responseJson) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup name="BillBillingHistoryDrawer">
|
||||
import { listBillHistory } from '@/api/price/billing'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
billId: { type: String, default: '' },
|
||||
orderNo: { type: String, default: '' }
|
||||
})
|
||||
defineEmits(['update:modelValue'])
|
||||
|
||||
const loading = ref(false)
|
||||
const rows = ref([])
|
||||
const total = ref(0)
|
||||
const query = reactive({ pageNum: 1, pageSize: 10 })
|
||||
const detailOpen = ref(false)
|
||||
const active = ref(null)
|
||||
|
||||
const emptyText = '暂无历史计费记录'
|
||||
const emptyDetail = '暂无详情'
|
||||
const colTime = '写入时间'
|
||||
const colAction = '类型'
|
||||
const colStatus = '状态'
|
||||
const colSet = '套餐'
|
||||
const colVer = '版本'
|
||||
const colPsd = '明细ID'
|
||||
const colAmount = '应收总额'
|
||||
const colOps = '操作'
|
||||
const lbMatch = '命中说明'
|
||||
const lbError = '错误信息'
|
||||
|
||||
const title = computed(() => {
|
||||
const no = props.orderNo || ''
|
||||
return no ? '历史计费记录 · ' + no : '历史计费记录'
|
||||
})
|
||||
const detailTitle = computed(() => '计费记录详情')
|
||||
|
||||
function parseJson(val) {
|
||||
if (val == null || val === '') return {}
|
||||
if (typeof val === 'object') return val
|
||||
try { return JSON.parse(val) } catch (e) { return {} }
|
||||
}
|
||||
|
||||
function respOf(row) {
|
||||
return parseJson(row && row.responseJson)
|
||||
}
|
||||
|
||||
function reqOf(row) {
|
||||
return parseJson(row && row.requestJson)
|
||||
}
|
||||
|
||||
function actionLabel(row) {
|
||||
const req = reqOf(row)
|
||||
if (req.action === 'UPSTREAM_SYNC') return '上游同步'
|
||||
if (row && row.status === 'FAIL') return '计费失败'
|
||||
const resp = respOf(row)
|
||||
if (resp.matchSource === 'BILLING_FAIL' || resp.billingFailStatus) return '计费失败'
|
||||
return '计费'
|
||||
}
|
||||
|
||||
function actionTag(row) {
|
||||
const label = actionLabel(row)
|
||||
if (label === '上游同步') return 'info'
|
||||
if (label === '计费失败') return 'danger'
|
||||
return 'success'
|
||||
}
|
||||
|
||||
function statusLabel(status) {
|
||||
return status === 'FAIL' ? '失败' : '成功'
|
||||
}
|
||||
|
||||
function statusTag(status) {
|
||||
return status === 'FAIL' ? 'danger' : 'success'
|
||||
}
|
||||
|
||||
function priceSetOf(row) {
|
||||
const r = respOf(row)
|
||||
if (r.priceSetName && r.priceSetCode) return r.priceSetName + ' (' + r.priceSetCode + ')'
|
||||
return r.priceSetName || r.priceSetCode || (r.priceSetId != null ? String(r.priceSetId) : '-')
|
||||
}
|
||||
|
||||
function versionOf(row) {
|
||||
const r = respOf(row)
|
||||
if (r.versionNo) return r.versionNo
|
||||
return r.psvId != null ? ('psv=' + r.psvId) : '-'
|
||||
}
|
||||
|
||||
function psdOf(row) {
|
||||
const r = respOf(row)
|
||||
return r.psdId != null ? String(r.psdId) : '-'
|
||||
}
|
||||
|
||||
function amountOf(row) {
|
||||
const r = respOf(row)
|
||||
const v = r.incomeTotal != null ? r.incomeTotal : r.payableTotal
|
||||
if (v == null || v === '') return '-'
|
||||
const n = Number(v)
|
||||
return Number.isNaN(n) ? String(v) : n.toFixed(2)
|
||||
}
|
||||
|
||||
function matchMsgOf(row) {
|
||||
return respOf(row).matchMessage || '-'
|
||||
}
|
||||
|
||||
function formatJson(val) {
|
||||
if (val == null || val === '') return '无数据'
|
||||
if (typeof val === 'object') {
|
||||
try { return JSON.stringify(val, null, 2) } catch (e) { return String(val) }
|
||||
}
|
||||
try { return JSON.stringify(JSON.parse(val), null, 2) } catch (e) { return String(val) }
|
||||
}
|
||||
|
||||
function openDetail(row) {
|
||||
active.value = row
|
||||
detailOpen.value = true
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
if (!props.billId) {
|
||||
rows.value = []
|
||||
total.value = 0
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listBillHistory(props.billId, {
|
||||
pageNum: query.pageNum,
|
||||
pageSize: query.pageSize
|
||||
})
|
||||
rows.value = res.rows || []
|
||||
total.value = Number(res.total) || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.billId],
|
||||
([open]) => {
|
||||
if (open) {
|
||||
query.pageNum = 1
|
||||
detailOpen.value = false
|
||||
active.value = null
|
||||
reload()
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hist-wrap { min-height: 200px; }
|
||||
.pager {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.mb12 { margin-bottom: 12px; }
|
||||
.err { color: #f56c6c; word-break: break-all; }
|
||||
.json-panel {
|
||||
margin-bottom: 12px;
|
||||
background: #f7f9fb;
|
||||
border: 1px solid #e8ecf0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.json-panel__head {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
background: #fafbfc;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
.code-pre {
|
||||
margin: 0;
|
||||
padding: 12px 14px;
|
||||
max-height: 260px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -41,7 +41,17 @@
|
||||
</section>
|
||||
|
||||
<section class="detail-block">
|
||||
<div class="sec-title">计费信息</div>
|
||||
<div class="sec-title">
|
||||
计费信息
|
||||
<el-button
|
||||
v-if="detail && detail.billId"
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
class="hist-btn"
|
||||
@click="historyOpen = true"
|
||||
>历史计费</el-button>
|
||||
</div>
|
||||
<div class="amount-summary">
|
||||
<div class="amount-card">
|
||||
<div class="amount-label">运费金额</div>
|
||||
@@ -184,12 +194,18 @@
|
||||
:current-charge-weight="detail?.chargeWeight"
|
||||
@success="onChargeWeightUpdated"
|
||||
/>
|
||||
<bill-billing-history-drawer
|
||||
v-model="historyOpen"
|
||||
:bill-id="detail?.billId || ''"
|
||||
:order-no="detail?.orderNo || ''"
|
||||
/>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AddIncomeFeeDialog from './AddIncomeFeeDialog.vue'
|
||||
import EditChargeWeightDialog from './EditChargeWeightDialog.vue'
|
||||
import BillBillingHistoryDrawer from './BillBillingHistoryDrawer.vue'
|
||||
import { deleteIncomeBillFee } from '@/api/price/billing'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
@@ -205,6 +221,7 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update:modelValue', 'updated'])
|
||||
|
||||
const feeTab = ref('income')
|
||||
const historyOpen = ref(false)
|
||||
const addFeeOpen = ref(false)
|
||||
const editFeeRow = ref(null)
|
||||
const editCwOpen = ref(false)
|
||||
@@ -224,6 +241,7 @@ const payFees = computed(() => {
|
||||
|
||||
watch(() => props.detail, () => {
|
||||
feeTab.value = 'income'
|
||||
historyOpen.value = false
|
||||
})
|
||||
|
||||
function canEditManualFee(row) {
|
||||
@@ -465,6 +483,9 @@ function refundLabel(status) {
|
||||
border-radius: 2px;
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
.hist-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
.amount-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
|
||||
Reference in New Issue
Block a user