Commit a32e3ff6b2b265665503b71f552be4df3fcdb1af
1 parent
88a30b7a
feat: 添加修改库存使用申请记录功能并修复重复数据问题
- 新增修改库存使用申请记录接口,支持添加、删除、修改产品领用 - 修改后自动重新计算申请总金额,检查库存是否充足 - 允许修改待审批、审批中、已退回状态的申请 - 修复GetBatchInfo接口查询时未过滤无效记录导致重复数据的问题 - 添加LqInventoryUsageApplicationUpdateInput DTO类
Showing
5 changed files
with
279 additions
and
8 deletions
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqInventoryUsageApplication/LqInventoryUsageApplicationUpdateInput.cs
0 → 100644
| 1 | +using System.Collections.Generic; | |
| 2 | +using System.ComponentModel.DataAnnotations; | |
| 3 | +using NCC.Extend.Entitys.Dto.LqInventoryUsage; | |
| 4 | + | |
| 5 | +namespace NCC.Extend.Entitys.Dto.LqInventoryUsageApplication | |
| 6 | +{ | |
| 7 | + /// <summary> | |
| 8 | + /// 修改库存使用申请的使用记录输入 | |
| 9 | + /// </summary> | |
| 10 | + public class LqInventoryUsageApplicationUpdateInput | |
| 11 | + { | |
| 12 | + /// <summary> | |
| 13 | + /// 申请ID(必填) | |
| 14 | + /// </summary> | |
| 15 | + [Required(ErrorMessage = "申请ID不能为空")] | |
| 16 | + [StringLength(50, ErrorMessage = "申请ID长度不能超过50个字符")] | |
| 17 | + [Display(Name = "申请ID")] | |
| 18 | + public string ApplicationId { get; set; } | |
| 19 | + | |
| 20 | + /// <summary> | |
| 21 | + /// 新的使用记录列表(必填,至少需要一条记录) | |
| 22 | + /// </summary> | |
| 23 | + [Required(ErrorMessage = "使用记录列表不能为空")] | |
| 24 | + [MinLength(1, ErrorMessage = "至少需要添加一条使用记录")] | |
| 25 | + [Display(Name = "使用记录列表")] | |
| 26 | + public List<LqInventoryUsageItemInput> UsageItems { get; set; } | |
| 27 | + } | |
| 28 | +} | |
| 29 | + | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_inventory_usage_application_node/LqInventoryUsageApplicationNodeEntity.cs
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_inventory_usage_approval_record/LqInventoryUsageApprovalRecordEntity.cs
netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs
| ... | ... | @@ -710,9 +710,9 @@ namespace NCC.Extend |
| 710 | 710 | throw NCCException.Oh("批次ID不能为空"); |
| 711 | 711 | } |
| 712 | 712 | |
| 713 | - // 查询该批次的所有使用记录 | |
| 713 | + // 查询该批次的所有有效使用记录 | |
| 714 | 714 | var usageRecords = await _db.Queryable<LqInventoryUsageEntity, LqProductEntity>((u, product) => u.ProductId == product.Id) |
| 715 | - .Where((u, product) => u.UsageBatchId == batchId) | |
| 715 | + .Where((u, product) => u.UsageBatchId == batchId && u.IsEffective == StatusEnum.有效.GetHashCode()) | |
| 716 | 716 | .Select((u, product) => new LqInventoryUsageListOutput |
| 717 | 717 | { |
| 718 | 718 | id = u.Id, |
| ... | ... | @@ -776,8 +776,7 @@ namespace NCC.Extend |
| 776 | 776 | |
| 777 | 777 | // 获取批次基本信息(使用第一条记录的创建信息) |
| 778 | 778 | var firstRecord = usageRecords.OrderBy(x => x.createTime).First(); |
| 779 | - var effectiveRecords = usageRecords.Where(x => x.isEffective == StatusEnum.有效.GetHashCode()).ToList(); | |
| 780 | - var ineffectiveRecords = usageRecords.Where(x => x.isEffective == StatusEnum.无效.GetHashCode()).ToList(); | |
| 779 | + // 注意:usageRecords 已经只包含有效记录了,因为查询时已经过滤了 IsEffective | |
| 781 | 780 | |
| 782 | 781 | // 查询申请记录(如果有) |
| 783 | 782 | var application = await _db.Queryable<LqInventoryUsageApplicationEntity>() |
| ... | ... | @@ -833,10 +832,10 @@ namespace NCC.Extend |
| 833 | 832 | CreateUser = firstRecord.createUser, |
| 834 | 833 | CreateUserName = firstRecord.createUserName, |
| 835 | 834 | TotalCount = usageRecords.Count, |
| 836 | - EffectiveCount = effectiveRecords.Count, | |
| 837 | - IneffectiveCount = ineffectiveRecords.Count, | |
| 838 | - TotalUsageQuantity = effectiveRecords.Sum(x => x.usageQuantity), | |
| 839 | - TotalUsageAmount = effectiveRecords.Sum(x => x.usageTotalValue), | |
| 835 | + EffectiveCount = usageRecords.Count, // 所有记录都是有效的(查询时已过滤) | |
| 836 | + IneffectiveCount = 0, // 无效记录不在查询结果中 | |
| 837 | + TotalUsageQuantity = usageRecords.Sum(x => x.usageQuantity), | |
| 838 | + TotalUsageAmount = usageRecords.Sum(x => x.usageTotalValue), | |
| 840 | 839 | UsageRecords = usageRecords, |
| 841 | 840 | ApplicationInfo = applicationInfo |
| 842 | 841 | }; |
| ... | ... | @@ -1372,6 +1371,246 @@ namespace NCC.Extend |
| 1372 | 1371 | } |
| 1373 | 1372 | } |
| 1374 | 1373 | |
| 1374 | + /// <summary> | |
| 1375 | + /// 修改库存使用申请的使用记录 | |
| 1376 | + /// </summary> | |
| 1377 | + /// <remarks> | |
| 1378 | + /// 修改指定申请的使用记录,支持添加、删除、修改数量。修改后流程不变,主要是内容变更。 | |
| 1379 | + /// 修改后会自动重新计算申请总金额,并检查库存是否充足、关联数据是否需要更新。 | |
| 1380 | + /// 注意:只有待审批、审批中、已退回状态的申请才能修改,已通过或未通过的申请不能修改。 | |
| 1381 | + /// | |
| 1382 | + /// 示例请求: | |
| 1383 | + /// ```json | |
| 1384 | + /// PUT /api/Extend/LqInventoryUsage/UpdateApplicationUsageRecords | |
| 1385 | + /// { | |
| 1386 | + /// "applicationId": "申请ID", | |
| 1387 | + /// "usageItems": [ | |
| 1388 | + /// { | |
| 1389 | + /// "productId": "产品ID", | |
| 1390 | + /// "storeId": "门店ID", | |
| 1391 | + /// "usageTime": "2024-01-01T10:00:00", | |
| 1392 | + /// "usageQuantity": 10, | |
| 1393 | + /// "relatedConsumeId": "关联消耗ID(可选)" | |
| 1394 | + /// } | |
| 1395 | + /// ] | |
| 1396 | + /// } | |
| 1397 | + /// ``` | |
| 1398 | + /// | |
| 1399 | + /// 参数说明: | |
| 1400 | + /// - applicationId: 申请ID(必填) | |
| 1401 | + /// - usageItems: 新的使用记录列表(必填,至少需要一条记录) | |
| 1402 | + /// - productId: 产品ID(必填) | |
| 1403 | + /// - storeId: 门店ID(必填) | |
| 1404 | + /// - usageTime: 使用时间(必填) | |
| 1405 | + /// - usageQuantity: 使用数量(必填,必须大于0) | |
| 1406 | + /// - relatedConsumeId: 关联消耗ID(可选) | |
| 1407 | + /// | |
| 1408 | + /// 业务流程: | |
| 1409 | + /// 1. 验证申请状态(只有待审批、已退回状态的申请才能修改) | |
| 1410 | + /// 2. 验证库存是否充足 | |
| 1411 | + /// 3. 逻辑删除旧的使用记录 | |
| 1412 | + /// 4. 创建新的使用记录(自动计算单价和合计金额) | |
| 1413 | + /// 5. 重新计算申请总金额 | |
| 1414 | + /// 6. 检查关联数据是否需要更新 | |
| 1415 | + /// </remarks> | |
| 1416 | + /// <param name="input">修改输入</param> | |
| 1417 | + /// <returns>修改结果</returns> | |
| 1418 | + /// <response code="200">修改成功</response> | |
| 1419 | + /// <response code="400">申请不存在、状态不正确或库存不足</response> | |
| 1420 | + /// <response code="500">服务器错误</response> | |
| 1421 | + [HttpPut("UpdateApplicationUsageRecords")] | |
| 1422 | + public async Task UpdateApplicationUsageRecordsAsync([FromBody] LqInventoryUsageApplicationUpdateInput input) | |
| 1423 | + { | |
| 1424 | + try | |
| 1425 | + { | |
| 1426 | + if (input == null || string.IsNullOrWhiteSpace(input.ApplicationId)) | |
| 1427 | + { | |
| 1428 | + throw NCCException.Oh("申请ID不能为空"); | |
| 1429 | + } | |
| 1430 | + | |
| 1431 | + if (input.UsageItems == null || !input.UsageItems.Any()) | |
| 1432 | + { | |
| 1433 | + throw NCCException.Oh("使用记录列表不能为空,至少需要添加一条使用记录"); | |
| 1434 | + } | |
| 1435 | + | |
| 1436 | + // 获取申请记录 | |
| 1437 | + var application = await _db.Queryable<LqInventoryUsageApplicationEntity>() | |
| 1438 | + .Where(x => x.Id == input.ApplicationId && x.IsEffective == StatusEnum.有效.GetHashCode()) | |
| 1439 | + .FirstAsync(); | |
| 1440 | + | |
| 1441 | + if (application == null) | |
| 1442 | + { | |
| 1443 | + throw NCCException.Oh("申请记录不存在或已失效"); | |
| 1444 | + } | |
| 1445 | + | |
| 1446 | + // 验证申请状态(只有待审批、审批中、已退回状态的申请才能修改,已通过或未通过的申请不能修改) | |
| 1447 | + if (application.ApprovalStatus != "待审批" && application.ApprovalStatus != "审批中" && application.ApprovalStatus != "已退回") | |
| 1448 | + { | |
| 1449 | + throw NCCException.Oh($"申请当前状态为{application.ApprovalStatus},只有待审批、审批中或已退回状态的申请才能修改使用记录"); | |
| 1450 | + } | |
| 1451 | + | |
| 1452 | + // 验证是否已领取(已领取的申请不能修改) | |
| 1453 | + if (application.IsReceived == 1) | |
| 1454 | + { | |
| 1455 | + throw NCCException.Oh("该申请已领取,无法修改使用记录"); | |
| 1456 | + } | |
| 1457 | + | |
| 1458 | + // 获取该批次的所有旧使用记录 | |
| 1459 | + var oldUsageRecords = await _db.Queryable<LqInventoryUsageEntity>() | |
| 1460 | + .Where(x => x.UsageBatchId == application.UsageBatchId && x.IsEffective == StatusEnum.有效.GetHashCode()) | |
| 1461 | + .ToListAsync(); | |
| 1462 | + | |
| 1463 | + // 按产品ID分组,批量验证库存(在事务外先检查,避免不必要的回滚) | |
| 1464 | + var productGroups = input.UsageItems.Select((item, index) => new { Item = item, Index = index }).GroupBy(x => x.Item.ProductId).ToList(); | |
| 1465 | + | |
| 1466 | + // 获取所有需要检查的产品ID | |
| 1467 | + var productIds = productGroups.Select(x => x.Key).Distinct().ToList(); | |
| 1468 | + | |
| 1469 | + // 批量查询所有产品的库存信息(总库存) | |
| 1470 | + var inventoryList = await _db.Queryable<LqInventoryEntity>() | |
| 1471 | + .Where(x => productIds.Contains(x.ProductId) && x.IsEffective == StatusEnum.有效.GetHashCode()) | |
| 1472 | + .GroupBy(x => x.ProductId) | |
| 1473 | + .Select(x => new { ProductId = x.ProductId, TotalInventory = SqlFunc.AggregateSum(x.Quantity) }) | |
| 1474 | + .ToListAsync(); | |
| 1475 | + var inventoryMap = inventoryList.ToDictionary(x => x.ProductId, x => Convert.ToInt32(x.TotalInventory)); | |
| 1476 | + | |
| 1477 | + // 批量查询所有产品的已使用数量(排除当前批次的使用记录,因为我们要替换它们) | |
| 1478 | + var oldUsageRecordIds = oldUsageRecords.Select(x => x.Id).ToList(); | |
| 1479 | + var allUsageList = await _db.Queryable<LqInventoryUsageEntity>() | |
| 1480 | + .Where(x => productIds.Contains(x.ProductId) && x.IsEffective == StatusEnum.有效.GetHashCode()) | |
| 1481 | + .WhereIF(oldUsageRecordIds.Any(), x => !oldUsageRecordIds.Contains(x.Id)) // 排除当前批次的使用记录 | |
| 1482 | + .GroupBy(x => x.ProductId) | |
| 1483 | + .Select(x => new { ProductId = x.ProductId, TotalUsage = SqlFunc.AggregateSum(x.UsageQuantity) }) | |
| 1484 | + .ToListAsync(); | |
| 1485 | + var usageMap = allUsageList.ToDictionary(x => x.ProductId, x => Convert.ToInt32(x.TotalUsage)); | |
| 1486 | + | |
| 1487 | + // 批量查询所有产品的名称和价格 | |
| 1488 | + var productDict = await _db.Queryable<LqProductEntity>() | |
| 1489 | + .Where(x => productIds.Contains(x.Id)) | |
| 1490 | + .Select(x => new { x.Id, x.ProductName, x.Price }) | |
| 1491 | + .ToListAsync(); | |
| 1492 | + var productInfoMap = productDict.ToDictionary(k => k.Id, v => new { v.ProductName, v.Price }); | |
| 1493 | + | |
| 1494 | + // 验证每个产品的库存是否充足 | |
| 1495 | + foreach (var group in productGroups) | |
| 1496 | + { | |
| 1497 | + var productId = group.Key; | |
| 1498 | + var totalNewQuantity = group.Sum(x => x.Item.UsageQuantity); | |
| 1499 | + | |
| 1500 | + if (!productInfoMap.ContainsKey(productId)) | |
| 1501 | + { | |
| 1502 | + throw NCCException.Oh($"产品ID {productId} 不存在"); | |
| 1503 | + } | |
| 1504 | + | |
| 1505 | + var totalInventory = inventoryMap.GetValueOrDefault(productId, 0); | |
| 1506 | + var totalUsage = usageMap.GetValueOrDefault(productId, 0); | |
| 1507 | + var availableInventory = totalInventory - totalUsage; | |
| 1508 | + | |
| 1509 | + if (availableInventory < totalNewQuantity) | |
| 1510 | + { | |
| 1511 | + var productName = productInfoMap[productId].ProductName; | |
| 1512 | + throw NCCException.Oh($"产品 {productName} 库存不足,当前可用库存:{availableInventory},需要数量:{totalNewQuantity}"); | |
| 1513 | + } | |
| 1514 | + } | |
| 1515 | + | |
| 1516 | + _db.Ado.BeginTran(); | |
| 1517 | + | |
| 1518 | + try | |
| 1519 | + { | |
| 1520 | + // 1. 逻辑删除旧的使用记录 | |
| 1521 | + if (oldUsageRecords.Any()) | |
| 1522 | + { | |
| 1523 | + foreach (var oldRecord in oldUsageRecords) | |
| 1524 | + { | |
| 1525 | + oldRecord.IsEffective = StatusEnum.无效.GetHashCode(); | |
| 1526 | + oldRecord.UpdateUser = _userManager.UserId; | |
| 1527 | + oldRecord.UpdateTime = DateTime.Now; | |
| 1528 | + } | |
| 1529 | + await _db.Updateable(oldUsageRecords).ExecuteCommandAsync(); | |
| 1530 | + } | |
| 1531 | + | |
| 1532 | + // 2. 批量计算新使用记录的平均价格(根据库存计算的加权平均价格) | |
| 1533 | + var productAveragePriceMap = new Dictionary<string, decimal>(); | |
| 1534 | + foreach (var productId in productIds) | |
| 1535 | + { | |
| 1536 | + var product = productInfoMap[productId]; | |
| 1537 | + var averagePrice = await CalculateAveragePriceFromInventoryAsync(productId, product.Price); | |
| 1538 | + productAveragePriceMap[productId] = averagePrice; | |
| 1539 | + } | |
| 1540 | + | |
| 1541 | + // 3. 创建新的使用记录 | |
| 1542 | + var entitiesToInsert = new List<LqInventoryUsageEntity>(); | |
| 1543 | + foreach (var item in input.UsageItems) | |
| 1544 | + { | |
| 1545 | + // 从平均价格字典获取单价(根据库存计算的加权平均价格) | |
| 1546 | + var unitPrice = productAveragePriceMap.GetValueOrDefault(item.ProductId, 0); | |
| 1547 | + var totalAmount = unitPrice * item.UsageQuantity; | |
| 1548 | + | |
| 1549 | + var usageEntity = new LqInventoryUsageEntity | |
| 1550 | + { | |
| 1551 | + Id = YitIdHelper.NextId().ToString(), | |
| 1552 | + ProductId = item.ProductId, | |
| 1553 | + StoreId = item.StoreId, | |
| 1554 | + UsageTime = item.UsageTime, | |
| 1555 | + UsageQuantity = item.UsageQuantity, | |
| 1556 | + UnitPrice = unitPrice, | |
| 1557 | + TotalAmount = totalAmount, | |
| 1558 | + RelatedConsumeId = item.RelatedConsumeId, | |
| 1559 | + UsageBatchId = application.UsageBatchId, // 使用相同的批次ID | |
| 1560 | + CreateUser = _userManager.UserId, | |
| 1561 | + CreateTime = DateTime.Now, | |
| 1562 | + IsEffective = StatusEnum.有效.GetHashCode() | |
| 1563 | + }; | |
| 1564 | + | |
| 1565 | + entitiesToInsert.Add(usageEntity); | |
| 1566 | + } | |
| 1567 | + | |
| 1568 | + // 批量插入新使用记录 | |
| 1569 | + if (entitiesToInsert.Any()) | |
| 1570 | + { | |
| 1571 | + await _db.Insertable(entitiesToInsert).ExecuteCommandAsync(); | |
| 1572 | + } | |
| 1573 | + | |
| 1574 | + // 4. 重新计算申请总金额 | |
| 1575 | + var newTotalAmount = entitiesToInsert.Sum(x => x.TotalAmount); | |
| 1576 | + application.TotalAmount = newTotalAmount; | |
| 1577 | + application.UpdateUser = _userManager.UserId; | |
| 1578 | + application.UpdateTime = DateTime.Now; | |
| 1579 | + | |
| 1580 | + await _db.Updateable(application).ExecuteCommandAsync(); | |
| 1581 | + | |
| 1582 | + // 5. 检查关联数据是否需要更新 | |
| 1583 | + // 如果有关联消耗ID,检查消耗记录是否存在 | |
| 1584 | + var relatedConsumeIds = input.UsageItems | |
| 1585 | + .Where(x => !string.IsNullOrWhiteSpace(x.RelatedConsumeId)) | |
| 1586 | + .Select(x => x.RelatedConsumeId) | |
| 1587 | + .Distinct() | |
| 1588 | + .ToList(); | |
| 1589 | + | |
| 1590 | + if (relatedConsumeIds.Any()) | |
| 1591 | + { | |
| 1592 | + // 这里可以添加对关联消耗记录的检查逻辑 | |
| 1593 | + // 例如:检查消耗记录是否存在,是否需要更新数量等 | |
| 1594 | + // 由于不清楚具体的消耗记录表结构,这里只做记录 | |
| 1595 | + _logger.LogInformation($"修改申请 {input.ApplicationId} 的使用记录,关联消耗ID:{string.Join(",", relatedConsumeIds)}"); | |
| 1596 | + } | |
| 1597 | + | |
| 1598 | + _db.Ado.CommitTran(); | |
| 1599 | + } | |
| 1600 | + catch | |
| 1601 | + { | |
| 1602 | + _db.Ado.RollbackTran(); | |
| 1603 | + throw; | |
| 1604 | + } | |
| 1605 | + } | |
| 1606 | + catch (Exception ex) | |
| 1607 | + { | |
| 1608 | + _db.Ado.RollbackTran(); | |
| 1609 | + _logger.LogError(ex, "修改申请使用记录失败"); | |
| 1610 | + throw NCCException.Oh($"修改失败:{ex.Message}"); | |
| 1611 | + } | |
| 1612 | + } | |
| 1613 | + | |
| 1375 | 1614 | #endregion |
| 1376 | 1615 | |
| 1377 | 1616 | #region 门店领取统计 | ... | ... |