财务核算模块设计——多币种、多主体的挑战

引言:跨境电商财务的复杂性 如果说采购管理是跨境电商的"命脉",那么财务核算就是企业的"神经中枢"。一个5-7亿规模的跨境电商,每天可能涉及数十种币种、数百笔交易、多个法人主体的资金流转。 然而,很多企业的财务管理还停留在"事后记账"的阶段: 痛点 表现 后果 多币种混乱 手工换算汇率 成本核算不准、毛利失真 多主体割裂 各公司独立记账 合并报表困难、关联交易不清 汇兑损益不清 不知道赚了还是亏了 利润波动大、无法预测 平台对账困难 Amazon/eBay结算复杂 漏收、错收、对不上账 成本分摊粗放 物流费用一刀切 单品毛利算不清 本文目标:设计一套适合跨境电商的财务核算系统,解决多币种、多主体、多渠道的财务管理难题。 一、跨境电商财务的特殊性 1.1 多币种结算的复杂场景 一笔典型的跨境电商交易,可能涉及以下币种: ┌─────────────────────────────────────────────────────────────────┐ │ 一笔订单的币种流转 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 采购端 销售端 结算端 │ │ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │ CNY │ ──采购成本──▶ │ EUR │ ──销售收入──▶ │ USD │ │ │ │ USD │ │ GBP │ │ EUR │ │ │ └─────┘ │ USD │ └─────┘ │ │ └─────┘ │ │ ▲ │ │ │ │ │ │ │ │ │ 国内供应商 Amazon欧洲站 平台结算 │ │ 海外供应商 eBay英国站 到香港账户 │ │ │ └─────────────────────────────────────────────────────────────────┘ 币种转换的三个关键时点: ...

2026-02-04 · maneng

库存成本核算——移动加权平均法实战

