using Admin.NET.Plugin.AiDOP.Dto.S0.Manufacturing;
using Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
using Admin.NET.Plugin.AiDOP.Infrastructure;
namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Manufacturing;
///
/// 工序流转卡主数据 CRUD(数据源:S0ProcessFlowCard 主表)。
///
/// 恢复主表链路:本 Controller 完全读写 S0ProcessFlowCard,替换此前误接 legacy MissedPrint 的只读投影。
/// 路由沿用 /api/s0/manufacturing/process-flow-cards,接口地址不变 → 前端 / 菜单 / 权限链路兼容。
///
/// 组织兜底(后端强制,不只依赖前端 payload):
/// - 新增:公司/工厂由后端按 S0 默认组织(SysOrg Code=S0-C001 / S0-F001)解析并 stamp,忽略 payload 组织值 → 杜绝跨厂注入;
/// - 编辑:公司/工厂 pin 到既有记录原值,忽略 payload 组织值 → 杜绝跨公司/跨工厂篡改;
/// - 详情/删除/启停:按 id 操作,不接受组织入参。
/// 唯一性:依赖 DB 唯一索引 uk_S0ProcessFlowCard_factory_flowcard (factory_ref_id, flow_card_no);
/// 预检友好 409,并发竞态由唯一索引兜底(AdoS0ExceptionFilter 将重复键映射为友好 409)。
///
[ApiController]
[Route("api/s0/manufacturing/process-flow-cards")]
[AllowAnonymous]
[NonUnify]
public class AdoS0ProcessFlowCardsController : ControllerBase
{
private const string DefaultCompanyCode = "S0-C001";
private const string DefaultFactoryCode = "S0-F001";
private readonly SqlSugarRepository _rep;
private readonly SqlSugarRepository _routingRep;
private readonly SqlSugarRepository _orgRep;
public AdoS0ProcessFlowCardsController(
SqlSugarRepository rep,
SqlSugarRepository routingRep,
SqlSugarRepository orgRep)
{
_rep = rep;
_routingRep = routingRep;
_orgRep = orgRep;
}
[HttpGet]
public async Task GetPagedAsync([FromQuery] AdoS0ProcessFlowCardQueryDto q)
{
var page = q.EffectivePage;
var pageSize = q.PageSize;
(page, pageSize) = PagingGuard.Normalize(page, pageSize);
var query = _rep.AsQueryable()
.WhereIF(q.CompanyRefId.HasValue, x => x.CompanyRefId == q.CompanyRefId!.Value)
.WhereIF(q.FactoryRefId.HasValue, x => x.FactoryRefId == q.FactoryRefId!.Value)
.WhereIF(!string.IsNullOrWhiteSpace(q.DomainCode), x => x.DomainCode == q.DomainCode)
.WhereIF(!string.IsNullOrWhiteSpace(q.WorkOrderNo), x => x.WorkOrderNo.Contains(q.WorkOrderNo!))
.WhereIF(!string.IsNullOrWhiteSpace(q.FlowCardNo), x => x.FlowCardNo.Contains(q.FlowCardNo!))
.WhereIF(!string.IsNullOrWhiteSpace(q.ItemNum), x => x.ItemNum.Contains(q.ItemNum!))
.WhereIF(q.IsActive.HasValue, x => x.IsActive == q.IsActive!.Value)
.WhereIF(!string.IsNullOrWhiteSpace(q.Keyword),
x => x.WorkOrderNo.Contains(q.Keyword!)
|| x.FlowCardNo.Contains(q.Keyword!)
|| x.ItemNum.Contains(q.Keyword!)
|| (x.ItemName != null && x.ItemName.Contains(q.Keyword!)));
var total = await query.CountAsync();
var list = await query
.OrderByDescending(x => x.CreateTime)
.OrderByDescending(x => x.Id)
.Skip((page - 1) * pageSize).Take(pageSize)
.ToListAsync();
return Ok(new { total, page, pageSize, list });
}
[HttpGet("{id:long}")]
public async Task GetAsync(long id)
{
var item = await _rep.GetByIdAsync(id);
return item == null ? AdoS0ApiErrors.NotFound("流转卡不存在") : Ok(item);
}
[HttpPost]
public async Task CreateAsync([FromBody] AdoS0ProcessFlowCardUpsertDto dto)
{
var (companyId, factoryId) = await ResolveDefaultOrgAsync();
if (companyId <= 0 || factoryId <= 0)
return AdoS0ApiErrors.InvalidRequest("S0 默认组织未初始化(SysOrg S0-C001 / S0-F001 缺失),无法创建流转卡");
var flowCardNo = dto.FlowCardNo?.Trim() ?? string.Empty;
if (await _rep.IsAnyAsync(x => x.FactoryRefId == factoryId && x.FlowCardNo == flowCardNo))
return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "流转卡号已存在");
var now = DateTime.Now;
var entity = new AdoS0ProcessFlowCard
{
// 组织字段后端强制 stamp 默认 S0 组织,忽略 payload 组织值(杜绝跨厂注入)。
CompanyRefId = companyId,
FactoryRefId = factoryId,
DomainCode = dto.DomainCode?.Trim(),
WorkOrderNo = dto.WorkOrderNo?.Trim() ?? string.Empty,
WorkOrderBatchNo = dto.WorkOrderBatchNo?.Trim(),
FlowCardNo = flowCardNo,
Nature = dto.Nature?.Trim(),
ItemNum = dto.ItemNum?.Trim() ?? string.Empty,
ItemName = dto.ItemName?.Trim(),
DrawingNo = dto.DrawingNo?.Trim(),
BomVersion = dto.BomVersion?.Trim(),
RoutingCode = dto.RoutingCode?.Trim(),
Remarks = dto.Remarks?.Trim(),
IsActive = dto.IsActive,
CreateUser = dto.CreateUser,
CreateTime = now,
UpdateUser = dto.UpdateUser,
UpdateTime = null
};
await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
return Ok(entity);
}
[HttpPut("{id:long}")]
public async Task UpdateAsync(long id, [FromBody] AdoS0ProcessFlowCardUpsertDto dto)
{
var entity = await _rep.GetByIdAsync(id);
if (entity == null) return AdoS0ApiErrors.NotFound("流转卡不存在");
var flowCardNo = dto.FlowCardNo?.Trim() ?? string.Empty;
// 唯一性预检以既有记录的工厂为界(组织 pin 原值,不接受 payload 迁移工厂)。
if (await _rep.IsAnyAsync(x => x.Id != id && x.FactoryRefId == entity.FactoryRefId && x.FlowCardNo == flowCardNo))
return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "流转卡号已存在");
// 组织字段 pin 到既有原值,忽略 payload 组织值(杜绝跨公司/跨工厂篡改)。
entity.WorkOrderNo = dto.WorkOrderNo?.Trim() ?? string.Empty;
entity.WorkOrderBatchNo = dto.WorkOrderBatchNo?.Trim();
entity.FlowCardNo = flowCardNo;
entity.Nature = dto.Nature?.Trim();
entity.ItemNum = dto.ItemNum?.Trim() ?? string.Empty;
entity.ItemName = dto.ItemName?.Trim();
entity.DrawingNo = dto.DrawingNo?.Trim();
entity.BomVersion = dto.BomVersion?.Trim();
entity.RoutingCode = dto.RoutingCode?.Trim();
entity.Remarks = dto.Remarks?.Trim();
entity.IsActive = dto.IsActive;
entity.DomainCode = dto.DomainCode?.Trim();
entity.UpdateUser = dto.UpdateUser;
entity.UpdateTime = DateTime.Now;
await _rep.AsUpdateable(entity)
.IgnoreColumns(x => new { x.CompanyRefId, x.FactoryRefId, x.CreateUser, x.CreateTime })
.ExecuteCommandAsync();
return Ok(entity);
}
[HttpPatch("{id:long}/toggle-enabled")]
public async Task ToggleActiveAsync(long id, [FromBody] AdoS0ProcessFlowCardToggleActiveDto dto)
{
var entity = await _rep.GetByIdAsync(id);
if (entity == null) return AdoS0ApiErrors.NotFound("流转卡不存在");
entity.IsActive = dto.IsEnabled;
entity.UpdateTime = DateTime.Now;
await _rep.AsUpdateable(entity)
.UpdateColumns(x => new { x.IsActive, x.UpdateTime })
.ExecuteCommandAsync();
return Ok(entity);
}
[HttpDelete("{id:long}")]
public async Task DeleteAsync(long id)
{
var item = await _rep.GetByIdAsync(id);
if (item == null) return AdoS0ApiErrors.NotFound("流转卡不存在");
await _rep.DeleteAsync(item);
return Ok(new { message = "删除成功" });
}
///
/// 工艺路线联动选项:按所选物料编码过滤该物料关联的 RouteCode(去重)。
/// 用稳定物料编码 ItemNum 关联(RoutingOpDetail.MaterialCode),不用物料名称。
/// 返回空 → 前端允许手工填写兜底;返回非空 → 前端强制下拉。
///
[HttpGet("routing-options")]
public async Task GetRoutingOptionsAsync([FromQuery] string? itemNum)
{
var key = itemNum?.Trim();
if (string.IsNullOrWhiteSpace(key))
return Ok(Array.Empty