1.02 #3
24
db.sql
24
db.sql
@ -104,6 +104,7 @@ DROP TABLE IF EXISTS `t_order_sender`;
|
|||||||
DROP TABLE IF EXISTS `t_order_batch_job_item`;
|
DROP TABLE IF EXISTS `t_order_batch_job_item`;
|
||||||
DROP TABLE IF EXISTS `t_order_batch_job`;
|
DROP TABLE IF EXISTS `t_order_batch_job`;
|
||||||
DROP TABLE IF EXISTS `t_order_template`;
|
DROP TABLE IF EXISTS `t_order_template`;
|
||||||
|
DROP TABLE IF EXISTS `t_order_operation_log`;
|
||||||
DROP TABLE IF EXISTS `t_order_exception_event`;
|
DROP TABLE IF EXISTS `t_order_exception_event`;
|
||||||
DROP TABLE IF EXISTS `t_order`;
|
DROP TABLE IF EXISTS `t_order`;
|
||||||
|
|
||||||
@ -528,6 +529,29 @@ CREATE TABLE `t_order_exception_event` (
|
|||||||
KEY `idx_oee_order_time` (`OrderId`, `CreateTime`)
|
KEY `idx_oee_order_time` (`OrderId`, `CreateTime`)
|
||||||
) COMMENT='订单异常事件表';
|
) COMMENT='订单异常事件表';
|
||||||
|
|
||||||
|
CREATE TABLE `t_order_operation_log` (
|
||||||
|
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`OrderId` INT NOT NULL COMMENT '运单Id',
|
||||||
|
`ServiceNo` VARCHAR(64) NULL COMMENT '运单号冗余',
|
||||||
|
`Action` VARCHAR(32) NOT NULL COMMENT '动作码 editBasic/editWeight/recalcReceivable/recalcPayable/reallocate',
|
||||||
|
`ActionName` VARCHAR(64) NOT NULL COMMENT '动作中文名',
|
||||||
|
`OperatorId` INT NOT NULL DEFAULT 0 COMMENT '操作人用户Id',
|
||||||
|
`OperatorName` VARCHAR(64) NULL COMMENT '操作人展示名',
|
||||||
|
`OperatorType` VARCHAR(16) NOT NULL DEFAULT 'staff' COMMENT 'staff/customer/system/openapi',
|
||||||
|
`Source` VARCHAR(16) NOT NULL DEFAULT 'pc' COMMENT 'pc/h5/openapi/job',
|
||||||
|
`ChangesJson` JSON NULL COMMENT '字段 diff 数组 [{path,label,before,after}]',
|
||||||
|
`Remark` VARCHAR(512) NULL COMMENT '补充说明',
|
||||||
|
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
|
||||||
|
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳',
|
||||||
|
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`UpdateUserId` INT NULL COMMENT '更新人',
|
||||||
|
`UpdateTime` DATETIME NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`Id`),
|
||||||
|
KEY `idx_ool_order_time` (`OrderId`, `CreateTime`),
|
||||||
|
KEY `idx_ool_service_no` (`ServiceNo`)
|
||||||
|
) COMMENT='运单操作日志';
|
||||||
|
|
||||||
CREATE TABLE `t_sales_product` (
|
CREATE TABLE `t_sales_product` (
|
||||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增,步长 1',
|
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增,步长 1',
|
||||||
`ProductCode` VARCHAR(64) NOT NULL COMMENT '产品编码',
|
`ProductCode` VARCHAR(64) NOT NULL COMMENT '产品编码',
|
||||||
|
|||||||
213
sql/drop_all_unique_indexes.sql
Normal file
213
sql/drop_all_unique_indexes.sql
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- drop_all_unique_indexes.sql
|
||||||
|
-- 去掉当前库所有非 PRIMARY 唯一索引的「唯一性」(改为同名普通 INDEX)
|
||||||
|
-- 流程:备份并删除外键 → UNIQUE 改 KEY → 恢复外键
|
||||||
|
-- 解决 Error 1553:Cannot drop index ... needed in a foreign key constraint
|
||||||
|
-- 可重复执行;唯一性请在业务代码中校验。
|
||||||
|
-- 请在 Navicat / DBeaver / MySQL Workbench 中整段执行(需要 DELIMITER)。
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
SET @db := DATABASE();
|
||||||
|
|
||||||
|
-- ---------- 预览:将处理的唯一索引 ----------
|
||||||
|
SELECT
|
||||||
|
TABLE_NAME,
|
||||||
|
INDEX_NAME,
|
||||||
|
GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS columns
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @db
|
||||||
|
AND NON_UNIQUE = 0
|
||||||
|
AND INDEX_NAME <> 'PRIMARY'
|
||||||
|
GROUP BY TABLE_NAME, INDEX_NAME
|
||||||
|
ORDER BY TABLE_NAME, INDEX_NAME;
|
||||||
|
|
||||||
|
-- ---------- 预览:库中物理外键(会临时删除再重建)----------
|
||||||
|
SELECT
|
||||||
|
rc.CONSTRAINT_NAME,
|
||||||
|
rc.TABLE_NAME,
|
||||||
|
rc.REFERENCED_TABLE_NAME,
|
||||||
|
rc.UPDATE_RULE,
|
||||||
|
rc.DELETE_RULE
|
||||||
|
FROM information_schema.REFERENTIAL_CONSTRAINTS rc
|
||||||
|
WHERE rc.CONSTRAINT_SCHEMA = @db
|
||||||
|
ORDER BY rc.TABLE_NAME, rc.CONSTRAINT_NAME;
|
||||||
|
|
||||||
|
DROP PROCEDURE IF EXISTS `sp_drop_all_unique_indexes`;
|
||||||
|
DELIMITER $$
|
||||||
|
CREATE PROCEDURE `sp_drop_all_unique_indexes`()
|
||||||
|
BEGIN
|
||||||
|
DECLARE done INT DEFAULT 0;
|
||||||
|
DECLARE v_fk_name VARCHAR(128);
|
||||||
|
DECLARE v_fk_table VARCHAR(128);
|
||||||
|
DECLARE v_ref_table VARCHAR(128);
|
||||||
|
DECLARE v_update VARCHAR(16);
|
||||||
|
DECLARE v_delete VARCHAR(16);
|
||||||
|
DECLARE v_cols TEXT;
|
||||||
|
DECLARE v_ref_cols TEXT;
|
||||||
|
DECLARE v_table VARCHAR(128);
|
||||||
|
DECLARE v_index VARCHAR(128);
|
||||||
|
DECLARE v_index_cols TEXT;
|
||||||
|
DECLARE v_sql TEXT;
|
||||||
|
|
||||||
|
-- 1) 备份当前库全部外键定义
|
||||||
|
DROP TEMPORARY TABLE IF EXISTS `_tmp_fk_backup`;
|
||||||
|
CREATE TEMPORARY TABLE `_tmp_fk_backup` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`constraint_name` VARCHAR(128) NOT NULL,
|
||||||
|
`table_name` VARCHAR(128) NOT NULL,
|
||||||
|
`column_names` TEXT NOT NULL,
|
||||||
|
`ref_table_name` VARCHAR(128) NOT NULL,
|
||||||
|
`ref_column_names` TEXT NOT NULL,
|
||||||
|
`update_rule` VARCHAR(16) NOT NULL,
|
||||||
|
`delete_rule` VARCHAR(16) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `_tmp_fk_backup` (
|
||||||
|
`constraint_name`, `table_name`, `column_names`,
|
||||||
|
`ref_table_name`, `ref_column_names`, `update_rule`, `delete_rule`
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
kcu.CONSTRAINT_NAME,
|
||||||
|
kcu.TABLE_NAME,
|
||||||
|
GROUP_CONCAT(CONCAT('`', kcu.COLUMN_NAME, '`') ORDER BY kcu.ORDINAL_POSITION SEPARATOR ','),
|
||||||
|
kcu.REFERENCED_TABLE_NAME,
|
||||||
|
GROUP_CONCAT(CONCAT('`', kcu.REFERENCED_COLUMN_NAME, '`') ORDER BY kcu.ORDINAL_POSITION SEPARATOR ','),
|
||||||
|
rc.UPDATE_RULE,
|
||||||
|
rc.DELETE_RULE
|
||||||
|
FROM information_schema.KEY_COLUMN_USAGE kcu
|
||||||
|
INNER JOIN information_schema.REFERENTIAL_CONSTRAINTS rc
|
||||||
|
ON rc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA
|
||||||
|
AND rc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
|
||||||
|
AND rc.TABLE_NAME = kcu.TABLE_NAME
|
||||||
|
WHERE kcu.TABLE_SCHEMA = DATABASE()
|
||||||
|
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
|
||||||
|
GROUP BY
|
||||||
|
kcu.CONSTRAINT_NAME,
|
||||||
|
kcu.TABLE_NAME,
|
||||||
|
kcu.REFERENCED_TABLE_NAME,
|
||||||
|
rc.UPDATE_RULE,
|
||||||
|
rc.DELETE_RULE;
|
||||||
|
|
||||||
|
-- 2) 删除全部外键(解除对唯一索引的占用)
|
||||||
|
BEGIN
|
||||||
|
DECLARE cur_fk_drop CURSOR FOR
|
||||||
|
SELECT `constraint_name`, `table_name` FROM `_tmp_fk_backup` ORDER BY `id`;
|
||||||
|
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
|
||||||
|
SET done = 0;
|
||||||
|
OPEN cur_fk_drop;
|
||||||
|
fk_drop_loop: LOOP
|
||||||
|
FETCH cur_fk_drop INTO v_fk_name, v_fk_table;
|
||||||
|
IF done = 1 THEN
|
||||||
|
LEAVE fk_drop_loop;
|
||||||
|
END IF;
|
||||||
|
SET v_sql = CONCAT('ALTER TABLE `', v_fk_table, '` DROP FOREIGN KEY `', v_fk_name, '`');
|
||||||
|
SET @ddl := v_sql;
|
||||||
|
PREPARE stmt FROM @ddl;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
END LOOP;
|
||||||
|
CLOSE cur_fk_drop;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- 3) 唯一索引 → 同名普通 INDEX(去掉唯一性,保留列与查询能力,便于外键重建)
|
||||||
|
BEGIN
|
||||||
|
DECLARE cur_uk CURSOR FOR
|
||||||
|
SELECT
|
||||||
|
s.TABLE_NAME,
|
||||||
|
s.INDEX_NAME,
|
||||||
|
GROUP_CONCAT(CONCAT('`', s.COLUMN_NAME, '`') ORDER BY s.SEQ_IN_INDEX SEPARATOR ',')
|
||||||
|
FROM information_schema.STATISTICS s
|
||||||
|
WHERE s.TABLE_SCHEMA = DATABASE()
|
||||||
|
AND s.NON_UNIQUE = 0
|
||||||
|
AND s.INDEX_NAME <> 'PRIMARY'
|
||||||
|
GROUP BY s.TABLE_NAME, s.INDEX_NAME
|
||||||
|
ORDER BY s.TABLE_NAME, s.INDEX_NAME;
|
||||||
|
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
|
||||||
|
SET done = 0;
|
||||||
|
OPEN cur_uk;
|
||||||
|
uk_loop: LOOP
|
||||||
|
FETCH cur_uk INTO v_table, v_index, v_index_cols;
|
||||||
|
IF done = 1 THEN
|
||||||
|
LEAVE uk_loop;
|
||||||
|
END IF;
|
||||||
|
SET v_sql = CONCAT(
|
||||||
|
'ALTER TABLE `', v_table, '` DROP INDEX `', v_index,
|
||||||
|
'`, ADD INDEX `', v_index, '` (', v_index_cols, ')'
|
||||||
|
);
|
||||||
|
SET @ddl := v_sql;
|
||||||
|
PREPARE stmt FROM @ddl;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
END LOOP;
|
||||||
|
CLOSE cur_uk;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- 4) 按备份重建外键
|
||||||
|
-- 若原外键引用的是「非主键唯一键」,去掉唯一性后 InnoDB 可能拒绝重建,将跳过并写入警告表
|
||||||
|
DROP TEMPORARY TABLE IF EXISTS `_tmp_fk_restore_fail`;
|
||||||
|
CREATE TEMPORARY TABLE `_tmp_fk_restore_fail` (
|
||||||
|
`constraint_name` VARCHAR(128) NOT NULL,
|
||||||
|
`table_name` VARCHAR(128) NOT NULL,
|
||||||
|
`error_msg` TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
DECLARE cur_fk_add CURSOR FOR
|
||||||
|
SELECT
|
||||||
|
`constraint_name`, `table_name`, `column_names`,
|
||||||
|
`ref_table_name`, `ref_column_names`, `update_rule`, `delete_rule`
|
||||||
|
FROM `_tmp_fk_backup`
|
||||||
|
ORDER BY `id`;
|
||||||
|
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
|
||||||
|
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
|
||||||
|
BEGIN
|
||||||
|
GET DIAGNOSTICS CONDITION 1 @fk_err = MESSAGE_TEXT;
|
||||||
|
INSERT INTO `_tmp_fk_restore_fail` (`constraint_name`, `table_name`, `error_msg`)
|
||||||
|
VALUES (v_fk_name, v_fk_table, @fk_err);
|
||||||
|
END;
|
||||||
|
SET done = 0;
|
||||||
|
OPEN cur_fk_add;
|
||||||
|
fk_add_loop: LOOP
|
||||||
|
FETCH cur_fk_add INTO
|
||||||
|
v_fk_name, v_fk_table, v_cols, v_ref_table, v_ref_cols, v_update, v_delete;
|
||||||
|
IF done = 1 THEN
|
||||||
|
LEAVE fk_add_loop;
|
||||||
|
END IF;
|
||||||
|
SET v_sql = CONCAT(
|
||||||
|
'ALTER TABLE `', v_fk_table,
|
||||||
|
'` ADD CONSTRAINT `', v_fk_name,
|
||||||
|
'` FOREIGN KEY (', v_cols,
|
||||||
|
') REFERENCES `', v_ref_table, '` (', v_ref_cols,
|
||||||
|
') ON UPDATE ', v_update,
|
||||||
|
' ON DELETE ', v_delete
|
||||||
|
);
|
||||||
|
SET @ddl := v_sql;
|
||||||
|
PREPARE stmt FROM @ddl;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
END LOOP;
|
||||||
|
CLOSE cur_fk_add;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- 输出重建失败的外键(通常因引用侧不再是 UNIQUE/PRIMARY)
|
||||||
|
SELECT * FROM `_tmp_fk_restore_fail`;
|
||||||
|
|
||||||
|
DROP TEMPORARY TABLE IF EXISTS `_tmp_fk_backup`;
|
||||||
|
DROP TEMPORARY TABLE IF EXISTS `_tmp_fk_restore_fail`;
|
||||||
|
END$$
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
CALL `sp_drop_all_unique_indexes`();
|
||||||
|
DROP PROCEDURE IF EXISTS `sp_drop_all_unique_indexes`;
|
||||||
|
|
||||||
|
-- ---------- 校验:唯一索引应无结果(PRIMARY 除外)----------
|
||||||
|
SELECT
|
||||||
|
TABLE_NAME,
|
||||||
|
INDEX_NAME,
|
||||||
|
GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS columns
|
||||||
|
FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @db
|
||||||
|
AND NON_UNIQUE = 0
|
||||||
|
AND INDEX_NAME <> 'PRIMARY'
|
||||||
|
GROUP BY TABLE_NAME, INDEX_NAME
|
||||||
|
ORDER BY TABLE_NAME, INDEX_NAME;
|
||||||
42
sql/migrate_order_operation_log.sql
Normal file
42
sql/migrate_order_operation_log.sql
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- migrate_order_operation_log.sql
|
||||||
|
-- 运单操作日志表(业务动作 + 字段级 before/after diff)
|
||||||
|
-- 可重复执行
|
||||||
|
-- =============================================================================
|
||||||
|
SET @db := DATABASE();
|
||||||
|
|
||||||
|
SET @tbl := (
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = @db
|
||||||
|
AND TABLE_NAME = 't_order_operation_log'
|
||||||
|
);
|
||||||
|
SET @sql := IF(
|
||||||
|
@tbl = 0,
|
||||||
|
'CREATE TABLE `t_order_operation_log` (
|
||||||
|
`Id` INT NOT NULL AUTO_INCREMENT COMMENT ''主键'',
|
||||||
|
`OrderId` INT NOT NULL COMMENT ''运单Id'',
|
||||||
|
`ServiceNo` VARCHAR(64) NULL COMMENT ''运单号冗余'',
|
||||||
|
`Action` VARCHAR(32) NOT NULL COMMENT ''动作码 editBasic/editWeight/recalcReceivable/recalcPayable/reallocate'',
|
||||||
|
`ActionName` VARCHAR(64) NOT NULL COMMENT ''动作中文名'',
|
||||||
|
`OperatorId` INT NOT NULL DEFAULT 0 COMMENT ''操作人用户Id'',
|
||||||
|
`OperatorName` VARCHAR(64) NULL COMMENT ''操作人展示名'',
|
||||||
|
`OperatorType` VARCHAR(16) NOT NULL DEFAULT ''staff'' COMMENT ''staff/customer/system/openapi'',
|
||||||
|
`Source` VARCHAR(16) NOT NULL DEFAULT ''pc'' COMMENT ''pc/h5/openapi/job'',
|
||||||
|
`ChangesJson` JSON NULL COMMENT ''字段 diff 数组 [{path,label,before,after}]'',
|
||||||
|
`Remark` VARCHAR(512) NULL COMMENT ''补充说明'',
|
||||||
|
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT ''软删除:0-否,1-是'',
|
||||||
|
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT ''并发戳'',
|
||||||
|
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT ''创建人'',
|
||||||
|
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ''创建时间'',
|
||||||
|
`UpdateUserId` INT NULL COMMENT ''更新人'',
|
||||||
|
`UpdateTime` DATETIME NULL COMMENT ''更新时间'',
|
||||||
|
PRIMARY KEY (`Id`),
|
||||||
|
KEY `idx_ool_order_time` (`OrderId`, `CreateTime`),
|
||||||
|
KEY `idx_ool_service_no` (`ServiceNo`)
|
||||||
|
) COMMENT=''运单操作日志''',
|
||||||
|
'SELECT ''t_order_operation_log already exists'' AS info'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
44
src/Oms.Application/Dtos/Orders/OrderOperationLogDtos.cs
Normal file
44
src/Oms.Application/Dtos/Orders/OrderOperationLogDtos.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
namespace Oms.Application.Dtos.Orders;
|
||||||
|
|
||||||
|
public class OrderOperationLogChangeItem
|
||||||
|
{
|
||||||
|
public string Path { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Label { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? Before { get; set; }
|
||||||
|
|
||||||
|
public string? After { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrderOperationLogOutput
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public int OrderId { get; set; }
|
||||||
|
|
||||||
|
public string? ServiceNo { get; set; }
|
||||||
|
|
||||||
|
public string Action { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string ActionName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public int OperatorId { get; set; }
|
||||||
|
|
||||||
|
public string? OperatorName { get; set; }
|
||||||
|
|
||||||
|
public string OperatorType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Source { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public List<OrderOperationLogChangeItem> Changes { get; set; } = [];
|
||||||
|
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreateTime { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrderOperationLogListOutput
|
||||||
|
{
|
||||||
|
public List<OrderOperationLogOutput> Items { get; set; } = [];
|
||||||
|
}
|
||||||
247
src/Oms.Application/Services/Orders/OrderOperationLogSnapshot.cs
Normal file
247
src/Oms.Application/Services/Orders/OrderOperationLogSnapshot.cs
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Oms.Domain.OrdersAggregate.Consts;
|
||||||
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
|
||||||
|
namespace Oms.Application.Services.Orders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 运单操作日志白名单快照与字段 label。
|
||||||
|
/// </summary>
|
||||||
|
public static class OrderOperationLogSnapshot
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, string> Labels = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["orderStatus"] = "订单状态",
|
||||||
|
["clientNo"] = "客户单号",
|
||||||
|
["trackNo"] = "跟踪号",
|
||||||
|
["channelCode"] = "服务渠道",
|
||||||
|
["productCode"] = "销售产品",
|
||||||
|
["warehouseId"] = "仓库",
|
||||||
|
["cargoAttribute"] = "货物属性",
|
||||||
|
["isCod"] = "是否COD",
|
||||||
|
["codCurrency"] = "COD币种",
|
||||||
|
["codSum"] = "COD金额",
|
||||||
|
["declaredCurrency"] = "申报币种",
|
||||||
|
["declaredSum"] = "申报金额",
|
||||||
|
["quantity"] = "包裹数量",
|
||||||
|
["receivableSubtotal"] = "应收小计",
|
||||||
|
["payableAmount"] = "应付金额",
|
||||||
|
["downstreamServiceNo"] = "服务商单号",
|
||||||
|
["allocationScheme"] = "排货方案",
|
||||||
|
["isPaid"] = "是否已支付",
|
||||||
|
["sender.name"] = "发件人姓名",
|
||||||
|
["sender.phone"] = "发件人电话",
|
||||||
|
["sender.country"] = "发件国家",
|
||||||
|
["sender.province"] = "发件省/州",
|
||||||
|
["sender.city"] = "发件城市",
|
||||||
|
["sender.zipCode"] = "发件邮编",
|
||||||
|
["sender.address1"] = "发件地址1",
|
||||||
|
["sender.address2"] = "发件地址2",
|
||||||
|
["receiver.name"] = "收件人姓名",
|
||||||
|
["receiver.phone"] = "收件人电话",
|
||||||
|
["receiver.country"] = "收件国家",
|
||||||
|
["receiver.province"] = "收件省/州",
|
||||||
|
["receiver.city"] = "收件城市",
|
||||||
|
["receiver.zipCode"] = "收件邮编",
|
||||||
|
["receiver.address1"] = "收件地址1",
|
||||||
|
["receiver.address2"] = "收件地址2",
|
||||||
|
["fee.receivableTotal"] = "应收合计",
|
||||||
|
["fee.payableTotal"] = "应付合计",
|
||||||
|
};
|
||||||
|
|
||||||
|
public static Dictionary<string, string?> CaptureOrder(Order order)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["orderStatus"] = Norm(order.OrderStatus),
|
||||||
|
["clientNo"] = Norm(order.ClientNo),
|
||||||
|
["trackNo"] = Norm(order.TrackNo),
|
||||||
|
["channelCode"] = Norm(order.ChannelCode),
|
||||||
|
["productCode"] = Norm(order.ProductCode),
|
||||||
|
["warehouseId"] = FormatInt(order.WarehouseId),
|
||||||
|
["cargoAttribute"] = Norm(order.CargoAttribute),
|
||||||
|
["isCod"] = FormatByte(order.IsCod),
|
||||||
|
["codCurrency"] = Norm(order.CodCurrency),
|
||||||
|
["codSum"] = FormatDecimal(order.CodSum),
|
||||||
|
["declaredCurrency"] = Norm(order.DeclaredCurrency),
|
||||||
|
["declaredSum"] = FormatDecimal(order.DeclaredSum),
|
||||||
|
["quantity"] = FormatInt(order.Quantity),
|
||||||
|
["receivableSubtotal"] = FormatDecimal(order.ReceivableSubtotal),
|
||||||
|
["payableAmount"] = FormatDecimal(order.PayableAmount),
|
||||||
|
["downstreamServiceNo"] = Norm(order.DownstreamServiceNo),
|
||||||
|
["allocationScheme"] = Norm(order.AllocationScheme),
|
||||||
|
["isPaid"] = FormatByte(order.IsPaid),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Dictionary<string, string?> CaptureSender(OrderSender? sender)
|
||||||
|
{
|
||||||
|
if (sender == null)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["sender.name"] = Norm(sender.SenderName),
|
||||||
|
["sender.phone"] = Norm(sender.Phone),
|
||||||
|
["sender.country"] = FormatInt(sender.Country),
|
||||||
|
["sender.province"] = Norm(sender.Province),
|
||||||
|
["sender.city"] = Norm(sender.City),
|
||||||
|
["sender.zipCode"] = Norm(sender.ZipCode),
|
||||||
|
["sender.address1"] = Norm(sender.StreetAddress1),
|
||||||
|
["sender.address2"] = Norm(sender.StreetAddress2),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Dictionary<string, string?> CaptureReceiver(OrderRecipient? recipient)
|
||||||
|
{
|
||||||
|
if (recipient == null)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["receiver.name"] = Norm(recipient.RecipientsName),
|
||||||
|
["receiver.phone"] = Norm(recipient.Phone),
|
||||||
|
["receiver.country"] = FormatInt(recipient.Country),
|
||||||
|
["receiver.province"] = Norm(recipient.Province),
|
||||||
|
["receiver.city"] = Norm(recipient.City),
|
||||||
|
["receiver.zipCode"] = Norm(recipient.ZipCode),
|
||||||
|
["receiver.address1"] = Norm(recipient.StreetAddress1),
|
||||||
|
["receiver.address2"] = Norm(recipient.StreetAddress2),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Dictionary<string, string?> CaptureWeights(IEnumerable<OrderWeight> weights)
|
||||||
|
{
|
||||||
|
var map = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var row in weights)
|
||||||
|
{
|
||||||
|
var type = string.IsNullOrWhiteSpace(row.Type) ? "unknown" : row.Type.Trim().ToLowerInvariant();
|
||||||
|
var prefix = $"weight.{type}";
|
||||||
|
map[$"{prefix}.actualWeight"] = FormatDecimal(row.ActualWeight);
|
||||||
|
map[$"{prefix}.length"] = FormatDecimal(row.Length);
|
||||||
|
map[$"{prefix}.width"] = FormatDecimal(row.Width);
|
||||||
|
map[$"{prefix}.height"] = FormatDecimal(row.Height);
|
||||||
|
map[$"{prefix}.chargeableWeight"] = FormatDecimal(row.ChargeableWeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Dictionary<string, string?> CaptureFeeTotals(IEnumerable<OrderFee> fees)
|
||||||
|
{
|
||||||
|
var list = fees.ToList();
|
||||||
|
var receivable = list
|
||||||
|
.Where(x => string.Equals(x.Type, OrderFeeTypeCodes.Receivable, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Sum(x => x.Amount);
|
||||||
|
var payable = list
|
||||||
|
.Where(x => string.Equals(x.Type, OrderFeeTypeCodes.Payable, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Sum(x => x.Amount);
|
||||||
|
|
||||||
|
return new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["fee.receivableTotal"] = FormatDecimal(receivable),
|
||||||
|
["fee.payableTotal"] = FormatDecimal(payable),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Dictionary<string, string?> Merge(params IReadOnlyDictionary<string, string?>[] parts)
|
||||||
|
{
|
||||||
|
var map = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var part in parts)
|
||||||
|
{
|
||||||
|
foreach (var (key, value) in part)
|
||||||
|
{
|
||||||
|
map[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<(string Path, string Label, string? Before, string? After)> Diff(
|
||||||
|
IReadOnlyDictionary<string, string?> before,
|
||||||
|
IReadOnlyDictionary<string, string?> after)
|
||||||
|
{
|
||||||
|
var keys = before.Keys.Union(after.Keys, StringComparer.OrdinalIgnoreCase).OrderBy(x => x).ToList();
|
||||||
|
var changes = new List<(string, string, string?, string?)>();
|
||||||
|
foreach (var key in keys)
|
||||||
|
{
|
||||||
|
before.TryGetValue(key, out var b);
|
||||||
|
after.TryGetValue(key, out var a);
|
||||||
|
b = Norm(b);
|
||||||
|
a = Norm(a);
|
||||||
|
if (string.Equals(b, a, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
changes.Add((key, ResolveLabel(key), b, a));
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ResolveLabel(string path)
|
||||||
|
{
|
||||||
|
if (Labels.TryGetValue(path, out var label))
|
||||||
|
{
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.StartsWith("weight.", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var parts = path.Split('.');
|
||||||
|
if (parts.Length >= 3)
|
||||||
|
{
|
||||||
|
var typeName = parts[1] switch
|
||||||
|
{
|
||||||
|
"forecast" => "预报",
|
||||||
|
"receive" => "收货",
|
||||||
|
"ship" => "发货",
|
||||||
|
"downstream" => "对账",
|
||||||
|
_ => parts[1]
|
||||||
|
};
|
||||||
|
var fieldName = parts[2] switch
|
||||||
|
{
|
||||||
|
"actualWeight" => "实重",
|
||||||
|
"length" => "长",
|
||||||
|
"width" => "宽",
|
||||||
|
"height" => "高",
|
||||||
|
"chargeableWeight" => "计费重",
|
||||||
|
_ => parts[2]
|
||||||
|
};
|
||||||
|
return $"{typeName}{fieldName}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Norm(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? FormatDecimal(decimal? value)
|
||||||
|
{
|
||||||
|
if (!value.HasValue)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.Value.ToString("0.####", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? FormatInt(int? value) => value.HasValue ? value.Value.ToString(CultureInfo.InvariantCulture) : null;
|
||||||
|
|
||||||
|
private static string FormatByte(byte value) => value == 0 ? "0" : "1";
|
||||||
|
}
|
||||||
132
src/Oms.Application/Services/Orders/OrderOperationLogWriter.cs
Normal file
132
src/Oms.Application/Services/Orders/OrderOperationLogWriter.cs
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
using Fake.DependencyInjection;
|
||||||
|
using Fake.Security.Claims;
|
||||||
|
using Fake.Users;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Oms.Application.Dtos.Orders;
|
||||||
|
using Oms.Application.Shared;
|
||||||
|
using Oms.Domain.OrdersAggregate.Consts;
|
||||||
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
using Oms.Domain.OrdersAggregate.Repositories;
|
||||||
|
|
||||||
|
namespace Oms.Application.Services.Orders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 运单操作日志写入器:字段 diff 落库;失败不影响主业务。
|
||||||
|
/// </summary>
|
||||||
|
public class OrderOperationLogWriter : IScopedDependency
|
||||||
|
{
|
||||||
|
private readonly IOrderOperationLogRepository _repository;
|
||||||
|
private readonly ICurrentUser _currentUser;
|
||||||
|
private readonly OmsRbacUserDisplayNameResolver _userDisplayNameResolver;
|
||||||
|
private readonly ILogger<OrderOperationLogWriter> _logger;
|
||||||
|
|
||||||
|
public OrderOperationLogWriter(
|
||||||
|
IOrderOperationLogRepository repository,
|
||||||
|
ICurrentUser currentUser,
|
||||||
|
OmsRbacUserDisplayNameResolver userDisplayNameResolver,
|
||||||
|
ILogger<OrderOperationLogWriter> logger)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
_currentUser = currentUser;
|
||||||
|
_userDisplayNameResolver = userDisplayNameResolver;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task TryWriteAsync(
|
||||||
|
Order order,
|
||||||
|
string action,
|
||||||
|
IReadOnlyDictionary<string, string?> before,
|
||||||
|
IReadOnlyDictionary<string, string?> after,
|
||||||
|
string? remark = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var changes = OrderOperationLogSnapshot.Diff(before, after);
|
||||||
|
var operatorId = 0;
|
||||||
|
string? operatorName = null;
|
||||||
|
if (_currentUser.IsAuthenticated)
|
||||||
|
{
|
||||||
|
operatorId = _currentUser.GetId<int>();
|
||||||
|
operatorName = await _userDisplayNameResolver.ResolveAsync(operatorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
var entity = new OrderOperationLog
|
||||||
|
{
|
||||||
|
OrderId = order.Id,
|
||||||
|
ServiceNo = order.ServiceNo,
|
||||||
|
Action = action,
|
||||||
|
ActionName = OrderOperationActionCodes.GetActionName(action),
|
||||||
|
OperatorId = operatorId,
|
||||||
|
OperatorName = operatorName,
|
||||||
|
OperatorType = "staff",
|
||||||
|
Source = "pc",
|
||||||
|
ChangesJson = JsonConvert.SerializeObject(
|
||||||
|
changes.Select(x => new OrderOperationLogChangeItem
|
||||||
|
{
|
||||||
|
Path = x.Path,
|
||||||
|
Label = x.Label,
|
||||||
|
Before = x.Before,
|
||||||
|
After = x.After
|
||||||
|
}).ToList()),
|
||||||
|
Remark = string.IsNullOrWhiteSpace(remark) ? null : remark.Trim(),
|
||||||
|
CreateUserId = operatorId,
|
||||||
|
CreateTime = DateTime.Now
|
||||||
|
};
|
||||||
|
|
||||||
|
await _repository.InsertAsync(entity);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
ex,
|
||||||
|
"写入运单操作日志失败 OrderId={OrderId} Action={Action}",
|
||||||
|
order.Id,
|
||||||
|
action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OrderOperationLogListOutput> ListByOrderIdAsync(int orderId)
|
||||||
|
{
|
||||||
|
var rows = await _repository.GetListAsync(x => x.OrderId == orderId);
|
||||||
|
var items = rows
|
||||||
|
.OrderByDescending(x => x.CreateTime)
|
||||||
|
.ThenByDescending(x => x.Id)
|
||||||
|
.Select(MapOutput)
|
||||||
|
.ToList();
|
||||||
|
return new OrderOperationLogListOutput { Items = items };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OrderOperationLogOutput MapOutput(OrderOperationLog row)
|
||||||
|
{
|
||||||
|
var changes = new List<OrderOperationLogChangeItem>();
|
||||||
|
if (!string.IsNullOrWhiteSpace(row.ChangesJson))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
changes = JsonConvert.DeserializeObject<List<OrderOperationLogChangeItem>>(row.ChangesJson)
|
||||||
|
?? [];
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
changes = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OrderOperationLogOutput
|
||||||
|
{
|
||||||
|
Id = row.Id,
|
||||||
|
OrderId = row.OrderId,
|
||||||
|
ServiceNo = row.ServiceNo,
|
||||||
|
Action = row.Action,
|
||||||
|
ActionName = row.ActionName,
|
||||||
|
OperatorId = row.OperatorId,
|
||||||
|
OperatorName = row.OperatorName,
|
||||||
|
OperatorType = row.OperatorType,
|
||||||
|
Source = row.Source,
|
||||||
|
Changes = changes,
|
||||||
|
Remark = row.Remark,
|
||||||
|
CreateTime = row.CreateTime
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Oms.Application.Dtos.Orders;
|
||||||
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
|
||||||
|
namespace Oms.Application.Services.Orders;
|
||||||
|
|
||||||
|
public partial class OrderService
|
||||||
|
{
|
||||||
|
private OrderOperationLogWriter OperationLogWriter =>
|
||||||
|
LazyServiceProvider.GetRequiredService<OrderOperationLogWriter>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 运单操作日志列表(按时间倒序)。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("/order/operation-log/list")]
|
||||||
|
public async Task<OrderOperationLogListOutput> GetOperationLogListAsync([FromQuery] int orderId)
|
||||||
|
{
|
||||||
|
await GetOrderOrThrowAsync(orderId);
|
||||||
|
return await OperationLogWriter.ListByOrderIdAsync(orderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Dictionary<string, string?>> CaptureOrderAuditSnapshotAsync(
|
||||||
|
Order order,
|
||||||
|
bool includeParties = false,
|
||||||
|
bool includeWeights = false,
|
||||||
|
bool includeFees = false)
|
||||||
|
{
|
||||||
|
var parts = new List<IReadOnlyDictionary<string, string?>>
|
||||||
|
{
|
||||||
|
OrderOperationLogSnapshot.CaptureOrder(order)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (includeParties)
|
||||||
|
{
|
||||||
|
var senders = await OrderRepository.GetSendersByOrderIdsAsync([order.Id]);
|
||||||
|
var recipients = await OrderRepository.GetRecipientsByOrderIdsAsync([order.Id]);
|
||||||
|
parts.Add(OrderOperationLogSnapshot.CaptureSender(senders.FirstOrDefault()));
|
||||||
|
parts.Add(OrderOperationLogSnapshot.CaptureReceiver(recipients.FirstOrDefault()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeWeights)
|
||||||
|
{
|
||||||
|
var weights = await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]);
|
||||||
|
parts.Add(OrderOperationLogSnapshot.CaptureWeights(weights));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeFees)
|
||||||
|
{
|
||||||
|
var fees = await OrderRepository.GetFeesByOrderIdsAsync([order.Id]);
|
||||||
|
parts.Add(OrderOperationLogSnapshot.CaptureFeeTotals(fees));
|
||||||
|
}
|
||||||
|
|
||||||
|
return OrderOperationLogSnapshot.Merge(parts.ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -28,6 +28,7 @@ public partial class OrderService
|
|||||||
public async Task<OrderOutput> UpdateOrderDetailAsync(OrderUpdateInput input)
|
public async Task<OrderOutput> UpdateOrderDetailAsync(OrderUpdateInput input)
|
||||||
{
|
{
|
||||||
var order = await GetOrderOrThrowAsync(input.Id);
|
var order = await GetOrderOrThrowAsync(input.Id);
|
||||||
|
var beforeSnap = await CaptureOrderAuditSnapshotAsync(order, includeParties: true);
|
||||||
|
|
||||||
if (input.ClientNo != null)
|
if (input.ClientNo != null)
|
||||||
{
|
{
|
||||||
@ -134,6 +135,16 @@ public partial class OrderService
|
|||||||
input.Items.Select(x => MapDeclarationInput(order.Id, x)));
|
input.Items.Select(x => MapDeclarationInput(order.Id, x)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
order = await GetOrderOrThrowAsync(order.Id);
|
||||||
|
var afterSnap = await CaptureOrderAuditSnapshotAsync(order, includeParties: true);
|
||||||
|
var remark = input.Items != null ? "申报信息已更新" : null;
|
||||||
|
await OperationLogWriter.TryWriteAsync(
|
||||||
|
order,
|
||||||
|
OrderOperationActionCodes.EditBasic,
|
||||||
|
beforeSnap,
|
||||||
|
afterSnap,
|
||||||
|
remark);
|
||||||
|
|
||||||
return (await MapOrderOutputsAsync([order])).First();
|
return (await MapOrderOutputsAsync([order])).First();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,6 +160,7 @@ public partial class OrderService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var order = await GetOrderOrThrowAsync(input.Id);
|
var order = await GetOrderOrThrowAsync(input.Id);
|
||||||
|
var beforeSnap = await CaptureOrderAuditSnapshotAsync(order, includeWeights: true);
|
||||||
if (string.IsNullOrWhiteSpace(order.ProductCode))
|
if (string.IsNullOrWhiteSpace(order.ProductCode))
|
||||||
{
|
{
|
||||||
throw new BusinessException("订单未绑定销售产品,无法重算计费重");
|
throw new BusinessException("订单未绑定销售产品,无法重算计费重");
|
||||||
@ -224,6 +236,14 @@ public partial class OrderService
|
|||||||
order.UpdateTime = DateTime.Now;
|
order.UpdateTime = DateTime.Now;
|
||||||
await OrderRepository.UpdateAsync(order);
|
await OrderRepository.UpdateAsync(order);
|
||||||
|
|
||||||
|
order = await GetOrderOrThrowAsync(order.Id);
|
||||||
|
var afterSnap = await CaptureOrderAuditSnapshotAsync(order, includeWeights: true);
|
||||||
|
await OperationLogWriter.TryWriteAsync(
|
||||||
|
order,
|
||||||
|
OrderOperationActionCodes.EditWeight,
|
||||||
|
beforeSnap,
|
||||||
|
afterSnap);
|
||||||
|
|
||||||
return (await MapOrderOutputsAsync([order])).First();
|
return (await MapOrderOutputsAsync([order])).First();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,8 +254,16 @@ public partial class OrderService
|
|||||||
public async Task<OrderOutput> RecalculateReceivableFeesAsync(OrderActionInput input)
|
public async Task<OrderOutput> RecalculateReceivableFeesAsync(OrderActionInput input)
|
||||||
{
|
{
|
||||||
var order = await GetOrderOrThrowAsync(input.Id);
|
var order = await GetOrderOrThrowAsync(input.Id);
|
||||||
|
var beforeSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
|
||||||
EnsureReceivableMutable(order);
|
EnsureReceivableMutable(order);
|
||||||
await RecalculateReceivableCoreAsync(order);
|
await RecalculateReceivableCoreAsync(order);
|
||||||
|
order = await GetOrderOrThrowAsync(order.Id);
|
||||||
|
var afterSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
|
||||||
|
await OperationLogWriter.TryWriteAsync(
|
||||||
|
order,
|
||||||
|
OrderOperationActionCodes.RecalcReceivable,
|
||||||
|
beforeSnap,
|
||||||
|
afterSnap);
|
||||||
return (await MapOrderOutputsAsync([order])).First();
|
return (await MapOrderOutputsAsync([order])).First();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,7 +274,15 @@ public partial class OrderService
|
|||||||
public async Task<OrderOutput> RecalculatePayableFeesAsync(OrderActionInput input)
|
public async Task<OrderOutput> RecalculatePayableFeesAsync(OrderActionInput input)
|
||||||
{
|
{
|
||||||
var order = await GetOrderOrThrowAsync(input.Id);
|
var order = await GetOrderOrThrowAsync(input.Id);
|
||||||
|
var beforeSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
|
||||||
await RecalculatePayableCoreAsync(order);
|
await RecalculatePayableCoreAsync(order);
|
||||||
|
order = await GetOrderOrThrowAsync(order.Id);
|
||||||
|
var afterSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
|
||||||
|
await OperationLogWriter.TryWriteAsync(
|
||||||
|
order,
|
||||||
|
OrderOperationActionCodes.RecalcPayable,
|
||||||
|
beforeSnap,
|
||||||
|
afterSnap);
|
||||||
return (await MapOrderOutputsAsync([order])).First();
|
return (await MapOrderOutputsAsync([order])).First();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -566,8 +602,17 @@ public partial class OrderService
|
|||||||
public async Task<OrderOutput> ReallocateAsync([FromBody] OrderReallocateInput input)
|
public async Task<OrderOutput> ReallocateAsync([FromBody] OrderReallocateInput input)
|
||||||
{
|
{
|
||||||
var order = await GetOrderOrThrowAsync(input.Id);
|
var order = await GetOrderOrThrowAsync(input.Id);
|
||||||
|
var beforeSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
|
||||||
await ReallocateCoreAsync(order, input.Scheme, input.ServiceChannelId);
|
await ReallocateCoreAsync(order, input.Scheme, input.ServiceChannelId);
|
||||||
order = await GetOrderOrThrowAsync(input.Id);
|
order = await GetOrderOrThrowAsync(input.Id);
|
||||||
|
var afterSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
|
||||||
|
var remark = string.IsNullOrWhiteSpace(input.Scheme) ? null : $"scheme={input.Scheme.Trim()}";
|
||||||
|
await OperationLogWriter.TryWriteAsync(
|
||||||
|
order,
|
||||||
|
OrderOperationActionCodes.Reallocate,
|
||||||
|
beforeSnap,
|
||||||
|
afterSnap,
|
||||||
|
remark);
|
||||||
return (await MapOrderOutputsAsync([order])).First();
|
return (await MapOrderOutputsAsync([order])).First();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -580,12 +625,22 @@ public partial class OrderService
|
|||||||
NormalizeReallocateOptions(input.Scheme, input.ServiceChannelId, out var scheme, out var channelId);
|
NormalizeReallocateOptions(input.Scheme, input.ServiceChannelId, out var scheme, out var channelId);
|
||||||
var result = new OrderBatchActionResultOutput();
|
var result = new OrderBatchActionResultOutput();
|
||||||
var orderIds = (input.OrderIds ?? []).Where(x => x > 0).Distinct().ToList();
|
var orderIds = (input.OrderIds ?? []).Where(x => x > 0).Distinct().ToList();
|
||||||
|
var remark = string.IsNullOrWhiteSpace(scheme) ? null : $"scheme={scheme}";
|
||||||
foreach (var orderId in orderIds)
|
foreach (var orderId in orderIds)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var order = await GetOrderOrThrowAsync(orderId);
|
var order = await GetOrderOrThrowAsync(orderId);
|
||||||
|
var beforeSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
|
||||||
await ReallocateCoreAsync(order, scheme, channelId);
|
await ReallocateCoreAsync(order, scheme, channelId);
|
||||||
|
order = await GetOrderOrThrowAsync(orderId);
|
||||||
|
var afterSnap = await CaptureOrderAuditSnapshotAsync(order, includeFees: true);
|
||||||
|
await OperationLogWriter.TryWriteAsync(
|
||||||
|
order,
|
||||||
|
OrderOperationActionCodes.Reallocate,
|
||||||
|
beforeSnap,
|
||||||
|
afterSnap,
|
||||||
|
remark);
|
||||||
result.SuccessCount++;
|
result.SuccessCount++;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -990,15 +1045,15 @@ public partial class OrderService
|
|||||||
|
|
||||||
private static void EnsureReallocateAllowed(Order order)
|
private static void EnsureReallocateAllowed(Order order)
|
||||||
{
|
{
|
||||||
if (order.IsPaid != 0)
|
// 已付款也可排货:只重算应付与服务渠道,不改应收。
|
||||||
{
|
if (order.OrderStatus is not (
|
||||||
throw new BusinessException("订单已付款,不允许重新排货");
|
OrderStatusCodes.Predeclared
|
||||||
}
|
or OrderStatusCodes.PendingAllocation
|
||||||
|
or OrderStatusCodes.PendingPayment
|
||||||
if (order.OrderStatus is not (OrderStatusCodes.PendingAllocation or OrderStatusCodes.PendingPayment))
|
or OrderStatusCodes.PendingShip))
|
||||||
{
|
{
|
||||||
throw new BusinessException(
|
throw new BusinessException(
|
||||||
$"订单状态为「{OmsDocHelper.GetOrderStatusName(order.OrderStatus ?? string.Empty)}」,仅待排货/待支付可重新排货");
|
$"订单状态为「{OmsDocHelper.GetOrderStatusName(order.OrderStatus ?? string.Empty)}」,仅已预报/待排货/待支付/待发货可重新排货");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
namespace Oms.Domain.OrdersAggregate.Consts;
|
||||||
|
|
||||||
|
public static class OrderOperationActionCodes
|
||||||
|
{
|
||||||
|
public const string EditBasic = "editBasic";
|
||||||
|
public const string EditWeight = "editWeight";
|
||||||
|
public const string RecalcReceivable = "recalcReceivable";
|
||||||
|
public const string RecalcPayable = "recalcPayable";
|
||||||
|
public const string Reallocate = "reallocate";
|
||||||
|
|
||||||
|
public static string GetActionName(string action) => action switch
|
||||||
|
{
|
||||||
|
EditBasic => "编辑订单详情",
|
||||||
|
EditWeight => "修改件重尺",
|
||||||
|
RecalcReceivable => "重算应收",
|
||||||
|
RecalcPayable => "重算应付",
|
||||||
|
Reallocate => "重新排货",
|
||||||
|
_ => action
|
||||||
|
};
|
||||||
|
}
|
||||||
36
src/Oms.Domain/OrdersAggregate/Entities/OrderOperationLog.cs
Normal file
36
src/Oms.Domain/OrdersAggregate/Entities/OrderOperationLog.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using Oms.Domain.Shared;
|
||||||
|
using SqlSugar;
|
||||||
|
|
||||||
|
namespace Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
|
||||||
|
[SugarTable("t_order_operation_log")]
|
||||||
|
public class OrderOperationLog : OmsAuditedAggregateRoot
|
||||||
|
{
|
||||||
|
public int OrderId { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(Length = 64, IsNullable = true)]
|
||||||
|
public string? ServiceNo { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(Length = 32)]
|
||||||
|
public string Action { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[SugarColumn(Length = 64)]
|
||||||
|
public string ActionName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public int OperatorId { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(Length = 64, IsNullable = true)]
|
||||||
|
public string? OperatorName { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(Length = 16)]
|
||||||
|
public string OperatorType { get; set; } = "staff";
|
||||||
|
|
||||||
|
[SugarColumn(Length = 16)]
|
||||||
|
public string Source { get; set; } = "pc";
|
||||||
|
|
||||||
|
[SugarColumn(ColumnDataType = "json", IsNullable = true)]
|
||||||
|
public string? ChangesJson { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(Length = 512, IsNullable = true)]
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
using Fake.Domain.Repositories;
|
||||||
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
|
||||||
|
namespace Oms.Domain.OrdersAggregate.Repositories;
|
||||||
|
|
||||||
|
public interface IOrderOperationLogRepository : IRepository<OrderOperationLog>
|
||||||
|
{
|
||||||
|
}
|
||||||
13
src/Oms.SqlSugarCore/Orders/OrderOperationLogRepository.cs
Normal file
13
src/Oms.SqlSugarCore/Orders/OrderOperationLogRepository.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using Fake.DependencyInjection;
|
||||||
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
using Oms.Domain.OrdersAggregate.Repositories;
|
||||||
|
using Oms.SqlSugarCore.Repositories;
|
||||||
|
|
||||||
|
namespace Oms.SqlSugarCore.Orders;
|
||||||
|
|
||||||
|
public class OrderOperationLogRepository
|
||||||
|
: OmsSqlSugarRepository<OrderOperationLog>,
|
||||||
|
IOrderOperationLogRepository,
|
||||||
|
IScopedDependency
|
||||||
|
{
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user