feat: 管理端列表返回客户名称/编码,并加固绑定与客户会话

订单/包裹/库存/店铺授权/店铺订单列表补充 customerName、customerCode;库存绑定按会话客户解析并兼容可用量;修复客户 mapId 识别与 ConcurrencyStamp 迁移。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-07-18 11:10:53 +08:00
parent dc3071c90d
commit f4ddd993b9
15 changed files with 283 additions and 49 deletions

5
.gitignore vendored
View File

@ -9,8 +9,13 @@ riderModule.iml
.idea/ .idea/
/.vs /.vs
/Fake.sln.DotSettings.user /Fake.sln.DotSettings.user
*.DotSettings.user
SimpleAdmin.sln.DotSettings.user
Logs/ Logs/
*.log
api-*.log
build-warnings.txt
node_modules/ node_modules/

View File

@ -1,25 +0,0 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AClient_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fc7d66b24252bb52da52d1262d99fd654ffc18aedde818f147c3daf6aee3e63_003FClient_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityMaintenance_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F38bf349295324397b2c739e7e632b4bd2d5000_003F49_003Fee705f67_003FEntityMaintenance_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIAggregateRoot_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6015bad29db142679ead208ae4ecd55c7800_003F0e_003F66feb813_003FIAggregateRoot_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIAggregateRoot_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6015bad29db142679ead208ae4ecd55c7800_003F0e_003F66feb813_003FIAggregateRoot_002Ecs_002Fz_003A2_002D0/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIAggregateRoot_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa1494e50f25d476db4fff59a546c2b857800_003F4e_003Fb27c424d_003FIAggregateRoot_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIAggregateRoot_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa1494e50f25d476db4fff59a546c2b857800_003F4e_003Fb27c424d_003FIAggregateRoot_002Ecs_002Fz_003A2_002D1/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIAggregateRoot_00601_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6015bad29db142679ead208ae4ecd55c7800_003F9a_003Fea4337c7_003FIAggregateRoot_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIAggregateRoot_00601_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa1494e50f25d476db4fff59a546c2b857800_003F7b_003Fe45647d2_003FIAggregateRoot_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIAggregateRoot_00601_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa1494e50f25d476db4fff59a546c2b857800_003F7b_003Fe45647d2_003FIAggregateRoot_00601_002Ecs_002Fz_003A2_002D1/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIEntity_00601_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa1494e50f25d476db4fff59a546c2b857800_003F59_003F8ca0c929_003FIEntity_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIRepository_00601_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6015bad29db142679ead208ae4ecd55c7800_003F85_003Fa6db724e_003FIRepository_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIRepository_00601_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F98ce6bfbfce34b1eabb7b03e820f0eea7800_003F66_003F56b27e93_003FIRepository_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIRepository_00601_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa1494e50f25d476db4fff59a546c2b857800_003Fcc_003F73f30804_003FIRepository_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObjectMethodExecutorAwaitable_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F61426bb2f5bd47ebb048d0e7051942f91de910_003Fe1_003F409ba3a2_003FObjectMethodExecutorAwaitable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AQueryBuilder_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F38bf349295324397b2c739e7e632b4bd2d5000_003Fc7_003F431a0b15_003FQueryBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AReflectionInoCacheService_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F38bf349295324397b2c739e7e632b4bd2d5000_003Fbb_003F446201ec_003FReflectionInoCacheService_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AReflectionInoCore_00601_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F38bf349295324397b2c739e7e632b4bd2d5000_003Fc1_003F8f35c8ed_003FReflectionInoCore_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASqlBuilderProvider_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F38bf349295324397b2c739e7e632b4bd2d5000_003F71_003F77c8fbe4_003FSqlBuilderProvider_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASqlSugarClient_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F38bf349295324397b2c739e7e632b4bd2d5000_003Fe8_003Ffcf6435d_003FSqlSugarClient_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASqlSugarProvider_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F38bf349295324397b2c739e7e632b4bd2d5000_003F26_003Fce9a7a77_003FSqlSugarProvider_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUnauthorizedAccessException_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F3e10648195244ebfa276c71cb5b3f877c90750_003F74_003F8418b441_003FUnauthorizedAccessException_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=9c7e285e_002D68dc_002D4e8e_002D88cb_002De8df37de5da5/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from &amp;lt;src&amp;gt;\&amp;lt;SimpleAdmin.Api.IntegrationTests&amp;gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;Project Location="D:\my\itfx\admin\src\SimpleAdmin.Api.IntegrationTests" Presentation="&amp;lt;src&amp;gt;\&amp;lt;SimpleAdmin.Api.IntegrationTests&amp;gt;" /&gt;&#xD;
&lt;/SessionState&gt;</s:String></wpf:ResourceDictionary>