引言 在跨境电商业务中,库存成本核算是一个看似简单实则复杂的问题。同一个SKU,不同批次的采购价格可能差异很大——汇率波动、供应商调价、运费变化都会影响入库成本。当这个SKU出库时,应该按什么成本计算? 这个问题直接影响: 毛利计算:成本算错,毛利就错 库存估值:影响资产负债表 定价决策:成本不准,定价就没有依据 税务合规:成本核算方法需要符合会计准则 本文将深入讲解库存成本核算的核心方法,重点介绍移动加权平均法的算法实现、特殊场景处理和月结流程设计。 一、成本核算方法对比 1.1 三种主流方法 方法 原理 优点 缺点 适用场景 先进先出法(FIFO) 先入库的先出库 符合实物流转,成本反映真实 计算复杂,需追踪批次 有保质期的商品 移动加权平均法 每次入库重新计算平均成本 成本平滑,计算相对简单 无法追溯具体批次成本 标准化商品 个别计价法 每个商品单独计价 成本最准确 管理成本高 高价值、可识别商品 1.2 跨境电商的选择 对于年营收5-7亿的跨境电商,移动加权平均法是最佳选择: 选择理由: SKU数量大:通常有数万SKU,FIFO管理成本太高 标准化商品:大部分是标准化产品,无需追溯批次 价格波动频繁:汇率、运费变化大,平均法能平滑波动 财务软件兼容:金蝶、用友等主流财务软件都支持 不适用场景: 有保质期的商品(食品、化妆品)→ 建议FIFO 高价值单品(珠宝、艺术品)→ 建议个别计价 有序列号管理需求的商品 → 建议个别计价 二、移动加权平均算法详解 2.1 核心公式 新单位成本 = (原库存金额 + 本次入库金额) / (原库存数量 + 本次入库数量) 示例: 原库存:100件,单位成本10元,库存金额1000元 本次入库:50件,单位成本12元,入库金额600元 新单位成本 = (1000 + 600) / (100 + 50) = 10.67元 2.2 数据模型设计 -- 库存成本主表 CREATE TABLE inventory_cost ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_code VARCHAR(50) NOT NULL COMMENT 'SKU编码', warehouse_code VARCHAR(50) NOT NULL COMMENT '仓库编码', quantity DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '当前库存数量', unit_cost DECIMAL(18,6) NOT NULL DEFAULT 0 COMMENT '单位成本(6位小数)', total_cost DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '库存总成本', last_in_cost DECIMAL(18,6) COMMENT '最近入库成本', last_in_time DATETIME COMMENT '最近入库时间', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_sku_warehouse (sku_code, warehouse_code) ) COMMENT '库存成本主表'; -- 成本变动流水表 CREATE TABLE inventory_cost_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_code VARCHAR(50) NOT NULL COMMENT 'SKU编码', warehouse_code VARCHAR(50) NOT NULL COMMENT '仓库编码', biz_type VARCHAR(30) NOT NULL COMMENT '业务类型:PURCHASE_IN/SALE_OUT/RETURN_IN/TRANSFER/ADJUST', biz_no VARCHAR(50) NOT NULL COMMENT '业务单号', direction TINYINT NOT NULL COMMENT '方向:1入库,-1出库', quantity DECIMAL(18,4) NOT NULL COMMENT '变动数量', unit_cost DECIMAL(18,6) NOT NULL COMMENT '本次单位成本', amount DECIMAL(18,4) NOT NULL COMMENT '本次金额', before_quantity DECIMAL(18,4) NOT NULL COMMENT '变动前数量', before_unit_cost DECIMAL(18,6) NOT NULL COMMENT '变动前单位成本', before_total_cost DECIMAL(18,4) NOT NULL COMMENT '变动前总成本', after_quantity DECIMAL(18,4) NOT NULL COMMENT '变动后数量', after_unit_cost DECIMAL(18,6) NOT NULL COMMENT '变动后单位成本', after_total_cost DECIMAL(18,4) NOT NULL COMMENT '变动后总成本', created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_sku_warehouse (sku_code, warehouse_code), INDEX idx_biz_no (biz_no), INDEX idx_created_time (created_time) ) COMMENT '成本变动流水表'; 2.3 核心算法实现 /** * 库存成本服务 * 实现移动加权平均法 */ @Service @Slf4j public class InventoryCostService { @Autowired private InventoryCostMapper costMapper; @Autowired private InventoryCostLogMapper logMapper; /** * 入库成本计算 * 核心公式:新单位成本 = (原库存金额 + 本次入库金额) / (原库存数量 + 本次入库数量) */ @Transactional(rollbackFor = Exception.class) public void processInbound(CostChangeRequest request) { String skuCode = request.getSkuCode(); String warehouseCode = request.getWarehouseCode(); BigDecimal inQuantity = request.getQuantity(); BigDecimal inUnitCost = request.getUnitCost(); // 1. 获取当前库存成本(加锁) InventoryCost cost = costMapper.selectForUpdate(skuCode, warehouseCode); // 2. 记录变动前状态 CostSnapshot before = createSnapshot(cost); // 3. 计算新成本 BigDecimal beforeQuantity = cost != null ? cost.getQuantity() : BigDecimal.ZERO; BigDecimal beforeTotalCost = cost != null ? cost.getTotalCost() : BigDecimal.ZERO; BigDecimal inAmount = inQuantity.multiply(inUnitCost); BigDecimal afterQuantity = beforeQuantity.add(inQuantity); BigDecimal afterTotalCost = beforeTotalCost.add(inAmount); // 移动加权平均计算 BigDecimal afterUnitCost; if (afterQuantity.compareTo(BigDecimal.ZERO) > 0) { afterUnitCost = afterTotalCost.divide(afterQuantity, 6, RoundingMode.HALF_UP); } else { afterUnitCost = inUnitCost; // 库存为0时,使用入库成本 } // 4. 更新库存成本 if (cost == null) { cost = new InventoryCost(); cost.setSkuCode(skuCode); cost.setWarehouseCode(warehouseCode); cost.setQuantity(afterQuantity); cost.setUnitCost(afterUnitCost); cost.setTotalCost(afterTotalCost); cost.setLastInCost(inUnitCost); cost.setLastInTime(new Date()); costMapper.insert(cost); } else { cost.setQuantity(afterQuantity); cost.setUnitCost(afterUnitCost); cost.setTotalCost(afterTotalCost); cost.setLastInCost(inUnitCost); cost.setLastInTime(new Date()); int rows = costMapper.updateWithVersion(cost); if (rows == 0) { throw new ConcurrentModificationException("库存成本更新冲突,请重试"); } } // 5. 记录成本流水 CostSnapshot after = createSnapshot(cost); saveCostLog(request, before, after, 1); log.info("入库成本计算完成: sku={}, warehouse={}, 入库数量={}, 入库成本={}, " + "新单位成本={}, 新库存数量={}", skuCode, warehouseCode, inQuantity, inUnitCost, afterUnitCost, afterQuantity); } /** * 出库成本计算 * 出库时按当前单位成本计算 */ @Transactional(rollbackFor = Exception.class) public BigDecimal processOutbound(CostChangeRequest request) { String skuCode = request.getSkuCode(); String warehouseCode = request.getWarehouseCode(); BigDecimal outQuantity = request.getQuantity(); // 1. 获取当前库存成本(加锁) InventoryCost cost = costMapper.selectForUpdate(skuCode, warehouseCode); if (cost == null || cost.getQuantity().compareTo(outQuantity) < 0) { throw new BusinessException("库存不足,无法出库"); } // 2. 记录变动前状态 CostSnapshot before = createSnapshot(cost); // 3. 计算出库金额(使用当前单位成本) BigDecimal outUnitCost = cost.getUnitCost(); BigDecimal outAmount = outQuantity.multiply(outUnitCost); // 4. 更新库存 BigDecimal afterQuantity = cost.getQuantity().subtract(outQuantity); BigDecimal afterTotalCost = cost.getTotalCost().subtract(outAmount); // 单位成本保持不变(出库不改变单位成本) cost.setQuantity(afterQuantity); cost.setTotalCost(afterTotalCost); int rows = costMapper.updateWithVersion(cost); if (rows == 0) { throw new ConcurrentModificationException("库存成本更新冲突,请重试"); } // 5. 记录成本流水 CostSnapshot after = createSnapshot(cost); request.setUnitCost(outUnitCost); // 设置出库成本 saveCostLog(request, before, after, -1); log.info("出库成本计算完成: sku={}, warehouse={}, 出库数量={}, 出库成本={}, " + "出库金额={}", skuCode, warehouseCode, outQuantity, outUnitCost, outAmount); return outAmount; // 返回出库金额,用于计算销售成本 } /** * 创建成本快照 */ private CostSnapshot createSnapshot(InventoryCost cost) { CostSnapshot snapshot = new CostSnapshot(); if (cost != null) { snapshot.setQuantity(cost.getQuantity()); snapshot.setUnitCost(cost.getUnitCost()); snapshot.setTotalCost(cost.getTotalCost()); } else { snapshot.setQuantity(BigDecimal.ZERO); snapshot.setUnitCost(BigDecimal.ZERO); snapshot.setTotalCost(BigDecimal.ZERO); } return snapshot; } /** * 保存成本流水 */ private void saveCostLog(CostChangeRequest request, CostSnapshot before, CostSnapshot after, int direction) { InventoryCostLog log = new InventoryCostLog(); log.setSkuCode(request.getSkuCode()); log.setWarehouseCode(request.getWarehouseCode()); log.setBizType(request.getBizType()); log.setBizNo(request.getBizNo()); log.setDirection(direction); log.setQuantity(request.getQuantity()); log.setUnitCost(request.getUnitCost()); log.setAmount(request.getQuantity().multiply(request.getUnitCost())); log.setBeforeQuantity(before.getQuantity()); log.setBeforeUnitCost(before.getUnitCost()); log.setBeforeTotalCost(before.getTotalCost()); log.setAfterQuantity(after.getQuantity()); log.setAfterUnitCost(after.getUnitCost()); log.setAfterTotalCost(after.getTotalCost()); logMapper.insert(log); } } 2.4 并发控制 库存成本计算必须保证并发安全,否则会导致成本计算错误。 ...

2026-02-04 · maneng

采购管理系统设计——从需求到入库的全流程

引言:为什么采购管理是跨境电商的命脉 在跨境电商的成本结构中,商品采购成本通常占到销售额的40%-60%。采购管理的好坏,直接决定了企业的毛利率和现金流健康度。 然而,很多跨境电商企业的采购管理还停留在"Excel+微信"的原始阶段: 痛点 表现 后果 信息孤岛 采购数据散落在各处 无法追溯、难以分析 流程混乱 没有标准化审批流程 越权采购、价格失控 库存失控 凭感觉补货 要么断货、要么积压 供应商管理缺失 没有评级和淘汰机制 质量不稳定、交期延误 成本核算困难 多币种、多批次混乱 毛利算不清 本文目标:设计一套适合5-7亿规模跨境电商的采购管理系统,从需求计划到入库结算,实现全流程数字化管理。 一、跨境电商采购的特殊性 1.1 与传统采购的核心差异 维度 传统企业采购 跨境电商采购 供应商分布 国内为主 国内+海外(1688、阿里国际站、海外工厂) 结算币种 单一(CNY) 多币种(CNY、USD、EUR等) 付款方式 银行转账、承兑 T/T、信用证、PayPal、西联 采购周期 相对固定 差异大(国内7天 vs 海运45天) 质检要求 标准化 需适应目的国标准(CE、FCC、FDA等) 单据要求 国内发票 形式发票、装箱单、原产地证等 1.2 多币种采购的挑战 跨境电商经常面临这样的场景: 场景:从美国供应商采购一批电子配件 - 报价币种:USD - 付款币种:USD(通过香港公司付款) - 入库成本:需转换为CNY(国内公司记账) - 销售币种:EUR(欧洲站销售) 汇率波动的影响: // 假设采购时汇率 1 USD = 7.2 CNY // 采购成本:$100 × 7.2 = ¥720 // 一个月后付款时汇率 1 USD = 7.0 CNY // 实际付款:$100 × 7.0 = ¥700 // 汇兑收益:¥20 // 但如果汇率变成 1 USD = 7.4 CNY // 实际付款:$100 × 7.4 = ¥740 // 汇兑损失:¥20 1.3 海外供应商管理难点 时差问题: ...

2026-02-04 · maneng

API设计最佳实践——RESTful接口规范

引言:API是系统的门面 在多系统集成的架构中,API是系统之间沟通的语言。一个设计良好的API: 让调用方一看就懂,减少沟通成本 让维护方易于扩展,降低变更风险 让系统更加稳定,提升整体可靠性 糟糕的API设计会带来什么问题? 接口命名混乱,调用方需要反复确认 响应格式不统一,每个接口都要单独处理 版本管理缺失,升级时牵一发动全身 安全机制薄弱,容易被攻击或滥用 本文将系统性地介绍API设计的最佳实践,帮你建立企业级的API规范。 一、RESTful设计原则 1.1 什么是RESTful REST(Representational State Transfer)是一种架构风格,核心思想是: 资源导向:一切皆资源,用URL标识 统一接口:用HTTP方法表示操作 无状态:每次请求包含所有必要信息 1.2 资源命名规范 规则1:使用名词复数 ✅ 正确 GET /api/v1/orders # 订单列表 GET /api/v1/products # 商品列表 GET /api/v1/warehouses # 仓库列表 ❌ 错误 GET /api/v1/getOrders # 动词命名 GET /api/v1/order # 单数形式 GET /api/v1/orderList # 冗余后缀 规则2:使用小写字母和连字符 ✅ 正确 GET /api/v1/order-items GET /api/v1/shipping-addresses GET /api/v1/purchase-orders ❌ 错误 GET /api/v1/orderItems # 驼峰命名 GET /api/v1/order_items # 下划线 GET /api/v1/OrderItems # 大写字母 规则3:层级关系用路径表示 ...

2026-02-04 · maneng

履约路由与调度——智能选仓选物流

引言:履约路由的价值 履约路由:决定订单从哪个仓库发货、用哪个物流承运商。 好的履约路由能带来: 降低物流成本(选择最优物流) 提升时效(就近发货) 提高客户满意度 均衡仓库负载 差的履约路由会导致: 物流成本高 时效差 某些仓库爆仓,某些仓库闲置 一、履约路由架构 1.1 路由流程 订单 ──> 库存检查 ──> 选仓 ──> 选物流 ──> 生成履约单 ──> 下发WMS │ │ │ │ │ │ ▼ ▼ ▼ 有货仓库 最优仓库 最优物流 1.2 路由因素 因素 说明 权重 库存 仓库是否有货 必要条件 距离 仓库到收货地的距离 影响时效 成本 物流费用 影响利润 时效 物流时效要求 影响体验 仓库负载 仓库当前处理能力 影响发货速度 承运商能力 承运商覆盖范围、服务质量 影响可选项 二、选仓策略 2.1 策略类型 策略 说明 适用场景 就近发货 选择离收货地最近的仓库 时效优先 成本最优 选择物流成本最低的仓库 成本优先 库存均衡 优先选择库存多的仓库 均衡库存 负载均衡 优先选择负载低的仓库 均衡产能 综合评分 多因素加权评分 综合考虑 2.2 就近发货策略 @Component public class NearestWarehouseStrategy implements WarehouseStrategy { @Override public String getStrategyType() { return "NEAREST"; } @Override public String selectWarehouse(Order order, List<WarehouseInventory> candidates) { Address destination = order.getShippingAddress(); return candidates.stream() .filter(w -> w.getAvailableQty() >= getRequiredQty(order, w.getSkuId())) .min(Comparator.comparingDouble(w -> calculateDistance(w.getWarehouseId(), destination))) .map(WarehouseInventory::getWarehouseId) .orElseThrow(() -> new NoAvailableWarehouseException(order.getOrderId())); } /** * 计算仓库到目的地的距离 * 简化版:使用经纬度计算直线距离 */ private double calculateDistance(String warehouseId, Address destination) { Warehouse warehouse = warehouseService.getWarehouse(warehouseId); double lat1 = warehouse.getLatitude(); double lon1 = warehouse.getLongitude(); double lat2 = getLatitude(destination); double lon2 = getLongitude(destination); // Haversine公式计算球面距离 double R = 6371; // 地球半径(公里) double dLat = Math.toRadians(lat2 - lat1); double dLon = Math.toRadians(lon2 - lon1); double a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * Math.sin(dLon/2) * Math.sin(dLon/2); double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); return R * c; } } 2.3 成本最优策略 @Component public class CostOptimalWarehouseStrategy implements WarehouseStrategy { @Override public String getStrategyType() { return "COST_OPTIMAL"; } @Override public String selectWarehouse(Order order, List<WarehouseInventory> candidates) { Address destination = order.getShippingAddress(); double totalWeight = calculateTotalWeight(order); return candidates.stream() .filter(w -> w.getAvailableQty() >= getRequiredQty(order, w.getSkuId())) .min(Comparator.comparingDouble(w -> calculateShippingCost(w.getWarehouseId(), destination, totalWeight))) .map(WarehouseInventory::getWarehouseId) .orElseThrow(() -> new NoAvailableWarehouseException(order.getOrderId())); } /** * 计算物流成本 */ private double calculateShippingCost(String warehouseId, Address destination, double weight) { // 获取该仓库可用的物流商 List<Carrier> carriers = carrierService.getCarriersForWarehouse(warehouseId); // 计算每个物流商的费用,取最低 return carriers.stream() .filter(c -> c.canDeliver(destination)) .mapToDouble(c -> c.calculateFee(warehouseId, destination, weight)) .min() .orElse(Double.MAX_VALUE); } } 2.4 综合评分策略(推荐) @Component public class ScoringWarehouseStrategy implements WarehouseStrategy { // 权重配置 private static final double DISTANCE_WEIGHT = 0.3; private static final double COST_WEIGHT = 0.4; private static final double INVENTORY_WEIGHT = 0.2; private static final double LOAD_WEIGHT = 0.1; @Override public String getStrategyType() { return "SCORING"; } @Override public String selectWarehouse(Order order, List<WarehouseInventory> candidates) { Address destination = order.getShippingAddress(); // 过滤有库存的仓库 List<WarehouseInventory> available = candidates.stream() .filter(w -> w.getAvailableQty() >= getRequiredQty(order, w.getSkuId())) .collect(Collectors.toList()); if (available.isEmpty()) { throw new NoAvailableWarehouseException(order.getOrderId()); } // 计算各维度的归一化值 double maxDistance = available.stream() .mapToDouble(w -> calculateDistance(w.getWarehouseId(), destination)) .max().orElse(1); double maxCost = available.stream() .mapToDouble(w -> calculateShippingCost(w.getWarehouseId(), destination, order)) .max().orElse(1); double maxInventory = available.stream() .mapToDouble(WarehouseInventory::getAvailableQty) .max().orElse(1); double maxLoad = available.stream() .mapToDouble(w -> getWarehouseLoad(w.getWarehouseId())) .max().orElse(1); // 计算综合得分 return available.stream() .max(Comparator.comparingDouble(w -> { double distanceScore = 1 - calculateDistance(w.getWarehouseId(), destination) / maxDistance; double costScore = 1 - calculateShippingCost(w.getWarehouseId(), destination, order) / maxCost; double inventoryScore = w.getAvailableQty() / maxInventory; double loadScore = 1 - getWarehouseLoad(w.getWarehouseId()) / maxLoad; return distanceScore * DISTANCE_WEIGHT + costScore * COST_WEIGHT + inventoryScore * INVENTORY_WEIGHT + loadScore * LOAD_WEIGHT; })) .map(WarehouseInventory::getWarehouseId) .orElseThrow(); } } 三、选物流策略 3.1 物流选择因素 因素 说明 覆盖范围 物流商是否能送达目的地 时效 预计送达时间 费用 物流费用 服务质量 丢包率、破损率、投诉率 追踪能力 是否支持物流追踪 3.2 物流选择实现 @Service public class CarrierSelectionService { /** * 选择最优物流 */ public Carrier selectCarrier(String warehouseId, Address destination, double weight, String serviceLevel) { // 1. 获取仓库可用的物流商 List<Carrier> carriers = carrierService.getCarriersForWarehouse(warehouseId); // 2. 过滤能送达的物流商 List<Carrier> available = carriers.stream() .filter(c -> c.canDeliver(destination)) .filter(c -> c.getMaxWeight() >= weight) .collect(Collectors.toList()); if (available.isEmpty()) { throw new NoAvailableCarrierException(warehouseId, destination); } // 3. 根据服务等级选择 switch (serviceLevel) { case "EXPRESS": // 时效优先 return selectByTimeEfficiency(available, destination); case "ECONOMY": // 成本优先 return selectByCost(available, warehouseId, destination, weight); default: // 综合评分 return selectByScore(available, warehouseId, destination, weight); } } /** * 时效优先选择 */ private Carrier selectByTimeEfficiency(List<Carrier> carriers, Address destination) { return carriers.stream() .min(Comparator.comparingInt(c -> c.getEstimatedDays(destination))) .orElseThrow(); } /** * 成本优先选择 */ private Carrier selectByCost(List<Carrier> carriers, String warehouseId, Address destination, double weight) { return carriers.stream() .min(Comparator.comparingDouble(c -> c.calculateFee(warehouseId, destination, weight))) .orElseThrow(); } /** * 综合评分选择 */ private Carrier selectByScore(List<Carrier> carriers, String warehouseId, Address destination, double weight) { return carriers.stream() .max(Comparator.comparingDouble(c -> { double costScore = 100 - c.calculateFee(warehouseId, destination, weight); double timeScore = 100 - c.getEstimatedDays(destination) * 10; double qualityScore = c.getQualityScore(); // 0-100 return costScore * 0.4 + timeScore * 0.3 + qualityScore * 0.3; })) .orElseThrow(); } } 四、规则引擎 4.1 规则配置 -- 路由规则表 CREATE TABLE t_routing_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, rule_name VARCHAR(64) NOT NULL COMMENT '规则名称', rule_type VARCHAR(32) NOT NULL COMMENT '类型:WAREHOUSE/CARRIER', priority INT DEFAULT 0 COMMENT '优先级(越大越优先)', -- 条件 condition_channel VARCHAR(32) COMMENT '渠道条件', condition_country VARCHAR(32) COMMENT '国家条件', condition_sku_category VARCHAR(32) COMMENT 'SKU分类条件', condition_weight_min DECIMAL(10,2) COMMENT '最小重量', condition_weight_max DECIMAL(10,2) COMMENT '最大重量', condition_amount_min DECIMAL(12,2) COMMENT '最小金额', -- 动作 action_warehouse_id VARCHAR(32) COMMENT '指定仓库', action_carrier_id VARCHAR(32) COMMENT '指定物流', action_strategy VARCHAR(32) COMMENT '选择策略', enabled TINYINT DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_type_priority (rule_type, priority DESC) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='路由规则表'; -- 示例规则 INSERT INTO t_routing_rule (rule_name, rule_type, priority, condition_country, action_warehouse_id) VALUES ('美国订单走美西仓', 'WAREHOUSE', 100, 'US', 'WH_US_WEST'), ('欧洲订单走德国仓', 'WAREHOUSE', 100, 'DE,FR,IT,ES', 'WH_DE'); INSERT INTO t_routing_rule (rule_name, rule_type, priority, condition_channel, action_carrier_id) VALUES ('Amazon订单用UPS', 'CARRIER', 100, 'AMAZON', 'UPS'), ('eBay订单用USPS', 'CARRIER', 90, 'EBAY', 'USPS'); 4.2 规则引擎实现 @Service public class RoutingRuleEngine { @Autowired private RoutingRuleRepository ruleRepository; /** * 匹配仓库规则 */ public String matchWarehouseRule(Order order) { List<RoutingRule> rules = ruleRepository.findByTypeAndEnabled("WAREHOUSE", true); // 按优先级排序 rules.sort(Comparator.comparingInt(RoutingRule::getPriority).reversed()); for (RoutingRule rule : rules) { if (matchCondition(rule, order)) { if (rule.getActionWarehouseId() != null) { return rule.getActionWarehouseId(); } if (rule.getActionStrategy() != null) { return rule.getActionStrategy(); // 返回策略名称 } } } return "SCORING"; // 默认使用评分策略 } /** * 匹配物流规则 */ public String matchCarrierRule(Order order, String warehouseId) { List<RoutingRule> rules = ruleRepository.findByTypeAndEnabled("CARRIER", true); rules.sort(Comparator.comparingInt(RoutingRule::getPriority).reversed()); for (RoutingRule rule : rules) { if (matchCondition(rule, order)) { if (rule.getActionCarrierId() != null) { return rule.getActionCarrierId(); } } } return null; // 无匹配规则,使用默认策略 } /** * 条件匹配 */ private boolean matchCondition(RoutingRule rule, Order order) { // 渠道匹配 if (rule.getConditionChannel() != null && !rule.getConditionChannel().contains(order.getChannel())) { return false; } // 国家匹配 if (rule.getConditionCountry() != null) { String country = order.getShippingAddress().getCountryCode(); if (!rule.getConditionCountry().contains(country)) { return false; } } // 重量匹配 double weight = calculateWeight(order); if (rule.getConditionWeightMin() != null && weight < rule.getConditionWeightMin()) { return false; } if (rule.getConditionWeightMax() != null && weight > rule.getConditionWeightMax()) { return false; } // 金额匹配 if (rule.getConditionAmountMin() != null && order.getTotalAmount().compareTo(rule.getConditionAmountMin()) < 0) { return false; } return true; } } 五、履约调度服务 5.1 完整调度流程 @Service @Slf4j public class FulfillmentRoutingService { @Autowired private RoutingRuleEngine ruleEngine; @Autowired private Map<String, WarehouseStrategy> warehouseStrategies; @Autowired private CarrierSelectionService carrierSelectionService; /** * 订单履约路由 */ @Transactional public FulfillmentOrder route(Order order) { log.info("开始履约路由: orderId={}", order.getOrderId()); // 1. 匹配仓库规则 String warehouseResult = ruleEngine.matchWarehouseRule(order); String warehouseId; if (isWarehouseId(warehouseResult)) { // 规则指定了具体仓库 warehouseId = warehouseResult; log.info("规则指定仓库: {}", warehouseId); } else { // 规则指定了策略,执行策略选仓 WarehouseStrategy strategy = warehouseStrategies.get(warehouseResult); List<WarehouseInventory> candidates = getWarehouseCandidates(order); warehouseId = strategy.selectWarehouse(order, candidates); log.info("策略选仓: strategy={}, warehouse={}", warehouseResult, warehouseId); } // 2. 匹配物流规则 String carrierId = ruleEngine.matchCarrierRule(order, warehouseId); if (carrierId == null) { // 无规则匹配,使用策略选物流 Carrier carrier = carrierSelectionService.selectCarrier( warehouseId, order.getShippingAddress(), calculateWeight(order), order.getServiceLevel() ); carrierId = carrier.getCarrierId(); } log.info("选择物流: {}", carrierId); // 3. 创建履约订单 FulfillmentOrder fulfillmentOrder = new FulfillmentOrder(); fulfillmentOrder.setFulfillmentOrderId(generateFulfillmentOrderId()); fulfillmentOrder.setSourceOrderId(order.getOrderId()); fulfillmentOrder.setWarehouseId(warehouseId); fulfillmentOrder.setCarrierId(carrierId); fulfillmentOrder.setShippingAddress(order.getShippingAddress()); fulfillmentOrder.setItems(convertItems(order.getItems())); fulfillmentOrder.setStatus(FulfillmentStatus.PENDING); fulfillmentOrderRepository.save(fulfillmentOrder); log.info("履约路由完成: orderId={}, fulfillmentOrderId={}, warehouse={}, carrier={}", order.getOrderId(), fulfillmentOrder.getFulfillmentOrderId(), warehouseId, carrierId); return fulfillmentOrder; } } 六、总结 6.1 核心要点 选仓策略:就近、成本最优、综合评分 选物流策略:时效优先、成本优先、综合评分 规则引擎:支持灵活配置路由规则 评分模型:多因素加权,可调整权重 6.2 实施建议 先实现基础的就近发货策略 再实现规则引擎,支持特殊场景 最后实现综合评分策略 持续优化权重参数 系列文章导航 ...

2026-01-29 · maneng

库存预占与释放——防止超卖的核心机制

引言:超卖的代价 超卖:卖出的数量超过实际库存,导致无法发货。 超卖的后果: 客户投诉、差评 平台处罚(Amazon可能封店) 紧急采购成本高 品牌信誉受损 防止超卖的核心:库存预占机制。 一、库存模型设计 1.1 库存类型 ┌─────────────────────────────────────────────────────┐ │ 库存类型 │ ├─────────────────────────────────────────────────────┤ │ 实物库存 = WMS系统中的实际库存数量 │ │ │ │ 可售库存 = 实物库存 - 预占库存 - 锁定库存 │ │ │ │ 预占库存 = 订单已预占但未发货的库存 │ │ │ │ 锁定库存 = 因其他原因锁定的库存(盘点、质量问题等) │ │ │ │ 在途库存 = 采购已下单但未入库的库存 │ └─────────────────────────────────────────────────────┘ 1.2 库存计算公式 可售库存 = 实物库存 - 预占库存 - 锁定库存 其中: - 实物库存:从WMS同步 - 预占库存:OMS计算(订单预占) - 锁定库存:手动锁定或系统锁定 1.3 数据模型 -- SKU库存汇总表(OMS) CREATE TABLE t_sku_inventory ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_id VARCHAR(32) NOT NULL COMMENT 'SKU编码', warehouse_id VARCHAR(32) NOT NULL COMMENT '仓库ID', physical_qty INT NOT NULL DEFAULT 0 COMMENT '实物库存(从WMS同步)', reserved_qty INT NOT NULL DEFAULT 0 COMMENT '预占库存', locked_qty INT NOT NULL DEFAULT 0 COMMENT '锁定库存', available_qty INT NOT NULL DEFAULT 0 COMMENT '可售库存(计算字段)', in_transit_qty INT NOT NULL DEFAULT 0 COMMENT '在途库存', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本', updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_sku_warehouse (sku_id, warehouse_id), KEY idx_sku_id (sku_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='SKU库存汇总表'; -- 库存预占明细表 CREATE TABLE t_inventory_reservation ( id BIGINT PRIMARY KEY AUTO_INCREMENT, reservation_id VARCHAR(32) NOT NULL COMMENT '预占ID', order_id VARCHAR(32) NOT NULL COMMENT '订单号', sku_id VARCHAR(32) NOT NULL COMMENT 'SKU编码', warehouse_id VARCHAR(32) NOT NULL COMMENT '仓库ID', reserved_qty INT NOT NULL COMMENT '预占数量', status VARCHAR(16) NOT NULL DEFAULT 'RESERVED' COMMENT '状态:RESERVED/RELEASED/DEDUCTED', reserved_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '预占时间', released_at DATETIME COMMENT '释放时间', UNIQUE KEY uk_reservation_id (reservation_id), KEY idx_order_id (order_id), KEY idx_sku_warehouse (sku_id, warehouse_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存预占明细表'; 二、预占流程设计 2.1 预占时机 下单 ──> 支付 ──> 审核 ──> 拆单 ──> 下发WMS ──> 发货 │ │ └──────── 预占库存 ─────────────────>│ │ 扣减库存 两种预占策略: ...

2026-01-29 · maneng

数据治理基础——打通数据孤岛的关键

引言:数据孤岛的代价 数字化转型后,企业有了多个系统,但如果数据不通: 同一个SKU,各系统编码不一样 同一个客户,各系统信息不一致 想做数据分析,要从多个系统导出再合并 数据治理的目标:建立统一的数据标准,让数据在各系统间自由流转。 一、主数据管理(MDM) 1.1 什么是主数据 主数据(Master Data):企业核心业务实体的基础数据,具有: 跨系统共享 相对稳定 唯一标识 跨境电商的核心主数据: 主数据 说明 使用系统 商品主数据 SKU信息 OMS、WMS、ERP 供应商主数据 供应商信息 ERP、WMS 客户主数据 买家信息 OMS、CRM 仓库主数据 仓库、库位信息 WMS、OMS 承运商主数据 物流商信息 TMS、OMS 1.2 商品主数据设计 -- 商品主数据表 CREATE TABLE t_sku_master ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_id VARCHAR(32) NOT NULL COMMENT 'SKU编码(唯一)', sku_name VARCHAR(256) NOT NULL COMMENT 'SKU名称', sku_name_en VARCHAR(256) COMMENT '英文名称', -- 分类信息 category_id VARCHAR(32) COMMENT '分类ID', category_name VARCHAR(128) COMMENT '分类名称', brand VARCHAR(64) COMMENT '品牌', -- 规格信息 spec VARCHAR(256) COMMENT '规格描述', color VARCHAR(32) COMMENT '颜色', size VARCHAR(32) COMMENT '尺寸', -- 物理属性 weight DECIMAL(10,2) COMMENT '重量(g)', length DECIMAL(10,2) COMMENT '长(cm)', width DECIMAL(10,2) COMMENT '宽(cm)', height DECIMAL(10,2) COMMENT '高(cm)', -- 条码 barcode VARCHAR(64) COMMENT '商品条码', upc VARCHAR(32) COMMENT 'UPC码', ean VARCHAR(32) COMMENT 'EAN码', -- 采购信息 default_supplier_id VARCHAR(32) COMMENT '默认供应商', purchase_price DECIMAL(12,4) COMMENT '采购价', moq INT COMMENT '最小起订量', -- 销售信息 retail_price DECIMAL(12,2) COMMENT '零售价', currency VARCHAR(8) DEFAULT 'USD' COMMENT '币种', -- 库存参数 safety_stock INT DEFAULT 0 COMMENT '安全库存', lead_time_days INT DEFAULT 7 COMMENT '采购周期(天)', -- 状态 status VARCHAR(16) DEFAULT 'ACTIVE' COMMENT '状态', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_sku_id (sku_id), KEY idx_barcode (barcode), KEY idx_category (category_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品主数据表'; -- 渠道SKU映射表 CREATE TABLE t_sku_channel_mapping ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_id VARCHAR(32) NOT NULL COMMENT '内部SKU编码', channel VARCHAR(32) NOT NULL COMMENT '渠道', channel_sku_id VARCHAR(64) NOT NULL COMMENT '渠道SKU编码', channel_sku_name VARCHAR(256) COMMENT '渠道SKU名称', asin VARCHAR(32) COMMENT 'Amazon ASIN', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_channel_sku (channel, channel_sku_id), KEY idx_sku_id (sku_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='渠道SKU映射表'; 1.3 供应商主数据设计 -- 供应商主数据表 CREATE TABLE t_supplier_master ( id BIGINT PRIMARY KEY AUTO_INCREMENT, supplier_id VARCHAR(32) NOT NULL COMMENT '供应商编码', supplier_name VARCHAR(128) NOT NULL COMMENT '供应商名称', supplier_type VARCHAR(32) COMMENT '类型:MANUFACTURER/TRADER', -- 联系信息 contact_name VARCHAR(64) COMMENT '联系人', contact_phone VARCHAR(32) COMMENT '联系电话', contact_email VARCHAR(128) COMMENT '邮箱', address VARCHAR(256) COMMENT '地址', -- 结算信息 payment_terms VARCHAR(64) COMMENT '账期:COD/NET30/NET60', bank_name VARCHAR(128) COMMENT '开户行', bank_account VARCHAR(64) COMMENT '银行账号', tax_id VARCHAR(32) COMMENT '税号', -- 评级 rating VARCHAR(8) DEFAULT 'B' COMMENT '评级:A/B/C/D', cooperation_years INT DEFAULT 0 COMMENT '合作年限', -- 状态 status VARCHAR(16) DEFAULT 'ACTIVE', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_supplier_id (supplier_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商主数据表'; 1.4 主数据管理服务 @Service public class MasterDataService { /** * 创建SKU主数据 */ @Transactional public SkuMaster createSku(SkuMasterRequest request) { // 1. 生成SKU编码 String skuId = generateSkuId(request.getCategoryId()); // 2. 校验唯一性 if (skuMasterRepository.existsBySkuId(skuId)) { throw new DuplicateSkuException(skuId); } // 3. 创建主数据 SkuMaster sku = new SkuMaster(); BeanUtils.copyProperties(request, sku); sku.setSkuId(skuId); sku.setStatus("ACTIVE"); skuMasterRepository.save(sku); // 4. 同步到各系统 syncToDownstreamSystems(sku); return sku; } /** * 同步到下游系统 */ private void syncToDownstreamSystems(SkuMaster sku) { // 发布主数据变更事件 MasterDataChangeEvent event = new MasterDataChangeEvent(); event.setDataType("SKU"); event.setDataId(sku.getSkuId()); event.setChangeType("CREATE"); event.setData(sku); eventPublisher.publish("master-data-change", event); } /** * 生成SKU编码 * 规则:分类前缀 + 6位序号 * 示例:EL000001(电子类) */ private String generateSkuId(String categoryId) { String prefix = getCategoryPrefix(categoryId); int seq = sequenceService.nextVal("SKU_SEQ"); return prefix + String.format("%06d", seq); } } 二、数据标准化 2.1 编码规范 SKU编码规范: ...

2026-01-29 · maneng

ERP核心模块设计——财务、采购、库存三位一体

引言:跨境电商ERP的特殊性 传统ERP(如SAP、Oracle)是为制造业设计的,而跨境电商有其特殊性: 维度 传统ERP 跨境电商ERP 销售渠道 单一/少量 多渠道(Amazon、eBay等) 币种 单一 多币种 仓库 集中 分布式(国内+海外) 订单量 少量大单 大量小单 库存管理 批次管理 SKU+批次+效期 因此,跨境电商需要一套适合自己的ERP设计。 一、ERP在系统矩阵中的定位 1.1 系统边界 ┌─────────────────────────────────────────────────────┐ │ OMS │ │ 订单管理(销售端) │ └─────────────────────┬───────────────────────────────┘ │ 销售数据 ▼ ┌─────────────────────────────────────────────────────┐ │ ERP │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ 财务模块 │ │ 采购模块 │ │ 库存模块 │ │ │ └─────────┘ └─────────┘ └─────────┘ │ └─────────────────────┬───────────────────────────────┘ │ 采购/入库指令 ▼ ┌─────────────────────────────────────────────────────┐ │ WMS │ │ 仓储执行(实物管理) │ └─────────────────────────────────────────────────────┘ 1.2 职责划分 功能 OMS ERP WMS 销售订单 ✓ 可售库存 ✓ 财务核算 ✓ 采购管理 ✓ 账面库存 ✓ 实物库存 ✓ 库位管理 ✓ 二、财务模块设计 2.1 跨境电商财务特点 多币种挑战: ...

2026-01-29 · maneng

订单拆分与合并策略——复杂场景的处理逻辑

引言:为什么需要拆单和合单 拆单场景: 一个订单的商品分布在不同仓库,需要从多个仓库发货 部分商品有货,部分商品需要预售 订单重量超过物流限制,需要拆成多个包裹 合单场景: 同一客户短时间内下了多个订单,可以合并发货节省运费 促销活动中的凑单场景 这些逻辑看似简单,但实现起来非常复杂,涉及库存、物流、财务等多个方面。 一、订单模型设计 1.1 三层订单模型 ┌─────────────────────────────────────────────────────┐ │ 原始订单 │ │ (Source Order) │ │ 客户下单时的原始订单 │ └─────────────────────┬───────────────────────────────┘ │ 拆分/合并 ▼ ┌─────────────────────────────────────────────────────┐ │ 履约订单 │ │ (Fulfillment Order) │ │ 实际执行发货的订单单元 │ └─────────────────────┬───────────────────────────────┘ │ 下发 ▼ ┌─────────────────────────────────────────────────────┐ │ 出库单 │ │ (Outbound Order) │ │ WMS执行的出库指令 │ └─────────────────────────────────────────────────────┘ 1.2 数据模型 /** * 原始订单 */ @Data public class SourceOrder { private String sourceOrderId; // 原始订单号 private String channelOrderId; // 渠道订单号 private String channel; // 渠道 private String buyerId; // 买家ID private Address shippingAddress; // 收货地址 private List<SourceOrderItem> items; // 订单明细 private Money totalAmount; // 订单总额 private OrderStatus status; // 状态 } /** * 履约订单 */ @Data public class FulfillmentOrder { private String fulfillmentOrderId; // 履约订单号 private String sourceOrderId; // 关联原始订单号 private String warehouseId; // 发货仓库 private String carrierId; // 承运商 private Address shippingAddress; // 收货地址 private List<FulfillmentOrderItem> items; // 履约明细 private Money shippingFee; // 运费 private FulfillmentStatus status; // 状态 } /** * 订单关系表 */ @Data public class OrderRelation { private String sourceOrderId; // 原始订单号 private String fulfillmentOrderId; // 履约订单号 private String relationType; // 关系类型:SPLIT/MERGE } 1.3 数据库设计 -- 原始订单表 CREATE TABLE t_source_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, source_order_id VARCHAR(32) NOT NULL, channel_order_id VARCHAR(64), channel VARCHAR(32), buyer_id VARCHAR(64), total_amount DECIMAL(12,2), currency VARCHAR(8), status VARCHAR(32), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_source_order_id (source_order_id) ); -- 履约订单表 CREATE TABLE t_fulfillment_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, fulfillment_order_id VARCHAR(32) NOT NULL, source_order_id VARCHAR(32) NOT NULL, warehouse_id VARCHAR(32), carrier_id VARCHAR(32), shipping_fee DECIMAL(12,2), status VARCHAR(32), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_fulfillment_order_id (fulfillment_order_id), KEY idx_source_order_id (source_order_id) ); -- 订单关系表 CREATE TABLE t_order_relation ( id BIGINT PRIMARY KEY AUTO_INCREMENT, source_order_id VARCHAR(32) NOT NULL, fulfillment_order_id VARCHAR(32) NOT NULL, relation_type VARCHAR(16) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_source_order_id (source_order_id), KEY idx_fulfillment_order_id (fulfillment_order_id) ); 二、拆单策略 2.1 拆单场景分类 场景 触发条件 处理方式 多仓拆单 商品分布在不同仓库 按仓库拆分 预售拆单 部分商品无货 有货先发,无货等待 超重拆单 包裹超过物流限重 按重量拆分 赠品拆单 赠品从不同仓库发 赠品单独发货 组合商品拆单 组合商品需要拆开 拆成单品发货 2.2 多仓拆单 场景:订单包含SKU-A和SKU-B,SKU-A在深圳仓,SKU-B在义乌仓 ...

2026-01-29 · maneng

多渠道订单接入——Amazon、eBay、独立站统一处理

引言:多渠道的挑战 跨境电商通常在多个平台销售: Amazon(美国、欧洲、日本) eBay Shopify独立站 速卖通 Wish TikTok Shop 每个渠道的订单格式和API都不同,如果不做统一处理: 每个渠道单独处理,代码重复 下游系统(WMS、TMS)要对接多种格式 数据分析困难 解决方案:设计统一的订单模型,通过适配器模式对接各渠道。 一、各渠道订单差异分析 1.1 Amazon订单特点 订单结构: { "AmazonOrderId": "111-1234567-1234567", "PurchaseDate": "2024-01-29T10:00:00Z", "OrderStatus": "Unshipped", "FulfillmentChannel": "MFN", "SalesChannel": "Amazon.com", "OrderTotal": { "CurrencyCode": "USD", "Amount": "99.99" }, "ShippingAddress": { "Name": "John Doe", "AddressLine1": "123 Main St", "City": "Seattle", "StateOrRegion": "WA", "PostalCode": "98101", "CountryCode": "US" } } 特殊点: FBA/FBM区分 多站点(US、UK、DE、JP等) 订单状态较多 有Prime标识 1.2 eBay订单特点 订单结构: { "orderId": "12-12345-12345", "creationDate": "2024-01-29T10:00:00.000Z", "orderFulfillmentStatus": "NOT_STARTED", "pricingSummary": { "total": { "value": "99.99", "currency": "USD" } }, "fulfillmentStartInstructions": [{ "shippingStep": { "shipTo": { "fullName": "John Doe", "contactAddress": { "addressLine1": "123 Main St", "city": "Seattle", "stateOrProvince": "WA", "postalCode": "98101", "countryCode": "US" } } } }] } 特殊点: ...

2026-01-29 · maneng

如约数科科技工作室

浙ICP备2025203501号

👀 本站总访问量 ...| 👤 访客数 ...| 📅 今日访问 ...