|
|
@@ -9,10 +9,25 @@ namespace Admin.NET.Plugin.AiDOP.ProcurementExecution;
|
|
|
/// S4 供应商欠料看板(只读列表 + 刷新)
|
|
|
/// 数据表:WorkOrdDetailTotalKB(结果)/ WorkOrdDetailTotal(中间)
|
|
|
/// 路由:/api/ProcurementExecution/supplier-shortage-kanban/...
|
|
|
+///
|
|
|
+/// 【租户安全边界】租户一律经 <see cref="AidopTenantScope.ResolveOrThrow"/> 从认证后 JWT 解析,
|
|
|
+/// 无有效租户即拒绝,不读前端 tenantId、无默认回退;公司域经
|
|
|
+/// <see cref="SourceDomainTenantResolver"/> 按当前租户解析,解析不出即 fail closed。
|
|
|
+/// <para>
|
|
|
+/// 原实现:类级 <c>[AllowAnonymous]</c> + <c>AidopTenantHelper.Resolve(App.HttpContext)</c>
|
|
|
+/// (解析链含 query 参数 tenantId 与 DefaultTenantId 兜底)+ <c>SELECT Domain ... LIMIT 1</c> 取全局第一条、
|
|
|
+/// 取不到则硬编码回落 <c>'1001'</c>。叠加后果:匿名 POST <c>supplier-shortage-kanban/refresh</c>
|
|
|
+/// 可指定任意租户,而 <c>Refresh</c> 的两条 <c>DELETE</c> 当时不带任何 WHERE,会清空**全部租户**的
|
|
|
+/// WorkOrdDetailTotal / WorkOrdDetailTotalKB,随后只重建被指定的那个租户。
|
|
|
+/// </para>
|
|
|
+/// <para>
|
|
|
+/// <b>Refresh 的写 SQL 一律按租户收口</b>:DELETE / UPDATE 的目标侧都带 <c>tenant_id = @TenantId</c>,
|
|
|
+/// 派生量的来源侧(MissedPrint 在检数、ItemMaster 物料主数据等)同样按租户限定 ——
|
|
|
+/// 业务键 ItemNum / WorkOrd / PurOrd 在库中都没有全局唯一约束,仅按业务键关联会跨租户串数。
|
|
|
+/// </para>
|
|
|
/// </summary>
|
|
|
[ApiDescriptionSettings(Order = 323, Description = "S4供应商欠料看板")]
|
|
|
[Route("api/ProcurementExecution")]
|
|
|
-[AllowAnonymous]
|
|
|
[NonUnify]
|
|
|
public class SupplierShortageKanbanService : IDynamicApiController, ITransient
|
|
|
{
|
|
|
@@ -21,15 +36,18 @@ public class SupplierShortageKanbanService : IDynamicApiController, ITransient
|
|
|
private readonly ISqlSugarClient _db;
|
|
|
private readonly UserManager _userManager;
|
|
|
private readonly MdpSourceScopeFactory _scopeFactory;
|
|
|
+ private readonly SourceDomainTenantResolver _domainTenant;
|
|
|
|
|
|
public SupplierShortageKanbanService(
|
|
|
ISqlSugarClient db,
|
|
|
UserManager userManager,
|
|
|
- MdpSourceScopeFactory scopeFactory)
|
|
|
+ MdpSourceScopeFactory scopeFactory,
|
|
|
+ SourceDomainTenantResolver domainTenant)
|
|
|
{
|
|
|
_db = db;
|
|
|
_userManager = userManager;
|
|
|
_scopeFactory = scopeFactory;
|
|
|
+ _domainTenant = domainTenant;
|
|
|
}
|
|
|
|
|
|
private static string NormalizeOrderBy(string field, string order)
|
|
|
@@ -73,22 +91,10 @@ public class SupplierShortageKanbanService : IDynamicApiController, ITransient
|
|
|
var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
|
|
|
var offset = (page - 1) * pageSize;
|
|
|
|
|
|
- var tenantId = AidopTenantHelper.Resolve(App.HttpContext);
|
|
|
- var cond = new List<string> { "1=1" };
|
|
|
- var pars = new List<SugarParameter>();
|
|
|
-
|
|
|
- var hasTenantColumn = await _db.Ado.GetIntAsync("""
|
|
|
- SELECT COUNT(1)
|
|
|
- FROM information_schema.COLUMNS
|
|
|
- WHERE TABLE_SCHEMA = DATABASE()
|
|
|
- AND TABLE_NAME = 'WorkOrdDetailTotalKB'
|
|
|
- AND COLUMN_NAME = 'tenant_id'
|
|
|
- """) > 0;
|
|
|
- if (hasTenantColumn)
|
|
|
- {
|
|
|
- cond.Add("t.tenant_id = @TenantId");
|
|
|
- pars.Add(new("@TenantId", tenantId));
|
|
|
- }
|
|
|
+ var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
|
|
|
+ // 租户过滤是硬条件,不做运行期列探测:列若缺失应当报错,而不是静默退化成读全部租户
|
|
|
+ var cond = new List<string> { "t.tenant_id = @TenantId" };
|
|
|
+ var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(input.ItemNum))
|
|
|
{
|
|
|
@@ -146,16 +152,17 @@ public class SupplierShortageKanbanService : IDynamicApiController, ITransient
|
|
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
|
|
try
|
|
|
{
|
|
|
- var tenantId = AidopTenantHelper.Resolve(App.HttpContext);
|
|
|
+ var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
|
|
|
|
|
|
- // ── Step 0: 取公司域 ──
|
|
|
- var domain = await _db.Ado.GetStringAsync(
|
|
|
- "SELECT Domain FROM GeneralizedCodeMaster WHERE FldName='SystemConfig' AND Val='CompanyCode' LIMIT 1");
|
|
|
- domain = string.IsNullOrWhiteSpace(domain) ? "1001" : domain.Trim();
|
|
|
+ // ── Step 0: 取公司域(按当前租户解析,取不到即 fail closed,不回落 1001/8010)──
|
|
|
+ var domain = await _domainTenant.ResolveDomainAsync(SqlServerSourceCode, tenantId);
|
|
|
+ var tenantPar = () => new List<SugarParameter> { new("@TenantId", tenantId) };
|
|
|
|
|
|
- // ── 清空中间表与结果表 ──
|
|
|
- await _db.Ado.ExecuteCommandAsync("DELETE FROM WorkOrdDetailTotal");
|
|
|
- await _db.Ado.ExecuteCommandAsync("DELETE FROM WorkOrdDetailTotalKB");
|
|
|
+ // ── 清空中间表与结果表(**仅本租户**:原写法无 WHERE,会清空全部租户)──
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ "DELETE FROM WorkOrdDetailTotal WHERE tenant_id = @TenantId", tenantPar());
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ "DELETE FROM WorkOrdDetailTotalKB WHERE tenant_id = @TenantId", tenantPar());
|
|
|
|
|
|
// ── Step 1: 基础数据写入 WorkOrdDetailTotal ──
|
|
|
await _db.Ado.ExecuteCommandAsync("""
|
|
|
@@ -203,8 +210,8 @@ public class SupplierShortageKanbanService : IDynamicApiController, ITransient
|
|
|
OR Status = 'p'
|
|
|
)
|
|
|
) a
|
|
|
- LEFT JOIN mes_morder b ON a.workord = b.morder_no
|
|
|
- LEFT JOIN ItemMaster c ON b.product_code = c.ItemNum AND (c.tenant_id = @TenantId OR @TenantId IS NULL)
|
|
|
+ LEFT JOIN mes_morder b ON a.workord = b.morder_no AND b.tenant_id = @TenantId
|
|
|
+ LEFT JOIN ItemMaster c ON b.product_code = c.ItemNum AND c.tenant_id = @TenantId
|
|
|
LEFT JOIN WorkOrdDetail d ON a.WorkOrd = d.WorkOrd AND d.tenant_id = @TenantId
|
|
|
LEFT JOIN (
|
|
|
SELECT a0.number,
|
|
|
@@ -243,10 +250,13 @@ public class SupplierShortageKanbanService : IDynamicApiController, ITransient
|
|
|
WHERE po_bill = p0.PurOrd AND po_billline = p0.Line
|
|
|
AND tenant_id = p0.tenant_id
|
|
|
) s0 ON TRUE
|
|
|
+ -- 在检数量只能统计本租户的标签:(PurOrd, PurLine) 无全局唯一约束,
|
|
|
+ -- 缺 tenant 条件会把别租户的 MissedPrint 加进本租户的 zjsl(与 s0/Do0 口径对齐)
|
|
|
LEFT JOIN LATERAL (
|
|
|
SELECT SUM(Qty) AS zjsl
|
|
|
FROM MissedPrint m
|
|
|
WHERE m.Status = 'I' AND m.PurOrd = p0.PurOrd AND m.PurLine = p0.Line
|
|
|
+ AND m.tenant_id = p0.tenant_id
|
|
|
) i0 ON TRUE
|
|
|
LEFT JOIN LATERAL (
|
|
|
SELECT SUM(t.QtyReturn) AS thsl
|
|
|
@@ -262,7 +272,8 @@ public class SupplierShortageKanbanService : IDynamicApiController, ITransient
|
|
|
) b ON a.ItemNum = b.ItemNum
|
|
|
SET a.ztsl = IFNULL(b.ztsl, 0),
|
|
|
a.zjsl = IFNULL(b.zjsl, 0)
|
|
|
- """, new List<SugarParameter> { new("@TenantId", tenantId) });
|
|
|
+ WHERE a.tenant_id = @TenantId
|
|
|
+ """, tenantPar());
|
|
|
|
|
|
// ── Step 4: 承诺 jqsl ──
|
|
|
await _db.Ado.ExecuteCommandAsync("""
|
|
|
@@ -278,9 +289,11 @@ public class SupplierShortageKanbanService : IDynamicApiController, ITransient
|
|
|
LEFT JOIN (
|
|
|
SELECT SUM(sh_delivery_quantity) AS sh_delivery_quantity, jhdbh
|
|
|
FROM scm_shdzb
|
|
|
+ WHERE tenant_id = @TenantId
|
|
|
GROUP BY jhdbh
|
|
|
) c ON ds.dsnum = c.jhdbh
|
|
|
WHERE jq.flag = 0
|
|
|
+ AND jq.tenant_id = @TenantId
|
|
|
AND IFNULL(c.sh_delivery_quantity, 0) < ds.schedqty
|
|
|
AND ds.status = 'P'
|
|
|
AND ds.schedqty - ds.sentqty > 0
|
|
|
@@ -289,34 +302,39 @@ public class SupplierShortageKanbanService : IDynamicApiController, ITransient
|
|
|
) jqview
|
|
|
) b ON a.ItemNum = b.ItemNum AND DATE(a.OrdDate) = b.jqhf
|
|
|
SET a.jqsl = CAST(IFNULL(b.jqsl, 0) AS DECIMAL(18,6))
|
|
|
- """);
|
|
|
+ WHERE a.tenant_id = @TenantId
|
|
|
+ """, tenantPar());
|
|
|
|
|
|
// ── Step 5: 可用 / 欠料(按天数偏移累加)──
|
|
|
await _db.Ado.ExecuteCommandAsync("""
|
|
|
UPDATE WorkOrdDetailTotal
|
|
|
SET available = IFNULL(locStock, 0),
|
|
|
shortage = IFNULL(locStock, 0) - IFNULL(ljsl, 0)
|
|
|
- WHERE DATE(OrdDate) = DATE(NOW()) OR DATE(OrdDate) = DATE(DATE_ADD(NOW(), INTERVAL 1 DAY))
|
|
|
- """);
|
|
|
+ WHERE tenant_id = @TenantId
|
|
|
+ AND (DATE(OrdDate) = DATE(NOW()) OR DATE(OrdDate) = DATE(DATE_ADD(NOW(), INTERVAL 1 DAY)))
|
|
|
+ """, tenantPar());
|
|
|
await _db.Ado.ExecuteCommandAsync("""
|
|
|
UPDATE WorkOrdDetailTotal
|
|
|
SET available = IFNULL(locStock, 0) + IFNULL(zjsl, 0),
|
|
|
shortage = IFNULL(locStock, 0) + IFNULL(zjsl, 0) - IFNULL(ljsl, 0)
|
|
|
- WHERE DATE(OrdDate) = DATE(DATE_ADD(NOW(), INTERVAL 2 DAY))
|
|
|
- """);
|
|
|
+ WHERE tenant_id = @TenantId
|
|
|
+ AND DATE(OrdDate) = DATE(DATE_ADD(NOW(), INTERVAL 2 DAY))
|
|
|
+ """, tenantPar());
|
|
|
await _db.Ado.ExecuteCommandAsync("""
|
|
|
UPDATE WorkOrdDetailTotal
|
|
|
SET available = IFNULL(locStock, 0) + IFNULL(zjsl, 0) + IFNULL(ztsl, 0),
|
|
|
shortage = IFNULL(locStock, 0) + IFNULL(zjsl, 0) + IFNULL(ztsl, 0) - IFNULL(ljsl, 0)
|
|
|
- WHERE DATE(OrdDate) = DATE(DATE_ADD(NOW(), INTERVAL 3 DAY))
|
|
|
- """);
|
|
|
+ WHERE tenant_id = @TenantId
|
|
|
+ AND DATE(OrdDate) = DATE(DATE_ADD(NOW(), INTERVAL 3 DAY))
|
|
|
+ """, tenantPar());
|
|
|
await _db.Ado.ExecuteCommandAsync("""
|
|
|
UPDATE WorkOrdDetailTotal
|
|
|
SET available = IFNULL(locStock, 0) + IFNULL(zjsl, 0) + IFNULL(ztsl, 0) + IFNULL(jqsl, 0),
|
|
|
shortage = IFNULL(locStock, 0) + IFNULL(zjsl, 0) + IFNULL(ztsl, 0) + IFNULL(jqsl, 0) - IFNULL(ljsl, 0)
|
|
|
- WHERE DATE(OrdDate) >= DATE(DATE_ADD(NOW(), INTERVAL 4 DAY))
|
|
|
+ WHERE tenant_id = @TenantId
|
|
|
+ AND DATE(OrdDate) >= DATE(DATE_ADD(NOW(), INTERVAL 4 DAY))
|
|
|
AND DATE(OrdDate) <= DATE(DATE_ADD(NOW(), INTERVAL 14 DAY))
|
|
|
- """);
|
|
|
+ """, tenantPar());
|
|
|
|
|
|
// ── Step 6: 透视写入 KB 表 ──
|
|
|
await _db.Ado.ExecuteCommandAsync("""
|
|
|
@@ -349,11 +367,12 @@ public class SupplierShortageKanbanService : IDynamicApiController, ITransient
|
|
|
a.supplier_number, a.supplier_name, a.ItemNum,
|
|
|
b.Descr, b.Descr1, a.shortage
|
|
|
FROM WorkOrdDetailTotal a
|
|
|
- LEFT JOIN ItemMaster b ON a.ItemNum = b.ItemNum
|
|
|
- WHERE a.shortage < 0
|
|
|
+ -- ItemMaster 的 ItemNum 跨租户重名、同 Domain 内亦有重复,不限租户会放大透视行数
|
|
|
+ LEFT JOIN ItemMaster b ON a.ItemNum = b.ItemNum AND b.tenant_id = @TenantId
|
|
|
+ WHERE a.tenant_id = @TenantId AND a.shortage < 0
|
|
|
) src
|
|
|
GROUP BY supplier_number, supplier_name, ItemNum, Descr, Descr1
|
|
|
- """, new List<SugarParameter> { new("@TenantId", tenantId) });
|
|
|
+ """, tenantPar());
|
|
|
|
|
|
return new { ok = true, elapsedMs = sw.ElapsedMilliseconds };
|
|
|
}
|
|
|
@@ -370,7 +389,8 @@ public class SupplierShortageKanbanService : IDynamicApiController, ITransient
|
|
|
{
|
|
|
// 读取 MRPLocationRange 配置(逗号分隔的库位列表)
|
|
|
var locRange = await _db.Ado.GetStringAsync(
|
|
|
- "SELECT Val FROM ConfigurationItem WHERE FldName='MRPLocationRange' LIMIT 1");
|
|
|
+ "SELECT Val FROM ConfigurationItem WHERE FldName='MRPLocationRange' AND tenant_id = @TenantId LIMIT 1",
|
|
|
+ new List<SugarParameter> { new("@TenantId", tenantId) });
|
|
|
if (string.IsNullOrWhiteSpace(locRange))
|
|
|
return;
|
|
|
|
|
|
@@ -403,10 +423,20 @@ public class SupplierShortageKanbanService : IDynamicApiController, ITransient
|
|
|
if (rows.Count == 0)
|
|
|
return;
|
|
|
|
|
|
- // 批量写入临时表,再单次 JOIN UPDATE(比逐行 UPDATE 快得多)
|
|
|
+ // 批量写入中间表,再单次 JOIN UPDATE(比逐行 UPDATE 快得多)。
|
|
|
+ // 中间表按租户分区:原写法 TRUNCATE + DROP 是全局的,两个租户并发 Refresh 会互相清表。
|
|
|
await _db.Ado.ExecuteCommandAsync(
|
|
|
- "CREATE TABLE IF NOT EXISTS _tmp_loc_stock (ItemNum VARCHAR(50) PRIMARY KEY, locStock DECIMAL(18,6))");
|
|
|
- await _db.Ado.ExecuteCommandAsync("TRUNCATE TABLE _tmp_loc_stock");
|
|
|
+ """
|
|
|
+ CREATE TABLE IF NOT EXISTS _tmp_loc_stock (
|
|
|
+ tenant_id BIGINT NOT NULL,
|
|
|
+ ItemNum VARCHAR(50) NOT NULL,
|
|
|
+ locStock DECIMAL(18,6),
|
|
|
+ PRIMARY KEY (tenant_id, ItemNum)
|
|
|
+ )
|
|
|
+ """);
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ "DELETE FROM _tmp_loc_stock WHERE tenant_id = @TenantId",
|
|
|
+ new List<SugarParameter> { new("@TenantId", tenantId) });
|
|
|
|
|
|
var insertPars = new List<SugarParameter>();
|
|
|
var valuesClauses = new List<string>();
|
|
|
@@ -414,7 +444,7 @@ public class SupplierShortageKanbanService : IDynamicApiController, ITransient
|
|
|
{
|
|
|
var r = rows[i];
|
|
|
if (string.IsNullOrWhiteSpace(r.ItemNum)) continue;
|
|
|
- valuesClauses.Add($"(@item{i}, @stock{i})");
|
|
|
+ valuesClauses.Add($"(@TenantId, @item{i}, @stock{i})");
|
|
|
insertPars.Add(new SugarParameter($"@item{i}", r.ItemNum));
|
|
|
insertPars.Add(new SugarParameter($"@stock{i}", r.LocStock));
|
|
|
}
|
|
|
@@ -430,21 +460,26 @@ public class SupplierShortageKanbanService : IDynamicApiController, ITransient
|
|
|
var batchPars = insertPars.GetRange(offset, batchEnd - offset);
|
|
|
var batchValues = new List<string>();
|
|
|
for (var j = offset; j < batchEnd; j += 2)
|
|
|
- batchValues.Add($"({insertPars[j].ParameterName}, {insertPars[j + 1].ParameterName})");
|
|
|
+ batchValues.Add($"(@TenantId, {insertPars[j].ParameterName}, {insertPars[j + 1].ParameterName})");
|
|
|
+ batchPars.Add(new SugarParameter("@TenantId", tenantId));
|
|
|
|
|
|
await _db.Ado.ExecuteCommandAsync(
|
|
|
- $"INSERT INTO _tmp_loc_stock (ItemNum, locStock) VALUES {string.Join(",", batchValues)}",
|
|
|
+ $"INSERT INTO _tmp_loc_stock (tenant_id, ItemNum, locStock) VALUES {string.Join(",", batchValues)}",
|
|
|
batchPars);
|
|
|
}
|
|
|
|
|
|
// 单次 JOIN UPDATE
|
|
|
await _db.Ado.ExecuteCommandAsync("""
|
|
|
UPDATE WorkOrdDetailTotal a
|
|
|
- INNER JOIN _tmp_loc_stock t ON a.ItemNum = t.ItemNum
|
|
|
+ INNER JOIN _tmp_loc_stock t ON a.ItemNum = t.ItemNum AND t.tenant_id = a.tenant_id
|
|
|
SET a.locStock = t.locStock
|
|
|
- """);
|
|
|
+ WHERE a.tenant_id = @TenantId
|
|
|
+ """, new List<SugarParameter> { new("@TenantId", tenantId) });
|
|
|
|
|
|
- await _db.Ado.ExecuteCommandAsync("DROP TABLE IF EXISTS _tmp_loc_stock");
|
|
|
+ // 只清本租户的中间数据;不再 DROP,避免打断并发的另一个租户
|
|
|
+ await _db.Ado.ExecuteCommandAsync(
|
|
|
+ "DELETE FROM _tmp_loc_stock WHERE tenant_id = @TenantId",
|
|
|
+ new List<SugarParameter> { new("@TenantId", tenantId) });
|
|
|
}
|
|
|
|
|
|
private sealed class LocStockRow
|
|
|
@@ -460,19 +495,13 @@ public class SupplierShortageKanbanService : IDynamicApiController, ITransient
|
|
|
[HttpPost("supplier-shortage-kanban/create-todo")]
|
|
|
public async Task<object> CreateTodo()
|
|
|
{
|
|
|
- var tenantId = AidopTenantHelper.Resolve(App.HttpContext);
|
|
|
+ var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
|
|
|
var userId = _userManager?.UserId ?? 0;
|
|
|
|
|
|
- // 查询当前全量欠料数据作为快照
|
|
|
- var hasTenantColumn = await _db.Ado.GetIntAsync("""
|
|
|
- SELECT COUNT(1) FROM information_schema.COLUMNS
|
|
|
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'WorkOrdDetailTotalKB' AND COLUMN_NAME = 'tenant_id'
|
|
|
- """) > 0;
|
|
|
-
|
|
|
- var where = hasTenantColumn ? "WHERE tenant_id = @TenantId" : "";
|
|
|
- var pars = hasTenantColumn ? new[] { new SugarParameter("@TenantId", tenantId) } : Array.Empty<SugarParameter>();
|
|
|
+ // 查询本租户欠料数据作为快照(租户过滤是硬条件,不做运行期列探测)
|
|
|
+ var pars = new[] { new SugarParameter("@TenantId", tenantId) };
|
|
|
|
|
|
- var snapshotSql = $"""
|
|
|
+ var snapshotSql = """
|
|
|
SELECT
|
|
|
IFNULL(supplier_number, '') AS supplier_number,
|
|
|
IFNULL(supplier_name, '') AS supplier_name,
|
|
|
@@ -484,7 +513,8 @@ public class SupplierShortageKanbanService : IDynamicApiController, ITransient
|
|
|
IFNULL(D6, 0) AS D6, IFNULL(D7, 0) AS D7, IFNULL(D8, 0) AS D8,
|
|
|
IFNULL(D9, 0) AS D9, IFNULL(D10, 0) AS D10, IFNULL(D11, 0) AS D11,
|
|
|
IFNULL(D12, 0) AS D12, IFNULL(D13, 0) AS D13, IFNULL(D14, 0) AS D14
|
|
|
- FROM WorkOrdDetailTotalKB {where}
|
|
|
+ FROM WorkOrdDetailTotalKB
|
|
|
+ WHERE tenant_id = @TenantId
|
|
|
ORDER BY IFNULL(D0, 0) DESC
|
|
|
""";
|
|
|
|
|
|
@@ -512,8 +542,9 @@ public class SupplierShortageKanbanService : IDynamicApiController, ITransient
|
|
|
[HttpGet("supplier-shortage-kanban/todo-detail/{id:long}")]
|
|
|
public async Task<object> GetTodoDetail(long id)
|
|
|
{
|
|
|
+ var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
|
|
|
var todo = await _db.Queryable<AdoMaterialShortageTodo>()
|
|
|
- .Where(t => t.Id == id)
|
|
|
+ .Where(t => t.Id == id && t.TenantId == tenantId)
|
|
|
.FirstAsync();
|
|
|
|
|
|
if (todo == null) return new { code = 404, message = "待办不存在" };
|