Commit a32e3ff6b2b265665503b71f552be4df3fcdb1af

Authored by “wangming”
1 parent 88a30b7a

feat: 添加修改库存使用申请记录功能并修复重复数据问题

- 新增修改库存使用申请记录接口,支持添加、删除、修改产品领用
- 修改后自动重新计算申请总金额,检查库存是否充足
- 允许修改待审批、审批中、已退回状态的申请
- 修复GetBatchInfo接口查询时未过滤无效记录导致重复数据的问题
- 添加LqInventoryUsageApplicationUpdateInput DTO类
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
... ... @@ -65,3 +65,4 @@ namespace NCC.Extend.Entitys.lq_inventory_usage_application_node
65 65  
66 66  
67 67  
  68 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_inventory_usage_approval_record/LqInventoryUsageApprovalRecordEntity.cs
... ... @@ -83,3 +83,4 @@ namespace NCC.Extend.Entitys.lq_inventory_usage_approval_record
83 83  
84 84  
85 85  
  86 +
... ...
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 门店领取统计
... ...
sql/创建库存使用申请审批流程表.sql
... ... @@ -109,3 +109,4 @@ WHERE u.`F_UnitPrice` = 0 OR u.`F_UnitPrice` IS NULL;
109 109  
110 110  
111 111  
  112 +
... ...