Files
F8WebAI/backend/AgriculturalPlatform.Api/Controllers/PurchasesController.cs
T

346 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using AgriculturalPlatform.Api.Data;
using AgriculturalPlatform.Api.Dtos;
using AgriculturalPlatform.Api.Models;
using AgriculturalPlatform.Api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace AgriculturalPlatform.Api.Controllers;
[ApiController]
[Route("api/purchases")]
[Authorize]
public class PurchasesController(
AppDbContext db,
DataScopeService scope,
CurrentUserService currentUser,
NumberGenerator numberGen) : ControllerBase
{
/// <summary>收购单列表(分页 + 多条件 + 数据权限)</summary>
[HttpGet]
public async Task<ActionResult<PagedResult<PurchaseDto>>> List(
string? status, int? farmerId, int? productId, int? orgId, DateTime? from, DateTime? to,
[FromQuery] int page = 1, [FromQuery] int pageSize = 20, [FromQuery] string? keyword = null)
{
var visible = await scope.GetVisibleOrgIdsAsync(currentUser.Get());
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)
query = query.Where(p => visible.Contains(p.PurchaserOrgId));
if (!string.IsNullOrWhiteSpace(status) && Enum.TryParse<PurchaseStatus>(status, out var ps))
query = query.Where(p => p.Status == ps);
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));
if (!string.IsNullOrWhiteSpace(keyword))
query = query.Where(p => p.OrderNo.Contains(keyword)
|| (p.Farmer != null && p.Farmer.Name.Contains(keyword))
|| (p.Farmer != null && p.Farmer.Phone.Contains(keyword)));
var total = await query.CountAsync();
var items = await query.OrderByDescending(p => p.CreatedAt)
.Skip((page - 1) * pageSize).Take(pageSize)
.ToListAsync();
return Ok(new PagedResult<PurchaseDto>(items.ToDtos(), total));
}
/// <summary>进行中的过磅单(触摸屏/大屏实时展示)</summary>
[HttpGet("weighing")]
public async Task<ActionResult> Weighing()
{
var visible = await scope.GetVisibleOrgIdsAsync(currentUser.Get());
var query = db.PurchaseOrders
.Include(p => p.Farmer).Include(p => p.Product)
.Where(p => p.Status == PurchaseStatus.Weighing)
.AsQueryable();
if (visible is not null)
query = query.Where(p => visible.Contains(p.PurchaserOrgId));
var items = await query.OrderByDescending(p => p.WeighInAt).Take(20).ToListAsync();
return Ok(items.ToDtos());
}
/// <summary>收购单详情</summary>
[HttpGet("{id:int}")]
public async Task<ActionResult<PurchaseDto>> Get(int id)
{
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)
{
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,
Grade = req.Grade,
Unit = req.Unit,
UnitPrice = req.UnitPrice,
GrossWeight = req.GrossWeight,
WeighCount = 1,
Status = PurchaseStatus.Weighing,
OperatorId = cu.Id,
Notes = req.Notes
};
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)
.FirstAsync(p => p.Id == order.Id)).ToDto());
}
/// <summary>回皮完成(第二次过磅:记录皮重,计算净重与金额)</summary>
[HttpPut("{id:int}/tare")]
public async Task<ActionResult<PurchaseDto>> CompleteTare(int id, PurchaseTareRequest req)
{
var order = await db.PurchaseOrders.FindAsync(id);
if (order is null) return NotFound();
if (order.Status != PurchaseStatus.Weighing)
return BadRequest(new { message = "该收购单当前状态不可回皮" });
if (req.TareWeight >= order.GrossWeight)
return BadRequest(new { message = "皮重不能大于等于毛重" });
order.TareWeight = req.TareWeight;
order.NetWeight = Math.Round(order.GrossWeight - req.TareWeight, 2);
order.Amount = Math.Round(order.NetWeight * order.UnitPrice, 2);
order.WeighCount = 2;
order.Status = PurchaseStatus.Completed;
order.WeighOutAt = DateTime.Now;
order.UpdatedAt = DateTime.Now;
await db.SaveChangesAsync();
return Ok((await db.PurchaseOrders
.Include(p => p.Farmer).Include(p => p.Product)
.Include(p => p.PurchaserOrg).Include(p => p.Operator)
.FirstAsync(p => p.Id == order.Id)).ToDto());
}
/// <summary>修改单价/等级(完成前可调整)</summary>
[HttpPut("{id:int}/price")]
public async Task<ActionResult<PurchaseDto>> UpdatePrice(int id, [FromBody] decimal unitPrice, string? grade = null)
{
var order = await db.PurchaseOrders.FindAsync(id);
if (order is null) return NotFound();
if (order.Status == PurchaseStatus.Cancelled)
return BadRequest(new { message = "作废单据不可修改" });
order.UnitPrice = unitPrice;
if (!string.IsNullOrWhiteSpace(grade)) order.Grade = grade;
order.Amount = Math.Round(order.NetWeight * order.UnitPrice, 2);
order.UpdatedAt = DateTime.Now;
await db.SaveChangesAsync();
return Ok((await db.PurchaseOrders
.Include(p => p.Farmer).Include(p => p.Product)
.Include(p => p.PurchaserOrg).Include(p => p.Operator)
.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)
{
var order = await db.PurchaseOrders.FindAsync(id);
if (order is null) return NotFound();
if (order.Status == PurchaseStatus.Cancelled)
return BadRequest(new { message = "该单据已作废" });
if (await db.PaymentRecords.AnyAsync(p => p.PurchaseOrderId == id && p.Status == PayStatus.Success))
return BadRequest(new { message = "该单据已付款,请先退款后再作废" });
order.Status = PurchaseStatus.Cancelled;
order.UpdatedAt = DateTime.Now;
await db.SaveChangesAsync();
return Ok(new { message = "作废成功" });
}
}