Files
toolbox/app/Services/ProductionDiagnosisService.php
T

565 lines
21 KiB
PHP
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.
<?php
namespace App\Services;
use App\Clients\CrmClient;
use Illuminate\Support\Facades\DB;
/**
* 进产诊断服务
*
* 复刻 agent-be 中以下三处进产/放行判断逻辑,给出失败原因:
* - App\Services\AgentCase\ConfirmProduction::canProduction
* - App\Services\AgentBusinessDocument\ConfirmProduction::canProduction
* - App\Services\AgentSaleDocument\ConfirmPermit::canPermit
*
* 通过 agentslave 库直查所需表,并通过 CrmClient 调用 CRM 接口判断一级代理账期。
*/
class ProductionDiagnosisService
{
public const TYPE_CASE = 'case';
public const TYPE_BUSINESS = 'business_document';
public const TYPE_SALE = 'sale_document';
/** @var string agent-be 数据库连接名 */
private string $connection = 'agentslave';
public function __construct(private readonly CrmClient $crm)
{
}
/**
* 执行单次诊断
*/
public function diagnose(string $type, string $code): array
{
$code = trim($code);
$entity = $this->findEntity($type, $code);
if (!$entity) {
return [
'type' => $type,
'type_label' => $this->typeLabel($type),
'code' => $code,
'found' => false,
'message' => '未在 '.$this->tableFor($type).' 表中找到对应记录',
];
}
$operatorCode = (string) $entity->agent_code;
$operatorAgent = $this->findAgent($operatorCode);
$checks = [];
$checks['status'] = $this->checkStatus($type, $entity);
if ($type === self::TYPE_CASE) {
$checks['need_pfp'] = $this->checkNeedPfp($entity);
}
$checks['owner_agent'] = $this->checkOwnerAgent($type, $entity, $operatorCode);
$checks['credit'] = $this->checkCredit($entity, $operatorAgent, $operatorCode);
$canProduce = collect($checks)->every(fn ($c) => ($c['pass'] ?? false) === true);
return [
'type' => $type,
'type_label' => $this->typeLabel($type),
'code' => $code,
'found' => true,
'entity' => $this->normalizeEntity($type, $entity),
'operating_agent_code' => $operatorCode,
'operating_agent' => $operatorAgent ? [
'code' => (string) $operatorAgent->code,
'name' => (string) ($operatorAgent->name ?? ''),
'level' => isset($operatorAgent->level) ? (int) $operatorAgent->level : null,
] : null,
'checks' => array_values($checks),
'can_production' => $canProduce,
];
}
// -----------------------------------------------------------------
// 实体查找
// -----------------------------------------------------------------
private function findEntity(string $type, string $code): ?object
{
return match ($type) {
self::TYPE_CASE => DB::connection($this->connection)
->table('cases')
->where('case_code', $code)
->where('deleted', 0)
->first(),
self::TYPE_BUSINESS => DB::connection($this->connection)
->table('business_documents')
->where('code', $code)
->where('deleted', 0)
->first(),
self::TYPE_SALE => DB::connection($this->connection)
->table('sale_documents')
->where('code', $code)
->where('deleted', 0)
->first(),
default => null,
};
}
private function findAgent(string $agentCode): ?object
{
if ($agentCode === '') {
return null;
}
return DB::connection($this->connection)
->table('agents')
->where('code', $agentCode)
->where('deleted', 0)
->first();
}
// -----------------------------------------------------------------
// 各项检查
// -----------------------------------------------------------------
/**
* 状态检查 - 三类单据的「期望状态」不同
*/
private function checkStatus(string $type, object $entity): array
{
[$expectedStatus, $expectedLabel] = $this->expectedStatusFor($type);
$actualStatus = (int) $entity->status;
$pass = $actualStatus === $expectedStatus;
return [
'key' => 'status',
'label' => '状态检查',
'pass' => $pass,
'expected' => sprintf('%s (%d)', $expectedLabel, $expectedStatus),
'actual' => sprintf('%s (%d)', $this->statusLabel($type, $actualStatus), $actualStatus),
'detail' => $pass
? '单据状态符合进产条件'
: '单据状态不在「'.$expectedLabel.'」,无法进产',
'hint' => $pass ? null : '需等待单据流转至「'.$expectedLabel.'」后才能进产',
];
}
/**
* 病例放行检查 - case.is_need_pfp > 0
*/
private function checkNeedPfp(object $caseEntity): array
{
$isNeedPfp = (int) ($caseEntity->is_need_pfp ?? 0);
$isPfp = (int) ($caseEntity->is_pfp ?? 0);
$pass = $isNeedPfp > 0;
$detail = $pass
? '病例 is_need_pfp = '.$isNeedPfp.',命中放行节点'
: '病例 is_need_pfp = 0,不需要放行(非欠款病例,不会触发进产流程)';
return [
'key' => 'need_pfp',
'label' => '放行节点检查 (is_need_pfp)',
'pass' => $pass,
'expected' => 'is_need_pfp > 0',
'actual' => 'is_need_pfp = '.$isNeedPfp.', is_pfp = '.$isPfp,
'detail' => $detail,
'hint' => $pass ? null : '检查 stuck_payment_reason 配置以及病例账期推算逻辑',
];
}
/**
* 归属代理检查 - 必须满足:
* 1) entity.agent_code === operatorCode
* 2) 结算代理表中存在 (code = entity.code, agent_code = operatorCode, deleted = 0)
*/
private function checkOwnerAgent(string $type, object $entity, string $operatorCode): array
{
$entityAgentCode = (string) $entity->agent_code;
$settlementTable = $this->settlementTableFor($type);
$entityCode = $this->primaryCodeOf($type, $entity);
$exists = DB::connection($this->connection)
->table($settlementTable)
->where('code', $entityCode)
->where('agent_code', $operatorCode)
->where('deleted', 0)
->exists();
$pass = $entityAgentCode === $operatorCode && $entityAgentCode !== '' && $exists;
$detail = match (true) {
$entityAgentCode === '' => '归属代理 agent_code 为空,无法定位操作代理',
$entityAgentCode !== $operatorCode => '当前操作代理 '.$operatorCode.' 与单据归属代理 '.$entityAgentCode.' 不一致',
!$exists => '结算代理表 '.$settlementTable.' 中未找到 code='.$entityCode.', agent_code='.$operatorCode.' 的有效记录',
default => '操作代理为归属代理,且在结算代理表中存在有效记录',
};
return [
'key' => 'owner_agent',
'label' => '归属代理权限',
'pass' => $pass,
'expected' => '操作代理 = 单据 agent_code,且在 '.$settlementTable.' 中 deleted=0 存在记录',
'actual' => sprintf(
'单据 agent_code=%s,结算代理表存在=%s',
$entityAgentCode === '' ? '(空)' : $entityAgentCode,
$exists ? '是' : '否'
),
'detail' => $detail,
'hint' => $pass ? null : '检查 '.$settlementTable.' 的记录是否被软删或代理归属是否被调整',
];
}
/**
* 账期检查 - 完整复现 AgentCredit::getLastCreditAgentCode 逻辑
*
* 通过 agent_agents 取链路(root → ... → operator),逐级查 contracts 表
* 一级代理账期通过 CRM 接口 /api/group/detail/{code} 取得
*/
private function checkCredit(object $entity, ?object $operatorAgent, string $operatorCode): array
{
$productCode = (string) ($entity->product_code ?? '');
if (!$operatorAgent) {
return [
'key' => 'credit',
'label' => '账期检查',
'pass' => false,
'expected' => '最后一级有账期的代理 = 单据 agent_code',
'actual' => '未找到归属代理 '.$operatorCode.' 的代理记录',
'detail' => 'agents 表中查不到该代理,无法计算账期链路',
'hint' => '确认 agents 表中是否存在该 code 且 deleted=0',
];
}
if ($productCode === '') {
return [
'key' => 'credit',
'label' => '账期检查',
'pass' => false,
'expected' => '存在 product_code 才能判断账期',
'actual' => 'product_code 为空',
'detail' => '单据未关联 product_code,账期无法判断',
'hint' => '检查单据数据是否完整',
];
}
$relation = DB::connection($this->connection)
->table('agent_agents')
->where('agent_code', $operatorCode)
->where('deleted', 0)
->first();
if (!$relation) {
return [
'key' => 'credit',
'label' => '账期检查',
'pass' => false,
'expected' => '存在 agent_agents 链路',
'actual' => 'agent_agents 中无 '.$operatorCode.' 的记录',
'detail' => '无法构建代理链路,AgentCredit 直接返回空,账期判断必然失败',
'hint' => '检查 agent_agents 数据是否同步',
];
}
$rootAgentCode = (string) $relation->root_agent_code;
// 取整条链路 root -> operator,按 lft 升序
$chain = DB::connection($this->connection)
->table('agent_agents')
->where('root_agent_code', $rootAgentCode)
->where('lft', '<=', (int) $relation->lft)
->where('rgt', '>=', (int) $relation->rgt)
->where('deleted', 0)
->orderBy('lft')
->get();
if ($chain->isEmpty()) {
return [
'key' => 'credit',
'label' => '账期检查',
'pass' => false,
'expected' => '存在代理链路',
'actual' => '代理链路为空',
'detail' => '无法构建代理链路',
'hint' => '检查 agent_agents 数据',
];
}
// 一级代理账期 - 调 CRM
$firstAgentCreditMap = $this->crm->isConfigured()
? $this->crm->firstAgentCreditMap($rootAgentCode)
: null;
$crmConfigured = $this->crm->isConfigured();
$firstAgentHasCredit = $firstAgentCreditMap !== null
&& isset($firstAgentCreditMap[$productCode])
&& $firstAgentCreditMap[$productCode] === true;
// 子级代理逐级取合同 is_credit
$subChain = $chain->slice(1)->values();
$chainEvaluation = $this->evaluateChain($rootAgentCode, $subChain, $productCode, $firstAgentHasCredit);
$lastCreditAgentCode = $chainEvaluation['last_credit_agent_code'];
$pass = $lastCreditAgentCode !== '' && $lastCreditAgentCode === $operatorCode;
$detail = match (true) {
!$crmConfigured => 'CRM 接口未配置(CRM_SERVICE_BASE_URI),一级代理账期视为「未知」,链路计算可能与生产不一致',
$firstAgentCreditMap === null => '调用 CRM 一级代理详情失败,无法判断一级代理账期',
!$firstAgentHasCredit => '一级代理 '.$rootAgentCode.' 在产品 '.$productCode.' 上无账期,AgentCredit 返回空,账期判断必然失败',
$lastCreditAgentCode === '' => '账期链路计算结果为空',
$pass => '最后一级有账期的代理 = 单据归属代理('.$operatorCode.'',
default => '最后一级有账期的代理为 '.$lastCreditAgentCode.',与单据归属代理 '.$operatorCode.' 不一致',
};
return [
'key' => 'credit',
'label' => '账期检查',
'pass' => $pass,
'expected' => '最后一级有账期的代理 = '.$operatorCode,
'actual' => '最后一级有账期的代理 = '.($lastCreditAgentCode === '' ? '(空)' : $lastCreditAgentCode),
'detail' => $detail,
'hint' => $pass ? null : $this->creditHint($crmConfigured, $firstAgentCreditMap, $firstAgentHasCredit),
'chain' => $chainEvaluation['chain'],
'product_code' => $productCode,
'root_agent_code' => $rootAgentCode,
'crm_configured' => $crmConfigured,
'first_agent_credit_resolved' => $firstAgentCreditMap !== null,
'first_agent_has_credit' => $firstAgentHasCredit,
];
}
/**
* 复现 AgentCredit::getLastCreditAgentCode 中遍历子代理的部分
*
* @return array{last_credit_agent_code:string, chain:array<int,array<string,mixed>>}
*/
private function evaluateChain(string $rootAgentCode, \Illuminate\Support\Collection $subChain, string $productCode, bool $firstAgentHasCredit): array
{
$chainView = [];
// root 节点
$rootAgent = $this->findAgent($rootAgentCode);
$chainView[] = [
'agent_code' => $rootAgentCode,
'agent_name' => $rootAgent->name ?? null,
'level' => $rootAgent ? (int) $rootAgent->level : null,
'is_root' => true,
'has_credit' => $firstAgentHasCredit,
'credit_source' => 'crm:getAgentByCode',
];
if (!$firstAgentHasCredit) {
return [
'last_credit_agent_code' => '',
'chain' => $chainView,
];
}
$lastCreditAgentCode = $rootAgentCode;
$broken = false;
foreach ($subChain as $node) {
$agentCode = (string) $node->agent_code;
$agent = $this->findAgent($agentCode);
$hasCredit = $this->subAgentHasCredit($agentCode, $productCode);
$chainView[] = [
'agent_code' => $agentCode,
'agent_name' => $agent->name ?? null,
'level' => $agent ? (int) $agent->level : null,
'is_root' => false,
'has_credit' => $hasCredit,
'credit_source' => 'db:agent_contracts',
'broken' => $broken,
];
if ($broken) {
continue;
}
if (!$hasCredit) {
$broken = true;
continue;
}
$lastCreditAgentCode = $agentCode;
}
return [
'last_credit_agent_code' => $lastCreditAgentCode,
'chain' => $chainView,
];
}
/**
* 子代理在某产品上是否有账期 - 复现 AgentCredit::get
*
* agent_contracts JOIN contracts WHERE contracts.status = ENABLE
* 然后取 product_code 对应的 is_credit > 0
*/
private function subAgentHasCredit(string $agentCode, string $productCode): bool
{
// ContractModel::STATUS_ENABLE = 1
$contracts = DB::connection($this->connection)
->table('agent_contracts as ac')
->join('contracts as c', 'c.id', '=', 'ac.contract_id')
->where('ac.agent_code', $agentCode)
->where('ac.deleted', 0)
->where('c.deleted', 0)
->where('c.status', 1)
->where('c.product_code', $productCode)
->select('c.is_credit')
->get();
if ($contracts->isEmpty()) {
return false;
}
// 与 AgentCredit::get 一致:取该 product 下最后一个值(map 覆盖)
$hasCredit = false;
foreach ($contracts as $row) {
$hasCredit = ((int) $row->is_credit) > 0;
}
return $hasCredit;
}
// -----------------------------------------------------------------
// 标签与映射
// -----------------------------------------------------------------
private function expectedStatusFor(string $type): array
{
return match ($type) {
// CaseEnum::STATUS_3D_CONFIRMED
self::TYPE_CASE => [12, '3D设计已确认'],
// BusinessDocumentEnum::STATUS_TO_BE_PAYMENT
self::TYPE_BUSINESS => [5, '异常暂停(款项待支付)'],
// SaleDocumentEnum::STATUS_WAIT_PERMIT
self::TYPE_SALE => [3, '待放行'],
};
}
private function statusLabel(string $type, int $status): string
{
$map = match ($type) {
self::TYPE_CASE => [
1 => '资料处理中',
2 => '文字方案设计中',
3 => '文字方案待确认',
4 => '3D设计中',
5 => '3D设计待确认',
6 => '加工中',
7 => '已发货',
8 => '暂停',
9 => '结束',
10 => '不收治',
11 => '文字方案已确认',
12 => '3D设计已确认',
20 => '目标位设计中',
21 => '目标位待确认',
22 => '目标位已确认',
30 => '产品待确认',
31 => '产品已确认',
],
self::TYPE_BUSINESS => [
1 => '资料未收到',
2 => '资料处理中',
3 => '风险待确认',
4 => '风险已确认',
5 => '异常暂停(款项待支付)',
9 => '加工中',
10 => '已发货',
11 => '暂停',
12 => '终止',
],
self::TYPE_SALE => [
1 => '新建',
2 => '待付款',
3 => '待放行',
4 => '待发货',
5 => '已发货',
6 => '待审批',
7 => '审批拒绝',
8 => '部分发货',
],
};
return $map[$status] ?? '未知状态';
}
private function typeLabel(string $type): string
{
return match ($type) {
self::TYPE_CASE => '病例',
self::TYPE_BUSINESS => '业务单据',
self::TYPE_SALE => '销售单据',
};
}
private function tableFor(string $type): string
{
return match ($type) {
self::TYPE_CASE => 'cases',
self::TYPE_BUSINESS => 'business_documents',
self::TYPE_SALE => 'sale_documents',
};
}
private function settlementTableFor(string $type): string
{
return match ($type) {
self::TYPE_CASE => 'settlement_agent_case',
self::TYPE_BUSINESS => 'settlement_agent_business_order',
self::TYPE_SALE => 'settlement_agent_sales_order',
};
}
private function primaryCodeOf(string $type, object $entity): string
{
return $type === self::TYPE_CASE ? (string) $entity->case_code : (string) $entity->code;
}
private function normalizeEntity(string $type, object $entity): array
{
$base = [
'status' => (int) $entity->status,
'status_label' => $this->statusLabel($type, (int) $entity->status),
'agent_code' => (string) ($entity->agent_code ?? ''),
'settlement_agent_code' => (string) ($entity->settlement_agent_code ?? ''),
'product_code' => (string) ($entity->product_code ?? ''),
];
if ($type === self::TYPE_CASE) {
return $base + [
'code' => (string) $entity->case_code,
'hospital_code' => (string) ($entity->hospital_code ?? ''),
'doctor_code' => (string) ($entity->doctor_code ?? ''),
'patient_name' => (string) ($entity->patient_name ?? ''),
'is_need_pfp' => (int) ($entity->is_need_pfp ?? 0),
'is_pfp' => (int) ($entity->is_pfp ?? 0),
];
}
return $base + [
'code' => (string) $entity->code,
'hospital_code' => (string) ($entity->hospital_code ?? ''),
];
}
private function creditHint(bool $crmConfigured, ?array $firstAgentCreditMap, bool $firstAgentHasCredit): string
{
if (!$crmConfigured) {
return '配置 .env 中的 CRM_SERVICE_BASE_URI 后可获得准确的一级代理账期判断';
}
if ($firstAgentCreditMap === null) {
return 'CRM 接口调用失败,可查看 laravel.log';
}
if (!$firstAgentHasCredit) {
return '需在 CRM 「集团详情」productList 中确认该产品的 agentAccountingPeriod > 0';
}
return '检查链路中各代理的 agent_contracts / contracts 是否启用了对应产品账期';
}
}