查看历史试算功能
This commit is contained in:
+27
@@ -1,14 +1,19 @@
|
||||
package com.ruoyi.web.controller.price;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.price.domain.PrcQuote;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceQuoteRequest;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceQuoteResult;
|
||||
import com.ruoyi.price.service.ITmsPriceQuoteService;
|
||||
@@ -25,6 +30,28 @@ public class BillingController extends BaseController
|
||||
@Autowired
|
||||
private ITmsPriceQuoteService tmsPriceQuoteService;
|
||||
|
||||
/**
|
||||
* Historical quote list.
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:billing:quote')")
|
||||
@GetMapping("/quote/list")
|
||||
public TableDataInfo list(PrcQuote query)
|
||||
{
|
||||
startPage();
|
||||
List<PrcQuote> list = tmsPriceQuoteService.selectQuoteList(query);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Historical quote detail with fee lines.
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('price:billing:quote')")
|
||||
@GetMapping("/quote/{quoteId}")
|
||||
public AjaxResult getInfo(@PathVariable Long quoteId)
|
||||
{
|
||||
return success(tmsPriceQuoteService.getQuote(quoteId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Same matching logic as TMS open API, for BMS console trial.
|
||||
*/
|
||||
|
||||
@@ -107,3 +107,7 @@ price.quote.productCodeRequired=productCode is required
|
||||
price.quote.countryRequired=countryCode or countryName is required
|
||||
price.quote.weightInvalid=weight must be > 0
|
||||
price.quote.bizDateInvalid=bizDate format must be yyyy-MM-dd
|
||||
price.quote.fixedPackageNoDetails=套餐固定价格没有明细,使用产品默认价格
|
||||
price.quote.noCustomerPackageOverride=没有客户套餐覆盖,使用产品默认运费/挂号费
|
||||
price.quote.quoteIdRequired=quoteId is required
|
||||
price.quote.hitCustomerFixedPackageDetail=命中客户套餐固定价格明细
|
||||
@@ -1,8 +1,9 @@
|
||||
package com.ruoyi.price.mapper;
|
||||
|
||||
import com.ruoyi.price.domain.PrcQuote;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.price.domain.PrcQuote;
|
||||
|
||||
/** billing quote header mapper */
|
||||
public interface PrcQuoteMapper
|
||||
@@ -11,6 +12,8 @@ public interface PrcQuoteMapper
|
||||
|
||||
public PrcQuote selectPrcQuoteById(Long quoteId);
|
||||
|
||||
public List<PrcQuote> selectPrcQuoteList(PrcQuote quote);
|
||||
|
||||
public int insertPrcQuote(PrcQuote quote);
|
||||
|
||||
public int updateQuoteAmounts(@Param("quoteId") Long quoteId,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.ruoyi.price.service;
|
||||
|
||||
import com.ruoyi.price.domain.PrcQuote;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceQuoteRequest;
|
||||
import com.ruoyi.price.domain.vo.TmsPriceQuoteResult;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* TMS freight quote service.
|
||||
@@ -15,4 +17,8 @@ import com.ruoyi.price.domain.vo.TmsPriceQuoteResult;
|
||||
public interface ITmsPriceQuoteService
|
||||
{
|
||||
TmsPriceQuoteResult quote(TmsPriceQuoteRequest request);
|
||||
|
||||
List<PrcQuote> selectQuoteList(PrcQuote query);
|
||||
|
||||
TmsPriceQuoteResult getQuote(Long quoteId);
|
||||
}
|
||||
|
||||
+26
-3
@@ -112,6 +112,27 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PrcQuote> selectQuoteList(PrcQuote query)
|
||||
{
|
||||
return quoteMapper.selectPrcQuoteList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TmsPriceQuoteResult getQuote(Long quoteId)
|
||||
{
|
||||
if (quoteId == null)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.quote.quoteIdRequired"));
|
||||
}
|
||||
PrcQuote quote = quoteMapper.selectPrcQuoteById(quoteId);
|
||||
if (quote == null)
|
||||
{
|
||||
throw new ServiceException("QUOTE_NOT_FOUND: quoteId=" + quoteId);
|
||||
}
|
||||
return toResult(quote, quoteFeeMapper.selectByQuoteId(quoteId), true);
|
||||
}
|
||||
|
||||
private TmsPriceQuoteResult doQuote(TmsPriceQuoteRequest request, Date bizDate)
|
||||
{
|
||||
String productCode = request.getProductCode().trim();
|
||||
@@ -164,7 +185,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
|
||||
return buildDefault(request.getCustomerId(), base, productCode, productName, weight,
|
||||
"no customer package override; use product default freight/registration");
|
||||
MessageUtils.message("price.quote.noCustomerPackageOverride"));
|
||||
}
|
||||
|
||||
private TmsPriceQuoteResult tryFixed(PrcCustomerPriceSet bind, PrcPriceSet set,
|
||||
@@ -176,7 +197,8 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
TmsPriceQuoteResult r = buildFromRates(SRC_DEFAULT, base.getFreightPrice(), null, null,
|
||||
base.getRegistrationFee(), base, productCode, productName, weight, null);
|
||||
fillPackage(r, bind, set);
|
||||
r.setMatchMessage("FIXED package has no details; use bms_product_country default");
|
||||
// 套餐固定价格没有明细,使用产品默认价格
|
||||
r.setMatchMessage(MessageUtils.message("price.quote.fixedPackageNoDetails"));
|
||||
r.setCustomerId(bind.getCustomerId());
|
||||
return r;
|
||||
}
|
||||
@@ -208,7 +230,8 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
{
|
||||
r.setProductCountryId(detail.getProductCountryId());
|
||||
}
|
||||
r.setMatchMessage("hit customer FIXED package detail");
|
||||
// 命中客户套餐固定价格明细
|
||||
r.setMatchMessage(MessageUtils.message("price.quote.hitCustomerFixedPackageDetail"));
|
||||
r.setCustomerId(bind.getCustomerId());
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -66,6 +66,40 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where quote_id = #{quoteId}
|
||||
</select>
|
||||
|
||||
<select id="selectPrcQuoteList" parameterType="PrcQuote" resultMap="PrcQuoteResult">
|
||||
<include refid="selectPrcQuoteVo"/>
|
||||
<where>
|
||||
<if test="customerId != null">
|
||||
AND customer_id = #{customerId}
|
||||
</if>
|
||||
<if test="productCode != null and productCode != ''">
|
||||
AND product_code like concat('%', #{productCode}, '%')
|
||||
</if>
|
||||
<if test="productName != null and productName != ''">
|
||||
AND product_name like concat('%', #{productName}, '%')
|
||||
</if>
|
||||
<if test="countryCode != null and countryCode != ''">
|
||||
AND country_code = #{countryCode}
|
||||
</if>
|
||||
<if test="priceSetName != null and priceSetName != ''">
|
||||
AND price_set_name like concat('%', #{priceSetName}, '%')
|
||||
</if>
|
||||
<if test="matchSource != null and matchSource != ''">
|
||||
AND match_source = #{matchSource}
|
||||
</if>
|
||||
<if test="bizDate != null">
|
||||
AND biz_date = #{bizDate}
|
||||
</if>
|
||||
<if test="params.beginTime != null and params.beginTime != ''">
|
||||
AND date_format(created_at,'%y%m%d') >= date_format(#{params.beginTime},'%y%m%d')
|
||||
</if>
|
||||
<if test="params.endTime != null and params.endTime != ''">
|
||||
AND date_format(created_at,'%y%m%d') <= date_format(#{params.endTime},'%y%m%d')
|
||||
</if>
|
||||
</where>
|
||||
order by quote_id desc
|
||||
</select>
|
||||
|
||||
<insert id="insertPrcQuote" parameterType="PrcQuote" useGeneratedKeys="true" keyProperty="quoteId">
|
||||
insert into prc_quote (
|
||||
quote_key, customer_id, product_code, product_name,
|
||||
|
||||
@@ -13,6 +13,10 @@ INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component,
|
||||
SELECT 2302, N'试算执行', 2301, 1, '', NULL, 1, 0, 'F', '0', '0', 'price:billing:quote', '#', 'admin', sysdate()
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2302);
|
||||
|
||||
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
|
||||
SELECT 2303, N'试算查询', 2301, 2, '', NULL, 1, 0, 'F', '0', '0', 'price:billing:quote', '#', 'admin', sysdate()
|
||||
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2303);
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT 1, m.menu_id FROM (SELECT 2300 AS menu_id UNION SELECT 2301 UNION SELECT 2302) m
|
||||
SELECT 1, m.menu_id FROM (SELECT 2300 AS menu_id UNION SELECT 2301 UNION SELECT 2302 UNION SELECT 2303) m
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id);
|
||||
|
||||
@@ -8,3 +8,20 @@ export function quoteBilling(data) {
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 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'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ payable: row.feeType === 'SURCHARGE' && Number(row.amount) > 0 }">{{ formatMoney(row.amount) }}</span>
|
||||
<span>{{ formatMoney(row.amount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="币种" width="80" align="center" prop="currency" />
|
||||
@@ -143,21 +143,138 @@
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card shadow="never" class="history-card">
|
||||
<template #header><span>历史试算</span></template>
|
||||
<basic-table
|
||||
ref="historyTableRef"
|
||||
:columns="historyColumns"
|
||||
:search-columns="historySearchColumns"
|
||||
:query="queryHistory"
|
||||
row-key="quoteId"
|
||||
>
|
||||
<template #col-product="{ row }">
|
||||
{{ row.productName || '-' }} ({{ row.productCode }})
|
||||
</template>
|
||||
<template #col-country="{ row }">
|
||||
{{ row.countryName || '-' }} ({{ row.countryCode || '-' }})
|
||||
</template>
|
||||
<template #col-matchSource="{ row }">
|
||||
<el-tag size="small" :type="sourceTagType(row.matchSource)">{{ sourceLabel(row.matchSource) }}</el-tag>
|
||||
</template>
|
||||
<template #col-payableTotal="{ row }">
|
||||
<span class="payable">{{ formatMoney(row.payableTotal) }}</span>
|
||||
</template>
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" icon="View" @click="handleViewHistory(row)">查看</el-button>
|
||||
</template>
|
||||
</basic-table>
|
||||
</el-card>
|
||||
|
||||
<el-drawer v-model="historyOpen" size="720px" :title="historyTitle" append-to-body>
|
||||
<div v-loading="historyLoading">
|
||||
<el-empty v-if="!viewResult" description="暂无详情" />
|
||||
<div v-else class="quote-result">
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="试算ID">{{ viewResult.quoteId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户ID">{{ viewResult.customerId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="产品">{{ viewResult.productName || '-' }} ({{ viewResult.productCode }})</el-descriptions-item>
|
||||
<el-descriptions-item label="国家">{{ viewResult.countryName || '-' }} ({{ viewResult.countryCode }})</el-descriptions-item>
|
||||
<el-descriptions-item label="计费重量(kg)">{{ viewResult.chargeWeight ?? viewResult.weight }}</el-descriptions-item>
|
||||
<el-descriptions-item label="重量段">{{ viewResult.weightLabel || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="计价来源">
|
||||
<el-tag size="small" :type="sourceTagType(viewResult.matchSource)">{{ sourceLabel(viewResult.matchSource) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="套餐">{{ viewResult.priceSetName || '-' }} {{ viewResult.priceSetCode ? '(' + viewResult.priceSetCode + ')' : '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="运费金额">{{ formatMoney(viewResult.freightAmount) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="挂号费">{{ formatMoney(viewResult.registrationFee) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="附加费">{{ formatMoney(viewResult.surchargeAmount) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="应付总额">
|
||||
<span class="payable">{{ formatMoney(viewResult.payableTotal) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="命中说明" :span="2">{{ viewResult.matchMessage || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-table
|
||||
v-if="viewResult.feeDetails && viewResult.feeDetails.length"
|
||||
:data="viewResult.feeDetails"
|
||||
border
|
||||
size="small"
|
||||
class="fee-table"
|
||||
>
|
||||
<el-table-column label="费用名称" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ displayFeeName(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="费用类型" width="110" align="center">
|
||||
<template #default="{ row }">{{ feeTypeLabel(row.feeType) || row.feeType }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" min-width="120" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="币种" width="80" align="center" prop="currency" />
|
||||
<el-table-column label="说明" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.remark || '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="BillingQuote">
|
||||
import { quoteBilling } from '@/api/price/billing'
|
||||
import { quoteBilling, listQuote, getQuote } from '@/api/price/billing'
|
||||
import { listProduct, listProductCountries } from '@/api/price/product'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { bms_fee_type } = useDict('bms_fee_type')
|
||||
const formRef = ref()
|
||||
const historyTableRef = ref()
|
||||
const loading = ref(false)
|
||||
const productOptions = ref([])
|
||||
const countryOptions = ref([])
|
||||
const result = ref(null)
|
||||
const errorMsg = ref('')
|
||||
const historyOpen = ref(false)
|
||||
const historyTitle = ref('试算详情')
|
||||
const historyLoading = ref(false)
|
||||
const viewResult = ref(null)
|
||||
|
||||
const historyColumns = [
|
||||
{ label: '试算ID', prop: 'quoteId', align: 'center', width: 90 },
|
||||
{ label: '客户ID', prop: 'customerId', align: 'center', width: 90 },
|
||||
{ label: '产品', prop: 'product', minWidth: 160, slot: 'product' },
|
||||
{ label: '国家', prop: 'country', minWidth: 130, slot: 'country' },
|
||||
{ label: '计费重量', prop: 'weight', align: 'center', width: 100 },
|
||||
{ label: '计费日期', prop: 'bizDate', align: 'center', width: 120 },
|
||||
{ label: '计价来源', prop: 'matchSource', align: 'center', width: 140, slot: 'matchSource' },
|
||||
{ label: '套餐', prop: 'priceSetName', minWidth: 120, showOverflowTooltip: true },
|
||||
{ label: '应付总额', prop: 'payableTotal', align: 'right', width: 120, slot: 'payableTotal' },
|
||||
{ label: '创建时间', prop: 'createTime', align: 'center', width: 170, parseTime: true },
|
||||
{ label: '操作', prop: 'action', align: 'center', width: 90, fixed: 'right', slot: 'action', showInToolbar: false }
|
||||
]
|
||||
|
||||
const historySearchColumns = [
|
||||
{ label: '客户ID', prop: 'customerId', component: 'input', placeholder: '客户ID', width: '140px' },
|
||||
{ label: '产品代码', prop: 'productCode', component: 'input', placeholder: '产品代码', width: '150px' },
|
||||
{ label: '国家代码', prop: 'countryCode', component: 'input', placeholder: '如 DE', width: '120px' },
|
||||
{ label: '套餐名称', prop: 'priceSetName', component: 'input', placeholder: '套餐名称', width: '150px' }
|
||||
]
|
||||
|
||||
function queryHistory(params) {
|
||||
return listQuote(params)
|
||||
}
|
||||
|
||||
function handleViewHistory(row) {
|
||||
historyTitle.value = '试算详情 #' + row.quoteId
|
||||
historyOpen.value = true
|
||||
historyLoading.value = true
|
||||
viewResult.value = null
|
||||
getQuote(row.quoteId).then(res => {
|
||||
viewResult.value = res.data || null
|
||||
}).finally(() => {
|
||||
historyLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const form = reactive({
|
||||
customerId: undefined,
|
||||
@@ -227,6 +344,8 @@ function handleQuote() {
|
||||
result.value = res.data || null
|
||||
if (!result.value) {
|
||||
errorMsg.value = res.msg || '计算失败'
|
||||
} else if (historyTableRef.value && historyTableRef.value.refresh) {
|
||||
historyTableRef.value.refresh()
|
||||
}
|
||||
}).catch(err => {
|
||||
errorMsg.value = (err && (err.msg || err.message)) || '计算失败'
|
||||
@@ -346,4 +465,7 @@ loadProducts()
|
||||
.billing-quote .fee-table {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.billing-quote .history-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user