13 Commits

Author SHA1 Message Date
caojia b6718f9361 调整命名2 2026-11-11 09:54:09 +08:00
wuweihua a58de9bb7e 修改计费逻辑仅 billing_fail_status = 0 才写 prc_income_bill 2026-11-11 09:49:59 +08:00
wuweihua a211f06663 修改定时任务时间 2026-11-11 09:29:47 +08:00
caojia 41cefd5fd5 调整命名 2026-11-11 00:18:12 +08:00
wuweihua fca999c384 修改比较逻辑 2026-11-10 18:10:10 +08:00
caojia 2e5ab2bfc1 ✅ 跳过依赖本机价卡文件的测试 2026-11-10 18:04:19 +08:00
caojia 3e5ef25153 ✨ 完善应收账单毛利分析与提成展示 2026-11-10 17:37:17 +08:00
caojia 9004a1b921 🔥 删除毛利汇总说明文字 2026-11-10 17:37:17 +08:00
wuweihua cf6b90008d 修改规则 2026-11-10 17:30:11 +08:00
liuminghui fbc138f041 提单费用均摊处理 2026-11-10 17:24:52 +08:00
wuweihua 3c49175cea 增加规则配置 2026-11-10 17:15:38 +08:00
wuweihua f26e03efb1 Merge branch 'test' of https://git.lionquexpress.com/wuweihua/BMS into test 2026-11-10 16:21:16 +08:00
wuweihua 1a0483c7fe 增加一个渠道绑定 2026-11-10 16:21:07 +08:00
155 changed files with 3131 additions and 575 deletions
@@ -13,13 +13,13 @@ import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.bms.ar.domain.ArBill;
import com.bms.ar.domain.ArBalanceWriteoff;
import com.bms.ar.domain.vo.ArBillListExportRow;
import com.bms.ar.domain.vo.ArBillQuery;
import com.bms.ar.domain.vo.ArBillSummary;
import com.bms.ar.service.IArBalanceWriteoffService;
import com.bms.ar.service.IArBillService;
import com.bms.receivable.domain.ArBill;
import com.bms.receivable.domain.ArBalanceWriteoff;
import com.bms.receivable.domain.vo.ArBillListExportRow;
import com.bms.receivable.domain.vo.ArBillQuery;
import com.bms.receivable.domain.vo.ArBillSummary;
import com.bms.receivable.service.IArBalanceWriteoffService;
import com.bms.receivable.service.IArBillService;
import com.bms.common.annotation.Log;
import com.bms.common.core.controller.BaseController;
import com.bms.common.core.domain.AjaxResult;
@@ -2,6 +2,7 @@ package com.bms.web.controller.price;
import java.util.Date;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -173,6 +174,15 @@ public class LabelPackageController extends BaseController
return success(payload);
}
@PreAuthorize("@ss.hasPermi('price:label:remove')")
@Log(title = "打单套餐删除版本", businessType = BusinessType.DELETE)
@DeleteMapping("/{priceSetId}/versions/{versionNo}")
public AjaxResult deleteVersion(@PathVariable Long priceSetId, @PathVariable Integer versionNo)
{
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL);
return toAjax(payProductService.deleteLabelVersion(priceSetId, versionNo));
}
@PreAuthorize("@ss.hasPermi('price:label:list')")
@GetMapping("/{priceSetId}/products")
public AjaxResult products(@PathVariable Long priceSetId,
@@ -207,6 +217,55 @@ public class LabelPackageController extends BaseController
return toAjax(payProductService.removeLabelProducts(priceSetId, productIds));
}
@PreAuthorize("@ss.hasPermi('price:label:query')")
@GetMapping("/{priceSetId}/channels")
public AjaxResult listChannels(@PathVariable Long priceSetId)
{
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL);
return success(payProductService.listLabelChannels(priceSetId));
}
@PreAuthorize("@ss.hasPermi('price:label:edit')")
@Log(title = "打单套餐绑定渠道", businessType = BusinessType.UPDATE)
@PutMapping("/{priceSetId}/channels")
public AjaxResult bindChannels(@PathVariable Long priceSetId, @RequestBody Map<String, Object> body)
{
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL);
List<String> channelIds = new ArrayList<String>();
Object rawIds = body == null ? null : body.get("channelIds");
if (rawIds instanceof List<?>)
{
for (Object item : (List<?>) rawIds)
{
if (item != null && !String.valueOf(item).trim().isEmpty())
{
channelIds.add(String.valueOf(item).trim());
}
}
}
Long productId = null;
Object rawProductId = body == null ? null : body.get("productId");
if (rawProductId instanceof Number)
{
productId = ((Number) rawProductId).longValue();
}
else if (rawProductId != null && !String.valueOf(rawProductId).trim().isEmpty()
&& !"null".equalsIgnoreCase(String.valueOf(rawProductId).trim()))
{
productId = Long.valueOf(String.valueOf(rawProductId).trim());
}
return success(payProductService.bindLabelChannels(priceSetId, channelIds, productId));
}
@PreAuthorize("@ss.hasPermi('price:label:edit')")
@Log(title = "打单套餐解绑渠道", businessType = BusinessType.DELETE)
@DeleteMapping("/{priceSetId}/channels/{channelId}")
public AjaxResult unbindChannel(@PathVariable Long priceSetId, @PathVariable String channelId)
{
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL);
return toAjax(payProductService.unbindLabelChannel(priceSetId, channelId));
}
@PreAuthorize("@ss.hasPermi('price:label:list')")
@GetMapping("/customer/list")
public TableDataInfo customerList(PrcCustomerPriceSet customer)
@@ -289,8 +348,8 @@ public class LabelPackageController extends BaseController
{
return null;
}
// 兼容 yyyy-MM-dd / yyyy-MM-dd HH:mm:ss
if (text.length() >= 19)
// 兼容 yyyy-MM-dd / yyyy-MM-dd HH:mm / yyyy-MM-dd HH:mm:ss
if (text.length() >= 16)
{
return com.bms.common.utils.DateUtils.parseDate(text);
}
@@ -1,6 +1,8 @@
package com.bms.web.controller.price;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -139,6 +141,29 @@ public class PayProductController extends BaseController
return success(payProductService.selectChannelPrice(channelId, payType));
}
/** 批量绑定渠道应收/应付产品 */
@PreAuthorize("@ss.hasAnyPermi('price:pay:edit,data:serviceChannel:edit')")
@Log(title = "渠道批量绑定产品", businessType = BusinessType.UPDATE)
@PutMapping("/channel/batchBind")
public AjaxResult batchBindChannel(@RequestBody Map<String, Object> body)
{
List<String> channelIds = new ArrayList<String>();
Object rawIds = body == null ? null : body.get("channelIds");
if (rawIds instanceof List<?>)
{
for (Object item : (List<?>) rawIds)
{
if (item != null && !String.valueOf(item).trim().isEmpty())
{
channelIds.add(String.valueOf(item).trim());
}
}
}
Long apProductId = toLong(body == null ? null : body.get("apProductId"));
Long arProductId = toLong(body == null ? null : body.get("arProductId"));
return success(payProductService.batchBindChannel(channelIds, apProductId, arProductId));
}
/** 对照两个价格版本 */
@PreAuthorize("@ss.hasPermi('price:pay:query')")
@GetMapping("/compare")
@@ -247,4 +272,22 @@ public class PayProductController extends BaseController
throw new ServiceException("价卡文件不能超过30MB");
}
}
private Long toLong(Object raw)
{
if (raw == null)
{
return null;
}
if (raw instanceof Number)
{
return ((Number) raw).longValue();
}
String text = String.valueOf(raw).trim();
if (text.isEmpty() || "null".equalsIgnoreCase(text))
{
return null;
}
return Long.valueOf(text);
}
}
@@ -16,10 +16,12 @@ import com.bms.common.core.domain.AjaxResult;
import com.bms.common.core.page.TableDataInfo;
import com.bms.common.enums.BusinessType;
import com.bms.common.utils.StringUtils;
import com.bms.price.airbill.domain.SrvBillAirAllocCommand;
import com.bms.price.airbill.domain.SrvBillAirFeeLog;
import com.bms.price.airbill.domain.SrvBillAirFeeSave;
import com.bms.price.domain.vo.SrvBillAirQuery;
import com.bms.price.domain.vo.SrvBillAirVo;
import com.bms.price.service.ISrvBillAirAllocService;
import com.bms.price.service.ISrvBillAirFeeService;
import com.bms.price.service.ISrvBillAirService;
@@ -36,6 +38,9 @@ public class SrvBillAirController extends BaseController
@Autowired
private ISrvBillAirFeeService srvBillAirFeeService;
@Autowired
private ISrvBillAirAllocService srvBillAirAllocService;
/**
* 提单列表
*/
@@ -94,4 +99,57 @@ public class SrvBillAirController extends BaseController
{
return success(srvBillAirFeeService.save(command));
}
/** 已分摊的费用改为已确定 */
@PreAuthorize("@ss.hasPermi('price:airBill:feeConfirm')")
@Log(title = "提单费用确定", businessType = BusinessType.UPDATE)
@PostMapping("/fee/confirm")
public AjaxResult confirmFee(@RequestParam("billCode") String billCode, @RequestParam("feeId") Long feeId)
{
return success(srvBillAirFeeService.confirm(billCode, feeId));
}
/** 作废已分摊或已确定的费用,并逻辑删除已分摊到订单的明细 */
@PreAuthorize("@ss.hasPermi('price:airBill:feeVoid')")
@Log(title = "提单费用作废", businessType = BusinessType.DELETE)
@PostMapping("/fee/void")
public AjaxResult voidFee(@RequestParam("billCode") String billCode, @RequestParam("feeId") Long feeId)
{
return success(srvBillAirFeeService.voidFee(billCode, feeId));
}
/** 勾选提单后,查出需要分摊到订单的费用 */
@PreAuthorize("@ss.hasPermi('price:airBill:fee')")
@GetMapping("/fee/alloc/preview")
public AjaxResult allocPreview(@RequestParam("billCodes") String billCodes)
{
return success(srvBillAirAllocService.preview(splitCodes(billCodes)));
}
/** 按重量或订单数把费用分摊到订单,并把费用状态改为已分摊 */
@PreAuthorize("@ss.hasPermi('price:airBill:fee')")
@Log(title = "提单费用分摊", businessType = BusinessType.UPDATE)
@PostMapping("/fee/alloc")
public AjaxResult allocate(@RequestBody SrvBillAirAllocCommand command)
{
List<String> billCodes = command == null ? null : command.getBillCodes();
return success(srvBillAirAllocService.allocate(billCodes));
}
private static List<String> splitCodes(String billCodes)
{
List<String> codes = new ArrayList<>();
if (StringUtils.isEmpty(billCodes))
{
return codes;
}
for (String part : billCodes.split("[,,\\s]+"))
{
if (StringUtils.isNotEmpty(part))
{
codes.add(part.trim());
}
}
return codes;
}
}
@@ -1,4 +1,4 @@
package com.bms.web.controller.ar;
package com.bms.web.controller.receivable;
import java.util.List;
@@ -7,8 +7,8 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.bms.ar.domain.ArBankAccount;
import com.bms.ar.service.IArBillService;
import com.bms.receivable.domain.ArBankAccount;
import com.bms.receivable.service.IArBillService;
import com.bms.common.core.controller.BaseController;
import com.bms.common.core.domain.AjaxResult;
@@ -1,4 +1,4 @@
package com.bms.web.controller.ar;
package com.bms.web.controller.receivable;
import java.util.ArrayList;
import java.util.List;
@@ -23,21 +23,20 @@ import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.bms.ar.domain.ArBill;
import com.bms.ar.domain.ArBankAccount;
import com.bms.ar.domain.ArBillDetailFee;
import com.bms.ar.domain.ArBillExport;
import com.bms.ar.domain.ArBillGenerateTask;
import com.bms.ar.domain.ArBillLog;
import com.bms.ar.domain.vo.ArBillActionRequest;
import com.bms.ar.domain.vo.ArBillGenerateQuery;
import com.bms.ar.domain.vo.ArBillGeneratePreviewExportRow;
import com.bms.ar.domain.vo.ArBillGenerateRequest;
import com.bms.ar.domain.vo.ArBillListExportRow;
import com.bms.ar.domain.vo.ArBillQuery;
import com.bms.ar.enums.ArBillDateType;
import com.bms.ar.service.IArBillService;
import com.bms.ar.security.ArBillScopeResolver;
import com.bms.receivable.domain.ArBill;
import com.bms.receivable.domain.ArBankAccount;
import com.bms.receivable.domain.ArBillDetailFee;
import com.bms.receivable.domain.ArBillGenerateTask;
import com.bms.receivable.domain.ArBillLog;
import com.bms.receivable.domain.vo.ArBillActionRequest;
import com.bms.receivable.domain.vo.ArBillGenerateQuery;
import com.bms.receivable.domain.vo.ArBillGeneratePreviewExportRow;
import com.bms.receivable.domain.vo.ArBillGenerateRequest;
import com.bms.receivable.domain.vo.ArBillListExportRow;
import com.bms.receivable.domain.vo.ArBillQuery;
import com.bms.receivable.enums.ArBillDateType;
import com.bms.receivable.service.IArBillService;
import com.bms.receivable.security.ArBillScopeResolver;
import com.bms.common.annotation.Log;
import com.bms.common.core.controller.BaseController;
import com.bms.common.core.domain.AjaxResult;
@@ -1,4 +1,4 @@
package com.bms.web.controller.ar;
package com.bms.web.controller.receivable;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
@@ -12,9 +12,8 @@ import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.bms.ar.domain.ArBalanceWriteoff;
import com.bms.ar.domain.ArBill;
import com.bms.ar.service.IArBalanceWriteoffService;
import com.bms.receivable.domain.ArBalanceWriteoff;
import com.bms.receivable.service.IArBalanceWriteoffService;
import com.bms.common.annotation.Log;
import com.bms.common.core.controller.BaseController;
import com.bms.common.core.domain.AjaxResult;
@@ -6,16 +6,16 @@ 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.bms.ar.profit.ArCommissionRate;
import com.bms.ar.profit.ArCommissionService;
import com.bms.ar.profit.ArProfitQuery;
import com.bms.ar.profit.ArProfitService;
import com.bms.ar.profit.ArWriteoffRate;
import com.bms.receivable.profit.ArCommissionRate;
import com.bms.receivable.profit.ArCommissionService;
import com.bms.receivable.profit.ArProfitQuery;
import com.bms.receivable.profit.ArProfitService;
import com.bms.receivable.profit.ArWriteoffRate;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.PathVariable;
import jakarta.servlet.http.HttpServletResponse;
import com.bms.ar.profit.ArProfitRow;
import com.bms.ar.profit.ArCommissionResult;
import com.bms.receivable.profit.ArProfitRow;
import com.bms.receivable.profit.ArCommissionResult;
import com.bms.common.utils.poi.ExcelUtil;
import com.bms.price.enums.ProfitAreaRule;
import com.bms.common.annotation.Log;
@@ -99,7 +99,7 @@ public class ArProfitController extends BaseController
return success();
}
@PreAuthorize("@ss.hasPermi('report:arProfit:commission')")
@PreAuthorize("@ss.hasPermi('report:arProfit:list')")
@GetMapping("/commissions")
public TableDataInfo commissions(@RequestParam(required = false) Long billId,
@RequestParam(defaultValue = "false") boolean includeReversed)
@@ -108,7 +108,7 @@ public class ArProfitController extends BaseController
return getDataTable(commissions.results(billId, includeReversed));
}
@PreAuthorize("@ss.hasPermi('report:arProfit:commission')")
@PreAuthorize("@ss.hasPermi('report:arProfit:list')")
@GetMapping("/commissions/summary")
public AjaxResult commissionSummary(@RequestParam(required = false) Long billId) { return success(commissions.summary(billId)); }
@@ -121,7 +121,7 @@ public class ArProfitController extends BaseController
@PostMapping("/commissions/export")
public void exportCommissions(HttpServletResponse response, @RequestParam(required = false) Long billId)
{
new ExcelUtil<>(ArCommissionResult.class).exportExcel(response, commissions.results(billId, false), "应收账单提成明细");
new ExcelUtil<>(ArCommissionResult.class).exportExcel(response, commissions.exportResults(billId), "应收账单提成明细");
}
@Log(title = "应收账单毛利分析", businessType = BusinessType.EXPORT)
@@ -42,14 +42,14 @@ bms:
order-source-sync-threads: 16
# Order-list local rebill workers (2~24). Pair with Druid maxActive >= threads + 8.
local-rebill-threads: 16
# Night auto-rebill: hours 0-8, pull 500 (all statuses) -> bill -> next 500 until empty / 09:00.
# Empty page => no billing.
# Auto-rebill: every hour all day, pull 500 (all statuses) -> bill -> next 500 until empty.
# Empty page => no billing. Window [start-hour, end-hour).
night-retry:
enabled: true
start-hour: 0
end-hour: 9
end-hour: 24
batch-size: 500
cron: "0 0/1 0-8 * * ?"
cron: "0 0 * * * ?"
oracle:
# SLAVE datasource Oracle main-order table (schema.table allowed)
order-table: WEISHI_USER.TAB_ORDER
@@ -7,6 +7,7 @@ import java.nio.file.Path;
import java.util.List;
import java.util.Arrays;
import java.util.stream.Stream;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import com.bms.price.domain.PayPostcodeGroup;
@@ -18,6 +19,7 @@ class PayPostcodeImporterTest
private final PayPostcodeImporter importer = new PayPostcodeImporter();
@Test
@Disabled("依赖开发机 D:/应付供应商 下的价卡文件,未随项目提供")
void stripsStarAndMergesSameFee() throws Exception
{
List<PayPostcodeGroup> groups = importer.parse(Files.readAllBytes(file("32、")), "32.xlsx");
@@ -88,6 +90,7 @@ class PayPostcodeImporterTest
}
@Test
@Disabled("依赖开发机 D:/应付供应商 下的价卡文件,未随项目提供")
void reachableUnreachableAndRemoteKeepTheirOwnCountry() throws Exception
{
List<PayPostcodeGroup> may = importer.parse(Files.readAllBytes(file("20、")), "20.xlsx");
@@ -8,6 +8,7 @@ import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import com.bms.price.domain.PayOrderCostResult;
import com.bms.price.domain.PayPreview;
@@ -23,6 +24,7 @@ class PayWorkbookParserTest
private final PayWorkbookParser parser = new PayWorkbookParser();
@Test
@Disabled("依赖开发机 D:/应付供应商 下的价卡文件,未随项目提供")
void octoberCatalogKeepsTwelveProductSheets() throws Exception
{
PayPreview preview = parse("9、");
@@ -36,6 +38,7 @@ class PayWorkbookParserTest
}
@Test
@Disabled("依赖开发机 D:/应付供应商 下的价卡文件,未随项目提供")
void mayCardReadsDhlAndDpdWithoutZoneSheets() throws Exception
{
PayPreview preview = parse("8、");
@@ -88,6 +91,7 @@ class PayWorkbookParserTest
}
@Test
@Disabled("依赖开发机 D:/应付供应商 下的价卡文件,未随项目提供")
void upsZonesStayLeftToRightAndColissimoFuelIsMarked() throws Exception
{
PayPreview preview = parse("32、");
@@ -148,6 +152,7 @@ class PayWorkbookParserTest
}
@Test
@Disabled("依赖开发机 D:/应付供应商 下的价卡文件,未随项目提供")
void imilePostcodesResolveZoneAndGermanCoverage() throws Exception
{
PayPreview preview = parse("32、");
@@ -168,6 +173,7 @@ class PayWorkbookParserTest
}
@Test
@Disabled("依赖开发机 D:/应付供应商 下的价卡文件,未随项目提供")
void gweFileOverridesOnlyPanEuroSupplier() throws Exception
{
PayPreview preview = parse("8.1");
@@ -182,6 +188,7 @@ class PayWorkbookParserTest
}
@Test
@Disabled("依赖开发机 D:/应付供应商 下的价卡文件,未随项目提供")
void wholeWorkbookPreviewSerializes() throws Exception
{
PayPreview preview = parse("9、");
@@ -191,6 +198,7 @@ class PayWorkbookParserTest
}
@Test
@Disabled("依赖开发机 D:/应付供应商 下的价卡文件,未随项目提供")
void yideCatalogIncludesPostcodeProducts() throws Exception
{
PayPreview preview = parse("26、");
@@ -439,6 +447,7 @@ class PayWorkbookParserTest
}
@Test
@Disabled("依赖开发机 D:/payment 下的 CTT 48H.xlsx 价卡文件,未随项目提供")
void ctt48hFileKeepsSurchargeTablesOutOfWeightBands() throws Exception
{
Path file = priceCard("CTT 48H.xlsx");
@@ -2,6 +2,8 @@ package com.bms.report;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.lang.reflect.Proxy;
import java.math.BigDecimal;
@@ -9,15 +11,55 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;
import com.bms.ar.domain.ArBalanceWriteoff;
import com.bms.ar.domain.ArBill;
import com.bms.ar.mapper.ArCommissionMapper;
import com.bms.ar.profit.ArCommissionRate;
import com.bms.ar.profit.ArCommissionResult;
import com.bms.ar.profit.ArCommissionService;
import com.bms.receivable.domain.ArBalanceWriteoff;
import com.bms.receivable.domain.ArBill;
import com.bms.receivable.mapper.ArCommissionMapper;
import com.bms.receivable.profit.ArCommissionRate;
import com.bms.receivable.profit.ArCommissionResult;
import com.bms.receivable.profit.ArCommissionService;
import com.bms.receivable.profit.ArProfitScope;
import com.bms.receivable.profit.ArProfitScopeResolver;
import com.bms.common.exception.ServiceException;
class ArCommissionServiceTest
{
@Test
void commissionReadsUseServerResolvedScopeAndManagementRemainsRestricted()
{
AtomicReference<ArProfitScope> actorScope = new AtomicReference<>(new ArProfitScope(false, true, false, 7L, 10L));
AtomicReference<Object[]> selected = new AtomicReference<>();
ArProfitScopeResolver resolver = new ArProfitScopeResolver() {
@Override public ArProfitScope resolve() { return actorScope.get(); }
};
ArCommissionMapper mapper = (ArCommissionMapper) Proxy.newProxyInstance(getClass().getClassLoader(),
new Class<?>[] { ArCommissionMapper.class }, (proxy, method, args) -> {
if ("selectResults".equals(method.getName()) || "selectCommissionSummary".equals(method.getName())) {
selected.set(args.clone());
return List.of();
}
throw new AssertionError(method.getName());
});
ArCommissionService service = new ArCommissionService(mapper, resolver, null, null);
service.results(999L, true);
assertEquals(999L, selected.get()[0]);
assertSame(actorScope.get(), selected.get()[2]);
service.summary(999L);
assertSame(actorScope.get(), selected.get()[1]);
assertThrows(ServiceException.class, service::selectRates);
assertThrows(ServiceException.class, service::missingRates);
assertThrows(ServiceException.class, () -> service.exportResults(null));
actorScope.set(new ArProfitScope(false, false, true, 8L, 10L));
service.results(null, false);
assertSame(actorScope.get(), selected.get()[2]);
assertThrows(ServiceException.class, service::selectRates);
actorScope.set(new ArProfitScope(true, false, false, 9L, 10L));
service.exportResults(null);
assertSame(actorScope.get(), selected.get()[2]);
}
@Test
void snapshotsTheRateOnceAtWriteoffAndPreservesMissingRates()
{
@@ -8,17 +8,64 @@ import java.util.Map;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.session.Configuration;
import org.junit.jupiter.api.Test;
import com.bms.ar.profit.ArProfitQuery;
import com.bms.ar.profit.ArProfitScope;
import com.bms.ar.profit.ArCommissionResult;
import com.bms.receivable.profit.ArProfitQuery;
import com.bms.receivable.profit.ArProfitScope;
import com.bms.receivable.profit.ArCommissionResult;
class ArProfitMapperSqlTest
{
@Test
void commissionInReportSummaryUsesBillFiltersAndDataScope() throws Exception
{
Configuration configuration = new Configuration();
String resource = "mapper/receivable/ArProfitMapper.xml";
try (InputStream input = getClass().getClassLoader().getResourceAsStream(resource))
{
new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse();
}
var statement = configuration.getMappedStatement("com.bms.ar.mapper.ArProfitMapper.selectSummary");
ArProfitQuery query = new ArProfitQuery();
query.setCustomerId(5L);
query.setBillNo("AR123");
query.setStatus("WRITTEN_OFF");
query.setSalesUserId(8L);
query.setBeginBillDate(new java.util.Date(0));
query.setEndBillDate(new java.util.Date());
String sql = statement.getBoundSql(Map.of("query", query,
"scope", new ArProfitScope(false, true, false, 8L, 10L))).getSql();
int commissionStart = sql.indexOf("from ar_bill b join ar_profit_commission_result r");
assertTrue(commissionStart >= 0);
String commission = sql.substring(commissionStart);
assertTrue(commission.contains("r.result_status = 'READY'"));
assertTrue(commission.contains("r.currency = b.currency"));
assertTrue(commission.contains("b.status = 'WRITTEN_OFF'"));
assertTrue(commission.contains("b.profit_sales_user_id = ?"));
assertTrue(commission.contains("b.customer_id = ?"));
assertTrue(commission.contains("b.bill_no like"));
assertTrue(commission.contains("b.status = ?"));
assertTrue(commission.contains("b.bill_date >= ?"));
assertTrue(commission.contains("b.bill_date <= ?"));
assertTrue(commission.contains("group by b.currency"));
for (ArProfitScope scope : new ArProfitScope[] {
new ArProfitScope(false, false, true, 8L, 10L),
new ArProfitScope(false, false, false, 8L, 10L),
new ArProfitScope(true, false, false, 8L, 10L) })
{
String scopedSql = statement.getBoundSql(Map.of("query", new ArProfitQuery(), "scope", scope)).getSql();
String scopedCommission = scopedSql.substring(scopedSql.indexOf("from ar_bill b join ar_profit_commission_result r"));
if (scope.department()) assertTrue(scopedCommission.contains("find_in_set"));
else if (scope.all()) assertFalse(scopedCommission.contains("b.profit_sales_user_id = ?"));
else assertTrue(scopedCommission.contains("and 1 = 0"));
}
}
@Test
void parsesReportAndCommissionMappersAndScopesEachReportQuery() throws Exception
{
Configuration configuration = new Configuration();
for (String resource : new String[] { "mapper/ar/ArProfitMapper.xml", "mapper/ar/ArCommissionMapper.xml" })
// 提成 Mapper 先加载时也必须能解析引用的报表权限片段。
for (String resource : new String[] {"mapper/receivable/ArCommissionMapper.xml", "mapper/receivable/ArProfitMapper.xml"})
{
try (InputStream input = getClass().getClassLoader().getResourceAsStream(resource))
{
@@ -40,11 +87,32 @@ class ArProfitMapperSqlTest
String bills = configuration.getMappedStatement("com.bms.ar.mapper.ArProfitMapper.selectBills")
.getBoundSql(Map.of("query", new ArProfitQuery(), "scope", new ArProfitScope(true, false, false, 8L, 10L))).getSql();
assertTrue(bills.contains("from ar_bill b"));
assertTrue(bills.contains("b.bill_name"));
assertFalse(bills.contains("order_no"));
String billCommission = configuration.getMappedStatement("com.bms.ar.mapper.ArCommissionMapper.selectCommissionSummary")
.getBoundSql(Map.of("billId", 9L)).getSql();
.getBoundSql(Map.of("billId", 9L, "scope", new ArProfitScope(true, false, false, 8L, 10L))).getSql();
assertTrue(billCommission.contains("r.ar_bill_id = ?"));
assertTrue(billCommission.contains("then null"));
for (String id : new String[] { "selectResults", "selectCommissionSummary" })
{
var statement = configuration.getMappedStatement("com.bms.ar.mapper.ArCommissionMapper." + id);
String self = statement.getBoundSql(Map.of("billId", 9L, "includeReversed", true,
"scope", new ArProfitScope(false, true, false, 8L, 10L))).getSql();
String department = statement.getBoundSql(Map.of("includeReversed", false,
"scope", new ArProfitScope(false, false, true, 8L, 10L))).getSql();
String denied = statement.getBoundSql(Map.of("includeReversed", false,
"scope", new ArProfitScope(false, false, false, 8L, 10L))).getSql();
String all = statement.getBoundSql(Map.of("includeReversed", false,
"scope", new ArProfitScope(true, false, false, 8L, 10L))).getSql();
assertTrue(self.contains("b.profit_sales_user_id = ?"));
assertTrue(self.contains("r.ar_bill_id = ?"));
assertFalse(self.contains("find_in_set"));
assertTrue(department.contains("u.user_id = b.profit_sales_user_id"));
assertTrue(department.contains("find_in_set"));
assertTrue(denied.contains("and 1 = 0"));
assertFalse(all.contains("b.profit_sales_user_id = ?"));
assertFalse(all.contains("find_in_set"));
}
var salesSql = configuration.getMappedStatement("com.bms.ar.mapper.ArProfitMapper.selectSalesOptions")
.getBoundSql(Map.of("keyword", "sales"));
assertTrue(salesSql.getSql().contains("from sys_user u"));
@@ -4,10 +4,9 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
import com.bms.ar.profit.ArProfitScopeResolver;
import com.bms.receivable.profit.ArProfitScopeResolver;
import com.bms.common.core.domain.entity.SysRole;
import com.bms.common.core.domain.entity.SysUser;
import com.bms.common.core.domain.model.LoginUser;
@@ -6,9 +6,9 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import java.math.BigDecimal;
import java.util.List;
import com.bms.ar.profit.CommissionCalculator;
import com.bms.ar.profit.ArCommissionResult;
import com.bms.ar.profit.CommissionCalculator.Writeoff;
import com.bms.receivable.profit.CommissionCalculator;
import com.bms.receivable.profit.ArCommissionResult;
import com.bms.receivable.profit.CommissionCalculator.Writeoff;
import org.junit.jupiter.api.Test;
class CommissionCalculatorTest
@@ -15,8 +15,8 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import com.bms.common.exception.ServiceException;
import com.bms.common.utils.SecurityUtils;
import com.bms.ar.domain.ArBankAccount;
import com.bms.ar.mapper.ArBankAccountMapper;
import com.bms.receivable.domain.ArBankAccount;
import com.bms.receivable.mapper.ArBankAccountMapper;
import com.bms.customer.customerinfo.domain.BmsCustomer;
import com.bms.customer.customerinfo.domain.BmsCustomerAttachment;
import com.bms.customer.customerinfo.mapper.BmsCustomerMapper;
@@ -0,0 +1,11 @@
package com.bms.price.airbill.domain;
import java.util.List;
import lombok.Data;
/** 提单费用分摊请求。一次最多 5 个提单号。 */
@Data
public class SrvBillAirAllocCommand
{
private List<String> billCodes;
}
@@ -0,0 +1,45 @@
package com.bms.price.airbill.domain;
import java.math.BigDecimal;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
/** 待分摊的一条提单费用,供费用处理弹框展示。 */
@Data
public class SrvBillAirAllocFee
{
private Long id;
private String billCode;
/** 提单主键 srv_bill_air.guid,写入订单费用的 source_id */
private String ladingId;
/** 费用阶段,字典 bms_fee_level */
private String feeLevel;
/** 费用状态,字典 bill_fee_status */
private String billFeeStatus;
private String feeName;
private BigDecimal price;
private String currency;
/** 分摊维度:1订单 2重量 */
private String allocDim;
/** 提单实际重量。只在按重量分摊时按提单号填入,不返回页面 */
@JsonIgnore
private BigDecimal actualWeight;
/** 干线打板重量。只在按重量分摊时按提单号填入,不返回页面 */
@JsonIgnore
private BigDecimal bandBoxWeight;
private Integer orderCount;
/** 不能分摊时的原因。空表示可以分摊 */
private String blockReason;
}
@@ -0,0 +1,19 @@
package com.bms.price.airbill.domain;
import java.math.BigDecimal;
import lombok.Data;
/** 提单下参与分摊的一票订单。 */
@Data
public class SrvBillAirAllocOrder
{
private String billCode;
private String orderNo;
/** 计费订单 ID,对应 prc_order_bill.bill_id */
private String billId;
/** 计费重,没有计费重时依次取实际重量、收货重量 */
private BigDecimal chargeWeight;
}
@@ -17,6 +17,12 @@ public class SrvBillAirFeeLog
public static final String DELETE = "3";
/** 已分摊费用确定。日志不记录字段变化。 */
public static final String CONFIRM = "4";
/** 已分摊或已确定费用作废。 */
public static final String VOID = "5";
private Long logId;
private String billCode;
@@ -25,7 +25,7 @@ public class BillingProperties
* Keep below Druid maxActive with headroom for other traffic.
*/
private int localRebillThreads = 16;
/** Night window auto-retry of prc_billing_task (all statuses) */
/** Hourly auto-retry of prc_billing_task (all statuses) */
private NightRetry nightRetry = new NightRetry();
private Oracle oracle = new Oracle();
@@ -120,8 +120,8 @@ public class BillingProperties
}
/**
* Overnight rebill window: pull prc_billing_task (all success_status) and retryTask.
* Default hours [0, 9) local time; empty result skips billing.
* Auto rebill window: pull prc_billing_task (all success_status) and retryTask.
* Default hours [0, 24) local time (all day); empty result skips billing.
*/
public static class NightRetry
{
@@ -129,8 +129,8 @@ public class BillingProperties
private boolean enabled = true;
/** Inclusive start hour (0 = 00:00) */
private int startHour = 0;
/** Exclusive end hour (9 = stop at 09:00) */
private int endHour = 9;
/** Exclusive end hour (24 = all day) */
private int endHour = 24;
/** Page size: bill this many then pull next page (default 500) */
private int batchSize = 500;
@@ -20,6 +20,15 @@ public class ChannelProductMap
private Long productId;
/** 打单套餐 ID,对应 prc_price_set.price_set_id(应收可填) */
private Long priceSetId;
/** 渠道名称(列表回显) */
private String channelName;
/** 产品名称(列表回显) */
private String productName;
/** AR 应收,AP 应付 */
private String payType;
@@ -122,7 +122,7 @@ public class PrcOrderBill extends BaseEntity
private String creditArrearsStatus;
/** NONE / REFUNDED */
private String refundStatus;
/** Billing fail: 0 ok; 1/2/3 sales miss; 23/24/25 catalog miss; soft-fail amount=0, no fee rows */
/** Billing fail: 0 ok writes income bill+fee; non-0 amount=0, no income bill/fee */
private Integer billingFailStatus;
/** 失败原因 */
private String errorMessage;
@@ -19,6 +19,9 @@ public class PrcPayBillFee implements Serializable
private String feeType;
private String feeName;
private Long scId;
/** 费用所属阶段:1 a段,2 b段,3 c段,4 d段 */
private String feeLevel;
private BigDecimal amount;
private String currency;
private String roundMode;
@@ -103,7 +103,7 @@ public class TmsPriceQuoteResult implements Serializable
private String creditArrearsStatus;
/** NONE / REFUNDED */
private String refundStatus;
/** Billing fail: 0 ok; 1/2/3 sales; 23/24/25 catalog (soft-fail on bill sync) */
/** Billing fail: 0 ok (income bill+fee); 1/2/3 sales; 23/24/25 catalog */
private Integer billingFailStatus;
private Integer pieces;
private BigDecimal actualWeight;
@@ -6,7 +6,8 @@ package com.bms.price.enums;
* 1/2/3 sales-price (package) miss: product / country / weight;
* 23/24/25 product-catalog price miss: product / country / weight;
* 4+ map to prc_billing_task.fail_code.
* Soft-fail income bill: 0/1/2/3/23/24/25; fee rows only for 0.
* 26 not billed yet (order-source sync only).
* Income bill + fee rows: only status 0.
*/
public enum BillBillingFailStatus
{
@@ -38,7 +39,9 @@ public enum BillBillingFailStatus
/** Product catalog miss: product hit but country/zone miss. */
CATALOG_COUNTRY(24),
/** Product catalog miss: product+country hit but weight band miss. */
CATALOG_WEIGHT(25);
CATALOG_WEIGHT(25),
/** Order landed from upstream but not billed yet. */
NOT_BILLED(26);
private final int code;
@@ -70,16 +73,23 @@ public enum BillBillingFailStatus
public static boolean isFail(Integer code)
{
return code != null && code.intValue() > SUCCESS.code;
if (code == null)
{
return false;
}
int value = code.intValue();
return value != SUCCESS.code && value != NOT_BILLED.code;
}
/** Soft-fail sales/catalog misses also persist prc_income_bill. */
public static boolean isNotBilled(Integer code)
{
return code != null && code.intValue() == NOT_BILLED.code;
}
/** Only success (0) persists prc_income_bill. */
public static boolean storesIncomeBill(Integer code)
{
int value = code == null ? SUCCESS.code : code.intValue();
return value == SUCCESS.code
|| value == PRODUCT.code || value == COUNTRY.code || value == WEIGHT.code
|| value == CATALOG_PRODUCT.code || value == CATALOG_COUNTRY.code || value == CATALOG_WEIGHT.code;
return code == null || code.intValue() == SUCCESS.code;
}
/** 仅 0 落 prc_income_bill_fee。 */
@@ -96,7 +106,7 @@ public enum BillBillingFailStatus
}
for (BillBillingFailStatus status : values())
{
if (status == SUCCESS)
if (status == SUCCESS || status == NOT_BILLED)
{
continue;
}
@@ -113,6 +123,7 @@ public enum BillBillingFailStatus
switch (this)
{
case SUCCESS:
case NOT_BILLED:
return null;
case PRODUCT:
return BillTaskFailCode.PRODUCT_NOT_HIT.getCode();
@@ -1,5 +1,6 @@
package com.bms.price.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.price.domain.ChannelProductMap;
@@ -10,9 +11,14 @@ public interface ChannelProductMapMapper
{
ChannelProductMap selectByChannelAndType(@Param("channelId") String channelId, @Param("payType") String payType);
List<ChannelProductMap> selectByPriceSetId(@Param("priceSetId") Long priceSetId);
int upsert(ChannelProductMap row);
int deleteByChannelAndType(@Param("channelId") String channelId, @Param("payType") String payType);
int deleteByChannelTypeAndPriceSet(@Param("channelId") String channelId, @Param("payType") String payType,
@Param("priceSetId") Long priceSetId);
int deleteByProductId(@Param("productId") Long productId);
}
@@ -17,6 +17,9 @@ public interface PayProductTermMapper
PayProductTerm selectByProductId(Long productId);
/** 打单产品所属套餐 ID */
Long selectLabelPriceSetIdByProduct(@Param("productId") Long productId);
List<PayProductTerm> selectByPriceSetId(@Param("priceSetId") Long priceSetId);
List<PayProductTerm> selectByPriceSetIdAndVersion(@Param("priceSetId") Long priceSetId,
@@ -68,6 +71,9 @@ public interface PayProductTermMapper
int restorePrematureClosed(@Param("priceSetId") Long priceSetId, @Param("today") Date today,
@Param("updateBy") String updateBy);
/** 打单:删除待生效版后,清掉旧版预约失效日 */
int clearScheduledEnd(@Param("priceSetId") Long priceSetId, @Param("updateBy") String updateBy);
int deleteByProductIds(@Param("productIds") List<Long> productIds);
int deleteByTermIds(@Param("termIds") List<Long> termIds);
@@ -38,4 +38,21 @@ public interface PrcPayBillFeeMapper
* @return 影响行数
*/
public int deleteByBillId(String billId);
/**
* 逻辑删除某一条提单费用分摊到订单上的明细。
*
* @param feeId 提单费用 ID,保存在 sc_id
* @return 影响行数
*/
public int markLadingFeeDeleted(@Param("feeId") Long feeId);
/**
* 这条提单费用分摊到的订单里,取一个已经生成应付账单的订单号。
* 是否出账看 prc_order_bill_ext.is_meet:1 已生成。
*
* @param feeId 提单费用 ID,保存在 sc_id
* @return 订单号,没有已出账订单时为空
*/
public String selectBilledOrderNo(@Param("feeId") Long feeId);
}
@@ -0,0 +1,56 @@
package com.bms.price.mapper;
import java.util.Date;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.price.airbill.domain.SrvBillAirAllocFee;
import com.bms.price.airbill.domain.SrvBillAirAllocOrder;
import com.bms.price.domain.PrcPayBill;
/**
* 提单费用分摊到订单。
*/
public interface SrvBillAirAllocMapper
{
/**
* 查询这些提单上尚未分摊、且标记为需要分摊的费用。
*
* @param billCodes 提单号
* @return 待分摊费用
*/
List<SrvBillAirAllocFee> selectPendingFees(@Param("billCodes") List<String> billCodes);
/**
* 按提单号取出实际重量和打板重量,只供按重量分摊计算。
*
* @param billCodes 提单号
* @return 提单号、实际重量、打板重量
*/
List<SrvBillAirAllocFee> selectLadingWeight(@Param("billCodes") List<String> billCodes);
/**
* 按提单号查出关联订单。订单号在扩展表,计费单号和重量在订单表。
*
* @param billCodes 提单号,对应 prc_order_bill_ext.air_no_old
* @return 订单
*/
List<SrvBillAirAllocOrder> selectOrders(@Param("billCodes") List<String> billCodes);
/**
* 按计费订单号取出已有的应付主表。
*
* @param billIds 计费订单 ID
* @return 应付主表编号
*/
List<PrcPayBill> selectPayBills(@Param("billIds") List<String> billIds);
/**
* 把仍是未分摊的费用改为已分摊。已经不是未分摊的行不会被改到。
*
* @param ids 提单费用 ID
* @param updateBy 操作人
* @param updateTime 操作时间
* @return 实际改到的行数
*/
int markAllocated(@Param("ids") List<Long> ids, @Param("updateBy") String updateBy, @Param("updateTime") Date updateTime);
}
@@ -22,6 +22,14 @@ public interface SrvBillAirFeeMapper
int deleteFee(@Param("id") Long id, @Param("billCode") String billCode, @Param("updateBy") String updateBy,
@Param("updateTime") java.util.Date updateTime);
/** 已分摊改为已确定。 */
int confirmFee(@Param("id") Long id, @Param("billCode") String billCode, @Param("updateBy") String updateBy,
@Param("updateTime") java.util.Date updateTime);
/** 已分摊或已确定的费用作废:状态改为已作废,并逻辑删除。 */
int voidFee(@Param("id") Long id, @Param("billCode") String billCode, @Param("updateBy") String updateBy,
@Param("updateTime") java.util.Date updateTime);
int insertLog(SrvBillAirFeeLog log);
List<SrvBillAirFeeLog> selectLogs(String billCode);
@@ -7,6 +7,7 @@ import com.bms.price.domain.LabelPackageVersion;
import com.bms.price.domain.PayParseProgress;
import com.bms.price.domain.PayPreview;
import com.bms.price.domain.PayPriceCompare;
import com.bms.price.domain.ChannelProductMap;
import com.bms.price.domain.PayProduct;
import com.bms.price.domain.PayProductTerm;
import com.bms.price.domain.PaySaveCommand;
@@ -57,6 +58,9 @@ public interface IPayProductService
/** 只绑定产品编号,不产生新版本;同时落 channel_code。payType 为空时按应付 AP */
void bindChannel(String channelId, Long productId, String payType);
/** 批量绑定渠道应收/应付产品;未传的产品不改原绑定 */
String batchBindChannel(java.util.List<String> channelIds, Long apProductId, Long arProductId);
/**
* 打单套餐预览:解析复用应付解析器,但「已存在」只按本套餐 price_set_id 判断,
* 不与应付公共模板互相挡导入。
@@ -72,6 +76,9 @@ public interface IPayProductService
/** 启动/修改打单套餐某一版生效时间,并视情况启用套餐 */
String activateLabelVersion(Long priceSetId, Integer versionNo, Date effectiveTime);
/** 删除打单套餐待生效版本(仅 status=0) */
int deleteLabelVersion(Long priceSetId, Integer versionNo);
/** 打单套餐内当前生效/待生效产品列表 */
List<PayProduct> selectLabelProducts(Long priceSetId);
@@ -84,6 +91,15 @@ public interface IPayProductService
/** 从打单套餐移除产品(失效/清理对应 term 明细) */
int removeLabelProducts(Long priceSetId, Long[] productIds);
/** 打单套餐已绑定渠道列表 */
java.util.List<ChannelProductMap> listLabelChannels(Long priceSetId);
/** 打单套餐批量绑定渠道(写 AR + price_set_id) */
String bindLabelChannels(Long priceSetId, java.util.List<String> channelIds, Long productId);
/** 打单套餐解绑渠道 */
int unbindLabelChannel(Long priceSetId, String channelId);
/** 取消渠道上指定收付类型的产品。payType 为空时按应付 AP */
void unbindChannel(String channelId, String payType);
@@ -0,0 +1,26 @@
package com.bms.price.service;
import java.util.List;
import com.bms.price.airbill.domain.SrvBillAirAllocFee;
/**
* 把提单上待分摊的费用均摊到关联订单。
*/
public interface ISrvBillAirAllocService
{
/**
* 查出勾选提单上需要分摊、且尚未分摊的费用。
*
* @param billCodes 提单号,最多 5 个
* @return 费用和不能分摊的原因
*/
List<SrvBillAirAllocFee> preview(List<String> billCodes);
/**
* 按费用上的分摊维度把金额摊到每个订单,并改费用状态为已分摊。整次处理在一个事务里,写入按 1000 条一批。
*
* @param billCodes 提单号,最多 5 个
* @return 给页面的处理结果
*/
String allocate(List<String> billCodes);
}
@@ -16,4 +16,22 @@ public interface ISrvBillAirFeeService
/** 只写入有变动的行,并按行记录修改前后。返回给页面的提示。 */
String save(SrvBillAirFeeSave command);
/**
* 已分摊的费用确认为已确定。
*
* @param billCode 提单号
* @param feeId 费用 ID
* @return 给页面的提示
*/
String confirm(String billCode, Long feeId);
/**
* 作废已分摊或已确定的费用。订单上的分摊明细逻辑删除,这条提单费用也逻辑删除。
*
* @param billCode 提单号
* @param feeId 费用 ID
* @return 给页面的提示
*/
String voidFee(String billCode, Long feeId);
}
@@ -893,8 +893,8 @@ public class PayProductServiceImpl implements IPayProductService
boolean editPendingVersion = command.getVersionNo() != null;
if (editPendingVersion)
{
// 编辑待生效版:清掉该 version 后按原号重写,保留原生效日
LabelVersionKeep keep = clearPendingLabelVersion(priceSetId, command.getVersionNo(), operator);
// 编辑待生效/已生效版:清掉该 version 后按原号重写,保留原生效日与状态
LabelVersionKeep keep = clearEditableLabelVersion(priceSetId, command.getVersionNo(), operator);
keepStart = keep.effectiveStart;
keepStatus = keep.status == null ? TERM_PENDING : keep.status;
nextVersionNo = command.getVersionNo();
@@ -971,8 +971,8 @@ public class PayProductServiceImpl implements IPayProductService
private String status;
}
/** 仅允许清待生效版;返回原生效日便于编辑后回写 */
private LabelVersionKeep clearPendingLabelVersion(Long priceSetId, Integer versionNo, String operator)
/** 编辑版本:允许待生效或已生效;返回原生效日与状态便于回写 */
private LabelVersionKeep clearEditableLabelVersion(Long priceSetId, Integer versionNo, String operator)
{
if (priceSetId == null || versionNo == null)
{
@@ -987,15 +987,13 @@ public class PayProductServiceImpl implements IPayProductService
List<Long> termIds = new ArrayList<>();
for (PayProductTerm term : terms)
{
if (!TERM_PENDING.equals(term.getStatus()))
boolean pending = TERM_PENDING.equals(term.getStatus());
boolean effect = TERM_EFFECT.equals(term.getStatus());
boolean futureStart = term.getEffectiveStart() != null
&& toLocalDate(term.getEffectiveStart()).isAfter(toLocalDate(today()));
if (!pending && !effect && !futureStart)
{
// 兼容:生效日未到但 status 误为 1,仍允许编辑
boolean futureStart = term.getEffectiveStart() != null
&& toLocalDate(term.getEffectiveStart()).isAfter(toLocalDate(today()));
if (!futureStart)
{
throw new ServiceException("只能编辑待生效版本");
}
throw new ServiceException("只能编辑待生效或已生效版本");
}
if (keep.effectiveStart == null && term.getEffectiveStart() != null)
{
@@ -1003,7 +1001,7 @@ public class PayProductServiceImpl implements IPayProductService
}
if (keep.status == null)
{
keep.status = TERM_PENDING;
keep.status = term.getStatus() == null ? TERM_PENDING : term.getStatus();
}
termIds.add(term.getTermId());
}
@@ -1014,6 +1012,148 @@ public class PayProductServiceImpl implements IPayProductService
return keep;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteLabelVersion(Long priceSetId, Integer versionNo)
{
if (priceSetId == null || versionNo == null)
{
throw new ServiceException("请选择要删除的版本");
}
PrcPriceSet set = priceSetMapper.selectPrcPriceSetById(priceSetId);
if (set == null)
{
throw new ServiceException("打单套餐不存在");
}
if (!PrcPriceSet.CATEGORY_LABEL.equalsIgnoreCase(String.valueOf(set.getSetCategory())))
{
throw new ServiceException("只能删除打单套餐版本");
}
List<PayProductTerm> terms = payProductTermMapper.selectByPriceSetIdAndVersion(priceSetId, versionNo);
if (terms == null || terms.isEmpty())
{
throw new ServiceException("该版本不存在或没有产品");
}
List<Long> termIds = new ArrayList<>();
for (PayProductTerm term : terms)
{
boolean pending = TERM_PENDING.equals(term.getStatus());
boolean futureStart = term.getEffectiveStart() != null
&& toLocalDate(term.getEffectiveStart()).isAfter(toLocalDate(today()));
if (!pending && !futureStart)
{
throw new ServiceException("只能删除待生效版本");
}
termIds.add(term.getTermId());
}
String operator = SecurityUtils.getUsername();
payProductMapper.deleteWeightBandsByTerm(termIds);
payProductMapper.deleteSurchargesByTerm(termIds);
payProductMapper.deleteExtraLinesByTerm(termIds);
int n = payProductTermMapper.deleteByTermIds(termIds);
payProductTermMapper.restorePrematureClosed(priceSetId, today(), operator);
payProductTermMapper.clearScheduledEnd(priceSetId, operator);
return n;
}
@Override
public List<ChannelProductMap> listLabelChannels(Long priceSetId)
{
if (priceSetId == null)
{
return Collections.emptyList();
}
List<ChannelProductMap> list = channelProductMapMapper.selectByPriceSetId(priceSetId);
return list == null ? Collections.emptyList() : list;
}
@Override
@Transactional(rollbackFor = Exception.class)
public String bindLabelChannels(Long priceSetId, List<String> channelIds, Long productId)
{
if (priceSetId == null)
{
throw new ServiceException("请选择打单套餐");
}
if (channelIds == null || channelIds.isEmpty())
{
throw new ServiceException("请选择要绑定的渠道");
}
if (productId == null)
{
throw new ServiceException("请选择套餐内应收产品");
}
PrcPriceSet set = priceSetMapper.selectPrcPriceSetById(priceSetId);
if (set == null || !PrcPriceSet.CATEGORY_LABEL.equalsIgnoreCase(String.valueOf(set.getSetCategory())))
{
throw new ServiceException("打单套餐不存在");
}
Long root = resolveRoot(productId);
boolean inSet = false;
List<PayProduct> products = selectLabelProducts(priceSetId);
for (PayProduct p : products)
{
if (p != null && root.equals(p.getProductId()))
{
inSet = true;
break;
}
}
if (!inSet)
{
throw new ServiceException("所选产品不属于本打单套餐");
}
String operator = SecurityUtils.getUsername();
int n = 0;
for (String channelId : channelIds)
{
if (isBlank(channelId))
{
continue;
}
ServiceChannel channel = serviceChannelMapper.selectServiceChannelById(channelId.trim());
if (channel == null || isBlank(channel.getChannelCode()))
{
throw new ServiceException("渠道不存在或渠道编码为空:" + channelId);
}
ChannelProductMap row = new ChannelProductMap();
row.setChannelId(channelId.trim());
row.setChannelCode(channel.getChannelCode());
row.setProductId(root);
row.setPriceSetId(priceSetId);
row.setPayType(PAY_TYPE_AR);
row.setCreateBy(operator);
row.setUpdateBy(operator);
channelProductMapMapper.upsert(row);
n++;
}
if (n <= 0)
{
throw new ServiceException("没有可绑定的渠道");
}
return "已绑定 " + n + " 个渠道";
}
@Override
@Transactional(rollbackFor = Exception.class)
public int unbindLabelChannel(Long priceSetId, String channelId)
{
if (priceSetId == null || isBlank(channelId))
{
return 0;
}
return channelProductMapMapper.deleteByChannelTypeAndPriceSet(channelId.trim(), PAY_TYPE_AR, priceSetId);
}
private Long resolveLabelPriceSetId(Long productId)
{
if (productId == null)
{
return null;
}
return payProductTermMapper.selectLabelPriceSetIdByProduct(productId);
}
@Override
public List<PayProduct> selectLabelProducts(Long priceSetId)
{
@@ -1199,9 +1339,9 @@ public class PayProductServiceImpl implements IPayProductService
{
return Collections.emptyList();
}
Date now = today();
Date now = nowDateTime();
// 纠正历史:未来生效版启动时把旧版提前标成已失效
payProductTermMapper.restorePrematureClosed(priceSetId, now, "system");
payProductTermMapper.restorePrematureClosed(priceSetId, today(), "system");
List<LabelPackageVersion> groups = payProductTermMapper.selectLabelVersionGroups(priceSetId);
if (groups == null || groups.isEmpty())
{
@@ -1215,16 +1355,16 @@ public class PayProductServiceImpl implements IPayProductService
int pending = ver.getPendingCount() == null ? 0 : ver.getPendingCount();
Date start = ver.getEffectiveTime();
Date end = ver.getExpireTime();
// 按日期判定:未到生效日=待生效;已过失效日=已失效;中间段=生效
// 按时间(到小时)判定:未到生效=待生效;已过失效=已失效;中间段=生效
if (start == null)
{
ver.setStatus(pending > 0 ? TERM_PENDING : (active > 0 ? TERM_EFFECT : TERM_EXPIRED));
}
else if (toLocalDate(start).isAfter(toLocalDate(now)))
else if (toLocalDateTime(start).isAfter(toLocalDateTime(now)))
{
ver.setStatus(TERM_PENDING);
}
else if (end != null && toLocalDate(end).isBefore(toLocalDate(now)))
else if (end != null && !toLocalDateTime(end).isAfter(toLocalDateTime(now)))
{
ver.setStatus(TERM_EXPIRED);
}
@@ -1258,20 +1398,22 @@ public class PayProductServiceImpl implements IPayProductService
throw new ServiceException("只能启动打单套餐版本");
}
String operator = SecurityUtils.getUsername();
boolean future = toLocalDate(effectiveTime).isAfter(toLocalDate(today()));
Date endDate = toDate(toLocalDate(effectiveTime).minusDays(1));
Date effectiveHour = truncateToHour(effectiveTime);
boolean future = toLocalDateTime(effectiveHour).isAfter(toLocalDateTime(nowDateTime()));
// 旧版失效时间 = 新版生效时间(取价按 start <= biz < end)
Date endDate = effectiveHour;
if (future)
{
// 未到生效日:本版保持待生效;旧版只预约失效日,当前仍生效
// 未到生效时间:本版保持待生效;旧版只预约失效时间,当前仍生效
payProductTermMapper.scheduleEndExceptVersion(priceSetId, versionNo, endDate, today(), operator);
}
else
{
// 当天或已过:本版立刻生效,旧版关掉
// 已到/已过:本版立刻生效,旧版关掉
payProductTermMapper.closeEffectExceptVersion(priceSetId, versionNo, endDate, operator);
}
String termStatus = future ? TERM_PENDING : TERM_EFFECT;
int n = payProductTermMapper.updateLabelVersionPeriod(priceSetId, versionNo, effectiveTime, null, termStatus, operator);
int n = payProductTermMapper.updateLabelVersionPeriod(priceSetId, versionNo, effectiveHour, null, termStatus, operator);
if (n <= 0)
{
throw new ServiceException("该版本没有可启动的产品价卡");
@@ -1614,12 +1756,52 @@ public class PayProductServiceImpl implements IPayProductService
row.setChannelCode(channel.getChannelCode());
row.setProductId(storedId);
row.setPayType(type);
if (PAY_TYPE_AR.equals(type))
{
row.setPriceSetId(resolveLabelPriceSetId(storedId));
}
String operator = SecurityUtils.getUsername();
row.setCreateBy(operator);
row.setUpdateBy(operator);
channelProductMapMapper.upsert(row);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String batchBindChannel(List<String> channelIds, Long apProductId, Long arProductId)
{
if (channelIds == null || channelIds.isEmpty())
{
throw new ServiceException("请先勾选渠道");
}
if (apProductId == null && arProductId == null)
{
throw new ServiceException("请至少选择应付或应收产品");
}
int n = 0;
for (String channelId : channelIds)
{
if (isBlank(channelId))
{
continue;
}
if (apProductId != null)
{
bindChannel(channelId.trim(), apProductId, PAY_TYPE_AP);
}
if (arProductId != null)
{
bindChannel(channelId.trim(), arProductId, PAY_TYPE_AR);
}
n++;
}
if (n <= 0)
{
throw new ServiceException("没有可绑定的渠道");
}
return "已为 " + n + " 个渠道绑定产品";
}
@Override
public void unbindChannel(String channelId, String payType)
{
@@ -2161,6 +2343,12 @@ public class PayProductServiceImpl implements IPayProductService
return toDate(java.time.LocalDate.now(java.time.ZoneId.of("GMT+8")));
}
private static Date nowDateTime()
{
return java.util.Date.from(java.time.LocalDateTime.now(java.time.ZoneId.of("GMT+8"))
.atZone(java.time.ZoneId.of("GMT+8")).toInstant());
}
private static Date toDate(java.time.LocalDate day)
{
return java.sql.Date.valueOf(day);
@@ -2168,11 +2356,30 @@ public class PayProductServiceImpl implements IPayProductService
private static java.time.LocalDate toLocalDate(Date date)
{
return toLocalDateTime(date).toLocalDate();
}
private static java.time.LocalDateTime toLocalDateTime(Date date)
{
if (date == null)
{
return null;
}
if (date instanceof java.sql.Date sql)
{
return sql.toLocalDate();
return sql.toLocalDate().atStartOfDay();
}
return date.toInstant().atZone(java.time.ZoneId.of("GMT+8")).toLocalDate();
if (date instanceof java.sql.Timestamp ts)
{
return ts.toLocalDateTime();
}
return date.toInstant().atZone(java.time.ZoneId.of("GMT+8")).toLocalDateTime();
}
private static Date truncateToHour(Date date)
{
java.time.LocalDateTime dt = toLocalDateTime(date).withMinute(0).withSecond(0).withNano(0);
return java.util.Date.from(dt.atZone(java.time.ZoneId.of("GMT+8")).toInstant());
}
private static String formatDay(Date date)
@@ -6,7 +6,6 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
@@ -28,8 +27,8 @@ import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.bms.ar.enums.ArIncomeBillFeeStatus;
import com.bms.ar.enums.ArOrderBillStatus;
import com.bms.receivable.enums.ArIncomeBillFeeStatus;
import com.bms.receivable.enums.ArOrderBillStatus;
import com.bms.common.annotation.DataScope;
import com.bms.common.core.domain.entity.SysUser;
import com.bms.common.exception.ServiceException;
@@ -207,21 +206,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
assertOrderBillEditable(request.getOrderNo());
PrcOrderBill existed = orderBillMapper.selectPrcOrderBillByOrderNo(request.getOrderNo());
BigDecimal systemWeight = request.getWeight();
// Upstream Oracle sync: always use TAB_ORDER weight (same overwrite as weight/charge_weight).
// Local rebill / manual charge_weight edit: keep resolveCalcWeight preference.
BigDecimal calcWeight = isUpstreamSyncRequest(request)
? systemWeight
: resolveCalcWeight(existed, systemWeight);
// Three-side sum rule disabled: upstream already applies it.
// Manual charge_weight wins; otherwise apply active biz rules (e.g. three-side).
// if (!isManualChargeWeight(existed))
// {
// BigDecimal ruled = bizRuleService.resolveWeightByRule(request, systemWeight);
// if (ruled != null)
// {
// calcWeight = ruled;
// }
// }
BigDecimal calcWeight = resolveBillCalcWeight(request, existed, systemWeight);
request.setWeight(calcWeight);
TmsPriceQuoteResult result = tmsPriceQuoteService.calculate(request);
persistBill(request, systemWeight, bizDate, result);
@@ -1836,8 +1821,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
{
bill.setCountryName(result.getCountryName());
}
// weight = system/original; charge_weight follows weight on upstream sync,
// otherwise may keep manual override via resolvePersistChargeWeight.
// weight = system/original upstream weight; charge_weight may differ when VOL_8000
// (or manual override) raised/lowered the billed weight.
bill.setWeight(weight);
bill.setBizDate(bizDate);
bill.setTmsId(request.getTmsId() == null ? null : Long.valueOf(request.getTmsId()));
@@ -1857,7 +1842,9 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
bill.setEffectiveDate(result.getEffectiveDate());
if (isUpstreamSyncRequest(request))
{
bill.setChargeWeight(weight);
// request.weight already carries ruled/calc weight used for quote
BigDecimal cw = request.getWeight() != null ? request.getWeight() : weight;
bill.setChargeWeight(cw);
}
else
{
@@ -2883,6 +2870,51 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
return systemWeight;
}
/**
* Resolve weight used for quote/persist.
* Upstream: TAB_ORDER weight then biz-rule. Local: manual charge_weight wins, else biz-rule.
*/
private BigDecimal resolveBillCalcWeight(TmsPriceBillRequest request, PrcOrderBill existed,
BigDecimal systemWeight)
{
BigDecimal calcWeight = isUpstreamSyncRequest(request)
? systemWeight
: resolveCalcWeight(existed, systemWeight);
if (isUpstreamSyncRequest(request) || !isManualChargeWeight(existed))
{
calcWeight = applyBizRuleWeight(request, calcWeight);
}
return calcWeight;
}
/**
* Upstream sync / soft-fail: keep original weight, charge_weight may follow biz-rule.
*/
private void applyUpstreamWeightFields(PrcOrderBill bill, TmsPriceBillRequest request)
{
if (bill == null || request == null || request.getWeight() == null)
{
return;
}
BigDecimal systemWeight = request.getWeight();
bill.setWeight(systemWeight);
bill.setChargeWeight(applyBizRuleWeight(request, systemWeight));
}
/**
* Biz-rule charge weight (VOL_8000 etc).
* To disable: comment out the resolveWeightByRule call and always return baseWeight.
*/
private BigDecimal applyBizRuleWeight(TmsPriceBillRequest request, BigDecimal baseWeight)
{
if (request == null || baseWeight == null)
{
return baseWeight;
}
BigDecimal ruled = bizRuleService.resolveWeightByRule(request, baseWeight);
return ruled != null ? ruled : baseWeight;
}
/**
* Persist charge_weight: keep manual override when calc weight != system weight,
* or when existing bill already had a manual override.
@@ -3057,7 +3089,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
bill.setBillId(UUID.randomUUID().toString());
bill.setArBillStatus(ArOrderBillStatus.UNBILLED.getCode());
bill.setRefundStatus(BillRefundStatus.NONE.getCode());
bill.setBillingFailStatus(Integer.valueOf(BillBillingFailStatus.SUCCESS.getCode()));
// Order-source sync only: mark not billed until quote/bill runs.
bill.setBillingFailStatus(Integer.valueOf(BillBillingFailStatus.NOT_BILLED.getCode()));
bill.setFreightRate(BigDecimal.ZERO);
bill.setBaseFreightRate(BigDecimal.ZERO);
bill.setRegistrationFee(BigDecimal.ZERO);
@@ -3077,12 +3110,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
applyProductCodeMapping(bill, request);
Long customerId = resolveCustomerIdByCode(request.getCustomerCode(), bill.getCustomerId());
bill.setCustomerId(customerId == null ? Long.valueOf(0L) : customerId);
// Order-source sync: overwrite weight and charge_weight the same way from upstream.
if (request.getWeight() != null)
{
bill.setWeight(request.getWeight());
bill.setChargeWeight(request.getWeight());
}
applyUpstreamWeightFields(bill, request);
if (creating)
{
Date bizDate = request.getPickupDate() != null ? request.getPickupDate() : new Date();
@@ -3194,12 +3222,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
{
bill.setCustomerActiveStatus(BillCustomerActiveStatus.ACTIVE.getCode());
}
// Soft-fail snapshot: overwrite weight and charge_weight the same way from upstream.
bill.setWeight(request.getWeight());
if (request.getWeight() != null)
{
bill.setChargeWeight(request.getWeight());
}
applyUpstreamWeightFields(bill, request);
Date bizDate = StringUtils.isEmpty(request.getBizDate()) ? new Date() : resolveBizDate(request.getBizDate());
bill.setBizDate(bizDate);
if (StringUtils.isEmpty(bill.getCurrency()))
@@ -3267,11 +3290,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
{
applyUpstreamBillHeaderFields(bill, request);
applyProductCodeMapping(bill, request);
if (request.getWeight() != null)
{
bill.setWeight(request.getWeight());
bill.setChargeWeight(request.getWeight());
}
applyUpstreamWeightFields(bill, request);
}
applyBillingFailText(bill, billingFailStatus, errorMessage);
orderBillMapper.updatePrcOrderBill(bill);
@@ -0,0 +1,472 @@
package com.bms.price.service.impl;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.alibaba.fastjson2.JSON;
import com.bms.common.core.domain.entity.SysUser;
import com.bms.common.exception.ServiceException;
import com.bms.common.utils.DateUtils;
import com.bms.common.utils.SecurityUtils;
import com.bms.price.airbill.domain.SrvBillAirAllocFee;
import com.bms.price.airbill.domain.SrvBillAirAllocOrder;
import com.bms.price.airbill.domain.SrvBillAirFeeLog;
import com.bms.price.domain.PrcPayBill;
import com.bms.price.domain.PrcPayBillFee;
import com.bms.price.mapper.SrvBillAirAllocMapper;
import com.bms.price.mapper.SrvBillAirFeeMapper;
import com.bms.price.mapper.SrvBillAirMapper;
import com.bms.price.mapper.PrcPayBillFeeMapper;
import com.bms.price.service.ISrvBillAirAllocService;
/**
* 提单费用分摊。只处理标记为分摊、状态仍是未分摊的费用。
*/
@Service
public class SrvBillAirAllocServiceImpl implements ISrvBillAirAllocService
{
private static final int MAX_BILLS = 5;
private static final int BATCH_SIZE = 1000;
private static final String DIM_ORDER = "1";
private static final String DIM_WEIGHT = "2";
private static final String FEE_TYPE = "LADING";
private static final String FEE_SOURCE = "LADING";
private static final BigDecimal ZERO = BigDecimal.ZERO;
@Autowired
private SrvBillAirAllocMapper allocMapper;
@Autowired
private SrvBillAirMapper billMapper;
@Autowired
private SrvBillAirFeeMapper feeMapper;
@Autowired
private PrcPayBillFeeMapper payBillFeeMapper;
@Override
public List<SrvBillAirAllocFee> preview(List<String> billCodes)
{
List<String> codes = normalize(billCodes);
List<SrvBillAirAllocFee> fees = dedupe(allocMapper.selectPendingFees(codes));
fillWeight(fees);
Map<String, List<SrvBillAirAllocOrder>> orders = ordersOf(codes);
for (SrvBillAirAllocFee fee : fees)
{
List<SrvBillAirAllocOrder> rows = orders.getOrDefault(fee.getBillCode(), List.of());
fee.setOrderCount(rows.size());
fee.setBlockReason(blockReason(fee, rows));
}
return fees;
}
@Override
@Transactional(rollbackFor = Exception.class)
public String allocate(List<String> billCodes)
{
List<String> codes = normalize(billCodes);
List<SrvBillAirAllocFee> fees = dedupe(allocMapper.selectPendingFees(codes));
fillWeight(fees);
if (fees.isEmpty())
{
throw new ServiceException("所选提单没有需要分摊的费用");
}
Map<String, List<SrvBillAirAllocOrder>> orders = ordersOf(codes);
for (SrvBillAirAllocFee fee : fees)
{
List<SrvBillAirAllocOrder> rows = orders.getOrDefault(fee.getBillCode(), List.of());
fee.setOrderCount(rows.size());
String reason = blockReason(fee, rows);
if (!reason.isEmpty())
{
throw new ServiceException(fee.getBillCode() + " " + blankName(fee) + ":" + reason);
}
}
Map<String, String> payBillIds = payBillIds(orders);
List<PrcPayBillFee> batch = new ArrayList<>(BATCH_SIZE);
int lineCount = 0;
for (SrvBillAirAllocFee fee : fees)
{
List<SrvBillAirAllocOrder> rows = orders.get(fee.getBillCode());
fee.setOrderCount(rows.size());
boolean byWeight = DIM_WEIGHT.equals(fee.getAllocDim());
List<BigDecimal> shares = split(byWeight ? weightAmount(fee) : fee.getPrice(), rows, byWeight);
for (int i = 0; i < rows.size(); i++)
{
batch.add(toOrderFee(fee, rows.get(i), shares.get(i), payBillIds));
if (batch.size() == BATCH_SIZE)
{
lineCount += flush(batch);
}
}
}
lineCount += flush(batch);
Date now = DateUtils.getNowDate();
String operator = SecurityUtils.getUsername();
int updated = allocMapper.markAllocated(feeIds(fees), operator, now);
if (updated != fees.size())
{
throw new ServiceException("费用状态已变化,请重新打开后再分摊");
}
writeLogs(fees, operator, now);
return "已将 " + fees.size() + " 条费用分摊为 " + lineCount + " 笔订单费用";
}
/**
* 按重量分摊的费用,按提单号补上实际重量和打板重量。按订单数量分摊的不查、不填。
* 这两个值只参与计算,不返回给页面。
*/
private void fillWeight(List<SrvBillAirAllocFee> fees)
{
List<String> codes = new ArrayList<>();
for (SrvBillAirAllocFee fee : fees)
{
if (fee != null && DIM_WEIGHT.equals(fee.getAllocDim()) && fee.getBillCode() != null && !codes.contains(fee.getBillCode()))
{
codes.add(fee.getBillCode());
}
}
if (codes.isEmpty())
{
return;
}
Map<String, SrvBillAirAllocFee> weights = new LinkedHashMap<>();
for (SrvBillAirAllocFee row : allocMapper.selectLadingWeight(codes))
{
if (row != null && row.getBillCode() != null)
{
weights.put(row.getBillCode(), row);
}
}
for (SrvBillAirAllocFee fee : fees)
{
if (fee == null || !DIM_WEIGHT.equals(fee.getAllocDim()))
{
continue;
}
SrvBillAirAllocFee weight = weights.get(fee.getBillCode());
if (weight == null)
{
continue;
}
fee.setActualWeight(weight.getActualWeight());
fee.setBandBoxWeight(weight.getBandBoxWeight());
}
}
/** 提单号去重、去空白,并限制单次 5 条。 */
private List<String> normalize(List<String> billCodes)
{
LinkedHashSet<String> codes = new LinkedHashSet<>();
if (billCodes != null)
{
for (String raw : billCodes)
{
if (raw == null)
{
continue;
}
for (String part : raw.split("[,,\\s]+"))
{
if (!part.isBlank())
{
codes.add(part.trim());
}
}
}
}
if (codes.isEmpty())
{
throw new ServiceException("请勾选提单");
}
if (codes.size() > MAX_BILLS)
{
throw new ServiceException("单次最多处理5条提单");
}
for (String code : codes)
{
if (billMapper.selectSrvBillAirByBillCode(code) == null)
{
throw new ServiceException("提单不存在:" + code);
}
}
return new ArrayList<>(codes);
}
private Map<String, List<SrvBillAirAllocOrder>> ordersOf(List<String> codes)
{
Map<String, List<SrvBillAirAllocOrder>> grouped = new LinkedHashMap<>();
for (SrvBillAirAllocOrder order : allocMapper.selectOrders(codes))
{
if (order.getBillId() == null || order.getBillId().isBlank())
{
continue;
}
grouped.computeIfAbsent(order.getBillCode(), key -> new ArrayList<>()).add(order);
}
return grouped;
}
private Map<String, String> payBillIds(Map<String, List<SrvBillAirAllocOrder>> orders)
{
List<String> billIds = new ArrayList<>();
for (List<SrvBillAirAllocOrder> rows : orders.values())
{
for (SrvBillAirAllocOrder order : rows)
{
billIds.add(order.getBillId());
}
}
Map<String, String> payIds = new LinkedHashMap<>();
for (int from = 0; from < billIds.size(); from += BATCH_SIZE)
{
List<String> chunk = billIds.subList(from, Math.min(from + BATCH_SIZE, billIds.size()));
for (PrcPayBill payBill : allocMapper.selectPayBills(chunk))
{
if (payBill.getBillId() != null && payBill.getPayBillId() != null)
{
payIds.put(payBill.getBillId(), payBill.getPayBillId());
}
}
}
return payIds;
}
private String blockReason(SrvBillAirAllocFee fee, List<SrvBillAirAllocOrder> orders)
{
if (fee.getPrice() == null)
{
return "费用没有价格";
}
if (fee.getPrice().signum() < 0)
{
return "费用价格不能小于0";
}
if (fee.getCurrency() == null || fee.getCurrency().isBlank())
{
return "费用没有币种";
}
if (fee.getLadingId() == null || fee.getLadingId().isBlank())
{
return "提单缺少主键";
}
if (!DIM_ORDER.equals(fee.getAllocDim()) && !DIM_WEIGHT.equals(fee.getAllocDim()))
{
return "未选择分摊维度";
}
if (orders == null || orders.isEmpty())
{
return "提单下没有关联订单";
}
if (DIM_WEIGHT.equals(fee.getAllocDim()))
{
if (fee.getActualWeight() == null || fee.getActualWeight().signum() <= 0)
{
return "提单没有实际重量,无法按重量分摊";
}
if (fee.getBandBoxWeight() == null || fee.getBandBoxWeight().signum() <= 0)
{
return "干线没有打板重量,无法按重量分摊";
}
if (totalWeight(orders).signum() <= 0)
{
return "订单没有可用重量,无法按重量分摊";
}
}
return "";
}
private PrcPayBillFee toOrderFee(SrvBillAirAllocFee fee, SrvBillAirAllocOrder order, BigDecimal amount, Map<String, String> payBillIds)
{
PrcPayBillFee row = new PrcPayBillFee();
row.setFeeId(UUID.randomUUID().toString());
row.setBillId(order.getBillId());
row.setPayBillId(payBillIds.get(order.getBillId()));
row.setFeeType(FEE_TYPE);
row.setFeeName(blankName(fee));
row.setScId(fee.getId() == null ? 0L : fee.getId());
row.setFeeLevel(fee.getFeeLevel());
row.setAmount(amount);
row.setCurrency(fee.getCurrency().trim());
row.setRoundMode("HALF_UP");
row.setRemark(remark(fee));
row.setFeeSource(FEE_SOURCE);
row.setSourceId(fee.getLadingId().trim());
row.setIsDeleted("0");
return row;
}
private int flush(List<PrcPayBillFee> batch)
{
if (batch.isEmpty())
{
return 0;
}
int rows = batch.size();
payBillFeeMapper.insertBatch(batch);
batch.clear();
return rows;
}
private void writeLogs(List<SrvBillAirAllocFee> fees, String operator, Date now)
{
SysUser user = SecurityUtils.getLoginUser().getUser();
String name = user.getNickName() == null || user.getNickName().isBlank() ? operator : user.getNickName();
for (SrvBillAirAllocFee fee : fees)
{
SrvBillAirFeeLog log = new SrvBillAirFeeLog();
log.setBillCode(fee.getBillCode());
log.setFeeId(fee.getId());
log.setOperationType(SrvBillAirFeeLog.UPDATE);
log.setSummary("分摊到 " + fee.getOrderCount() + " 票订单");
log.setChangeContent(JSON.toJSONString(List.of(
new SrvBillAirFeeLog.FieldChange("billFeeStatus", "费用状态", "未分摊", "已分摊"))));
log.setCreateBy(operator);
log.setCreateByName(name);
log.setCreateTime(now);
feeMapper.insertLog(log);
}
}
/**
* 按重量分摊时,先算出这一票提单实际可分摊的金额。
* 百分比 = 提单实际重量 srv_bill_air.actual_weight / 干线打板重量 srv_bill_air_flight.band_box_weight。
* 可分摊金额 = 费用价格 × 百分比,保留两位,四舍五入。
* 调用前须已确认实际重量和打板重量都大于 0。
*
* @param fee 待分摊的提单费用,带有实际重量和打板重量
* @return 按百分比算出的可分摊金额
*/
private static BigDecimal weightAmount(SrvBillAirAllocFee fee)
{
BigDecimal price = fee.getPrice().setScale(2, RoundingMode.HALF_UP);
return price.multiply(fee.getActualWeight()).divide(fee.getBandBoxWeight(), 2, RoundingMode.HALF_UP);
}
/**
* 把金额拆到每一票订单,结果保留两位。
* 按订单数量:每一票先向下取整,差额补在最后一票,合计等于费用价格。
* 按重量:传入的金额已是「费用价格 × 提单实际重量 / 干线打板重量」。
* 再按订单重量占这些订单总重量的比例分摊;前面的订单向下取整,差额补在最后一票有重量的订单上,合计等于这笔金额。
* 订单重量优先取计费重,没有计费重时取实际重量,再没有取收货重量。
*
* @param price 本次要拆开的金额。按重量时是百分比计算后的可分摊金额
* @param orders 该提单下的订单
* @param byWeight true 按订单重量占比,false 按订单数量均分
* @return 与订单顺序一致的分摊金额
*/
private static List<BigDecimal> split(BigDecimal price, List<SrvBillAirAllocOrder> orders, boolean byWeight)
{
BigDecimal total = price.setScale(2, RoundingMode.HALF_UP);
int count = orders.size();
List<BigDecimal> shares = new ArrayList<>(count);
if (!byWeight)
{
BigDecimal each = total.divide(BigDecimal.valueOf(count), 2, RoundingMode.DOWN);
BigDecimal used = ZERO;
for (int i = 0; i < count - 1; i++)
{
shares.add(each);
used = used.add(each);
}
shares.add(total.subtract(used));
return shares;
}
BigDecimal sum = totalWeight(orders);
int last = count - 1;
for (int i = count - 1; i >= 0; i--)
{
if (weight(orders.get(i)).signum() > 0)
{
last = i;
break;
}
}
BigDecimal used = ZERO;
for (int i = 0; i < count; i++)
{
if (i == last)
{
shares.add(null);
continue;
}
BigDecimal share = total.multiply(weight(orders.get(i))).divide(sum, 2, RoundingMode.DOWN);
shares.add(share);
used = used.add(share);
}
shares.set(last, total.subtract(used));
return shares;
}
private static BigDecimal totalWeight(List<SrvBillAirAllocOrder> orders)
{
BigDecimal sum = ZERO;
for (SrvBillAirAllocOrder order : orders)
{
sum = sum.add(weight(order));
}
return sum;
}
private static BigDecimal weight(SrvBillAirAllocOrder order)
{
return order.getChargeWeight() == null ? ZERO : order.getChargeWeight();
}
private static List<SrvBillAirAllocFee> dedupe(List<SrvBillAirAllocFee> fees)
{
Map<Long, SrvBillAirAllocFee> unique = new LinkedHashMap<>();
if (fees != null)
{
for (SrvBillAirAllocFee fee : fees)
{
if (fee != null && fee.getId() != null)
{
unique.putIfAbsent(fee.getId(), fee);
}
}
}
return new ArrayList<>(unique.values());
}
private static List<Long> feeIds(List<SrvBillAirAllocFee> fees)
{
List<Long> ids = new ArrayList<>();
for (SrvBillAirAllocFee fee : fees)
{
ids.add(fee.getId());
}
return ids;
}
private static String blankName(SrvBillAirAllocFee fee)
{
if (fee.getFeeName() != null && !fee.getFeeName().isBlank())
{
return fee.getFeeName().trim();
}
return "提单费用";
}
private static String remark(SrvBillAirAllocFee fee)
{
String dim = DIM_WEIGHT.equals(fee.getAllocDim()) ? "重量" : "订单";
return "提单" + fee.getBillCode() + "按" + dim + "分摊";
}
}
@@ -22,6 +22,7 @@ import com.bms.price.airbill.domain.SrvBillAirFee;
import com.bms.price.airbill.domain.SrvBillAirFeeLog;
import com.bms.price.airbill.domain.SrvBillAirFeeSave;
import com.bms.price.domain.PrcSurcharge;
import com.bms.price.mapper.PrcPayBillFeeMapper;
import com.bms.price.mapper.SrvBillAirFeeMapper;
import com.bms.price.mapper.SrvBillAirMapper;
import com.bms.price.service.IPrcSurchargeService;
@@ -42,11 +43,18 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
private static final String STATUS_DICT = "bill_fee_status";
private static final String STATUS_ALLOCATED = "1";
private static final String STATUS_CONFIRMED = "3";
private static final String CURRENCY_DICT = "bms_currency";
@Autowired
private SrvBillAirFeeMapper feeMapper;
@Autowired
private PrcPayBillFeeMapper payBillFeeMapper;
@Autowired
private IPrcSurchargeService surchargeService;
@@ -121,6 +129,10 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
{
throw new ServiceException("费用已删除或不属于该提单");
}
if (locked(current))
{
throw new ServiceException("已分摊或已确定的费用不能删除,请使用作废");
}
int rows = feeMapper.deleteFee(current.getId(), billCode, operator.account, now);
if (rows > 0)
{
@@ -143,6 +155,10 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
{
throw new ServiceException("费用已删除或不属于该提单");
}
if (locked(current))
{
throw new ServiceException("已分摊或已确定的费用不能修改");
}
SrvBillAirFee next = normalize(incoming, levelLabels, allocLabels, dimLabels, currencyLabels);
next.setBillFeeStatus(current.getBillFeeStatus() == null || current.getBillFeeStatus().isBlank() ? "0" : current.getBillFeeStatus());
Map<String, String> feeNames = feeNames(current, next);
@@ -187,11 +203,90 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
return "已保存:新增 " + inserted + " 条,修改 " + updated + " 条,删除 " + removed + " 条";
}
@Override
@Transactional(rollbackFor = Exception.class)
public String confirm(String billCode, Long feeId)
{
String code = requireBill(billCode);
if (feeId == null)
{
throw new ServiceException("请选择要确定的费用");
}
SrvBillAirFee current = liveFee(feeId, code);
if (current == null)
{
throw new ServiceException("费用已删除或不属于该提单");
}
if (!STATUS_ALLOCATED.equals(current.getBillFeeStatus()))
{
throw new ServiceException("只有已分摊的费用可以确定");
}
Operator operator = operator();
Date now = DateUtils.getNowDate();
if (feeMapper.confirmFee(feeId, code, operator.account, now) != 1)
{
throw new ServiceException("费用状态已变化,请刷新后再确定");
}
writePlainLog(code, feeId, SrvBillAirFeeLog.CONFIRM, "确定费用", operator, now);
return "费用已确定";
}
@Override
@Transactional(rollbackFor = Exception.class)
public String voidFee(String billCode, Long feeId)
{
String code = requireBill(billCode);
if (feeId == null)
{
throw new ServiceException("请选择要作废的费用");
}
SrvBillAirFee current = liveFee(feeId, code);
if (current == null)
{
throw new ServiceException("费用已删除或不属于该提单");
}
if (!locked(current))
{
throw new ServiceException("只有已分摊或已确定的费用可以作废");
}
String billedOrderNo = payBillFeeMapper.selectBilledOrderNo(feeId);
if (billedOrderNo != null)
{
String orderNo = billedOrderNo.trim();
if (orderNo.isEmpty())
{
orderNo = "未知订单";
}
throw new ServiceException("该费用存在单笔或多笔订单已出账单-如:" + orderNo + ",不可作废");
}
Operator operator = operator();
Date now = DateUtils.getNowDate();
payBillFeeMapper.markLadingFeeDeleted(feeId);
if (feeMapper.voidFee(feeId, code, operator.account, now) != 1)
{
throw new ServiceException("费用状态已变化,请刷新后再作废");
}
Map<String, String> levelLabels = dictLabels(LEVEL_DICT, "费用阶段");
Map<String, String> allocLabels = dictLabels(ALLOC_DICT, "是否分摊");
Map<String, String> dimLabels = dictLabels(ALLOC_DIM_DICT, "分摊维度");
Map<String, String> statusLabels = dictLabels(STATUS_DICT, "费用状态");
Map<String, String> currencyLabels = dictLabels(CURRENCY_DICT, "币种");
writeLog(code, feeId, SrvBillAirFeeLog.VOID, current, null, feeNames(current, null),
levelLabels, allocLabels, dimLabels, statusLabels, currencyLabels, operator, now);
return "费用已作废";
}
private SrvBillAirFee liveFee(Long id, String billCode)
{
return feeMapper.selectLive(id, billCode);
}
private static boolean locked(SrvBillAirFee fee)
{
String status = fee == null ? "" : text(fee.getBillFeeStatus());
return STATUS_ALLOCATED.equals(status) || STATUS_CONFIRMED.equals(status);
}
private SrvBillAirFee normalize(SrvBillAirFee raw, Map<String, String> levelLabels,
Map<String, String> allocLabels, Map<String, String> dimLabels, Map<String, String> currencyLabels)
{
@@ -270,6 +365,22 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
return fee;
}
/**
* 费用日志组装
* @param billCode
* @param feeId
* @param operation
* @param before
* @param after
* @param feeLabels
* @param levelLabels
* @param allocLabels
* @param dimLabels
* @param statusLabels
* @param currencyLabels
* @param operator
* @param now
*/
private void writeLog(String billCode, Long feeId, String operation, SrvBillAirFee before, SrvBillAirFee after,
Map<String, String> feeLabels, Map<String, String> levelLabels, Map<String, String> allocLabels,
Map<String, String> dimLabels, Map<String, String> statusLabels, Map<String, String> currencyLabels, Operator operator, Date now)
@@ -287,6 +398,21 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
feeMapper.insertLog(log);
}
/** 确定费用不比较字段,更改前和更改后都留空。 */
private void writePlainLog(String billCode, Long feeId, String operation, String summary, Operator operator, Date now)
{
SrvBillAirFeeLog log = new SrvBillAirFeeLog();
log.setBillCode(billCode);
log.setFeeId(feeId);
log.setOperationType(operation);
log.setSummary(summary);
log.setChangeContent("[]");
log.setCreateBy(operator.account);
log.setCreateByName(operator.name);
log.setCreateTime(now);
feeMapper.insertLog(log);
}
private static List<SrvBillAirFeeLog.FieldChange> diff(SrvBillAirFee before, SrvBillAirFee after,
Map<String, String> feeLabels, Map<String, String> levelLabels, Map<String, String> allocLabels,
Map<String, String> dimLabels, Map<String, String> statusLabels, Map<String, String> currencyLabels)
@@ -323,7 +449,9 @@ public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService
private static String summary(String operation, List<SrvBillAirFeeLog.FieldChange> changes)
{
String action = SrvBillAirFeeLog.INSERT.equals(operation) ? "新增费用"
: SrvBillAirFeeLog.DELETE.equals(operation) ? "删除费用" : "修改费用";
: SrvBillAirFeeLog.DELETE.equals(operation) ? "删除费用"
: SrvBillAirFeeLog.CONFIRM.equals(operation) ? "确定费用"
: SrvBillAirFeeLog.VOID.equals(operation) ? "作废费用" : "修改费用";
StringBuilder text = new StringBuilder(action);
for (SrvBillAirFeeLog.FieldChange change : changes)
{
@@ -265,6 +265,7 @@ public final class OrderBillExportSupport
case 20: return "客户已停用";
case 21: return "运费为空";
case 22: return "未知失败";
case 26: return "未计费";
default: return String.valueOf(status);
}
}
@@ -16,13 +16,13 @@ import com.bms.price.mapper.PrcBillingTaskMapper;
import com.bms.price.service.IPrcBillingTaskService;
/**
* 夜间窗口按 prc_billing_task 自动重计费。
* Auto retry prc_billing_task on a schedule.
*
* <p>在 [startHour, endHour) 内(默认 00:00-09:00):最多拉取 {@value #DEFAULT_BATCH}
* 条任务 id(不限 success_status)-> 全部计费 -> 立刻拉下一页。
* 空页则跳过计费并结束本轮(下次 cron 再处理新数据)。</p>
* <p>Within [startHour, endHour) (default all day 00:00-24:00): pull up to {@value #DEFAULT_BATCH}
* task ids (any success_status) -> bill all -> pull next page.
* Empty page skips billing and ends this tick (next cron handles new rows).</p>
*
* <p>与监控手工重试 / 条件重试同一条重计费路径(先重置为 WAITING 再处理)。</p>
* <p>Same path as monitor manual / filter retry (reset to WAITING then process).</p>
*/
@Component
public class PrcBillingTaskNightRetryTask
@@ -52,10 +52,10 @@ public class PrcBillingTaskNightRetryTask
private IPrcBillingTaskService billingTaskService;
/**
* Fire every minute; if a previous run is still paging batches, skip.
* Fire every hour by default; if a previous run is still paging batches, skip.
* Hour gate uses bms.billing.night-retry start/end.
*/
@Scheduled(cron = "${bms.billing.night-retry.cron:0 0/1 0-8 * * ?}")
@Scheduled(cron = "${bms.billing.night-retry.cron:0 0 * * * ?}")
public void scheduledTick()
{
BillingProperties.NightRetry cfg = billingProperties == null ? null : billingProperties.getNightRetry();
@@ -1,4 +1,4 @@
package com.bms.ar.config;
package com.bms.receivable.config;
import java.util.concurrent.ThreadPoolExecutor;
import org.springframework.context.annotation.Bean;
@@ -1,4 +1,4 @@
package com.bms.ar.domain;
package com.bms.receivable.domain;
import java.math.BigDecimal;
import java.util.Date;
@@ -1,4 +1,4 @@
package com.bms.ar.domain;
package com.bms.receivable.domain;
import java.io.Serializable;
import java.util.Date;
@@ -1,4 +1,4 @@
package com.bms.ar.domain;
package com.bms.receivable.domain;
import java.math.BigDecimal;
import java.util.Date;
@@ -1,4 +1,4 @@
package com.bms.ar.domain;
package com.bms.receivable.domain;
import java.io.Serializable;
import java.math.BigDecimal;
@@ -1,4 +1,4 @@
package com.bms.ar.domain;
package com.bms.receivable.domain;
import java.io.Serializable;
import java.math.BigDecimal;
@@ -1,4 +1,4 @@
package com.bms.ar.domain;
package com.bms.receivable.domain;
import java.io.Serializable;
import java.util.Date;
@@ -1,4 +1,4 @@
package com.bms.ar.domain;
package com.bms.receivable.domain;
import java.io.Serializable;
import java.math.BigDecimal;
@@ -1,4 +1,4 @@
package com.bms.ar.domain;
package com.bms.receivable.domain;
import java.io.Serializable;
import java.util.Date;
@@ -1,4 +1,4 @@
package com.bms.ar.domain;
package com.bms.receivable.domain;
import java.io.Serializable;
import java.util.Date;
@@ -1,4 +1,4 @@
package com.bms.ar.domain.vo;
package com.bms.receivable.domain.vo;
import lombok.Data;
@@ -1,4 +1,4 @@
package com.bms.ar.domain.vo;
package com.bms.receivable.domain.vo;
import java.io.Serializable;
import java.math.BigDecimal;
@@ -1,4 +1,4 @@
package com.bms.ar.domain.vo;
package com.bms.receivable.domain.vo;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
@@ -1,4 +1,4 @@
package com.bms.ar.domain.vo;
package com.bms.receivable.domain.vo;
import java.util.Date;
import java.util.List;
@@ -8,7 +8,7 @@ import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import com.bms.common.config.serializer.StrictDateDeserializer;
import com.bms.ar.enums.ArBillSelectionMode;
import com.bms.receivable.enums.ArBillSelectionMode;
import lombok.Data;
/** 从订单计费结果生成正式账单的参数。 */
@@ -1,11 +1,11 @@
package com.bms.ar.domain.vo;
package com.bms.receivable.domain.vo;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import com.bms.ar.domain.ArBill;
import com.bms.ar.enums.ArBillStatus;
import com.bms.receivable.domain.ArBill;
import com.bms.receivable.enums.ArBillStatus;
import com.bms.common.annotation.Excel;
import lombok.Data;
@@ -1,9 +1,9 @@
package com.bms.ar.domain.vo;
package com.bms.receivable.domain.vo;
import java.util.Date;
import java.util.List;
import org.springframework.format.annotation.DateTimeFormat;
import com.bms.ar.domain.ArBill;
import com.bms.receivable.domain.ArBill;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -1,4 +1,4 @@
package com.bms.ar.domain.vo;
package com.bms.receivable.domain.vo;
import java.io.Serializable;
import java.math.BigDecimal;
@@ -1,4 +1,4 @@
package com.bms.ar.enums;
package com.bms.receivable.enums;
/**
* 应收账单余额核销单状态。
@@ -1,4 +1,4 @@
package com.bms.ar.enums;
package com.bms.receivable.enums;
/** 应收账单生成时用于截止日筛选的来源订单日期字段。 */
public enum ArBillDateType
@@ -1,4 +1,4 @@
package com.bms.ar.enums;
package com.bms.receivable.enums;
/** 应收账单异步生成任务状态。 */
public final class ArBillGenerateTaskStatus
@@ -1,4 +1,4 @@
package com.bms.ar.enums;
package com.bms.receivable.enums;
/** 应收账单生成状态,与账单业务状态分离。 */
public final class ArBillGenerationStatus
@@ -1,4 +1,4 @@
package com.bms.ar.enums;
package com.bms.receivable.enums;
/**
* 应收账单生成时的订单选择模式。
@@ -1,4 +1,4 @@
package com.bms.ar.enums;
package com.bms.receivable.enums;
/** 应收账单状态。数据库保存值使用 code,避免业务代码散落状态字符串。 */
public enum ArBillStatus
@@ -1,4 +1,4 @@
package com.bms.ar.enums;
package com.bms.receivable.enums;
/**
* 应收账单导出任务状态。
@@ -1,4 +1,4 @@
package com.bms.ar.enums;
package com.bms.receivable.enums;
/** 应收费用明细是否已经生成正式应收账单。 */
public enum ArIncomeBillFeeStatus
@@ -1,4 +1,4 @@
package com.bms.ar.enums;
package com.bms.receivable.enums;
/** 应收日志关联对象类型。 */
public enum ArLogObjectType
@@ -1,4 +1,4 @@
package com.bms.ar.enums;
package com.bms.receivable.enums;
/** 应收日志操作来源。 */
public enum ArLogOperationSource
@@ -1,4 +1,4 @@
package com.bms.ar.enums;
package com.bms.receivable.enums;
/** 应收日志操作类型。 */
public enum ArLogOperationType
@@ -1,4 +1,4 @@
package com.bms.ar.enums;
package com.bms.receivable.enums;
/** 应收操作日志结果。 */
public enum ArLogResult
@@ -1,4 +1,4 @@
package com.bms.ar.enums;
package com.bms.receivable.enums;
/** 计费订单的应收账单状态。 */
public enum ArOrderBillStatus
@@ -1,4 +1,4 @@
package com.bms.ar.event;
package com.bms.receivable.event;
/**
* 应收账单导出任务已提交事件。
@@ -1,4 +1,4 @@
package com.bms.ar.event;
package com.bms.receivable.event;
/**
* 应收账单异步生成任务已提交事件。
@@ -1,10 +1,10 @@
package com.bms.ar.mapper;
package com.bms.receivable.mapper;
import java.util.List;
import com.bms.ar.security.ArBillRowScope;
import com.bms.receivable.security.ArBillRowScope;
import org.apache.ibatis.annotations.Param;
import com.bms.ar.domain.ArBalanceWriteoff;
import com.bms.receivable.domain.ArBalanceWriteoff;
/**
* 应收账单余额核销 Mapper
@@ -1,9 +1,9 @@
package com.bms.ar.mapper;
package com.bms.receivable.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.ar.domain.ArBankAccount;
import com.bms.receivable.domain.ArBankAccount;
public interface ArBankAccountMapper {
ArBankAccount selectDefaultByCurrency(@Param("currency") String currency);
@@ -1,10 +1,10 @@
package com.bms.ar.mapper;
package com.bms.receivable.mapper;
import java.util.List;
import com.bms.ar.security.ArBillRowScope;
import com.bms.receivable.security.ArBillRowScope;
import org.apache.ibatis.annotations.Param;
import com.bms.ar.domain.ArBillDetailFee;
import com.bms.receivable.domain.ArBillDetailFee;
public interface ArBillDetailFeeMapper {
List<ArBillDetailFee> selectByBillIdAndDetailIdScoped(@Param("arBillId") Long arBillId, @Param("detailId") Long detailId, @Param("scope") ArBillRowScope scope);
@@ -1,12 +1,12 @@
package com.bms.ar.mapper;
package com.bms.receivable.mapper;
import java.util.List;
import com.bms.ar.security.ArBillRowScope;
import com.bms.receivable.security.ArBillRowScope;
import java.util.Map;
import org.apache.ibatis.annotations.Param;
import com.bms.ar.domain.ArBillDetail;
import com.bms.receivable.domain.ArBillDetail;
public interface ArBillDetailMapper {
List<ArBillDetail> selectByBillIdScoped(@Param("arBillId") Long arBillId, @Param("scope") ArBillRowScope scope);
@@ -1,10 +1,10 @@
package com.bms.ar.mapper;
package com.bms.receivable.mapper;
import java.util.Date;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.ar.domain.ArBillExport;
import com.bms.receivable.domain.ArBillExport;
public interface ArBillExportMapper {
List<ArBillExport> selectByBillId(Long arBillId);
@@ -1,17 +1,17 @@
package com.bms.ar.mapper;
package com.bms.receivable.mapper;
import java.util.Date;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.ar.domain.ArBillGenerateTask;
import com.bms.receivable.domain.ArBillGenerateTask;
/**
* 应收账单异步生成任务 Mapper。
*/
public interface ArBillGenerateTaskMapper {
List<ArBillGenerateTask> selectActiveByCreateByScoped(@Param("createBy") String createBy,
@Param("scope") com.bms.ar.security.ArBillRowScope scope);
@Param("scope") com.bms.receivable.security.ArBillRowScope scope);
ArBillGenerateTask selectById(Long taskId);
@@ -1,10 +1,10 @@
package com.bms.ar.mapper;
package com.bms.receivable.mapper;
import java.util.List;
import com.bms.ar.security.ArBillRowScope;
import com.bms.receivable.security.ArBillRowScope;
import org.apache.ibatis.annotations.Param;
import com.bms.ar.domain.ArBillLog;
import com.bms.receivable.domain.ArBillLog;
public interface ArBillLogMapper
{
@@ -1,14 +1,14 @@
package com.bms.ar.mapper;
package com.bms.receivable.mapper;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import com.bms.ar.security.ArBillRowScope;
import com.bms.receivable.security.ArBillRowScope;
import org.apache.ibatis.annotations.Param;
import com.bms.ar.domain.ArBill;
import com.bms.ar.domain.vo.ArBillQuery;
import com.bms.ar.domain.vo.ArBillSummary;
import com.bms.receivable.domain.ArBill;
import com.bms.receivable.domain.vo.ArBillQuery;
import com.bms.receivable.domain.vo.ArBillSummary;
public interface ArBillMapper {
List<ArBill> selectArBillList(@Param("query") ArBillQuery query, @Param("scope") ArBillRowScope scope);
@@ -1,8 +1,8 @@
package com.bms.ar.mapper;
package com.bms.receivable.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.ar.security.ArBillRowScope;
import com.bms.receivable.security.ArBillRowScope;
import com.bms.customer.customerinfo.domain.BmsCustomerOption;
public interface ArBillScopeMapper {
@@ -1,12 +1,13 @@
package com.bms.ar.mapper;
package com.bms.receivable.mapper;
import java.util.Date;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.ar.profit.ArCommissionRate;
import com.bms.ar.profit.ArWriteoffRate;
import com.bms.ar.profit.ArCommissionResult;
import com.bms.ar.profit.ArCommissionSummary;
import com.bms.receivable.profit.ArCommissionRate;
import com.bms.receivable.profit.ArWriteoffRate;
import com.bms.receivable.profit.ArCommissionResult;
import com.bms.receivable.profit.ArCommissionSummary;
import com.bms.receivable.profit.ArProfitScope;
public interface ArCommissionMapper
{
@@ -25,6 +26,7 @@ public interface ArCommissionMapper
int countMissingRates(@Param("billId") Long billId);
int confirmMissingRate(@Param("writeoffId") Long writeoffId, @Param("rate") java.math.BigDecimal rate,
@Param("operator") String operator, @Param("remark") String remark);
List<ArCommissionResult> selectResults(@Param("billId") Long billId, @Param("includeReversed") boolean includeReversed);
List<ArCommissionSummary> selectCommissionSummary(@Param("billId") Long billId);
List<ArCommissionResult> selectResults(@Param("billId") Long billId, @Param("includeReversed") boolean includeReversed,
@Param("scope") ArProfitScope scope);
List<ArCommissionSummary> selectCommissionSummary(@Param("billId") Long billId, @Param("scope") ArProfitScope scope);
}
@@ -1,9 +1,9 @@
package com.bms.ar.mapper;
package com.bms.receivable.mapper;
import java.util.Date;
import org.apache.ibatis.annotations.Param;
import com.bms.ar.domain.ArExportTemplate;
import com.bms.receivable.domain.ArExportTemplate;
public interface ArExportTemplateMapper {
/**
@@ -1,14 +1,14 @@
package com.bms.ar.mapper;
package com.bms.receivable.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.ar.profit.ArProfitQuery;
import com.bms.ar.profit.ArProfitRow;
import com.bms.ar.profit.ArProfitScope;
import com.bms.ar.profit.ArProfitSummary;
import com.bms.ar.profit.ArProfitSalesOption;
import com.bms.ar.domain.ArBillDetailFee;
import com.bms.ar.domain.ArBillDetail;
import com.bms.receivable.profit.ArProfitQuery;
import com.bms.receivable.profit.ArProfitRow;
import com.bms.receivable.profit.ArProfitScope;
import com.bms.receivable.profit.ArProfitSummary;
import com.bms.receivable.profit.ArProfitSalesOption;
import com.bms.receivable.domain.ArBillDetailFee;
import com.bms.receivable.domain.ArBillDetail;
public interface ArProfitMapper
{
@@ -1,4 +1,4 @@
package com.bms.ar.profit;
package com.bms.receivable.profit;
import java.math.BigDecimal;
import java.util.Date;
@@ -1,4 +1,4 @@
package com.bms.ar.profit;
package com.bms.receivable.profit;
import java.math.BigDecimal;
import java.util.Date;
@@ -1,17 +1,17 @@
package com.bms.ar.profit;
package com.bms.receivable.profit;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.bms.ar.domain.ArBalanceWriteoff;
import com.bms.ar.domain.ArBill;
import com.bms.ar.mapper.ArCommissionMapper;
import com.bms.ar.mapper.ArBillMapper;
import com.bms.ar.enums.ArBillStatus;
import com.bms.ar.enums.ArBillGenerationStatus;
import com.bms.ar.security.ArBillRoleKeys;
import com.bms.receivable.domain.ArBalanceWriteoff;
import com.bms.receivable.domain.ArBill;
import com.bms.receivable.mapper.ArCommissionMapper;
import com.bms.receivable.mapper.ArBillMapper;
import com.bms.receivable.enums.ArBillStatus;
import com.bms.receivable.enums.ArBillGenerationStatus;
import com.bms.receivable.security.ArBillRoleKeys;
import com.bms.common.exception.ServiceException;
import com.bms.common.core.domain.entity.SysUser;
import com.bms.common.utils.SecurityUtils;
@@ -97,14 +97,17 @@ public class ArCommissionService
public List<ArCommissionResult> results(Long billId, boolean includeReversed)
{
requireFinance();
return mapper.selectResults(billId, includeReversed);
return mapper.selectResults(billId, includeReversed, scopeResolver.resolve());
}
public List<ArCommissionSummary> summary(Long billId)
{
requireFinance();
return mapper.selectCommissionSummary(billId);
return mapper.selectCommissionSummary(billId, scopeResolver.resolve());
}
public List<ArCommissionResult> exportResults(Long billId)
{
return mapper.selectResults(billId, false, requireFinance());
}
public List<ArWriteoffRate> writeoffRates(Long billId)
@@ -141,8 +144,10 @@ public class ArCommissionService
}
}
private void requireFinance()
private ArProfitScope requireFinance()
{
if (!scopeResolver.resolve().all()) throw new ServiceException("仅财务和系统管理员可查看或维护提成");
ArProfitScope scope = scopeResolver.resolve();
if (!scope.all()) throw new ServiceException("仅财务和系统管理员可使用提成管理功能");
return scope;
}
}
@@ -1,4 +1,4 @@
package com.bms.ar.profit;
package com.bms.receivable.profit;
import java.math.BigDecimal;
import lombok.Data;
@@ -1,4 +1,4 @@
package com.bms.ar.profit;
package com.bms.receivable.profit;
import java.util.Date;
import lombok.Data;
@@ -1,4 +1,4 @@
package com.bms.ar.profit;
package com.bms.receivable.profit;
import java.math.BigDecimal;
import java.util.Date;
@@ -11,6 +11,7 @@ public class ArProfitRow
{
private Long arBillId;
@Excel(name = "账单号", width = 20) private String billNo;
@Excel(name = "账单名称", width = 24) private String billName;
@Excel(name = "客户代码", width = 16) private String customerCode;
@Excel(name = "客户名称", width = 24) private String customerName;
private String customerArea;
@@ -1,4 +1,4 @@
package com.bms.ar.profit;
package com.bms.receivable.profit;
import lombok.Data;
@@ -1,4 +1,4 @@
package com.bms.ar.profit;
package com.bms.receivable.profit;
/** 后端构造的数据范围,不接收前端传入的权限参数。 */
public record ArProfitScope(boolean all, boolean self, boolean department, Long userId, Long deptId)
@@ -1,9 +1,9 @@
package com.bms.ar.profit;
package com.bms.receivable.profit;
import java.util.Arrays;
import java.util.Set;
import org.springframework.stereotype.Component;
import com.bms.ar.security.ArBillRoleKeys;
import com.bms.receivable.security.ArBillRoleKeys;
import com.bms.common.constant.Constants;
import com.bms.common.core.domain.entity.SysRole;
import com.bms.common.core.domain.entity.SysUser;
@@ -1,10 +1,10 @@
package com.bms.ar.profit;
package com.bms.receivable.profit;
import java.util.List;
import org.springframework.stereotype.Service;
import com.bms.ar.mapper.ArProfitMapper;
import com.bms.ar.domain.ArBillDetailFee;
import com.bms.ar.domain.ArBillDetail;
import com.bms.receivable.mapper.ArProfitMapper;
import com.bms.receivable.domain.ArBillDetailFee;
import com.bms.receivable.domain.ArBillDetail;
@Service
public class ArProfitService
@@ -1,4 +1,4 @@
package com.bms.ar.profit;
package com.bms.receivable.profit;
import java.math.BigDecimal;
import lombok.Data;
@@ -12,6 +12,7 @@ public class ArProfitSummary
private BigDecimal productAmount;
private BigDecimal salesGrossProfit;
private BigDecimal salesGrossMarginRate;
private BigDecimal commissionAmount;
private BigDecimal writtenOffAmount;
private BigDecimal unwrittenAmount;
}
@@ -1,4 +1,4 @@
package com.bms.ar.profit;
package com.bms.receivable.profit;
import java.math.BigDecimal;
import java.util.Date;
@@ -1,4 +1,4 @@
package com.bms.ar.profit;
package com.bms.receivable.profit;
import java.math.BigDecimal;
import java.math.RoundingMode;
@@ -1,4 +1,4 @@
package com.bms.ar.security;
package com.bms.receivable.security;
import java.util.Set;
@@ -1,4 +1,4 @@
package com.bms.ar.security;
package com.bms.receivable.security;
/** 仅由后端构造,不能绑定到 HTTP 查询对象。 */
public final class ArBillRowScope {

Some files were not shown because too many files have changed in this diff Show More