新增打单产品明细保存接口,优化附加费管理逻辑

This commit is contained in:
wuweihua
2026-11-12 16:53:40 +08:00
parent 90147ed4e3
commit 8ca5af1ff0
10 changed files with 200 additions and 97 deletions
@@ -24,6 +24,7 @@ import com.bms.common.core.domain.AjaxResult;
import com.bms.common.core.page.TableDataInfo;
import com.bms.common.enums.BusinessType;
import com.bms.common.exception.ServiceException;
import com.bms.price.domain.PayProduct;
import com.bms.price.domain.PaySaveCommand;
import com.bms.price.domain.PrcCustomerPriceSet;
import com.bms.price.domain.PrcCustomerPriceSetBatch;
@@ -208,6 +209,15 @@ public class LabelPackageController extends BaseController
return success(payProductService.selectPayProductDetail(productId));
}
/** 保存打单产品明细(重量段/附加费等),按当前 term 覆盖子表 */
@PreAuthorize("@ss.hasPermi('price:label:edit')")
@Log(title = "打单产品明细", businessType = BusinessType.UPDATE)
@PutMapping("/product")
public AjaxResult updateProduct(@RequestBody PayProduct product)
{
return toAjax(payProductService.updateDetail(product));
}
/** 查询打单产品已绑定渠道(默认应收 AR) */
@PreAuthorize("@ss.hasPermi('price:label:query')")
@GetMapping("/product/{productId}/channels")
@@ -73,10 +73,4 @@ public class PaySurcharge
private BigDecimal weightMax;
private Integer sortNo;
/**
* 是否计费并落库:false 不写入 pay_surcharge;null/缺省视为要计费(兼容应付全量保存)。
* 仅作为前端传参,不落库字段。
*/
private Boolean needBill;
}
@@ -461,8 +461,6 @@ public class PayProductServiceImpl implements IPayProductService
{
product.setExtraLines(new ArrayList<>());
}
// 计数前先按「是否计费」过滤,与落库行数一致
retainBillableSurcharges(product);
product.setWeightCount(product.getWeightBands().size());
product.setSurchargeCount(product.getSurcharges().size());
product.setExtraCount(product.getExtraLines().size());
@@ -1531,7 +1529,6 @@ public class PayProductServiceImpl implements IPayProductService
private void insertSurcharges(PayProduct product)
{
retainBillableSurcharges(product);
List<PaySurcharge> rows = product.getSurcharges();
if (rows == null || rows.isEmpty())
{
@@ -1550,27 +1547,6 @@ public class PayProductServiceImpl implements IPayProductService
}
}
/**
* 仅保留需要计费的附加费:needBill=false 不入库;null 视为要计费。
*/
private void retainBillableSurcharges(PayProduct product)
{
List<PaySurcharge> rows = product.getSurcharges();
if (rows == null || rows.isEmpty())
{
return;
}
List<PaySurcharge> kept = new ArrayList<PaySurcharge>();
for (PaySurcharge fee : rows)
{
if (fee != null && !Boolean.FALSE.equals(fee.getNeedBill()))
{
kept.add(fee);
}
}
product.setSurcharges(kept);
}
/**
* 按附加费名称映射 prc_surcharge.sc_id;币种不能为空:附加费 -> 价卡 -> EUR。
*/
+11
View File
@@ -155,6 +155,17 @@ export function getLabelProduct(productId, termId) {
})
}
/** 保存打单产品明细(重量段/附加费) */
export function updateLabelProduct(data) {
return request({
url: '/price/label/product',
method: 'put',
data,
headers: { repeatSubmit: false },
timeout: 120000
})
}
export function removeLabelProducts(priceSetId, productIds) {
return request({
url: '/price/label/' + priceSetId + '/products/' + productIds,
@@ -79,16 +79,26 @@
<div class="pay-edit-bar">
<el-input v-model="pane.feeKeyword" clearable placeholder="筛选附加费用、地区、价格、货币或费用解释" class="pay-filter" @input="pane.feePageNo = 1" />
<el-button v-if="editable" size="small" type="primary" plain @click="openFeePicker(pane)">添加附加费</el-button>
<template v-if="billSelect">
<el-button size="small" @click="setFeesBill(pane, true)">全选计费</el-button>
<el-button size="small" @click="setFeesBill(pane, false)">取消全选</el-button>
<span class="pay-bill-tip">勾选后才会写入 pay_surcharge;默认不勾选</span>
</template>
<el-button
v-if="editable"
size="small"
type="danger"
plain
:disabled="!feeCheckedCount(pane)"
@click="removeCheckedFees(pane)"
>批量删除</el-button>
</div>
<el-table :data="pageFees(pane)" border size="small" empty-text="没有附加费">
<el-table-column v-if="billSelect" label="是否计费" width="88" align="center" fixed="left">
<el-table-column v-if="editable" width="48" align="center" fixed="left">
<template #header>
<el-checkbox
:model-value="isAllFilteredFeesChecked(pane)"
:indeterminate="isFeeCheckIndeterminate(pane)"
@update:model-value="v => setFilteredFeesChecked(pane, v)"
/>
</template>
<template #default="{ row }">
<el-checkbox :model-value="row.needBill === true" @update:model-value="v => { row.needBill = !!v }" />
<el-checkbox :model-value="row._checked === true" @update:model-value="v => { row._checked = !!v }" />
</template>
</el-table-column>
<el-table-column prop="feeName" label="附加费用" min-width="180" show-overflow-tooltip>
@@ -266,11 +276,6 @@ const props = defineProps({
type: Boolean,
default: false
},
/** 打单套餐:附加费表显示「是否计费」勾选,仅勾选行入库 */
billSelect: {
type: Boolean,
default: false
},
savingRowKey: {
type: [String, Number],
default: ''
@@ -541,18 +546,51 @@ function filteredFees(pane) {
function feeTabLabel(pane) {
const fees = pane.item.surcharges || []
if (!props.billSelect) return '附加费(' + fees.length + ')'
const n = fees.filter(f => f && f.needBill === true).length
return '附加费(已选 ' + n + '/' + fees.length + ')'
return '附加费(' + fees.length + ')'
}
function setFeesBill(pane, value) {
const fees = pane.item.surcharges || []
for (const fee of fees) {
if (fee) fee.needBill = !!value
function feeCheckedCount(pane) {
return (pane?.item?.surcharges || []).filter(fee => fee && fee._checked === true).length
}
function isAllFilteredFeesChecked(pane) {
const rows = filteredFees(pane)
return rows.length > 0 && rows.every(fee => fee && fee._checked === true)
}
function isFeeCheckIndeterminate(pane) {
const rows = filteredFees(pane)
if (!rows.length) return false
const n = rows.filter(fee => fee && fee._checked === true).length
return n > 0 && n < rows.length
}
function setFilteredFeesChecked(pane, value) {
for (const fee of filteredFees(pane)) {
if (fee) fee._checked = !!value
}
}
async function removeCheckedFees(pane) {
const list = pane?.item?.surcharges
if (!list?.length) return
const checked = list.filter(fee => fee && fee._checked === true)
if (!checked.length) {
proxy.$modal.msgWarning('请先勾选要删除的附加费')
return
}
try {
await proxy.$modal.confirm(`确认删除所选 ${checked.length} 项附加费?`)
} catch {
return
}
const remain = list.filter(fee => !(fee && fee._checked === true))
pane.item.surcharges = remain
const maxPage = Math.max(1, Math.ceil(remain.length / pageSize) || 1)
if (pane.feePageNo > maxPage) pane.feePageNo = maxPage
proxy.$modal.msgSuccess(`已删除 ${checked.length} 项`)
}
const { proxy } = getCurrentInstance()
const feePickerVisible = ref(false)
const feePickerPane = ref(null)
@@ -589,9 +627,7 @@ function configToFeeRow(cfg, product) {
priceUnit: isPercent ? '%' : (currency + '/ITEM'),
feeDesc: String(cfg?.scNote || '').trim(),
calcRule: toCalcRule(cfg?.chargeType),
calcParam: '',
// 从配置添加默认要计费
needBill: true
calcParam: ''
}
}
@@ -738,10 +774,6 @@ function statusText(status) {
gap: 8px;
margin-bottom: 8px;
}
.pay-bill-tip {
color: var(--el-text-color-secondary);
font-size: 12px;
}
.pay-hint {
margin: 0 0 8px;
color: var(--el-text-color-secondary);
@@ -14,6 +14,13 @@
placeholder="搜索附加费名称、计费方式、币种、国家或备注"
class="picker-search"
/>
<el-button
type="primary"
plain
icon="Plus"
v-hasPermi="['price:surcharge:add']"
@click="openCreate"
>新建附加费</el-button>
<span class="picker-meta">共 {{ filteredRows.length }} 项,已选 {{ picked.length }} 项</span>
</div>
@@ -43,17 +50,20 @@
<el-table-column prop="scNote" label="备注" min-width="160" show-overflow-tooltip />
</el-table>
<div class="picker-hint">勾选后点确定写入当前产品附加费表;已存在相同配置的不会重复添加。</div>
<div class="picker-hint">可先「新建附加费」写入配置库;勾选后点确定写入当前产品附加费表;已存在相同配置的不会重复添加。</div>
<template #footer>
<el-button @click="visible = false">取 消</el-button>
<el-button type="primary" :disabled="!picked.length" @click="submit">确 定</el-button>
</template>
</el-dialog>
<surcharge-form-dialog ref="formDialogRef" @success="onCreated" />
</template>
<script setup>
import { listSurchargeOptions } from '@/api/price/surcharge'
import SurchargeFormDialog from '@/views/price/basic/surcharge/components/SurchargeFormDialog.vue'
const props = defineProps({
modelValue: { type: Boolean, default: false },
@@ -64,6 +74,7 @@ const emit = defineEmits(['update:modelValue', 'confirm'])
const { proxy } = getCurrentInstance()
const tableRef = ref()
const formDialogRef = ref()
const loading = ref(false)
const keyword = ref('')
const rows = ref([])
@@ -187,9 +198,25 @@ function onOpened() {
nextTick(() => {
tableRef.value?.clearSelection?.()
})
if (!rows.value.length) {
loadRows()
loadRows()
}
function openCreate() {
formDialogRef.value?.open()
}
async function onCreated() {
const before = new Set((rows.value || []).map(row => String(row.scId)))
await loadRows()
// 自动勾选刚新建的配置
const next = new Set(pickedIds.value)
for (const row of rows.value || []) {
if (row?.scId == null) continue
const id = String(row.scId)
if (!before.has(id) && canSelect(row)) next.add(id)
}
pickedIds.value = next
nextTick(() => restoreSelection())
}
function submit() {
@@ -215,11 +242,12 @@ watch(() => props.modelValue, v => {
.picker-bar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 10px;
}
.picker-search {
width: 360px;
width: 320px;
}
.picker-meta {
color: var(--el-text-color-secondary);
@@ -87,7 +87,7 @@
</div>
<el-drawer v-model="detailVisible" :title="detailTitle" size="78%" destroy-on-close append-to-body>
<pay-config-detail :product="detailProduct" :editable="false" :bill-select="true" />
<pay-config-detail :product="detailProduct" :editable="false" />
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
</template>
@@ -187,9 +187,7 @@ async function onFile(uploadFile) {
const data = res.data || {}
data.products = (data.products || []).map((item, index) => ({
...item,
rowKey: `${item.sheetName || ''}-${item.productName || ''}-${index}`,
// 解析后默认不计费,勾选后才入 pay_surcharge
surcharges: (item.surcharges || []).map(fee => ({ ...fee, needBill: fee?.needBill === true }))
rowKey: `${item.sheetName || ''}-${item.productName || ''}-${index}`
}))
preview.value = data
const picked = data.products.filter(row => canSelect(row))
@@ -312,17 +310,16 @@ async function submit(payload = {}) {
const productsPayload = (payload.products && payload.products.length)
? payload.products
: selected.value.map(row => ({ ...row, productType: 'AR' }))
const filteredProducts = productsPayload.map(product => ({
const productsForSave = productsPayload.map(product => ({
...product,
productType: product.productType || 'AR',
surcharges: (product.surcharges || []).filter(fee => fee && fee.needBill === true)
productType: product.productType || 'AR'
}))
const res = await saveLabelImport({
priceSetId: payload.priceSetId || null,
priceSetName: String(payload.priceSetName || '').trim(),
priceSetNote: payload.priceSetNote || '',
overwrite: payload.overwrite != null ? payload.overwrite : overwrite.value,
products: filteredProducts
products: productsForSave
})
const data = res.data || {}
return {
@@ -22,11 +22,11 @@
<el-table-column :label="colTerm" min-width="180" align="center">
<template #default="{ row }">{{ termText(row) }}</template>
</el-table-column>
<el-table-column :label="colStatus" width="90" align="center">
<!-- <el-table-column :label="colStatus" width="90" align="center">
<template #default="{ row }">
<el-tag :type="termStatusTag(row.termStatus)" size="small">{{ termStatusText(row.termStatus) }}</el-tag>
</template>
</el-table-column>
</el-table-column> -->
<el-table-column prop="weightCount" :label="colWeight" width="80" align="center" />
<el-table-column prop="surchargeCount" :label="colSc" width="80" align="center" />
<el-table-column :label="colBoundChannels" min-width="180" show-overflow-tooltip>
@@ -34,16 +34,29 @@
</el-table-column>
<el-table-column :label="colAction" width="160" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openProduct(row)" v-hasPermi="['price:label:query']">查看</el-button>
<el-button link type="primary" @click="openProduct(row)" v-hasPermi="['price:label:query', 'price:label:edit']">编辑</el-button>
<el-button link type="primary" @click="openBindList(row)" v-hasPermi="['price:label:edit']">绑定</el-button>
</template>
</el-table-column>
</el-table>
<el-drawer v-model="detailVisible" :title="detailTitle" size="78%" destroy-on-close append-to-body>
<pay-config-detail v-loading="detailLoading" :product="detail" :editable="false" />
<pay-config-detail
v-loading="detailLoading"
:product="detail"
:editable="canEditDetail"
:saving-row-key="savingRowKey"
@save-row="saveDetailRow"
/>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
<el-button
v-if="canEditDetail"
type="primary"
:loading="detailSaving"
v-hasPermi="['price:label:edit']"
@click="saveDetail"
>保存明细</el-button>
</template>
</el-drawer>
@@ -125,9 +138,11 @@
import {
listLabelProducts,
getLabelProduct,
updateLabelProduct,
listLabelProductChannels,
replaceLabelProductChannels
} from '@/api/price/label'
import { checkPermi } from '@/utils/permission'
import { listChannelOptions } from '@/api/masterdata/serviceChannel'
import PayConfigDetail from '@/views/price/basic/pay/components/PayConfigDetail.vue'
@@ -144,7 +159,10 @@ const rows = ref([])
const keyword = ref('')
const detailVisible = ref(false)
const detailLoading = ref(false)
const detailSaving = ref(false)
const savingRowKey = ref('')
const detail = ref(null)
const canEditDetail = computed(() => checkPermi(['price:label:edit']))
const bindListVisible = ref(false)
const bindListLoading = ref(false)
@@ -258,9 +276,66 @@ async function load() {
}
}
function cleanMoney(product) {
if (!product) return
;(product.weightBands || []).forEach(row => {
if (row && row.price === '') row.price = null
})
;(product.surcharges || []).forEach(row => {
if (row && row.amount === '') row.amount = null
})
}
async function persistDetail() {
if (!detail.value?.productId) {
throw new Error('NO_PRODUCT')
}
cleanMoney(detail.value)
await updateLabelProduct(detail.value)
const termId = detail.value.termId
const res = await getLabelProduct(detail.value.productId, termId)
detail.value = res.data || detail.value
await load()
emit('changed')
}
async function saveDetail() {
if (!detail.value || detailSaving.value || savingRowKey.value) return
detailSaving.value = true
try {
await persistDetail()
proxy.$modal.msgSuccess('明细已保存')
} catch (e) {
// request 拦截器已提示
} finally {
detailSaving.value = false
}
}
async function saveDetailRow(payload) {
if (!detail.value || savingRowKey.value || detailSaving.value) {
payload?.done?.(false)
return
}
savingRowKey.value = payload?.rowKey || 'row'
let ok = false
try {
await persistDetail()
ok = true
proxy.$modal.msgSuccess('该行已保存')
} catch (e) {
ok = false
} finally {
savingRowKey.value = ''
payload?.done?.(ok)
}
}
async function openProduct(row) {
detailVisible.value = true
detailLoading.value = true
detailSaving.value = false
savingRowKey.value = ''
detail.value = null
try {
const res = await getLabelProduct(row.productId, row.termId)
@@ -38,11 +38,11 @@
<el-table-column label="失效时间" min-width="170" align="center">
<template #default="{ row }">{{ formatHour(row.expireTime) || '-' }}</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<!-- <el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTag(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
</template>
</el-table-column>
</el-table-column> -->
<el-table-column label="产品数" width="90" align="center">
<template #default="{ row }">{{ row.productCount || 0 }}</template>
</el-table-column>
+3 -23
View File
@@ -100,7 +100,6 @@
<pay-config-detail
:product="detailProduct"
:editable="true"
:bill-select="true"
:saving-row-key="savingRowKey"
@save-row="saveDetailRow"
/>
@@ -220,8 +219,7 @@ async function loadEditProducts() {
}
}
}))
// 库中已落库的附加费默认勾选(保留原计费项)
productRows.value = markSurchargeBillFlags(details, true)
productRows.value = details
selectedProducts.value = []
importMeta.sourceFile = details[0]?.sourceFile || ''
importMeta.supplierName = details[0]?.supplierName || ''
@@ -235,23 +233,6 @@ async function openImport() {
importRef.value?.open()
}
function markSurchargeBillFlags(products, defaultNeedBill) {
return (products || []).map(product => ({
...product,
surcharges: (product.surcharges || []).map(fee => ({
...fee,
needBill: fee?.needBill === true ? true : (fee?.needBill === false ? false : !!defaultNeedBill)
}))
}))
}
function withBillableSurcharges(products) {
return (products || []).map(product => ({
...product,
surcharges: (product.surcharges || []).filter(fee => fee && fee.needBill === true)
}))
}
function onProductSelect(rows) {
selectedProducts.value = rows || []
}
@@ -297,8 +278,7 @@ async function removeSelectedProducts() {
}
function onImportConfirm(payload) {
// 价卡解析行默认不计费,需手动勾选
productRows.value = markSurchargeBillFlags(payload?.products || [], false)
productRows.value = payload?.products || []
selectedProducts.value = []
importMeta.sourceFile = payload?.sourceFile || ''
importMeta.supplierName = payload?.supplierName || ''
@@ -414,7 +394,7 @@ async function finish() {
try {
const payload = {
overwrite: editVersionMode.value ? true : importMeta.overwrite,
products: withBillableSurcharges(productRows.value)
products: productRows.value
}
if (editVersionMode.value) {
payload.priceSetId = priceSetId.value