Compare commits
2 Commits
ce75fca721
...
314c5ac02f
| Author | SHA1 | Date | |
|---|---|---|---|
| 314c5ac02f | |||
| d38963055b |
3
db.sql
3
db.sql
@ -432,6 +432,9 @@ CREATE TABLE `t_order_declaration` (
|
|||||||
`ImHsCode` VARCHAR(50) NULL COMMENT '进口HsCode',
|
`ImHsCode` VARCHAR(50) NULL COMMENT '进口HsCode',
|
||||||
`ExHsCode` VARCHAR(50) NULL COMMENT '出口HsCode',
|
`ExHsCode` VARCHAR(50) NULL COMMENT '出口HsCode',
|
||||||
`DeclareElement` VARCHAR(255) NULL COMMENT '出口申报要素',
|
`DeclareElement` VARCHAR(255) NULL COMMENT '出口申报要素',
|
||||||
|
`ProductId` VARCHAR(100) NULL COMMENT '产品标识编码',
|
||||||
|
`NonStdProductId` VARCHAR(100) NULL COMMENT '非标产品标识编码',
|
||||||
|
`StdProductId` VARCHAR(100) NULL COMMENT '标准产品标识编码',
|
||||||
`SkuImage` VARCHAR(512) NULL COMMENT '图片(存 storage key 或短 URL)',
|
`SkuImage` VARCHAR(512) NULL COMMENT '图片(存 storage key 或短 URL)',
|
||||||
`Category` VARCHAR(20) NULL COMMENT '类别',
|
`Category` VARCHAR(20) NULL COMMENT '类别',
|
||||||
`Brand` VARCHAR(50) NULL COMMENT '品牌',
|
`Brand` VARCHAR(50) NULL COMMENT '品牌',
|
||||||
|
|||||||
34
sql/migrate_fee_item_marketing_discount_20261019.sql
Normal file
34
sql/migrate_fee_item_marketing_discount_20261019.sql
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
-- 费用项字典:营销抵扣(支付时写入的负数应收行)。可重复执行。
|
||||||
|
-- coupon_discount 优惠券抵扣、member_discount 会员折扣;由 /order/pay 自动生成,禁止手工添加。
|
||||||
|
|
||||||
|
-- 确保字典类型存在
|
||||||
|
INSERT INTO `t_dict_type` (`DictType`, `Remark`, `TypeSort`, `IsDeleted`, `CreateUserId`)
|
||||||
|
SELECT 'fee_item', '费用项', 0, 0, 0
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `t_dict_type`
|
||||||
|
WHERE `DictType` = 'fee_item' AND `IsDeleted` = 0
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 曾被软删的同 code 项直接恢复
|
||||||
|
UPDATE `t_dict`
|
||||||
|
SET `IsDeleted` = 0, `UpdateTime` = NOW()
|
||||||
|
WHERE `DictType` = 'fee_item'
|
||||||
|
AND `DictCode` IN ('coupon_discount', 'member_discount')
|
||||||
|
AND `IsDeleted` = 1;
|
||||||
|
|
||||||
|
INSERT INTO `t_dict` (`DictType`, `DictSort`, `DictCode`, `DictName`, `IsDeleted`, `CreateUserId`)
|
||||||
|
SELECT 'fee_item', 900, 'coupon_discount', '优惠券抵扣', 0, 0
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `t_dict`
|
||||||
|
WHERE `DictType` = 'fee_item' AND `DictCode` = 'coupon_discount'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `t_dict` (`DictType`, `DictSort`, `DictCode`, `DictName`, `IsDeleted`, `CreateUserId`)
|
||||||
|
SELECT 'fee_item', 901, 'member_discount', '会员折扣', 0, 0
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `t_dict`
|
||||||
|
WHERE `DictType` = 'fee_item' AND `DictCode` = 'member_discount'
|
||||||
|
);
|
||||||
55
sql/migrate_order_declaration_product_ids_20261019.sql
Normal file
55
sql/migrate_order_declaration_product_ids_20261019.sql
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
-- 申报明细:新增产品标识编码 / 非标产品标识编码 / 标准产品标识编码。
|
||||||
|
-- 可重复执行。
|
||||||
|
|
||||||
|
SET @db := DATABASE();
|
||||||
|
|
||||||
|
-- t_order_declaration.ProductId
|
||||||
|
SET @exists := (
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @db
|
||||||
|
AND TABLE_NAME = 't_order_declaration'
|
||||||
|
AND COLUMN_NAME = 'ProductId'
|
||||||
|
);
|
||||||
|
SET @sql := IF(
|
||||||
|
@exists = 0,
|
||||||
|
'ALTER TABLE `t_order_declaration` ADD COLUMN `ProductId` VARCHAR(100) NULL COMMENT ''产品标识编码'' AFTER `DeclareElement`',
|
||||||
|
'SELECT ''t_order_declaration.ProductId already exists'' AS info'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- t_order_declaration.NonStdProductId
|
||||||
|
SET @exists := (
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @db
|
||||||
|
AND TABLE_NAME = 't_order_declaration'
|
||||||
|
AND COLUMN_NAME = 'NonStdProductId'
|
||||||
|
);
|
||||||
|
SET @sql := IF(
|
||||||
|
@exists = 0,
|
||||||
|
'ALTER TABLE `t_order_declaration` ADD COLUMN `NonStdProductId` VARCHAR(100) NULL COMMENT ''非标产品标识编码'' AFTER `ProductId`',
|
||||||
|
'SELECT ''t_order_declaration.NonStdProductId already exists'' AS info'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- t_order_declaration.StdProductId
|
||||||
|
SET @exists := (
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @db
|
||||||
|
AND TABLE_NAME = 't_order_declaration'
|
||||||
|
AND COLUMN_NAME = 'StdProductId'
|
||||||
|
);
|
||||||
|
SET @sql := IF(
|
||||||
|
@exists = 0,
|
||||||
|
'ALTER TABLE `t_order_declaration` ADD COLUMN `StdProductId` VARCHAR(100) NULL COMMENT ''标准产品标识编码'' AFTER `NonStdProductId`',
|
||||||
|
'SELECT ''t_order_declaration.StdProductId already exists'' AS info'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@ -20,6 +20,11 @@ public class OrderFeeOutput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string CostItem { get; set; } = string.Empty;
|
public string CostItem { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 费用项显示名:仅营销抵扣行(优惠券抵扣/会员折扣)由后端给出,其余为空由前端按 fee_item 字典解析。
|
||||||
|
/// </summary>
|
||||||
|
public string? CostItemName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 金额。
|
/// 金额。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -70,6 +70,21 @@ public class OrderItemOutput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? DeclareElement { get; set; }
|
public string? DeclareElement { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 产品标识编码。
|
||||||
|
/// </summary>
|
||||||
|
public string? ProductId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 非标产品标识编码。
|
||||||
|
/// </summary>
|
||||||
|
public string? NonStdProductId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标准产品标识编码。
|
||||||
|
/// </summary>
|
||||||
|
public string? StdProductId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// SKU 图片。
|
/// SKU 图片。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -368,6 +368,31 @@ public class OrderOutput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal? ForecastPayableAmount { get; set; }
|
public decimal? ForecastPayableAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 营销快照:折前应收小计。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? ReceivableSubtotal { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 营销快照:会员折扣金额(与券互斥)。
|
||||||
|
/// </summary>
|
||||||
|
public decimal LevelDiscountAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 营销快照:优惠券抵扣金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal CouponDiscountAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 营销快照:客户实付金额(支付扣款口径;区别于应付成本)。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? ActualPayAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 营销快照:使用的优惠券实例 Id。
|
||||||
|
/// </summary>
|
||||||
|
public int? CouponInstanceId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 订单绑定的销售产品规则发布版本。
|
/// 订单绑定的销售产品规则发布版本。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace Oms.Application.Dtos.Orders;
|
namespace Oms.Application.Dtos.Orders;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -65,6 +67,24 @@ public class OrderParcelItemInput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? DeclareElement { get; set; }
|
public string? DeclareElement { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 产品标识编码(选填)。
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(100)]
|
||||||
|
public string? ProductId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 非标产品标识编码(选填)。
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(100)]
|
||||||
|
public string? NonStdProductId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标准产品标识编码(选填)。
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(100)]
|
||||||
|
public string? StdProductId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// SKU 图片。
|
/// SKU 图片。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -199,6 +199,9 @@ public static class AutoTriggerFormulaMeta
|
|||||||
yield return ("进口HS编码", "t_order_declaration", "ImHsCode", FormulaValueType.String, false);
|
yield return ("进口HS编码", "t_order_declaration", "ImHsCode", FormulaValueType.String, false);
|
||||||
yield return ("出口HS编码", "t_order_declaration", "ExHsCode", FormulaValueType.String, false);
|
yield return ("出口HS编码", "t_order_declaration", "ExHsCode", FormulaValueType.String, false);
|
||||||
yield return ("出口申报要素", "t_order_declaration", "DeclareElement", FormulaValueType.String, false);
|
yield return ("出口申报要素", "t_order_declaration", "DeclareElement", FormulaValueType.String, false);
|
||||||
|
yield return ("产品标识编码", "t_order_declaration", "ProductId", FormulaValueType.String, false);
|
||||||
|
yield return ("非标产品标识编码", "t_order_declaration", "NonStdProductId", FormulaValueType.String, false);
|
||||||
|
yield return ("标准产品标识编码", "t_order_declaration", "StdProductId", FormulaValueType.String, false);
|
||||||
yield return ("申报类别", "t_order_declaration", "Category", FormulaValueType.String, false);
|
yield return ("申报类别", "t_order_declaration", "Category", FormulaValueType.String, false);
|
||||||
yield return ("品牌", "t_order_declaration", "Brand", FormulaValueType.String, false);
|
yield return ("品牌", "t_order_declaration", "Brand", FormulaValueType.String, false);
|
||||||
yield return ("品牌类型", "t_order_declaration", "BrandType", FormulaValueType.String, false);
|
yield return ("品牌类型", "t_order_declaration", "BrandType", FormulaValueType.String, false);
|
||||||
|
|||||||
@ -29,7 +29,7 @@ public static class OrderFormulaFieldResolver
|
|||||||
|
|
||||||
if (fieldRef.Table == "t_order_fee")
|
if (fieldRef.Table == "t_order_fee")
|
||||||
{
|
{
|
||||||
return context.Fees.Any(f => ContainsValue(GetPropertyValue(f, fieldRef.Field), needle));
|
return RuleFees(context).Any(f => ContainsValue(GetPropertyValue(f, fieldRef.Field), needle));
|
||||||
}
|
}
|
||||||
|
|
||||||
var raw = GetRawValue(fieldRef, context);
|
var raw = GetRawValue(fieldRef, context);
|
||||||
@ -255,10 +255,16 @@ public static class OrderFormulaFieldResolver
|
|||||||
|
|
||||||
private static object? ResolveFeeField(string field, OrderRuleEvaluationContext context)
|
private static object? ResolveFeeField(string field, OrderRuleEvaluationContext context)
|
||||||
{
|
{
|
||||||
var fee = context.Fees.FirstOrDefault();
|
var fee = RuleFees(context).FirstOrDefault();
|
||||||
return fee == null ? null : GetPropertyValue(fee, field);
|
return fee == null ? null : GetPropertyValue(fee, field);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 规则只看计费产生的费用行,忽略支付写入的营销抵扣负数行。
|
||||||
|
/// </summary>
|
||||||
|
private static IEnumerable<OrderFee> RuleFees(OrderRuleEvaluationContext context) =>
|
||||||
|
context.Fees.Where(f => !OrderFeeDiscountCostItems.IsDiscount(f.CostItem));
|
||||||
|
|
||||||
private static object? GetDeclarationValue(OrderDeclaration declaration, string field) =>
|
private static object? GetDeclarationValue(OrderDeclaration declaration, string field) =>
|
||||||
field switch
|
field switch
|
||||||
{
|
{
|
||||||
|
|||||||
@ -263,6 +263,11 @@ public partial class OrderService
|
|||||||
HasInsurance = hasInsurance,
|
HasInsurance = hasInsurance,
|
||||||
ForecastReceivableAmount = order.ForecastReceivableAmount,
|
ForecastReceivableAmount = order.ForecastReceivableAmount,
|
||||||
ForecastPayableAmount = order.ForecastPayableAmount,
|
ForecastPayableAmount = order.ForecastPayableAmount,
|
||||||
|
ReceivableSubtotal = order.ReceivableSubtotal,
|
||||||
|
LevelDiscountAmount = order.LevelDiscountAmount,
|
||||||
|
CouponDiscountAmount = order.CouponDiscountAmount,
|
||||||
|
ActualPayAmount = order.PayableAmount,
|
||||||
|
CouponInstanceId = order.CouponInstanceId,
|
||||||
SalesProductRulesVersion = string.IsNullOrWhiteSpace(order.SalesProductRulesVersion)
|
SalesProductRulesVersion = string.IsNullOrWhiteSpace(order.SalesProductRulesVersion)
|
||||||
? null
|
? null
|
||||||
: order.SalesProductRulesVersion.Trim(),
|
: order.SalesProductRulesVersion.Trim(),
|
||||||
@ -812,6 +817,9 @@ public partial class OrderService
|
|||||||
ImHsCode = x.ImHsCode ?? x.HsCode,
|
ImHsCode = x.ImHsCode ?? x.HsCode,
|
||||||
ExHsCode = x.ExHsCode ?? x.HsCode,
|
ExHsCode = x.ExHsCode ?? x.HsCode,
|
||||||
DeclareElement = x.DeclareElement,
|
DeclareElement = x.DeclareElement,
|
||||||
|
ProductId = x.ProductId,
|
||||||
|
NonStdProductId = x.NonStdProductId,
|
||||||
|
StdProductId = x.StdProductId,
|
||||||
SkuImage = FileStorage.GetPublicUrl(x.SkuImage),
|
SkuImage = FileStorage.GetPublicUrl(x.SkuImage),
|
||||||
Category = x.Category,
|
Category = x.Category,
|
||||||
Brand = x.Brand,
|
Brand = x.Brand,
|
||||||
@ -851,6 +859,9 @@ public partial class OrderService
|
|||||||
Id = x.Id,
|
Id = x.Id,
|
||||||
Type = x.Type,
|
Type = x.Type,
|
||||||
CostItem = x.CostItem,
|
CostItem = x.CostItem,
|
||||||
|
CostItemName = OrderFeeDiscountCostItems.IsDiscount(x.CostItem)
|
||||||
|
? ParcelBillingCalculator.ResolveFeeDisplayName(x.CostItem, null)
|
||||||
|
: null,
|
||||||
Amount = x.Amount,
|
Amount = x.Amount,
|
||||||
Currency = x.Currency,
|
Currency = x.Currency,
|
||||||
DownstreamProvider = x.DownstreamProvider,
|
DownstreamProvider = x.DownstreamProvider,
|
||||||
|
|||||||
@ -900,6 +900,11 @@ public partial class OrderService
|
|||||||
throw new BusinessException("费用项不能为空");
|
throw new BusinessException("费用项不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (OrderFeeDiscountCostItems.IsDiscount(costItem))
|
||||||
|
{
|
||||||
|
throw new BusinessException("优惠券抵扣、会员折扣由支付自动生成,不能手工添加");
|
||||||
|
}
|
||||||
|
|
||||||
if (input.Amount <= 0)
|
if (input.Amount <= 0)
|
||||||
{
|
{
|
||||||
throw new BusinessException("金额必须大于0");
|
throw new BusinessException("金额必须大于0");
|
||||||
@ -1355,6 +1360,9 @@ public partial class OrderService
|
|||||||
ImHsCode = x.ImHsCode,
|
ImHsCode = x.ImHsCode,
|
||||||
ExHsCode = x.ExHsCode ?? x.ImHsCode,
|
ExHsCode = x.ExHsCode ?? x.ImHsCode,
|
||||||
DeclareElement = x.DeclareElement,
|
DeclareElement = x.DeclareElement,
|
||||||
|
ProductId = x.ProductId?.Trim(),
|
||||||
|
NonStdProductId = x.NonStdProductId?.Trim(),
|
||||||
|
StdProductId = x.StdProductId?.Trim(),
|
||||||
SkuImage = OmsObjectStorageExtensions.ToStorageKey(x.SkuImage),
|
SkuImage = OmsObjectStorageExtensions.ToStorageKey(x.SkuImage),
|
||||||
Category = x.Category,
|
Category = x.Category,
|
||||||
Brand = x.Brand,
|
Brand = x.Brand,
|
||||||
|
|||||||
@ -2213,6 +2213,9 @@ public partial class OrderService
|
|||||||
ImHsCode = x.ImHsCode,
|
ImHsCode = x.ImHsCode,
|
||||||
ExHsCode = x.ExHsCode,
|
ExHsCode = x.ExHsCode,
|
||||||
DeclareElement = x.DeclareElement,
|
DeclareElement = x.DeclareElement,
|
||||||
|
ProductId = x.ProductId?.Trim(),
|
||||||
|
NonStdProductId = x.NonStdProductId?.Trim(),
|
||||||
|
StdProductId = x.StdProductId?.Trim(),
|
||||||
SkuImage = OmsObjectStorageExtensions.ToStorageKey(x.SkuImage),
|
SkuImage = OmsObjectStorageExtensions.ToStorageKey(x.SkuImage),
|
||||||
Category = x.Category,
|
Category = x.Category,
|
||||||
Brand = x.Brand,
|
Brand = x.Brand,
|
||||||
@ -2449,6 +2452,12 @@ public partial class OrderService
|
|||||||
{
|
{
|
||||||
_ = cancellationToken;
|
_ = cancellationToken;
|
||||||
var order = await GetOrderOrThrowAsync(orderId);
|
var order = await GetOrderOrThrowAsync(orderId);
|
||||||
|
// 已付款订单应收已固化(含营销抵扣行),重复投递的入库计费消息不得重算
|
||||||
|
if (order.IsPaid != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(order.ProductCode))
|
if (string.IsNullOrWhiteSpace(order.ProductCode))
|
||||||
{
|
{
|
||||||
order.OrderStatus = OrderStatusCodes.PendingAllocation;
|
order.OrderStatus = OrderStatusCodes.PendingAllocation;
|
||||||
@ -2630,7 +2639,8 @@ public partial class OrderService
|
|||||||
throw new InvalidOperationException($"预报计费时订单尚未可见,稍后重试 OrderId={orderId}");
|
throw new InvalidOperationException($"预报计费时订单尚未可见,稍后重试 OrderId={orderId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(order.OrderStatus, OrderStatusCodes.Deleted, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(order.OrderStatus, OrderStatusCodes.Deleted, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| order.IsPaid != 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Oms.Application.Dtos.Marketing;
|
using Oms.Application.Dtos.Marketing;
|
||||||
using Oms.Application.Dtos.Orders;
|
using Oms.Application.Dtos.Orders;
|
||||||
using Oms.Application.Shared;
|
using Oms.Application.Shared;
|
||||||
|
using Oms.Domain.OrdersAggregate.Consts;
|
||||||
using Oms.Domain.OrdersAggregate.Entities;
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
|
||||||
namespace Oms.Application.Services.Orders;
|
namespace Oms.Application.Services.Orders;
|
||||||
@ -129,6 +130,75 @@ public partial class OrderService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付成功后把会员折扣 / 优惠券抵扣写成负数应收行,使订单应收与实付一致;币种与计费阶段跟随基础运费行。
|
||||||
|
/// </summary>
|
||||||
|
private async Task UpsertMarketingDiscountFeesAsync(
|
||||||
|
Order order,
|
||||||
|
IEnumerable<OrderFee> fees,
|
||||||
|
MarketingPriceBreakdown breakdown,
|
||||||
|
string? couponCode)
|
||||||
|
{
|
||||||
|
var discountRows = new List<OrderFee>();
|
||||||
|
if (breakdown.CouponDiscountAmount <= 0 && breakdown.LevelDiscountAmount <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var receivable = fees
|
||||||
|
.Where(x => x.Type == OrderFeeTypeCodes.Receivable
|
||||||
|
&& !OrderFeeDiscountCostItems.IsDiscount(x.CostItem))
|
||||||
|
.ToList();
|
||||||
|
var anchor = receivable
|
||||||
|
.Where(x => MarketingFeeHelper.IsBaseFreightCostItem(x.CostItem))
|
||||||
|
.OrderByDescending(x => ChargingStageRank(x.ChargingStage))
|
||||||
|
.FirstOrDefault()
|
||||||
|
?? receivable.OrderByDescending(x => ChargingStageRank(x.ChargingStage)).FirstOrDefault();
|
||||||
|
var currency = string.IsNullOrWhiteSpace(anchor?.Currency) ? "CNY" : anchor!.Currency;
|
||||||
|
var stage = anchor?.ChargingStage;
|
||||||
|
|
||||||
|
if (breakdown.CouponDiscountAmount > 0)
|
||||||
|
{
|
||||||
|
discountRows.Add(new OrderFee
|
||||||
|
{
|
||||||
|
Type = OrderFeeTypeCodes.Receivable,
|
||||||
|
CostItem = OrderFeeDiscountCostItems.CouponDiscount,
|
||||||
|
Amount = -breakdown.CouponDiscountAmount,
|
||||||
|
Currency = currency,
|
||||||
|
ChargingStage = stage,
|
||||||
|
PriceNo = couponCode,
|
||||||
|
PriceEffectiveTime = DateTime.Now
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (breakdown.LevelDiscountAmount > 0)
|
||||||
|
{
|
||||||
|
discountRows.Add(new OrderFee
|
||||||
|
{
|
||||||
|
Type = OrderFeeTypeCodes.Receivable,
|
||||||
|
CostItem = OrderFeeDiscountCostItems.MemberDiscount,
|
||||||
|
Amount = -breakdown.LevelDiscountAmount,
|
||||||
|
Currency = currency,
|
||||||
|
ChargingStage = stage,
|
||||||
|
PriceNo = breakdown.MemberLevelName,
|
||||||
|
PriceEffectiveTime = DateTime.Now
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await OrderRepository.UpsertFeesAsync(order.Id, discountRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 抵扣行取入库优先、其次预报的阶段;出库阶段客户侧不展示,排最后。
|
||||||
|
/// </summary>
|
||||||
|
private static int ChargingStageRank(string? stage) => stage?.Trim().ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
OrderFeeChargingStageCodes.Outbound => -1,
|
||||||
|
OrderFeeChargingStageCodes.Inbound => 2,
|
||||||
|
OrderFeeChargingStageCodes.Forecast => 1,
|
||||||
|
_ => 0
|
||||||
|
};
|
||||||
|
|
||||||
private static int? ResolveRecommendedPayCouponId(Order order, List<CouponAvailableOutput> coupons)
|
private static int? ResolveRecommendedPayCouponId(Order order, List<CouponAvailableOutput> coupons)
|
||||||
{
|
{
|
||||||
if (order.CouponInstanceId is > 0
|
if (order.CouponInstanceId is > 0
|
||||||
|
|||||||
@ -184,12 +184,16 @@ public partial class OrderService : ApplicationService
|
|||||||
|
|
||||||
LazyServiceProvider.GetRequiredService<MarketingOrderHelper>()
|
LazyServiceProvider.GetRequiredService<MarketingOrderHelper>()
|
||||||
.ApplySnapshotToOrder(order, breakdown, null);
|
.ApplySnapshotToOrder(order, breakdown, null);
|
||||||
|
string? couponCode = null;
|
||||||
if (breakdown.CouponInstanceId.HasValue)
|
if (breakdown.CouponInstanceId.HasValue)
|
||||||
{
|
{
|
||||||
var mainCustomerId = await couponManager.ResolveMainCustomerIdAsync(customer.Id);
|
var mainCustomerId = await couponManager.ResolveMainCustomerIdAsync(customer.Id);
|
||||||
await couponManager.MarkUsedAsync(breakdown.CouponInstanceId.Value, mainCustomerId, order.Id);
|
var coupon = await couponManager.MarkUsedAsync(breakdown.CouponInstanceId.Value, mainCustomerId, order.Id);
|
||||||
|
couponCode = coupon.CouponCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await UpsertMarketingDiscountFeesAsync(order, fees, breakdown, couponCode);
|
||||||
|
|
||||||
var mainCustomer = await MarketingCustomerHelper.ResolveMarketingMainCustomerAsync(CustomerRepository, customer);
|
var mainCustomer = await MarketingCustomerHelper.ResolveMarketingMainCustomerAsync(CustomerRepository, customer);
|
||||||
var pointsManager = LazyServiceProvider.GetRequiredService<MarketingPointsManager>();
|
var pointsManager = LazyServiceProvider.GetRequiredService<MarketingPointsManager>();
|
||||||
var paidBaseFreight = MarketingFeeHelper.ResolvePaidBaseFreight(
|
var paidBaseFreight = MarketingFeeHelper.ResolvePaidBaseFreight(
|
||||||
|
|||||||
@ -1075,6 +1075,8 @@ internal static class ParcelBillingCalculator
|
|||||||
"handling" => "操作费",
|
"handling" => "操作费",
|
||||||
"proxyPacking" => "代打包费",
|
"proxyPacking" => "代打包费",
|
||||||
"insurance" => "保险费",
|
"insurance" => "保险费",
|
||||||
|
OrderFeeDiscountCostItems.CouponDiscount => "优惠券抵扣",
|
||||||
|
OrderFeeDiscountCostItems.MemberDiscount => "会员折扣",
|
||||||
_ => costItem
|
_ => costItem
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -215,6 +215,9 @@ public static class OrderTemplateFieldRegistry
|
|||||||
Add("items[0].weight", "总重量(kg)", false, "number", group: "declaration", example: "自动计算", hint: "不填=单重×数量");
|
Add("items[0].weight", "总重量(kg)", false, "number", group: "declaration", example: "自动计算", hint: "不填=单重×数量");
|
||||||
Add("items[0].imHsCode", "进口HsCode", false, group: "declaration", hint: "选填");
|
Add("items[0].imHsCode", "进口HsCode", false, group: "declaration", hint: "选填");
|
||||||
Add("items[0].exHsCode", "出口HsCode", false, group: "declaration", hint: "选填");
|
Add("items[0].exHsCode", "出口HsCode", false, group: "declaration", hint: "选填");
|
||||||
|
Add("items[0].productId", "产品标识编码", false, group: "declaration", hint: "选填");
|
||||||
|
Add("items[0].nonStdProductId", "非标产品标识编码", false, group: "declaration", hint: "选填");
|
||||||
|
Add("items[0].stdProductId", "标准产品标识编码", false, group: "declaration", hint: "选填");
|
||||||
Add("items[0].skuImage", "商品图片URL", false, group: "declaration", hint: "选填");
|
Add("items[0].skuImage", "商品图片URL", false, group: "declaration", hint: "选填");
|
||||||
Add("items[0].category", "类别", false, group: "declaration", example: "Accessories", hint: "选填");
|
Add("items[0].category", "类别", false, group: "declaration", example: "Accessories", hint: "选填");
|
||||||
Add("items[0].brand", "品牌", false, group: "declaration", example: "DemoBrand", hint: "选填");
|
Add("items[0].brand", "品牌", false, group: "declaration", example: "DemoBrand", hint: "选填");
|
||||||
|
|||||||
@ -20,6 +20,7 @@ public static class OrderTemplateRowMapper
|
|||||||
"sender.countryCode", "sender.province", "sender.city", "sender.zipCode", "sender.streetAddress1",
|
"sender.countryCode", "sender.province", "sender.city", "sender.zipCode", "sender.streetAddress1",
|
||||||
"sender.streetAddress2", "sender.streetAddress3", "sender.houseNumber", "items[0].cnName", "items[0].enName",
|
"sender.streetAddress2", "sender.streetAddress3", "sender.houseNumber", "items[0].cnName", "items[0].enName",
|
||||||
"items[0].sku", "items[0].unit", "items[0].imHsCode", "items[0].exHsCode", "items[0].skuImage",
|
"items[0].sku", "items[0].unit", "items[0].imHsCode", "items[0].exHsCode", "items[0].skuImage",
|
||||||
|
"items[0].productId", "items[0].nonStdProductId", "items[0].stdProductId",
|
||||||
"items[0].category", "items[0].brand", "items[0].brandType", "items[0].distribution", "items[0].material",
|
"items[0].category", "items[0].brand", "items[0].brandType", "items[0].distribution", "items[0].material",
|
||||||
"items[0].enMaterial", "items[0].purpose", "items[0].attributes", "items[0].originCountry", "items[0].salesLink",
|
"items[0].enMaterial", "items[0].purpose", "items[0].attributes", "items[0].originCountry", "items[0].salesLink",
|
||||||
"items[0].model", "items[0].specifications", "items[0].comment"
|
"items[0].model", "items[0].specifications", "items[0].comment"
|
||||||
|
|||||||
@ -266,7 +266,10 @@ public class MarketingCouponManager : IScopedDependency
|
|||||||
await _instanceRepository.UpdateAsync(instance, cancellationToken);
|
await _instanceRepository.UpdateAsync(instance, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task MarkUsedAsync(int couponInstanceId, int mainCustomerId, int orderId, CancellationToken cancellationToken = default)
|
/// <summary>
|
||||||
|
/// 核销券并返回券实例(调用方可取券码写入费用行)。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<CouponInstance> MarkUsedAsync(int couponInstanceId, int mainCustomerId, int orderId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var instance = await GetOwnedInstanceAsync(couponInstanceId, mainCustomerId, cancellationToken);
|
var instance = await GetOwnedInstanceAsync(couponInstanceId, mainCustomerId, cancellationToken);
|
||||||
instance.Status = CouponInstanceStatus.Used;
|
instance.Status = CouponInstanceStatus.Used;
|
||||||
@ -274,6 +277,7 @@ public class MarketingCouponManager : IScopedDependency
|
|||||||
instance.UseTime = DateTime.Now;
|
instance.UseTime = DateTime.Now;
|
||||||
instance.LockExpireTime = null;
|
instance.LockExpireTime = null;
|
||||||
await _instanceRepository.UpdateAsync(instance, cancellationToken);
|
await _instanceRepository.UpdateAsync(instance, cancellationToken);
|
||||||
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RestoreOnOrderRefundAsync(Order order, bool isFullRefund, CancellationToken cancellationToken = default)
|
public async Task RestoreOnOrderRefundAsync(Order order, bool isFullRefund, CancellationToken cancellationToken = default)
|
||||||
|
|||||||
@ -12,8 +12,13 @@ public static class MarketingFeeHelper
|
|||||||
public static bool IsBaseFreightCostItem(string? costItem) =>
|
public static bool IsBaseFreightCostItem(string? costItem) =>
|
||||||
!string.IsNullOrWhiteSpace(costItem) && ParcelBillingCalculator.IsFreightCostItem(costItem);
|
!string.IsNullOrWhiteSpace(costItem) && ParcelBillingCalculator.IsFreightCostItem(costItem);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 折前应收小计:排除支付时写入的营销抵扣行,避免重算营销金额时重复扣减。
|
||||||
|
/// </summary>
|
||||||
public static decimal SumReceivable(IEnumerable<OrderFee> fees) =>
|
public static decimal SumReceivable(IEnumerable<OrderFee> fees) =>
|
||||||
fees.Where(x => x.Type == OrderFeeTypeCodes.Receivable).Sum(x => x.Amount);
|
fees.Where(x => x.Type == OrderFeeTypeCodes.Receivable
|
||||||
|
&& !OrderFeeDiscountCostItems.IsDiscount(x.CostItem))
|
||||||
|
.Sum(x => x.Amount);
|
||||||
|
|
||||||
public static decimal SumReceivableBaseFreight(IEnumerable<OrderFee> fees) =>
|
public static decimal SumReceivableBaseFreight(IEnumerable<OrderFee> fees) =>
|
||||||
fees.Where(x => x.Type == OrderFeeTypeCodes.Receivable && IsBaseFreightCostItem(x.CostItem))
|
fees.Where(x => x.Type == OrderFeeTypeCodes.Receivable && IsBaseFreightCostItem(x.CostItem))
|
||||||
|
|||||||
@ -201,6 +201,9 @@ public static class OrderDownstreamCreateOneBodyBuilder
|
|||||||
ImHsCode = x.ImHsCode,
|
ImHsCode = x.ImHsCode,
|
||||||
ExHsCode = x.ExHsCode ?? x.HsCode,
|
ExHsCode = x.ExHsCode ?? x.HsCode,
|
||||||
DeclareElement = x.DeclareElement,
|
DeclareElement = x.DeclareElement,
|
||||||
|
ProductId = x.ProductId,
|
||||||
|
NonStdProductId = x.NonStdProductId,
|
||||||
|
StdProductId = x.StdProductId,
|
||||||
SkuImage = fileStorage?.GetPublicUrl(x.SkuImage) ?? x.SkuImage,
|
SkuImage = fileStorage?.GetPublicUrl(x.SkuImage) ?? x.SkuImage,
|
||||||
Category = x.Category,
|
Category = x.Category,
|
||||||
Brand = x.Brand,
|
Brand = x.Brand,
|
||||||
|
|||||||
@ -6,6 +6,23 @@ public static class OrderFeeTypeCodes
|
|||||||
public const string Payable = "payable";
|
public const string Payable = "payable";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 营销抵扣费用项(支付时写入的负数应收行,fee_item 字典同 code)。
|
||||||
|
/// </summary>
|
||||||
|
public static class OrderFeeDiscountCostItems
|
||||||
|
{
|
||||||
|
/// <summary>优惠券抵扣。</summary>
|
||||||
|
public const string CouponDiscount = "coupon_discount";
|
||||||
|
|
||||||
|
/// <summary>会员折扣。</summary>
|
||||||
|
public const string MemberDiscount = "member_discount";
|
||||||
|
|
||||||
|
public static bool IsDiscount(string? costItem) =>
|
||||||
|
!string.IsNullOrWhiteSpace(costItem)
|
||||||
|
&& (string.Equals(costItem.Trim(), CouponDiscount, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(costItem.Trim(), MemberDiscount, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
public static class OrderFeeChargingStageCodes
|
public static class OrderFeeChargingStageCodes
|
||||||
{
|
{
|
||||||
public const string Forecast = "forecast";
|
public const string Forecast = "forecast";
|
||||||
|
|||||||
@ -37,6 +37,21 @@ public class OrderDeclaration : OmsAuditedEntity
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? DeclareElement { get; set; }
|
public string? DeclareElement { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 产品标识编码。
|
||||||
|
/// </summary>
|
||||||
|
public string? ProductId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 非标产品标识编码。
|
||||||
|
/// </summary>
|
||||||
|
public string? NonStdProductId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标准产品标识编码。
|
||||||
|
/// </summary>
|
||||||
|
public string? StdProductId { get; set; }
|
||||||
|
|
||||||
public string? SkuImage { get; set; }
|
public string? SkuImage { get; set; }
|
||||||
|
|
||||||
public string? Category { get; set; }
|
public string? Category { get; set; }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user