Platform upgrade: login auth fix, OCR/OSS upload, system menus and dicts, weather dashboard, weighing and invoice modules

This commit is contained in:
2026-08-14 01:12:33 +08:00
parent c377421a3e
commit 8099ebae0a
75 changed files with 5611 additions and 473 deletions
@@ -27,6 +27,7 @@ public class PurchasesController(
var query = db.PurchaseOrders
.Include(p => p.Farmer).Include(p => p.Product)
.Include(p => p.PurchaserOrg).Include(p => p.Operator)
.Include(p => p.Weighs)
.AsQueryable();
if (visible is not null)
@@ -73,11 +74,42 @@ public class PurchasesController(
var order = await db.PurchaseOrders
.Include(p => p.Farmer).Include(p => p.Product)
.Include(p => p.PurchaserOrg).Include(p => p.Operator)
.Include(p => p.Weighs)
.FirstOrDefaultAsync(p => p.Id == id);
if (order is null) return NotFound();
return Ok(order.ToDto());
}
/// <summary>收购单统计(单数、重量合计、均价、金额合计)</summary>
[HttpGet("summary")]
public async Task<ActionResult<PurchaseSummaryDto>> Summary(
int? farmerId, int? productId, int? orgId, DateTime? from, DateTime? to)
{
var visible = await scope.GetVisibleOrgIdsAsync(currentUser.Get());
var query = db.PurchaseOrders
.Where(p => p.Status == PurchaseStatus.Completed)
.AsQueryable();
if (visible is not null)
query = query.Where(p => visible.Contains(p.PurchaserOrgId));
if (farmerId.HasValue) query = query.Where(p => p.FarmerId == farmerId);
if (productId.HasValue) query = query.Where(p => p.ProductId == productId);
if (orgId.HasValue) query = query.Where(p => p.PurchaserOrgId == orgId);
if (from.HasValue) query = query.Where(p => p.CreatedAt >= from);
if (to.HasValue) query = query.Where(p => p.CreatedAt < to.Value.Date.AddDays(1));
var orderCount = await query.CountAsync();
var totalNet = await query.SumAsync(p => (decimal?)p.NetWeight) ?? 0;
var totalAmount = await query.SumAsync(p => (decimal?)p.Amount) ?? 0;
var farmerCount = await query.Select(p => p.FarmerId).Distinct().CountAsync();
var todayStart = DateTime.Today;
var todayOrderCount = await db.PurchaseOrders
.CountAsync(p => p.Status == PurchaseStatus.Completed && p.CreatedAt >= todayStart);
var avgPrice = totalNet > 0 ? Math.Round(totalAmount / totalNet, 2) : 0;
return Ok(new PurchaseSummaryDto(orderCount, Math.Round(totalNet, 2), avgPrice,
Math.Round(totalAmount, 2), farmerCount, todayOrderCount));
}
/// <summary>新建收购单(第一次过磅:记录毛重)</summary>
[HttpPost]
public async Task<ActionResult<PurchaseDto>> Create(PurchaseCreateRequest req)
@@ -162,6 +194,138 @@ public class PurchasesController(
.FirstAsync(p => p.Id == order.Id)).ToDto());
}
/// <summary>多磅收购单:一次性保存(一单多磅、多等级),完成后直接打印</summary>
[HttpPost("multi")]
public async Task<ActionResult<PurchaseDto>> CreateMulti(PurchaseMultiCreateRequest req)
{
if (req.Weighs is null || req.Weighs.Count == 0)
return BadRequest(new { message = "请至少录入一磅数据" });
if (!await db.Farmers.AnyAsync(f => f.Id == req.FarmerId))
return BadRequest(new { message = "农户不存在" });
if (!await db.Products.AnyAsync(p => p.Id == req.ProductId))
return BadRequest(new { message = "品种不存在" });
if (!await db.Organizations.AnyAsync(o => o.Id == req.PurchaserOrgId))
return BadRequest(new { message = "收购方组织不存在" });
var cu = currentUser.Get()!;
var orderNo = await numberGen.NextAsync("CG");
var order = new PurchaseOrder
{
OrderNo = orderNo,
FarmerId = req.FarmerId,
ProductId = req.ProductId,
PurchaserOrgId = req.PurchaserOrgId,
Unit = string.IsNullOrWhiteSpace(req.Unit) ? "公斤" : req.Unit,
WeighCount = req.Weighs.Count,
Status = PurchaseStatus.Completed,
OperatorId = cu.Id,
Notes = req.Notes ?? "",
WeighInAt = DateTime.Now,
WeighOutAt = DateTime.Now,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
var sortNo = 1;
foreach (var w in req.Weighs)
{
var net = w.NetWeight ?? Math.Round(w.GrossWeight - w.TareWeight, 2);
var amount = Math.Round(net * w.UnitPrice, 2);
order.Weighs.Add(new PurchaseWeigh
{
SortNo = sortNo++,
Grade = w.Grade,
PriceType = w.PriceType,
UnitPrice = w.UnitPrice,
GrossWeight = w.GrossWeight,
TareWeight = w.TareWeight,
NetWeight = net,
BoxCount = w.BoxCount,
Amount = amount,
CreatedAt = DateTime.Now
});
order.GrossWeight += w.GrossWeight;
order.TareWeight += w.TareWeight;
order.NetWeight += net;
order.Amount += amount;
order.BoxCount += w.BoxCount;
}
order.NetWeight = Math.Round(order.NetWeight, 2);
order.Amount = Math.Round(order.Amount, 2);
// 主字段取第一磅
var first = order.Weighs.First();
order.Grade = first.Grade;
order.UnitPrice = first.UnitPrice;
db.PurchaseOrders.Add(order);
await db.SaveChangesAsync();
return Ok((await db.PurchaseOrders
.Include(p => p.Farmer).Include(p => p.Product)
.Include(p => p.PurchaserOrg).Include(p => p.Operator)
.Include(p => p.Weighs)
.FirstAsync(p => p.Id == order.Id)).ToDto());
}
/// <summary>补打票据(记录补打日志到 Notes</summary>
[HttpPost("{id:int}/reprint")]
public async Task<ActionResult> Reprint(int id)
{
var order = await db.PurchaseOrders.FindAsync(id);
if (order is null) return NotFound();
var cu = currentUser.Get()!;
var log = $"补打票据 {DateTime.Now:yyyy-MM-dd HH:mm} by {cu.RealName}";
order.Notes = string.IsNullOrWhiteSpace(order.Notes) ? log : order.Notes + "" + log;
order.UpdatedAt = DateTime.Now;
await db.SaveChangesAsync();
return Ok(new { message = "已记录补打" });
}
/// <summary>票据过户(生成过户单号并记录)</summary>
[HttpPost("{id:int}/transfer")]
public async Task<ActionResult> Transfer(int id, PurchaseTransferRequest req)
{
var order = await db.PurchaseOrders.Include(p => p.Farmer).FirstOrDefaultAsync(p => p.Id == id);
if (order is null) return NotFound();
if (string.IsNullOrWhiteSpace(req.ToFarmer))
return BadRequest(new { message = "请填写过户到农户" });
if (order.Status != PurchaseStatus.Completed)
return BadRequest(new { message = "仅已完成单据可过户" });
var cu = currentUser.Get()!;
var transfer = new PurchaseTransfer
{
PurchaseOrderId = order.Id,
TransferNo = await numberGen.NextAsync("GH"),
FromFarmer = order.Farmer?.Name ?? "",
ToFarmer = req.ToFarmer,
ToIdCard = req.ToIdCard ?? "",
Reason = req.Reason ?? "",
OperatorId = cu.Id,
CreatedAt = DateTime.Now
};
db.PurchaseTransfers.Add(transfer);
order.UpdatedAt = DateTime.Now;
await db.SaveChangesAsync();
return Ok(new { message = "过户成功", transferNo = transfer.TransferNo });
}
/// <summary>收购单过户记录</summary>
[HttpGet("{id:int}/transfers")]
public async Task<ActionResult> Transfers(int id)
{
var items = await db.PurchaseTransfers.AsNoTracking()
.Include(t => t.Operator)
.Where(t => t.PurchaseOrderId == id)
.OrderByDescending(t => t.CreatedAt)
.Select(t => new PurchaseTransferDto(
t.Id, t.TransferNo, t.FromFarmer, t.ToFarmer, t.ToIdCard, t.Reason,
t.Operator != null ? t.Operator.RealName : "", t.CreatedAt))
.ToListAsync();
return Ok(items);
}
/// <summary>作废收购单</summary>
[HttpPut("{id:int}/cancel")]
public async Task<IActionResult> Cancel(int id)