feat(ClientBlack): 新增客户黑名单审批流程

- 黑名单新增、删除、批量导入、批量移出在配置审批节点后,审批通过才生效
- 新增黑名单待提交后可提交审批;仅首节点未审批时可撤回,进入后续节点禁止撤回
- 增加黑名单审批列表、审批弹窗、审批操作日志及状态筛选
- 支持审批配置中的“黑名单审批”流程及对应菜单、模块权限、操作权限
- 黑名单审批中禁止同名记录覆盖备注;已在黑名单中的同名导入会明确提示本次将修改备注
- 未配置审批节点时保持原有直接生效行为
- 增加黑名单审批状态机单测,39/39 通过
- 修正撤回全部失败时仍提示“撤回审批成功”的问题
This commit is contained in:
tengyufan
2026-08-13 17:21:04 +08:00
parent 9df39491da
commit 8afd449c40
26 changed files with 1090 additions and 106 deletions
@@ -0,0 +1,89 @@
using YLErp.Model;
namespace YLErp.Modules.ClientModule
{
public static class ClientBlackApprovalPolicy
{
public static readonly string[] EffectiveStates =
{
client_black.,
client_black.,
client_black.
};
public static bool IsEffective(string state)
{
return EffectiveStates.Contains(state);
}
public static bool CanSubmitAddition(string state)
{
return state == client_black. || state == client_black.;
}
public static ClientBlackAdditionResult GetAdditionResult(bool hasApprovalProcess)
{
return hasApprovalProcess
? new ClientBlackAdditionResult(client_black., 0, false)
: new ClientBlackAdditionResult(client_black., -2, true);
}
public static bool CanRequestRemoval(string state)
{
return state == client_black. || state == client_black.;
}
public static ClientBlackRemovalResult GetRemovalResult(bool hasApprovalProcess)
{
return hasApprovalProcess
? new ClientBlackRemovalResult(client_black., 1, false)
: new ClientBlackRemovalResult(client_black., -2, true);
}
public static bool CanReplaceRemarks(string state)
{
return state != client_black. && state != client_black.;
}
public static bool CanWithdraw(string state, int approvalProcess)
{
return approvalProcess == 1 &&
(state == client_black. || state == client_black.);
}
public static string GetRejectedState(string state)
{
return state switch
{
client_black. => client_black.,
client_black. => client_black.,
_ => throw new ArgumentException("当前状态不允许拒绝审批", nameof(state))
};
}
public static ClientBlackWithdrawResult GetWithdrawResult(string state)
{
return state switch
{
client_black. => new ClientBlackWithdrawResult(client_black., 0),
client_black. => new ClientBlackWithdrawResult(client_black., -2),
_ => throw new ArgumentException("当前状态不允许撤回审批", nameof(state))
};
}
public static ClientBlackFinalResult GetFinalResult(string state)
{
return state switch
{
client_black. => new ClientBlackFinalResult(client_black., false),
client_black. => new ClientBlackFinalResult(null, true),
_ => throw new ArgumentException("当前状态不允许完成审批", nameof(state))
};
}
}
public readonly record struct ClientBlackWithdrawResult(string State, int ApprovalProcess);
public readonly record struct ClientBlackFinalResult(string State, bool ShouldDelete);
public readonly record struct ClientBlackAdditionResult(string State, int ApprovalProcess, bool IsEffective);
public readonly record struct ClientBlackRemovalResult(string State, int ApprovalProcess, bool ShouldDelete);
}
@@ -41,6 +41,19 @@ namespace YLErp.Modules.ClientModule
{
predicate = predicate.And(d => d.Name.Contains(req.Name));
}
if (!string.IsNullOrEmpty(req.ClientBlackStates))
{
var states = req.ClientBlackStates.Split(',', StringSplitOptions.RemoveEmptyEntries);
predicate = predicate.And(d => states.Contains(d.State));
}
if (req.DateFromOptDate.HasValue)
{
predicate = predicate.And(d => d.OptDate >= req.DateFromOptDate.Value);
}
if (req.DateToOptDate.HasValue)
{
predicate = predicate.And(d => d.OptDate < req.DateToOptDate.Value.AddDays(1));
}
}
var query = DbContext.client_black.AsNoTracking().Where(predicate);
@@ -93,6 +106,314 @@ namespace YLErp.Modules.ClientModule
return retListResult;
}
public List<approvalprocess> ProcessList()
{
return DbContextFactory.GetYLDbContext().approvalprocess
.Where(s => s.processType == "ClientBlackProcess")
.OrderBy(s => s.order)
.ToList();
}
public void DeleteClientBlack(IEnumerable<int> ids)
{
var idList = ids?.Distinct().ToList() ?? new List<int>();
if (idList.Count == 0)
{
throw new ServiceException("请选择要移出的黑名单客户");
}
var rows = DbContext.client_black.Where(x => idList.Contains(x.id)).ToList();
if (rows.Count != idList.Count)
{
throw new ServiceException("未找到要删除的数据");
}
var hasProcess = ProcessList().Any();
foreach (var row in rows)
{
if (!ClientBlackApprovalPolicy.CanRequestRemoval(row.State))
{
throw new ServiceException($"黑名单客户{row.Name}当前状态不允许移出");
}
if (hasProcess)
{
var result = ClientBlackApprovalPolicy.GetRemovalResult(true);
row.State = result.State;
row.ApprovalProcess = result.ApprovalProcess;
row.ApprovalOptName = UserName;
row.ApprovalOptDate = DateTime.Now;
ClientBlackCategoryLog(row.id, client_black.);
}
else
{
RemoveEffectiveBlack(row);
}
}
DbContext.SaveChanges();
}
public void WithdrawApprovalClientBlack(List<int> ids, out int withdrawCount, out string msg)
{
withdrawCount = 0;
msg = "";
var rows = DbContext.client_black.Where(x => ids.Contains(x.id)).ToList();
foreach (var row in rows)
{
if (!ClientBlackApprovalPolicy.CanWithdraw(row.State, row.ApprovalProcess))
{
if (row.ApprovalProcess > 1)
{
msg += row.Name + ",";
}
continue;
}
var result = ClientBlackApprovalPolicy.GetWithdrawResult(row.State);
row.State = result.State;
row.ApprovalProcess = result.ApprovalProcess;
row.ApprovalOptName = null;
row.ApprovalOptDate = null;
ClientBlackCategoryLog(row.id, row.State);
withdrawCount++;
}
DbContext.SaveChanges();
}
public void SubmitApprovalClientBlack(List<int> ids)
{
var rows = DbContext.client_black.Where(x => ids.Contains(x.id)).ToList();
var process = ProcessList();
foreach (var row in rows)
{
if (!ClientBlackApprovalPolicy.CanSubmitAddition(row.State))
{
continue;
}
if (process.Count == 0)
{
row.State = client_black.;
row.ApprovalProcess = -2;
row.ApprovalOptName = UserName;
row.ApprovalOptDate = DateTime.Now;
var notifications = new List<(Client oldClient, Client newClient)>();
ApplyEffectiveAddition(row.Name, notifications);
ClientBlackCategoryLog(row.id, client_black., "未设置审批流程,直接通过");
DbContext.SaveChanges();
SendClientNotifications(notifications);
continue;
}
row.State = client_black.;
row.ApprovalProcess = 1;
row.ApprovalOptName = UserName;
row.ApprovalOptDate = DateTime.Now;
ClientBlackCategoryLog(row.id, client_black.);
}
DbContext.SaveChanges();
}
public string AuditClientBlack(ClientBlackAuditReq req, bool isBatch = false, string optType = "")
{
var row = DbContext.client_black.Find(req.id);
if (row == null)
{
throw new ServiceException("审批失败,系统中没有该黑名单记录");
}
if (row.State != client_black. && row.State != client_black.)
{
throw new ServiceException("当前黑名单不在审批中");
}
var process = ProcessList();
var currentNode = process.FirstOrDefault(x => x.order == row.ApprovalProcess);
if (currentNode == null || !UserBLL.GetRolesByUserId(UserId).Any(x => x.Id == currentNode.roleId))
{
throw new ServiceException("当前用户无权审批该节点");
}
if (req.status == "reject")
{
row.State = ClientBlackApprovalPolicy.GetRejectedState(row.State);
row.ApprovalProcess = -1;
row.ApprovalOptDate = DateTime.Now;
row.OptId = UserId;
row.OptName = UserName;
row.OptDate = DateTime.Now;
ClientBlackCategoryLog(row.id, row.State, req.auditComment);
DbContext.SaveChanges();
return "提交成功";
}
if (req.status != "pass")
{
throw new ServiceException("status参数不支持:" + req.status);
}
var nextNode = process.FirstOrDefault(x => x.order > row.ApprovalProcess);
if (nextNode != null)
{
row.ApprovalProcess = nextNode.order;
row.ApprovalOptDate = DateTime.Now;
row.OptId = UserId;
row.OptName = UserName;
row.OptDate = DateTime.Now;
ClientBlackCategoryLog(row.id, row.State, req.auditComment);
DbContext.SaveChanges();
return "提交成功";
}
var final = ClientBlackApprovalPolicy.GetFinalResult(row.State);
if (final.ShouldDelete)
{
RemoveEffectiveBlack(row, req.auditComment, isBatch ? optType : null);
DbContext.SaveChanges();
}
else
{
row.State = final.State;
row.ApprovalProcess = -2;
row.ApprovalOptDate = DateTime.Now;
var notifications = new List<(Client oldClient, Client newClient)>();
ApplyEffectiveAddition(row.Name, notifications);
ClientBlackCategoryLog(row.id, isBatch ? optType : row.State, req.auditComment);
DbContext.SaveChanges();
SendClientNotifications(notifications);
}
return "提交成功";
}
public SearchListResult<ClientBlackApprovalQueryRes> ClientBlackApprovalQuery(ClientBlackReq req)
{
var process = ProcessList();
var predicate = PredicateBuilder.Create<client_black>(x => x.ApprovalProcess > 0);
if (!string.IsNullOrWhiteSpace(req.Name))
{
predicate = predicate.And(x => x.Name.Contains(req.Name));
}
var query = from row in DbContext.client_black.AsNoTracking().Where(predicate)
select new ClientBlackApprovalQueryRes
{
id = row.id,
EncryptId = row.EncryptId,
ProcessOrderId = row.ApprovalProcess,
ProcessRoleId = 0,
ProcessStatus = "审批中 流程" + (row.ApprovalProcess - 1) + "/" + process.Count,
State = row.State,
ClientName = row.Name,
Comments = row.Remarks,
ApprovalOptName = row.ApprovalOptName,
ApprovalOptDate = row.ApprovalOptDate,
creator_id = row.creator_id,
creator_name = row.creator_name,
creator_time = row.creator_time
};
if (string.IsNullOrEmpty(req.sidx))
{
req.sidx = "ApprovalOptDate";
req.sord = "desc";
}
var result = query.OrderByDescending(x => x.ApprovalOptDate).ToSearchList(req);
var roles = new ErpBaseContext().Roles
.Select(x => new { x.Id, x.Name })
.ToDictionary(x => x.Id, x => x.Name);
foreach (var item in result.rows)
{
var node = process.FirstOrDefault(x => x.order == item.ProcessOrderId);
if (node == null)
{
continue;
}
item.ProcessRoleId = node.roleId;
item.ProcessRoleName = roles.TryGetValue(node.roleId, out var roleName) ? roleName : string.Empty;
}
return result;
}
private void ApplyEffectiveAddition(string name, List<(Client oldClient, Client newClient)> notifications)
{
var client = DbContext.client.FirstOrDefault(c => c.Name == name);
if (client == null)
{
return;
}
var dt = DateTime.Now;
var oldClient = client.Clone();
if (client.ProcessStatus == "已开户")
{
client.ProcessOrderId = -4;
client.ProcessStatus = "已休眠";
client.OptId = UserId;
client.OptName = UserName;
client.OptDate = dt;
DbContext.ClientAuditLog.Add(new ClientAuditLog
{
ClientId = client.id,
OptType = "休眠",
Changes = string.Empty,
DataType = "00",
OptId = UserId,
OptName = UserName,
OptDate = dt
});
notifications.Add((oldClient, client));
}
DbContext.ClientAuditLog.Add(new ClientAuditLog
{
ClientId = client.id,
OptType = "加入黑名单",
Changes = string.Empty,
DataType = "00",
OptId = UserId,
OptName = UserName,
OptDate = dt
});
}
private void RemoveEffectiveBlack(client_black row, string changes = null, string optType = null)
{
var client = DbContext.client.FirstOrDefault(c => c.Name == row.Name);
if (client != null)
{
DbContext.ClientAuditLog.Add(new ClientAuditLog
{
ClientId = client.id,
OptType = "移除黑名单",
Changes = string.Empty,
DataType = "00",
OptId = UserId,
OptName = UserName,
OptDate = DateTime.Now
});
}
DbContext.client_black.Remove(row);
ClientBlackCategoryLog(row.id, optType ?? "已删除", changes);
}
private void SendClientNotifications(List<(Client oldClient, Client newClient)> notifications)
{
foreach (var (oldClient, newClient) in notifications)
{
new ClientKafkaService(_kafkaProduce).Send(newClient, oldClient);
}
}
public void ClientBlackCategoryLog(int clientblackId, string optType, string changes = null)
{
DbContext.client_blacklog.Add(new ClientBlackLog
{
ClientBlackId = clientblackId,
OptType = optType,
Changes = changes,
DataType = "00",
OptId = UserId,
OptName = UserName,
OptDate = DateTime.Now
});
}
/// <summary>
/// 客户黑名单导入
/// </summary>
@@ -165,37 +486,47 @@ namespace YLErp.Modules.ClientModule
public void AddClientBlack(IEnumerable<client_black> list, bool checkStatus)
{
var inputList = list?.ToList() ?? new List<client_black>();
var errMsgList = new List<string>();
var nameList = list.Select(O => O.Name);
var dbList = DbContext.client_black.Where(O => nameList.Contains(O.Name));
var nameList = inputList.Select(O => O.Name).ToList();
var dbList = DbContext.client_black.Where(O => nameList.Contains(O.Name)).ToList();
foreach (var item in dbList)
{
var obj = inputList.FirstOrDefault(O => O.Name.Equals(item.Name, StringComparison.OrdinalIgnoreCase));
if (obj == null)
{
continue;
}
if (!ClientBlackApprovalPolicy.CanReplaceRemarks(item.State))
{
throw new ServiceException("黑名单客户在审批中无法修改!");
}
if (checkStatus && !string.IsNullOrWhiteSpace(item.Remarks) && item.Remarks != obj.Remarks)
{
errMsgList.Add($"{item.Name}");
}
}
if (checkStatus)
{
foreach (var item in dbList)
{
var obj = list.First(O => O.Name.Equals(item.Name, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(item.Remarks) && item.Remarks != obj.Remarks)
{
errMsgList.Add($"{item.Name}");
continue;
}
}
if (errMsgList.Count > 0)
{
var msg = "";
if (errMsgList.Count <= 5)
{
msg = $"客户:{string.Join(",", errMsgList)},备注已存在,是否替换?";
msg = $"客户:{string.Join(",", errMsgList)}当前已在黑名单中,本次将修改备注,备注已存在是否确认?";
}
else
{
msg = $"{string.Join(",", errMsgList.Take(5))} 等{errMsgList.Count}个客户,备注已存在,是否替换?";
msg = $"{string.Join(",", errMsgList.Take(5))} 等{errMsgList.Count}个客户当前已在黑名单中,本次将修改备注,备注已存在是否确认?";
}
throw new ServiceException(msg);
}
}
// 在外部定义列表来保存需要通知的客户对
var clientsToNotify = new List<(Client oldClient, Client newClient)>();
foreach (var item in list)
var newItems = new List<client_black>();
var processList = ProcessList();
foreach (var item in inputList)
{
if (string.IsNullOrWhiteSpace(item.Name))
{
@@ -206,61 +537,45 @@ namespace YLErp.Modules.ClientModule
item.OptId = UserId;
item.OptName = UserName;
item.OptDate = DateTime.Now;
var clientexistence = DbContext.client.FirstOrDefault(c => c.Name == item.Name);
if (clientexistence != null)
var existing = dbList.FirstOrDefault(x => x.Name.Equals(item.Name, StringComparison.OrdinalIgnoreCase));
if (existing != null)
{
var dt = DateTime.Now;
if (clientexistence.ProcessStatus == "已开户")
var oldRemarks = existing.Remarks;
existing.Remarks = item.Remarks;
existing.OptId = UserId;
existing.OptName = UserName;
existing.OptDate = DateTime.Now;
if (oldRemarks != existing.Remarks)
{
var oldClient= clientexistence.Clone();
clientexistence.ProcessOrderId = -4;
clientexistence.ProcessStatus = "已休眠";
clientexistence.OptId = UserId;
clientexistence.OptName = UserName;
clientexistence.OptDate = dt;
DbContext.ClientAuditLog.Add(new ClientAuditLog
{
ClientId = clientexistence.id,
OptType = "休眠",
Changes = string.Empty,
DataType = "00",
OptId = UserId,
OptName = UserName,
OptDate = dt
});
// 如果原有状态是已开户,添加到通知列表
if (oldClient != null)
{
clientsToNotify.Add((oldClient, clientexistence));
}
ClientBlackCategoryLog(existing.id, "修改备注", $"备注:{oldRemarks ?? string.Empty} -> {existing.Remarks ?? string.Empty}");
}
///日志记录
DbContext.ClientAuditLog.Add(new ClientAuditLog
{
ClientId = clientexistence.id,
OptType = "加入黑名单",
Changes = string.Empty,
DataType = "00",
OptId = UserId,
OptName = UserName,
OptDate = dt
});
continue;
}
var additionResult = ClientBlackApprovalPolicy.GetAdditionResult(processList.Any());
item.State = additionResult.State;
item.ApprovalProcess = additionResult.ApprovalProcess;
item.creator_id = UserId;
item.creator_name = UserName;
item.creator_time = DateTime.Now;
if (additionResult.IsEffective)
{
ApplyEffectiveAddition(item.Name, clientsToNotify);
}
newItems.Add(item);
}
if (dbList.Any())
DbContext.client_black.AddRange(newItems);
DbContext.SaveChanges();
foreach (var item in newItems)
{
DbContext.client_black.RemoveRange(dbList);
DbContext.SaveChanges();
ClientBlackCategoryLog(item.id, item.State);
}
DbContext.client_black.AddRange(list);
DbContext.SaveChanges();
// 发送Kafka消息
foreach (var (oldClient, newClient) in clientsToNotify)
{
new ClientKafkaService(_kafkaProduce).Send(newClient, oldClient);
}
var importHasTagClientNames = list.Where(p => p.Tags != null && p.Tags.Count > 0).Select(p => p.Name).Distinct().ToList();
var importHasTagClientNames = inputList.Where(p => p.Tags != null && p.Tags.Count > 0).Select(p => p.Name).Distinct().ToList();
if (importHasTagClientNames != null && importHasTagClientNames.Count > 0)
{
var dbClients = DbContext.client.AsNoTracking().Where(p => importHasTagClientNames.Contains(p.Name)).Select(p => new ClientSimpleDto
@@ -273,7 +588,7 @@ namespace YLErp.Modules.ClientModule
var tagService = new TagService(OptUser);
dbClients.ForEach(p =>
{
var importInfo = list.FirstOrDefault(d => d.Name.Equals(p.Name));
var importInfo = inputList.FirstOrDefault(d => d.Name.Equals(p.Name));
if (importInfo != null)
{
tagService.SetClientTagForClientImport(new TagModule.Dto.SetClientTagForClientEditRequest { ClientId = p.id, Tags = importInfo.Tags });
@@ -317,7 +317,7 @@ namespace YLErp.Modules.ClientModule
{
return "第" + rowNum + "行客户类别,机构属性,客户性质关联性质有误,导入失败";
}
if (DbContext.client_black.Any(c => c.Name == Name))
if (DbContext.client_black.Any(c => c.Name == Name && ClientBlackApprovalPolicy.EffectiveStates.Contains(c.State)))
{
return $"客户'{Name}'已经存在于黑名单中”";
}
@@ -1070,7 +1070,7 @@ namespace YLErp.Modules.ClientModule
}
}
}
if (DbContext.client_black.Any(c => c.Name == Name))
if (DbContext.client_black.Any(c => c.Name == Name && ClientBlackApprovalPolicy.EffectiveStates.Contains(c.State)))
{
return "" + Name + "客户已经存在于黑名单中”";
}
@@ -1733,7 +1733,7 @@ namespace YLErp.Modules.ClientModule
//默认为1
IsReceiveEmail = 1;
if (DbContext.client_black.Any(c => c.Name == Name))
if (DbContext.client_black.Any(c => c.Name == Name && ClientBlackApprovalPolicy.EffectiveStates.Contains(c.State)))
{
return "" + Name + "客户已经存在于黑名单中";
}
@@ -130,7 +130,7 @@ namespace YLErp.Modules.ClientModule
try
{
if (DbContext.client_black.Any(c => c.Name == client.Name))
if (DbContext.client_black.Any(c => c.Name == client.Name && ClientBlackApprovalPolicy.EffectiveStates.Contains(c.State)))
{
client.RejectOrderId = client.ApprovalOrderId;
client.ApprovalOrderId = -1;
@@ -158,7 +158,7 @@ namespace YLErp.Modules.ClientModule
throw new ServiceException("客户名称 必须填写");
}
if (DbContext.client_black.Any(c => c.Name == req.Name))
if (DbContext.client_black.Any(c => c.Name == req.Name && ClientBlackApprovalPolicy.EffectiveStates.Contains(c.State)))
{
throw new ServiceException("该客户为黑名单客户,无法进行下一步操作");
}
@@ -703,7 +703,7 @@ namespace YLErp.Modules.ClientModule
//新增时,新的客户名如果在黑名单里,不允许新增
//修改时,旧的客户名如果在黑名单里,不允许修改
if (!isAddNew && !req.Name.Equals(blackNameForCheck) && DbContext.client_black.Any(x => x.Name == blackNameForCheck))
if (!isAddNew && !req.Name.Equals(blackNameForCheck) && DbContext.client_black.Any(x => x.Name == blackNameForCheck && ClientBlackApprovalPolicy.EffectiveStates.Contains(x.State)))
{
throw new ServiceException("该客户为黑名单客户," + (req.id > 0 ? "不允许修改客户名称" : "不允许新增"));
}