- Region 表新增热度列,热门省份优先显示 - 区县下拉按地级市分组、已录区县优先、支持全省搜索 - 组选择内置一组至二十组 - 银行卡号独立行+粗体预览,开户行独立行 - 新增农户类型:农户/个体户/合作社/经营集体/公司 - 身份证正反面支持高拍仪/摄像头OCR与手机扫码上传,附件存本地/OSS - 新增农户头像采集组件(摄像头/本地上传),预留人脸识别收购扩展 - 同名村弹窗提醒(防选错乡镇) - 编辑窗体禁止遮罩/Esc误关,整体加宽适配高频操作 - 修复二维码局域网手机无法访问(监听0.0.0.0、局域网IP识别、Vite代理兜底)
206 lines
8.9 KiB
C#
206 lines
8.9 KiB
C#
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/payments")]
|
|
[Authorize]
|
|
public class PaymentsController(
|
|
AppDbContext db,
|
|
DataScopeService scope,
|
|
CurrentUserService currentUser,
|
|
NumberGenerator numberGen) : ControllerBase
|
|
{
|
|
/// <summary>付款记录列表</summary>
|
|
[HttpGet]
|
|
public async Task<ActionResult<PagedResult<PaymentDto>>> List(
|
|
string? status, int? farmerId, 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.PaymentRecords
|
|
.Include(p => p.Farmer).Include(p => p.PurchaseOrder)
|
|
.Include(p => p.PurchaserOrg).Include(p => p.Operator)
|
|
.AsQueryable();
|
|
|
|
if (visible is not null)
|
|
query = query.Where(p => p.PurchaserOrgId != null && visible.Contains(p.PurchaserOrgId.Value));
|
|
if (!string.IsNullOrWhiteSpace(status) && Enum.TryParse<PayStatus>(status, out var ps))
|
|
query = query.Where(p => p.Status == ps);
|
|
if (farmerId.HasValue) query = query.Where(p => p.FarmerId == farmerId);
|
|
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.PayNo.Contains(keyword)
|
|
|| p.TradeNo.Contains(keyword)
|
|
|| (p.Farmer != null && p.Farmer.Name.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<PaymentDto>(items.ToDtos(), total));
|
|
}
|
|
|
|
/// <summary>支付状态统计(待支付/成功/失败/异常)</summary>
|
|
[HttpGet("stats")]
|
|
public async Task<ActionResult> Stats()
|
|
{
|
|
var visible = await scope.GetVisibleOrgIdsAsync(currentUser.Get());
|
|
var query = db.PaymentRecords.AsQueryable();
|
|
if (visible is not null)
|
|
query = query.Where(p => p.PurchaserOrgId != null && visible.Contains(p.PurchaserOrgId.Value));
|
|
return Ok(new
|
|
{
|
|
pending = await query.CountAsync(p => p.Status == PayStatus.Pending),
|
|
success = await query.CountAsync(p => p.Status == PayStatus.Success),
|
|
failed = await query.CountAsync(p => p.Status == PayStatus.Failed),
|
|
abnormal = await query.CountAsync(p => p.Status == PayStatus.Abnormal)
|
|
});
|
|
}
|
|
|
|
/// <summary>农户应收/已付汇总(结算用)</summary>
|
|
[HttpGet("farmer-summary")]
|
|
public async Task<ActionResult> FarmerSummary(int farmerId, int? orgId)
|
|
{
|
|
var visible = await scope.GetVisibleOrgIdsAsync(currentUser.Get());
|
|
var orderQuery = db.PurchaseOrders
|
|
.Where(p => p.FarmerId == farmerId && p.Status == PurchaseStatus.Completed)
|
|
.AsQueryable();
|
|
if (visible is not null)
|
|
orderQuery = orderQuery.Where(p => visible.Contains(p.PurchaserOrgId));
|
|
if (orgId.HasValue)
|
|
orderQuery = orderQuery.Where(p => p.PurchaserOrgId == orgId);
|
|
|
|
var totalAmount = await orderQuery.SumAsync(p => (decimal?)p.Amount) ?? 0;
|
|
|
|
var payQuery = db.PaymentRecords
|
|
.Where(p => p.FarmerId == farmerId && p.Status == PayStatus.Success)
|
|
.AsQueryable();
|
|
if (visible is not null)
|
|
payQuery = payQuery.Where(p => p.PurchaserOrgId != null && visible.Contains(p.PurchaserOrgId.Value));
|
|
if (orgId.HasValue)
|
|
payQuery = payQuery.Where(p => p.PurchaserOrgId == orgId);
|
|
|
|
var paidAmount = await payQuery.SumAsync(p => (decimal?)p.Amount) ?? 0;
|
|
|
|
// 未结算的收购单(可批量付款)
|
|
var unpaidOrders = await db.PurchaseOrders
|
|
.Where(p => p.FarmerId == farmerId && p.Status == PurchaseStatus.Completed)
|
|
.Where(p => !db.PaymentRecords.Any(pay =>
|
|
pay.PurchaseOrderId == p.Id && pay.Status == PayStatus.Success))
|
|
.Include(p => p.Product)
|
|
.Select(p => new { p.Id, p.OrderNo, p.Product!.Name, p.NetWeight, p.Amount, p.UnitPrice })
|
|
.ToListAsync();
|
|
|
|
return Ok(new
|
|
{
|
|
farmerId,
|
|
totalAmount,
|
|
paidAmount,
|
|
unpaidAmount = Math.Round(totalAmount - paidAmount, 2),
|
|
unpaidOrders
|
|
});
|
|
}
|
|
|
|
/// <summary>发起支付(按收购单或对农户批量结算)</summary>
|
|
[HttpPost]
|
|
public async Task<ActionResult<PaymentDto>> Create(PaymentCreateRequest req)
|
|
{
|
|
if (!await db.Farmers.AnyAsync(f => f.Id == req.FarmerId))
|
|
return BadRequest(new { message = "农户不存在" });
|
|
if (!Enum.TryParse<PayMethod>(req.Method, out var method))
|
|
return BadRequest(new { message = "支付方式不正确" });
|
|
|
|
// 按收购单付款:校验订单并防止重复支付
|
|
if (req.PurchaseOrderId.HasValue)
|
|
{
|
|
var order = await db.PurchaseOrders.FindAsync(req.PurchaseOrderId);
|
|
if (order is null || order.FarmerId != req.FarmerId)
|
|
return BadRequest(new { message = "收购单不存在或与农户不匹配" });
|
|
if (await db.PaymentRecords.AnyAsync(p =>
|
|
p.PurchaseOrderId == req.PurchaseOrderId && p.Status == PayStatus.Success))
|
|
return BadRequest(new { message = "该收购单已支付成功" });
|
|
}
|
|
|
|
var cu = currentUser.Get()!;
|
|
int? purchaserOrgId = req.PurchaseOrderId.HasValue
|
|
? (await db.PurchaseOrders.FindAsync(req.PurchaseOrderId))?.PurchaserOrgId
|
|
: cu.OrgId;
|
|
|
|
var payNo = await numberGen.NextAsync("ZF");
|
|
var payment = new PaymentRecord
|
|
{
|
|
PayNo = payNo,
|
|
PurchaseOrderId = req.PurchaseOrderId,
|
|
PurchaserOrgId = purchaserOrgId,
|
|
FarmerId = req.FarmerId,
|
|
Amount = req.Amount,
|
|
Method = method,
|
|
Status = PayStatus.Pending,
|
|
OperatorId = cu.Id,
|
|
Notes = req.Notes
|
|
};
|
|
db.PaymentRecords.Add(payment);
|
|
await db.SaveChangesAsync();
|
|
|
|
return Ok((await db.PaymentRecords
|
|
.Include(p => p.Farmer).Include(p => p.PurchaseOrder)
|
|
.Include(p => p.PurchaserOrg).Include(p => p.Operator)
|
|
.FirstAsync(p => p.Id == payment.Id)).ToDto());
|
|
}
|
|
|
|
/// <summary>确认支付结果(模拟收单回调,默认支付成功,可选失败/异常)</summary>
|
|
[HttpPut("{id:int}/confirm")]
|
|
public async Task<ActionResult<PaymentDto>> Confirm(int id, PaymentConfirmRequest req)
|
|
{
|
|
var payment = await db.PaymentRecords.FindAsync(id);
|
|
if (payment is null) return NotFound();
|
|
if (payment.Status != PayStatus.Pending)
|
|
return BadRequest(new { message = "当前状态不可确认" });
|
|
|
|
// Status 缺省或非法时按成功处理;成功/失败/异常均可写入
|
|
var result = Enum.TryParse<PayStatus>(req.Status, out var st)
|
|
&& st is PayStatus.Success or PayStatus.Failed or PayStatus.Abnormal
|
|
? st : PayStatus.Success;
|
|
|
|
payment.Status = result;
|
|
if (result == PayStatus.Success)
|
|
{
|
|
payment.PaidAt = DateTime.Now;
|
|
payment.TradeNo = string.IsNullOrWhiteSpace(req.TradeNo) ? payment.TradeNo : req.TradeNo;
|
|
if (string.IsNullOrWhiteSpace(payment.TradeNo))
|
|
payment.TradeNo = $"TRADE{DateTime.Now:yyyyMMddHHmmss}{Random.Shared.Next(100, 999)}";
|
|
}
|
|
if (!string.IsNullOrWhiteSpace(req.Message))
|
|
payment.Notes = string.IsNullOrWhiteSpace(payment.Notes) ? req.Message : $"{payment.Notes}\n{req.Message}";
|
|
await db.SaveChangesAsync();
|
|
|
|
return Ok((await db.PaymentRecords
|
|
.Include(p => p.Farmer).Include(p => p.PurchaseOrder)
|
|
.Include(p => p.PurchaserOrg).Include(p => p.Operator)
|
|
.FirstAsync(p => p.Id == payment.Id)).ToDto());
|
|
}
|
|
|
|
/// <summary>退款</summary>
|
|
[HttpPut("{id:int}/refund")]
|
|
public async Task<IActionResult> Refund(int id)
|
|
{
|
|
var payment = await db.PaymentRecords.FindAsync(id);
|
|
if (payment is null) return NotFound();
|
|
if (payment.Status != PayStatus.Success)
|
|
return BadRequest(new { message = "仅已支付成功的记录可退款" });
|
|
|
|
payment.Status = PayStatus.Refunded;
|
|
await db.SaveChangesAsync();
|
|
return Ok(new { message = "退款成功" });
|
|
}
|
|
}
|