View File

@ -0,0 +1,63 @@
-- 给缺少 ConcurrencyStamp 的 OMS 表补列(可重复执行)
-- Fake AggregateRoot 自带该字段,SqlSugar 查询会带上;库表没有就会报:
-- Unknown column 'ConcurrencyStamp' in 'field list'
--
-- 注意:不要用 DEFAULT (UUID())。在 STATEMENT/MIXED 复制下会触发:
-- 1674 Statement is unsafe because it uses a system function that may return a different value on the replica.
-- 默认用固定 GUID;插入/更新时由 ORM 写入真实 UUID。
--
-- 用法:在业务库执行本脚本;若仍报错,把报错 SQL 里的表名追加到下方列表。
SET @tables := 't_order_stock_bind,C_inventory,C_parcel,C_parcel_item,C_parcel_arrival,C_sku,C_bin_location,C_inbound_notice,C_inbound_box,C_inbound_box_line,C_sku_platform_map,t_sales_store,t_sales_store_order,t_sales_store_order_line,t_customer_sender_address';
DROP PROCEDURE IF EXISTS sp_add_concurrency_stamp_if_missing;
DELIMITER $$
CREATE PROCEDURE sp_add_concurrency_stamp_if_missing()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE tname VARCHAR(128);
DECLARE cur CURSOR FOR
SELECT TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(@tables, ',', n.n), ',', -1)) AS t
FROM (
SELECT a.N + b.N * 10 + 1 AS n
FROM
(SELECT 0 AS N UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) a,
(SELECT 0 AS N UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) b
) n
WHERE n.n <= 1 + (LENGTH(@tables) - LENGTH(REPLACE(@tables, ',', '')));
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN cur;
read_loop: LOOP
FETCH cur INTO tname;
IF done THEN
LEAVE read_loop;
END IF;
IF tname IS NULL OR tname = '' THEN
ITERATE read_loop;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = tname
) THEN
ITERATE read_loop;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = tname AND COLUMN_NAME = 'ConcurrencyStamp'
) THEN
SET @ddl := CONCAT(
'ALTER TABLE `', tname,
'` ADD COLUMN `ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT ''00000000-0000-0000-0000-000000000000'' COMMENT ''并发戳'''
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END IF;
END LOOP;
CLOSE cur;
END$$
DELIMITER ;
CALL sp_add_concurrency_stamp_if_missing();
DROP PROCEDURE IF EXISTS sp_add_concurrency_stamp_if_missing;

View File

@ -1,5 +1,6 @@
-- 物流单绑定包裹/库存:t_order_stock_bind + C_inventory/C_parcel_item 冻结与可用字段 -- 物流单绑定包裹/库存:t_order_stock_bind + C_inventory/C_parcel_item 冻结与可用字段
-- 见 oms系统设计-050801.md §4.2.11 / §4.4.6 -- 见 oms系统设计-050801.md §4.2.11 / §4.4.6 / §3.1.2
-- ConcurrencyStamp 默认用固定 GUID,避免 DEFAULT (UUID()) 触发复制 1674
-- 可重复执行 -- 可重复执行
CREATE TABLE IF NOT EXISTS `t_order_stock_bind` ( CREATE TABLE IF NOT EXISTS `t_order_stock_bind` (
@ -23,6 +24,7 @@ CREATE TABLE IF NOT EXISTS `t_order_stock_bind` (
`AttributeText` VARCHAR(256) NULL, `AttributeText` VARCHAR(256) NULL,
`Remark` VARCHAR(512) NULL, `Remark` VARCHAR(512) NULL,
`IsDeleted` TINYINT NOT NULL DEFAULT 0, `IsDeleted` TINYINT NOT NULL DEFAULT 0,
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000' COMMENT '并发戳',
`CreateUserId` INT NOT NULL, `CreateUserId` INT NOT NULL,
`CreateTime` DATETIME NOT NULL, `CreateTime` DATETIME NOT NULL,
`UpdateUserId` INT NULL, `UpdateUserId` INT NULL,
@ -33,6 +35,18 @@ CREATE TABLE IF NOT EXISTS `t_order_stock_bind` (
KEY `idx_osb_sku` (`CustomerId`, `WarehouseId`, `SkuId`, `Status`) KEY `idx_osb_sku` (`CustomerId`, `WarehouseId`, `SkuId`, `Status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物流单仓储绑定明细'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物流单仓储绑定明细';
-- 已建表但缺 ConcurrencyStamp 时补列(可重复执行)
SET @col_exists := (
SELECT COUNT(1) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't_order_stock_bind' AND COLUMN_NAME = 'ConcurrencyStamp'
);
SET @sql := IF(
@col_exists = 0,
'ALTER TABLE `t_order_stock_bind` ADD COLUMN `ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT ''00000000-0000-0000-0000-000000000000'' COMMENT ''并发戳'' AFTER `IsDeleted`',
'SELECT ''t_order_stock_bind.ConcurrencyStamp already exists'' AS info'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- C_inventory.FrozenQty -- C_inventory.FrozenQty
SET @col_exists := ( SET @col_exists := (
SELECT COUNT(1) FROM information_schema.COLUMNS SELECT COUNT(1) FROM information_schema.COLUMNS

View File

@ -95,6 +95,16 @@ public class OrderOutput
/// </summary> /// </summary>
public int? CustomerId { get; set; } public int? CustomerId { get; set; }
/// <summary>
/// 客户名称。
/// </summary>
public string? CustomerName { get; set; }
/// <summary>
/// 客户编码。
/// </summary>
public string? CustomerCode { get; set; }
/// <summary> /// <summary>
/// 订单来源。 /// 订单来源。
/// </summary> /// </summary>

View File

@ -24,6 +24,10 @@ public class PlatformStoreOutput
public int CustomerId { get; set; } public int CustomerId { get; set; }
public string? CustomerName { get; set; }
public string? CustomerCode { get; set; }
public string PlatformCode { get; set; } = string.Empty; public string PlatformCode { get; set; } = string.Empty;
public string PlatformName { get; set; } = string.Empty; public string PlatformName { get; set; } = string.Empty;
@ -186,6 +190,10 @@ public class PlatformStoreOrderOutput
public int CustomerId { get; set; } public int CustomerId { get; set; }
public string? CustomerName { get; set; }
public string? CustomerCode { get; set; }
public int StoreId { get; set; } public int StoreId { get; set; }
public string StoreNickname { get; set; } = string.Empty; public string StoreNickname { get; set; } = string.Empty;

View File

@ -373,6 +373,10 @@ public class WarehouseParcelOutput
public int CustomerId { get; set; } public int CustomerId { get; set; }
public string? CustomerName { get; set; }
public string? CustomerCode { get; set; }
public int WarehouseId { get; set; } public int WarehouseId { get; set; }
public string TrackingNo { get; set; } = string.Empty; public string TrackingNo { get; set; } = string.Empty;
@ -441,6 +445,12 @@ public class WarehouseInventoryListInput : WarehousePageInput
public class WarehouseInventoryOutput public class WarehouseInventoryOutput
{ {
public int CustomerId { get; set; }
public string? CustomerName { get; set; }
public string? CustomerCode { get; set; }
public int SkuId { get; set; } public int SkuId { get; set; }
public string SkuCode { get; set; } = string.Empty; public string SkuCode { get; set; } = string.Empty;

View File

@ -79,6 +79,16 @@ public partial class OrderService
var auditUserNameMap = await UserDisplayNameResolver.ResolveManyAsync( var auditUserNameMap = await UserDisplayNameResolver.ResolveManyAsync(
orders.SelectMany(x => new[] { x.CreateUserId, x.UpdateUserId })); orders.SelectMany(x => new[] { x.CreateUserId, x.UpdateUserId }));
var customerIds = orders
.Where(x => x.CustomerId is > 0)
.Select(x => x.CustomerId!.Value)
.Distinct()
.ToArray();
var customers = customerIds.Length == 0
? []
: await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id));
var customerMap = customers.ToDictionary(x => x.Id);
return orders.Select(order => return orders.Select(order =>
{ {
senderMap.TryGetValue(order.Id, out var sender); senderMap.TryGetValue(order.Id, out var sender);
@ -91,6 +101,14 @@ public partial class OrderService
var receivableAmount = feeOutputs var receivableAmount = feeOutputs
.Where(x => x.Type == OrderFeeTypeCodes.Receivable) .Where(x => x.Type == OrderFeeTypeCodes.Receivable)
.Sum(x => x.Amount); .Sum(x => x.Amount);
string? customerName = null;
string? customerCode = null;
if (order.CustomerId is > 0 && customerMap.TryGetValue(order.CustomerId.Value, out var customer))
{
customerName = customer.CustomerName;
customerCode = customer.CustomerCode;
}
return new OrderOutput return new OrderOutput
{ {
Id = order.Id, Id = order.Id,
@ -119,6 +137,8 @@ public partial class OrderService
? warehouse.WarehouseName ? warehouse.WarehouseName
: null, : null,
CustomerId = order.CustomerId, CustomerId = order.CustomerId,
CustomerName = customerName,
CustomerCode = customerCode,
OrderSource = string.IsNullOrWhiteSpace(order.OrderSource) OrderSource = string.IsNullOrWhiteSpace(order.OrderSource)
? OrderSourceCodes.Manual ? OrderSourceCodes.Manual
: order.OrderSource, : order.OrderSource,

View File

@ -52,12 +52,12 @@ public partial class OrderService
var order = await GetOrderOrThrowAsync(input.OrderId); var order = await GetOrderOrThrowAsync(input.OrderId);
EnsureOrderCanBindStock(order); EnsureOrderCanBindStock(order);
var warehouseId = ResolveBindWarehouseId(order, input.WarehouseId); var warehouseId = ResolveBindWarehouseId(order, input.WarehouseId);
var customerId = RequireOrderCustomerId(order); var customerId = await ResolveStockBindCustomerIdAsync(order);
var inventories = await StockBindInventoryRepository.GetListAsync(x => var inventories = await StockBindInventoryRepository.GetListAsync(x =>
x.CustomerId == customerId x.CustomerId == customerId
&& x.WarehouseId == warehouseId && x.WarehouseId == warehouseId
&& x.AvailableQty > 0); && (x.AvailableQty > 0 || x.Qty > x.FrozenQty));
var skuIds = inventories.Select(x => x.SkuId).Distinct().ToArray(); var skuIds = inventories.Select(x => x.SkuId).Distinct().ToArray();
var skus = skuIds.Length == 0 var skus = skuIds.Length == 0
@ -111,7 +111,7 @@ public partial class OrderService
var order = await GetOrderOrThrowAsync(input.OrderId); var order = await GetOrderOrThrowAsync(input.OrderId);
EnsureOrderCanBindStock(order); EnsureOrderCanBindStock(order);
var warehouseId = ResolveBindWarehouseId(order, input.WarehouseId); var warehouseId = ResolveBindWarehouseId(order, input.WarehouseId);
var customerId = RequireOrderCustomerId(order); var customerId = await ResolveStockBindCustomerIdAsync(order);
var lifecycle = input.LifecycleStatus?.Trim(); var lifecycle = input.LifecycleStatus?.Trim();
var filterLifecycle = !string.IsNullOrWhiteSpace(lifecycle) var filterLifecycle = !string.IsNullOrWhiteSpace(lifecycle)
@ -194,7 +194,7 @@ public partial class OrderService
var order = await GetOrderOrThrowAsync(input.OrderId); var order = await GetOrderOrThrowAsync(input.OrderId);
EnsureOrderCanBindStock(order); EnsureOrderCanBindStock(order);
await EnsureNoDeductedBindsAsync(order.Id); await EnsureNoDeductedBindsAsync(order.Id);
var customerId = RequireOrderCustomerId(order); var customerId = await ResolveStockBindCustomerIdAsync(order);
var warehouseId = ResolveBindWarehouseId(order, null); var warehouseId = ResolveBindWarehouseId(order, null);
var items = input.Items ?? []; var items = input.Items ?? [];
@ -500,7 +500,7 @@ public partial class OrderService
x.CustomerId == customerId x.CustomerId == customerId
&& x.WarehouseId == warehouseId && x.WarehouseId == warehouseId
&& x.SkuId == skuId && x.SkuId == skuId
&& x.AvailableQty > 0); && (x.AvailableQty > 0 || x.Qty > x.FrozenQty));
rows = rows.OrderBy(x => x.BinLocationId).ToList(); rows = rows.OrderBy(x => x.BinLocationId).ToList();
} }
@ -641,6 +641,23 @@ public partial class OrderService
return order.CustomerId.Value; return order.CustomerId.Value;
} }
/// <summary>
/// 绑定客户上下文:客户登录与仓储模块一致(会话客户);管理端按订单客户。
/// </summary>
private async Task<int> ResolveStockBindCustomerIdAsync(Order order)
{
var sessionCustomerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
null);
if (sessionCustomerId is > 0)
{
return sessionCustomerId.Value;
}
return RequireOrderCustomerId(order);
}
private static int ResolveBindWarehouseId(Order order, int? requestedWarehouseId) private static int ResolveBindWarehouseId(Order order, int? requestedWarehouseId)
{ {
if (requestedWarehouseId is > 0) if (requestedWarehouseId is > 0)

View File

@ -483,10 +483,19 @@ public partial class OrderService : ApplicationService
return; return;
} }
if (order.CustomerId != customerId.Value) if (order.CustomerId == customerId.Value)
{ {
throw new BusinessException("订单不存在"); return;
} }
// 兼容历史脏数据:下单人是当前客户账号,但订单 CustomerId 未写对
var mapId = OmsSessionHelper.TryGetCustomerMapId(CurrentUser);
if (mapId is > 0 && order.CreateUserId == mapId.Value)
{
return;
}
throw new BusinessException("订单不存在");
} }
private async Task<Order> GetOrderOrThrowAsync(int id) private async Task<Order> GetOrderOrThrowAsync(int id)

View File

@ -71,6 +71,12 @@ public class PlatformStoreOrderService : ApplicationService
: await OrderRepository.GetListAsync(x => linkedOrderIds.Contains(x.Id)); : await OrderRepository.GetListAsync(x => linkedOrderIds.Contains(x.Id));
var linkedOrderMap = linkedOrders.ToDictionary(x => x.Id); var linkedOrderMap = linkedOrders.ToDictionary(x => x.Id);
var customerIds = entities.Select(x => x.CustomerId).Distinct().ToArray();
var customers = customerIds.Length == 0
? []
: await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id));
var customerMap = customers.ToDictionary(x => x.Id);
return new OmsPageResult<PlatformStoreOrderOutput> return new OmsPageResult<PlatformStoreOrderOutput>
{ {
Items = entities.Select(x => Items = entities.Select(x =>
@ -82,7 +88,14 @@ public class PlatformStoreOrderService : ApplicationService
linkedServiceNo = linkedOrder.ServiceNo; linkedServiceNo = linkedOrder.ServiceNo;
} }
return MapOrderOutput(x, store?.StoreNickname ?? string.Empty, [], linkedServiceNo: linkedServiceNo); customerMap.TryGetValue(x.CustomerId, out var customer);
return MapOrderOutput(
x,
store?.StoreNickname ?? string.Empty,
[],
linkedServiceNo: linkedServiceNo,
customerName: customer?.CustomerName,
customerCode: customer?.CustomerCode);
}).ToList(), }).ToList(),
Total = total Total = total
}; };
@ -110,8 +123,16 @@ public class PlatformStoreOrderService : ApplicationService
: await SkuRepository.GetListAsync(x => skuIds.Contains(x.Id)); : await SkuRepository.GetListAsync(x => skuIds.Contains(x.Id));
var skuMap = skus.ToDictionary(x => x.Id); var skuMap = skus.ToDictionary(x => x.Id);
var linkedServiceNo = await ResolveLinkedServiceNoAsync(order.LinkedOrderId); var linkedServiceNo = await ResolveLinkedServiceNoAsync(order.LinkedOrderId);
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId);
return MapOrderOutput(order, store?.StoreNickname ?? string.Empty, lines, skuMap, linkedServiceNo); return MapOrderOutput(
order,
store?.StoreNickname ?? string.Empty,
lines,
skuMap,
linkedServiceNo,
customer?.CustomerName,
customer?.CustomerCode);
} }
/// <summary> /// <summary>
@ -692,11 +713,15 @@ public class PlatformStoreOrderService : ApplicationService
string storeNickname, string storeNickname,
IReadOnlyCollection<SalesStoreOrderLine> lines, IReadOnlyCollection<SalesStoreOrderLine> lines,
IReadOnlyDictionary<int, Domain.WarehouseAggregate.Entities.CSku>? skuMap = null, IReadOnlyDictionary<int, Domain.WarehouseAggregate.Entities.CSku>? skuMap = null,
string? linkedServiceNo = null) => string? linkedServiceNo = null,
string? customerName = null,
string? customerCode = null) =>
new() new()
{ {
Id = order.Id, Id = order.Id,
CustomerId = order.CustomerId, CustomerId = order.CustomerId,
CustomerName = customerName,
CustomerCode = customerCode,
StoreId = order.StoreId, StoreId = order.StoreId,
StoreNickname = storeNickname, StoreNickname = storeNickname,
PlatformCode = order.PlatformCode, PlatformCode = order.PlatformCode,

View File

@ -43,9 +43,15 @@ public class PlatformStoreService : ApplicationService
new Dictionary<string, bool> { [nameof(SalesStore.UpdateTime)] = false, [nameof(SalesStore.Id)] = false }); new Dictionary<string, bool> { [nameof(SalesStore.UpdateTime)] = false, [nameof(SalesStore.Id)] = false });
var total = await StoreRepository.CountAsync(predicate); var total = await StoreRepository.CountAsync(predicate);
var customerIds = entities.Select(x => x.CustomerId).Distinct().ToArray();
var customers = customerIds.Length == 0
? []
: await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id));
var customerMap = customers.ToDictionary(x => x.Id);
return new OmsPageResult<PlatformStoreOutput> return new OmsPageResult<PlatformStoreOutput>
{ {
Items = entities.Select(MapOutput).ToList(), Items = entities.Select(x => MapOutput(x, customerMap)).ToList(),
Total = total Total = total
}; };
} }
@ -445,11 +451,24 @@ public class PlatformStoreService : ApplicationService
}; };
} }
private static PlatformStoreOutput MapOutput(SalesStore entity) => private static PlatformStoreOutput MapOutput(
new() SalesStore entity,
IReadOnlyDictionary<int, Domain.CustomersAggregate.Entities.Customer>? customerMap = null)
{
string? customerName = null;
string? customerCode = null;
if (customerMap != null && customerMap.TryGetValue(entity.CustomerId, out var customer))
{
customerName = customer.CustomerName;
customerCode = customer.CustomerCode;
}
return new PlatformStoreOutput
{ {
Id = entity.Id, Id = entity.Id,
CustomerId = entity.CustomerId, CustomerId = entity.CustomerId,
CustomerName = customerName,
CustomerCode = customerCode,
PlatformCode = entity.PlatformCode, PlatformCode = entity.PlatformCode,
PlatformName = ResolvePlatformName(entity.PlatformCode), PlatformName = ResolvePlatformName(entity.PlatformCode),
StoreNickname = entity.StoreNickname, StoreNickname = entity.StoreNickname,
@ -462,6 +481,7 @@ public class PlatformStoreService : ApplicationService
Remark = entity.Remark, Remark = entity.Remark,
CreateTime = entity.CreateTime CreateTime = entity.CreateTime
}; };
}
private static string ResolvePlatformName(string platformCode) => platformCode.ToLowerInvariant() switch private static string ResolvePlatformName(string platformCode) => platformCode.ToLowerInvariant() switch
{ {

View File

@ -32,14 +32,24 @@ public partial class WarehouseService
(!hasCustomerFilter || x.CustomerId == customerIdValue) && skuIds.Contains(x.Id)); (!hasCustomerFilter || x.CustomerId == customerIdValue) && skuIds.Contains(x.Id));
var skuMap = skus.ToDictionary(x => x.Id); var skuMap = skus.ToDictionary(x => x.Id);
var customerIds = inventories.Select(x => x.CustomerId).Distinct().ToArray();
var customers = customerIds.Length == 0
? []
: await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id));
var customerMap = customers.ToDictionary(x => x.Id);
var keyword = input.Keyword?.Trim(); var keyword = input.Keyword?.Trim();
var grouped = inventories var grouped = inventories
.GroupBy(x => new { x.SkuId, x.WarehouseId }) .GroupBy(x => new { x.CustomerId, x.SkuId, x.WarehouseId })
.Select(g => .Select(g =>
{ {
skuMap.TryGetValue(g.Key.SkuId, out var sku); skuMap.TryGetValue(g.Key.SkuId, out var sku);
customerMap.TryGetValue(g.Key.CustomerId, out var customer);
return new WarehouseInventoryOutput return new WarehouseInventoryOutput
{ {
CustomerId = g.Key.CustomerId,
CustomerName = customer?.CustomerName,
CustomerCode = customer?.CustomerCode,
SkuId = g.Key.SkuId, SkuId = g.Key.SkuId,
SkuCode = sku?.SkuCode ?? string.Empty, SkuCode = sku?.SkuCode ?? string.Empty,
SkuName = sku?.SkuName ?? string.Empty, SkuName = sku?.SkuName ?? string.Empty,
@ -53,8 +63,11 @@ public partial class WarehouseService
.Where(x => .Where(x =>
string.IsNullOrWhiteSpace(keyword) string.IsNullOrWhiteSpace(keyword)
|| x.SkuCode.Contains(keyword!, StringComparison.OrdinalIgnoreCase) || x.SkuCode.Contains(keyword!, StringComparison.OrdinalIgnoreCase)
|| x.SkuName.Contains(keyword!, StringComparison.OrdinalIgnoreCase)) || x.SkuName.Contains(keyword!, StringComparison.OrdinalIgnoreCase)
.OrderBy(x => x.SkuCode) || (x.CustomerName?.Contains(keyword!, StringComparison.OrdinalIgnoreCase) ?? false)
|| (x.CustomerCode?.Contains(keyword!, StringComparison.OrdinalIgnoreCase) ?? false))
.OrderBy(x => x.CustomerCode)
.ThenBy(x => x.SkuCode)
.ThenBy(x => x.WarehouseId) .ThenBy(x => x.WarehouseId)
.ToList(); .ToList();

View File

@ -55,9 +55,25 @@ public partial class WarehouseService
? [] ? []
: await ParcelItemRepository.GetListAsync(x => parcelIds.Contains(x.ParcelId)); : await ParcelItemRepository.GetListAsync(x => parcelIds.Contains(x.ParcelId));
var customerIds = entities.Select(x => x.CustomerId).Distinct().ToArray();
var customers = customerIds.Length == 0
? []
: await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id));
var customerMap = customers.ToDictionary(x => x.Id);
return new OmsPageResult<WarehouseParcelOutput> return new OmsPageResult<WarehouseParcelOutput>
{ {
Items = entities.Select(x => MapParcelOutput(x, items.Where(i => i.ParcelId == x.Id))).ToList(), Items = entities.Select(x =>
{
var output = MapParcelOutput(x, items.Where(i => i.ParcelId == x.Id));
if (customerMap.TryGetValue(x.CustomerId, out var customer))
{
output.CustomerName = customer.CustomerName;
output.CustomerCode = customer.CustomerCode;
}
return output;
}).ToList(),
Total = total Total = total
}; };
} }

View File

@ -1,3 +1,4 @@
using System.Security.Claims;
using Fake.Application; using Fake.Application;
using Fake.Users; using Fake.Users;
using Oms.Domain.CustomersAggregate.Entities; using Oms.Domain.CustomersAggregate.Entities;
@ -15,16 +16,32 @@ public static class OmsSessionHelper
/// </summary> /// </summary>
public const string LoginTypeClaim = "loginType"; public const string LoginTypeClaim = "loginType";
/// <summary>
/// JWT 中 RBAC mapId Claim 名称(与 FakeClaimTypes.UserId 对齐)。
/// </summary>
public const string MapIdClaim = "mapId";
private const string CustomerLoginType = "1"; private const string CustomerLoginType = "1";
private const string CustomerRoleName = "Customer";
/// <summary> /// <summary>
/// 当前会话是否为客户门户登录。 /// 当前会话是否为客户门户登录。
/// </summary> /// </summary>
public static bool IsCustomerLogin(ICurrentUser currentUser) => public static bool IsCustomerLogin(ICurrentUser currentUser)
string.Equals( {
if (string.Equals(
currentUser.FindClaimOrNull(LoginTypeClaim)?.Value, currentUser.FindClaimOrNull(LoginTypeClaim)?.Value,
CustomerLoginType, CustomerLoginType,
StringComparison.Ordinal); StringComparison.Ordinal))
{
return true;
}
// 兼容 loginType 丢失但 role=Customer 仍在的令牌
var role = currentUser.FindClaimOrNull(ClaimTypes.Role)?.Value
?? currentUser.FindClaimOrNull("role")?.Value;
return string.Equals(role, CustomerRoleName, StringComparison.OrdinalIgnoreCase);
}
/// <summary> /// <summary>
/// 客户登录时返回 mapId(对应 t_customer.UserId / 订单 CreateUserId);管理员返回 null。 /// 客户登录时返回 mapId(对应 t_customer.UserId / 订单 CreateUserId);管理员返回 null。
@ -37,7 +54,19 @@ public static class OmsSessionHelper
} }
var userId = currentUser.GetId<int>(); var userId = currentUser.GetId<int>();
return userId > 0 ? userId : null; if (userId > 0)
{
return userId;
}
// GetId 未绑到 mapId 时,直接读 claim,避免客户会话被当成管理员(不过滤)
var mapIdRaw = currentUser.FindClaimOrNull(MapIdClaim)?.Value;
if (int.TryParse(mapIdRaw, out var mapId) && mapId > 0)
{
return mapId;
}
return null;
} }
/// <summary> /// <summary>