优化附加费计算规则,调整相关逻辑和注释

This commit is contained in:
wuweihua
2026-11-13 15:25:10 +08:00
parent 880b4751c5
commit 87eab12d51
8 changed files with 89 additions and 41 deletions
@@ -78,7 +78,7 @@ public final class LblSurchargeRules
} }
if (name.contains("签名") || name.contains("重复") || name.contains("未预报") || name.contains("重新贴")) if (name.contains("签名") || name.contains("重复") || name.contains("未预报") || name.contains("重新贴"))
{ {
fee.setCalcRule(MANUAL); fee.setCalcRule("ITEM");
return; return;
} }
String postcodes = postcodeParam(desc); String postcodes = postcodeParam(desc);
@@ -104,10 +104,10 @@ public final class LblSurchargeRules
} }
if (blob.contains("固定收取") || "固定收取".equals(fee.getChargeTiming())) if (blob.contains("固定收取") || "固定收取".equals(fee.getChargeTiming()))
{ {
fee.setCalcRule(ALWAYS); fee.setCalcRule("ITEM");
return; return;
} }
fee.setCalcRule(MANUAL); fee.setCalcRule("ITEM");
} }
public static boolean matchesPostcode(String param, String postcode) public static boolean matchesPostcode(String param, String postcode)
@@ -3443,7 +3443,7 @@ public class LblWorkbookParser
product.getSurcharges().add(fee); product.getSurcharges().add(fee);
} }
/** 附加费币种为空时取价卡币种,价卡也没有则默认 EUR。 */ /** 附加费补齐币种;计费条件默认按票(ITEM),单位不默认。 */
private static void fillSurchargeCurrency(LblProduct product, String cardCurrency) private static void fillSurchargeCurrency(LblProduct product, String cardCurrency)
{ {
if (product == null || product.getSurcharges() == null || product.getSurcharges().isEmpty()) if (product == null || product.getSurcharges() == null || product.getSurcharges().isEmpty())
@@ -3461,6 +3461,23 @@ public class LblWorkbookParser
{ {
fee.setCurrency(fallback); fee.setCurrency(fallback);
} }
ensureSurchargeDefaults(fee);
}
}
private static void ensureSurchargeDefaults(LblSurcharge fee)
{
String name = fee.getFeeName() == null ? "" : fee.getFeeName();
boolean quoteNote = name.contains("报价说明");
if (quoteNote)
{
return;
}
String rule = fee.getCalcRule() == null ? "" : fee.getCalcRule().trim();
if (rule.isEmpty() || LblSurchargeRules.MANUAL.equalsIgnoreCase(rule)
|| LblSurchargeRules.ALWAYS.equalsIgnoreCase(rule))
{
fee.setCalcRule("ITEM");
} }
} }
@@ -4819,7 +4836,7 @@ public class LblWorkbookParser
return currency.isEmpty() ? "每票" : currency + "/票"; return currency.isEmpty() ? "每票" : currency + "/票";
} }
/** 附加费的货币单位跟重量段一样,价格数字单独放 amount,这里只放 EUR/ITEM、RMB/KG、%。 */ /** 附加费的货币单位跟重量段一样,价格数字单独放 amount,这里只放 EUR/ITEM、RMB/KG、%;无单位不默认。 */
private static String surchargeUnit(LblTexts.Money money, String standard) private static String surchargeUnit(LblTexts.Money money, String standard)
{ {
if (money == null || money.amount == null) if (money == null || money.amount == null)
@@ -406,7 +406,7 @@ public final class LabelArCostCalculator
addExtraKg(result.getFees(), fee, weight, currency); addExtraKg(result.getFees(), fee, weight, currency);
} }
} }
else if (PaySurchargeRules.ALWAYS.equals(rule)) else if (PaySurchargeRules.ALWAYS.equals(rule) || "ITEM".equals(rule))
{ {
PayCostLine matched = matchedFee(fee, freight, currency, blank(fee.getChargeText())); PayCostLine matched = matchedFee(fee, freight, currency, blank(fee.getChargeText()));
if (matched != null) if (matched != null)
@@ -414,6 +414,10 @@ public final class LabelArCostCalculator
result.getFees().add(matched); result.getFees().add(matched);
} }
} }
else if ("PER_KG".equals(rule))
{
addPerKg(result.getFees(), fee, weight, currency, blank(fee.getChargeText()));
}
else if (!name.isEmpty() && !name.contains("报价说明")) else if (!name.isEmpty() && !name.contains("报价说明"))
{ {
result.getSkipped().add(name + ":价卡没有可计算的条件,暂不计费"); result.getSkipped().add(name + ":价卡没有可计算的条件,暂不计费");
@@ -23,6 +23,8 @@ import com.bms.common.utils.DateUtils;
import com.bms.common.utils.SecurityUtils; import com.bms.common.utils.SecurityUtils;
import com.bms.common.utils.file.OssStorage; import com.bms.common.utils.file.OssStorage;
import com.bms.common.utils.uuid.IdUtils; import com.bms.common.utils.uuid.IdUtils;
import com.bms.masterdata.domain.ServiceChannel;
import com.bms.masterdata.mapper.ServiceChannelMapper;
import com.bms.price.domain.ChannelProductMap; import com.bms.price.domain.ChannelProductMap;
import com.bms.price.domain.LabelPackageVersion; import com.bms.price.domain.LabelPackageVersion;
import com.bms.price.domain.LblPreview; import com.bms.price.domain.LblPreview;
@@ -82,6 +84,9 @@ public class LblProductServiceImpl implements ILblProductService
@Autowired @Autowired
private ChannelProductMapMapper channelProductMapMapper; private ChannelProductMapMapper channelProductMapMapper;
@Autowired
private ServiceChannelMapper serviceChannelMapper;
@Autowired @Autowired
private ISysDictTypeService dictTypeService; private ISysDictTypeService dictTypeService;
@@ -546,12 +551,21 @@ public class LblProductServiceImpl implements ILblProductService
} }
} }
} }
String operator = SecurityUtils.getUsername();
for (String channelId : wanted) for (String channelId : wanted)
{ {
ServiceChannel channel = serviceChannelMapper.selectServiceChannelById(channelId);
if (channel == null || isBlank(channel.getChannelCode()))
{
throw new ServiceException("渠道不存在或渠道编码为空: " + channelId);
}
ChannelProductMap row = new ChannelProductMap(); ChannelProductMap row = new ChannelProductMap();
row.setChannelId(channelId); row.setChannelId(channelId);
row.setChannelCode(channel.getChannelCode());
row.setProductId(productId); row.setProductId(productId);
row.setPayType(type); row.setPayType(type);
row.setCreateBy(operator);
row.setUpdateBy(operator);
channelProductMapMapper.upsert(row); channelProductMapMapper.upsert(row);
} }
return "已绑定" + wanted.size() + "个渠道"; return "已绑定" + wanted.size() + "个渠道";
@@ -16,7 +16,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectByProductIdAndType" resultType="com.bms.price.domain.ChannelProductMap"> <select id="selectByProductIdAndType" resultType="com.bms.price.domain.ChannelProductMap">
select m.id, select m.id,
m.channel_id as channelId, m.channel_id as channelId,
m.channel_code as channelCode, coalesce(nullif(m.channel_code, ''), c.channel_code) as channelCode,
m.product_id as productId, m.product_id as productId,
c.channel_name as channelName, c.channel_name as channelName,
m.pay_type as payType, m.pay_type as payType,
@@ -28,13 +28,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
left join srv_service_channel c on c.id = m.channel_id left join srv_service_channel c on c.id = m.channel_id
where m.product_id = #{productId} where m.product_id = #{productId}
and m.pay_type = #{payType} and m.pay_type = #{payType}
order by m.channel_code asc, m.channel_id asc order by coalesce(nullif(m.channel_code, ''), c.channel_code) asc, m.channel_id asc
</select> </select>
<select id="selectByProductIdsAndType" resultType="com.bms.price.domain.ChannelProductMap"> <select id="selectByProductIdsAndType" resultType="com.bms.price.domain.ChannelProductMap">
select m.id, select m.id,
m.channel_id as channelId, m.channel_id as channelId,
m.channel_code as channelCode, coalesce(nullif(m.channel_code, ''), c.channel_code) as channelCode,
m.product_id as productId, m.product_id as productId,
c.channel_name as channelName, c.channel_name as channelName,
m.pay_type as payType, m.pay_type as payType,
@@ -49,7 +49,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<foreach collection="productIds" item="id" open="(" separator="," close=")"> <foreach collection="productIds" item="id" open="(" separator="," close=")">
#{id} #{id}
</foreach> </foreach>
order by m.product_id asc, m.channel_code asc, m.channel_id asc order by m.product_id asc, coalesce(nullif(m.channel_code, ''), c.channel_code) asc, m.channel_id asc
</select> </select>
<insert id="upsert"> <insert id="upsert">
@@ -61,6 +61,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
on duplicate key update on duplicate key update
channel_code = values(channel_code), channel_code = values(channel_code),
product_id = values(product_id), product_id = values(product_id),
create_by = ifnull(create_by, values(create_by)),
update_by = values(update_by), update_by = values(update_by),
update_time = sysdate() update_time = sysdate()
</insert> </insert>
@@ -79,7 +79,7 @@
<el-tab-pane :label="feeTabLabel(pane)" name="fee"> <el-tab-pane :label="feeTabLabel(pane)" name="fee">
<div class="label-edit-bar"> <div class="label-edit-bar">
<el-input v-model="pane.feeKeyword" clearable placeholder="筛选附加费用、地区、价格、货币或费用解释" class="label-filter" @input="pane.feePageNo = 1" /> <el-input v-model="pane.feeKeyword" clearable placeholder="筛选附加费用、价格、币种、计费条件或费用解释" class="label-filter" @input="pane.feePageNo = 1" />
<el-button v-if="editable" size="small" type="primary" plain @click="openFeePicker(pane)">添加附加费</el-button> <el-button v-if="editable" size="small" type="primary" plain @click="openFeePicker(pane)">添加附加费</el-button>
<el-button <el-button
v-if="editable" v-if="editable"
@@ -125,12 +125,6 @@
<span v-else>{{ row.feeName || '-' }}</span> <span v-else>{{ row.feeName || '-' }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="zoneCode" label="分区编码" width="110" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.zoneCode" :edit="isRowEditing(row)" /></template>
</el-table-column>
<el-table-column prop="zoneName" label="分区名称" min-width="140" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.zoneName" :edit="isRowEditing(row)" /></template>
</el-table-column>
<el-table-column label="价格" width="110" align="right"> <el-table-column label="价格" width="110" align="right">
<template #default="{ row }"> <template #default="{ row }">
<el-input v-if="isRowEditing(row)" v-model="row.amount" size="small" /> <el-input v-if="isRowEditing(row)" v-model="row.amount" size="small" />
@@ -185,24 +179,24 @@
<span v-else>{{ feeUnitOf(row) || '-' }}</span> <span v-else>{{ feeUnitOf(row) || '-' }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="收费标准" min-width="180" show-overflow-tooltip> <el-table-column label="计费条件" width="140">
<template #default="{ row }"> <template #default="{ row }">
<cell-text v-if="isRowEditing(row) && (!row.chargeText || feeNote(row))" v-model="row.chargeText" :edit="true" /> <el-select
<span v-else>{{ feeNote(row) || '-' }}</span> v-if="isRowEditing(row)"
:model-value="feeChargeModeOf(row)"
size="small"
style="width: 100%"
clearable
@update:model-value="value => setFeeChargeMode(row, value)"
>
<el-option v-for="d in feeChargeModeOptions(row)" :key="d.value" :label="d.label" :value="d.value" />
</el-select>
<span v-else>{{ dictLabel(lbl_charge_mode, feeChargeModeOf(row)) || '-' }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="feeDesc" label="费用解释" min-width="240" show-overflow-tooltip> <el-table-column prop="feeDesc" label="费用解释" min-width="240" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.feeDesc" :edit="isRowEditing(row)" /></template> <template #default="{ row }"><cell-text v-model="row.feeDesc" :edit="isRowEditing(row)" /></template>
</el-table-column> </el-table-column>
<el-table-column prop="calcRule" label="计费条件" width="140">
<template #default="{ row }">
<el-select v-if="isRowEditing(row)" :model-value="feeBillFlag(row)" size="small" @change="value => setFeeBill(row, value)">
<el-option label="需要计费" value="BILL" />
<el-option label="暂不计费" value="MANUAL" />
</el-select>
<span v-else>{{ feeBillFlag(row) === 'BILL' ? '需要计费' : '暂不计费' }}</span>
</template>
</el-table-column>
<el-table-column v-if="editable" label="操作" width="120" align="center" fixed="right"> <el-table-column v-if="editable" label="操作" width="120" align="center" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button v-if="!isRowEditing(row)" link type="primary" :disabled="rowBusy" @click="startEdit(row)">修改</el-button> <el-button v-if="!isRowEditing(row)" link type="primary" :disabled="rowBusy" @click="startEdit(row)">修改</el-button>
@@ -464,16 +458,30 @@ const CellText = {
} }
} }
function feeBillFlag(row) { function normalizeFeeChargeMode(rule) {
return row?.calcRule && row.calcRule !== 'MANUAL' ? 'BILL' : 'MANUAL' const v = String(rule || '').trim().toUpperCase()
if (!v || v === 'MANUAL') return ''
if (v === 'ALWAYS' || v === 'FLAT' || v === 'ZONE' || v === 'WEIGHT' || v === 'SIZE' || v === 'RETURN') return 'ITEM'
if (v === 'FIRST_EXTRA') return 'EXTRA_KG'
return v
} }
function setFeeBill(row, value) { function feeChargeModeOf(row) {
if (value === 'MANUAL') { return normalizeFeeChargeMode(row?.calcRule)
row.calcRule = 'MANUAL' }
return
function feeChargeModeOptions(row) {
const list = (lbl_charge_mode.value || []).map(d => ({ label: d.label || d.value, value: d.value }))
const cur = feeChargeModeOf(row)
if (cur && !list.some(d => String(d.value) === String(cur))) {
list.unshift({ label: cur, value: cur })
} }
if (!row.calcRule || row.calcRule === 'MANUAL') row.calcRule = 'ALWAYS' return list
}
function setFeeChargeMode(row, value) {
const mode = String(value || '').trim().toUpperCase()
row.calcRule = mode || 'MANUAL'
} }
const pageSize = 20 const pageSize = 20
@@ -603,7 +611,7 @@ function invalidateBandView(pane) {
} }
function filteredFees(pane) { function filteredFees(pane) {
return filterRows(pane.item.surcharges || [], pane.feeKeyword, ['feeName', 'zoneCode', 'zoneName', 'chargeText', 'amount', 'currency', 'priceUnit', 'feeDesc', 'calcRule', 'calcParam', 'postcodeParam']) return filterRows(pane.item.surcharges || [], pane.feeKeyword, ['feeName', 'amount', 'currency', 'priceUnit', 'feeDesc', 'calcRule', 'calcParam'])
} }
function feeTabLabel(pane) { function feeTabLabel(pane) {
@@ -670,8 +678,9 @@ function openFeePicker(pane) {
function toCalcRule(chargeType) { function toCalcRule(chargeType) {
const t = String(chargeType || '').trim().toUpperCase() const t = String(chargeType || '').trim().toUpperCase()
if (!t || t === 'RATE') return 'MANUAL' if (!t || t === 'RATE') return 'MANUAL'
if (t === 'FIXED') return 'ALWAYS' if (t === 'FIXED') return 'ITEM'
const allowed = ['ALWAYS', 'POSTCODE', 'ZONE', 'EXTRA_KG', 'WEIGHT', 'SIZE', 'RETURN', 'MANUAL'] const allowed = ['ITEM', 'PER_KG', 'ALWAYS', 'POSTCODE', 'ZONE', 'EXTRA_KG', 'WEIGHT', 'SIZE', 'RETURN', 'MANUAL']
if (t === 'ALWAYS') return 'ITEM'
return allowed.includes(t) ? t : 'MANUAL' return allowed.includes(t) ? t : 'MANUAL'
} }
@@ -54,7 +54,7 @@
<el-drawer <el-drawer
v-model="bindListVisible" v-model="bindListVisible"
:title="bindListTitle" :title="bindListTitle"
size="520px" size="1020px"
append-to-body append-to-body
destroy-on-close destroy-on-close
@closed="onBindListClosed" @closed="onBindListClosed"
@@ -170,6 +170,9 @@
<template #col-product="{ row }"> <template #col-product="{ row }">
{{ row.productName || '-' }} ({{ row.productCode || '-' }}) {{ row.productName || '-' }} ({{ row.productCode || '-' }})
</template> </template>
<template #col-channel="{ row }">
{{ row.productFloor || '-' }} ({{ row.productFloorCode || '-' }})
</template>
<template #col-country="{ row }"> <template #col-country="{ row }">
{{ row.countryName || '-' }} ({{ row.countryCode || '-' }}) {{ row.countryName || '-' }} ({{ row.countryCode || '-' }})
</template> </template>
@@ -593,8 +596,8 @@ const columns = [
{ label: '客户', prop: 'customerName', minWidth: 140, showOverflowTooltip: true }, { label: '客户', prop: 'customerName', minWidth: 140, showOverflowTooltip: true },
{ label: '客户代码', prop: 'customerCode', width: 120 }, { label: '客户代码', prop: 'customerCode', width: 120 },
{ label: '产品', prop: 'product', minWidth: 160, slot: 'product' }, { label: '产品', prop: 'product', minWidth: 160, slot: 'product' },
{ label: '渠道', prop: 'channel', minWidth: 160, slot: 'channel' },
{ label: '国家', prop: 'country', minWidth: 130, slot: 'country' }, { label: '国家', prop: 'country', minWidth: 130, slot: 'country' },
{ label: '件数', prop: 'pieces', align: 'center', width: 70 },
{ label: '计费重', prop: 'weight', align: 'center', width: 90, slot: 'weight' }, { label: '计费重', prop: 'weight', align: 'center', width: 90, slot: 'weight' },
{ label: '体积重', prop: 'volumeWeight', align: 'center', width: 90, slot: 'volumeWeight' }, { label: '体积重', prop: 'volumeWeight', align: 'center', width: 90, slot: 'volumeWeight' },
{ label: '实重', prop: 'receiveWeight', align: 'center', width: 90, slot: 'receiveWeight' }, { label: '实重', prop: 'receiveWeight', align: 'center', width: 90, slot: 'receiveWeight' },