#feature: some update
This commit is contained in:
@@ -3,7 +3,9 @@
|
||||
namespace App\Services;
|
||||
|
||||
use App\Clients\CrmClient;
|
||||
use App\Enums\CaseLabelBit;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 进产诊断服务
|
||||
@@ -18,15 +20,31 @@ use Illuminate\Support\Facades\DB;
|
||||
class ProductionDiagnosisService
|
||||
{
|
||||
public const TYPE_CASE = 'case';
|
||||
|
||||
public const TYPE_BUSINESS = 'business_document';
|
||||
|
||||
public const TYPE_SALE = 'sale_document';
|
||||
|
||||
/** @var string agent-be configs 表中的卡款原因配置键 */
|
||||
private const STUCK_PAYMENT_REASON_KEY = 'stuck_payment_reason';
|
||||
|
||||
/**
|
||||
* 读不到 configs 表时的兜底,与当前生产配置保持一致
|
||||
*
|
||||
* @var array<int,string>
|
||||
*/
|
||||
private const DEFAULT_STUCK_PAYMENT_REASONS = [
|
||||
CaseLabelBit::APPLIANCE_NEED_MONEY => '新病例进产',
|
||||
CaseLabelBit::UPGRADE_NEED_MONEY => '转产品',
|
||||
];
|
||||
|
||||
/** @var string agent-be 数据库连接名 */
|
||||
private string $connection = 'agentslave';
|
||||
|
||||
public function __construct(private readonly CrmClient $crm)
|
||||
{
|
||||
}
|
||||
/** @var string CRM 数据库连接名,用于回溯 label_bit 原始值 */
|
||||
private string $crmConnection = 'crmslave';
|
||||
|
||||
public function __construct(private readonly CrmClient $crm) {}
|
||||
|
||||
/**
|
||||
* 执行单次诊断
|
||||
@@ -36,7 +54,7 @@ class ProductionDiagnosisService
|
||||
$code = trim($code);
|
||||
$entity = $this->findEntity($type, $code);
|
||||
|
||||
if (!$entity) {
|
||||
if (! $entity) {
|
||||
return [
|
||||
'type' => $type,
|
||||
'type_label' => $this->typeLabel($type),
|
||||
@@ -49,11 +67,13 @@ class ProductionDiagnosisService
|
||||
$operatorCode = (string) $entity->agent_code;
|
||||
$operatorAgent = $this->findAgent($operatorCode);
|
||||
$checks = [];
|
||||
$pfpContext = null;
|
||||
|
||||
$checks['status'] = $this->checkStatus($type, $entity);
|
||||
|
||||
if ($type === self::TYPE_CASE) {
|
||||
$checks['need_pfp'] = $this->checkNeedPfp($entity);
|
||||
$pfpContext = $this->buildPfpContext($entity, $operatorCode);
|
||||
$checks['need_pfp'] = $this->checkNeedPfp($pfpContext);
|
||||
}
|
||||
|
||||
$checks['owner_agent'] = $this->checkOwnerAgent($type, $entity, $operatorCode);
|
||||
@@ -66,7 +86,7 @@ class ProductionDiagnosisService
|
||||
'type_label' => $this->typeLabel($type),
|
||||
'code' => $code,
|
||||
'found' => true,
|
||||
'entity' => $this->normalizeEntity($type, $entity),
|
||||
'entity' => $this->normalizeEntity($type, $entity, $pfpContext),
|
||||
'operating_agent_code' => $operatorCode,
|
||||
'operating_agent' => $operatorAgent ? [
|
||||
'code' => (string) $operatorAgent->code,
|
||||
@@ -144,29 +164,301 @@ class ProductionDiagnosisService
|
||||
}
|
||||
|
||||
/**
|
||||
* 病例放行检查 - case.is_need_pfp > 0
|
||||
* 进产原因检查 - case.is_need_pfp 位图
|
||||
*
|
||||
* is_need_pfp 来源于 CRM 的 ea_case_cstm.label_bit,经 stuck_payment_reason
|
||||
* 配置过滤后写入代理库;只有存在卡款原因的病例才会走代理端进产流程。
|
||||
*
|
||||
* @param array<string,mixed> $ctx buildPfpContext 的返回值
|
||||
*/
|
||||
private function checkNeedPfp(object $caseEntity): array
|
||||
private function checkNeedPfp(array $ctx): array
|
||||
{
|
||||
$isNeedPfp = (int) ($caseEntity->is_need_pfp ?? 0);
|
||||
$isPfp = (int) ($caseEntity->is_pfp ?? 0);
|
||||
$pass = $isNeedPfp > 0;
|
||||
$pass = $ctx['is_need_pfp'] > 0;
|
||||
$reasonText = $ctx['reason_text'];
|
||||
|
||||
$detail = $pass
|
||||
? '病例 is_need_pfp = '.$isNeedPfp.',命中放行节点'
|
||||
: '病例 is_need_pfp = 0,不需要放行(非欠款病例,不会触发进产流程)';
|
||||
$actual = $pass
|
||||
? sprintf('进产原因:%s(is_need_pfp = %d)', $reasonText, $ctx['is_need_pfp'])
|
||||
: sprintf('无进产原因(is_need_pfp = 0);放行状态:%s', $ctx['is_pfp_text']);
|
||||
|
||||
return [
|
||||
'key' => 'need_pfp',
|
||||
'label' => '放行节点检查 (is_need_pfp)',
|
||||
'label' => '进产原因检查',
|
||||
'pass' => $pass,
|
||||
'expected' => 'is_need_pfp > 0',
|
||||
'actual' => 'is_need_pfp = '.$isNeedPfp.', is_pfp = '.$isPfp,
|
||||
'detail' => $detail,
|
||||
'hint' => $pass ? null : '检查 stuck_payment_reason 配置以及病例账期推算逻辑',
|
||||
'expected' => '病例存在卡生产原因('.$this->configOptionText($ctx).')',
|
||||
'actual' => $actual,
|
||||
'detail' => $this->pfpDetail($ctx, $pass),
|
||||
'hint' => $pass ? null : $this->pfpHint($ctx),
|
||||
] + $ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* 汇总进产原因所需的全部上下文:代理库位图、配置项、CRM 原始 label_bit
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function buildPfpContext(object $caseEntity, string $operatorCode): array
|
||||
{
|
||||
$isNeedPfp = (int) ($caseEntity->is_need_pfp ?? 0);
|
||||
$isPfp = (int) ($caseEntity->is_pfp ?? 0);
|
||||
|
||||
$config = $this->stuckPaymentReasonConfig($operatorCode);
|
||||
$reasons = $this->describeReasons($isNeedPfp, $config['reasons']);
|
||||
|
||||
$crmLabelBit = $this->crmLabelBit((string) $caseEntity->case_code);
|
||||
$crmAvailable = $crmLabelBit !== null;
|
||||
$expectedNeedPfp = $crmAvailable ? ($crmLabelBit & $config['mask']) : null;
|
||||
|
||||
// CRM 上有卡款标记,但对应的 bit 没有配进 stuck_payment_reason,代理端会直接忽略
|
||||
$ignoredBits = $crmAvailable
|
||||
? array_values(array_filter(
|
||||
CaseLabelBit::split($crmLabelBit & ~$config['mask']),
|
||||
static fn (int $bit): bool => $bit !== CaseLabelBit::ALLOW_PROCESS_BY_HONEST
|
||||
))
|
||||
: [];
|
||||
|
||||
return [
|
||||
'is_need_pfp' => $isNeedPfp,
|
||||
'is_pfp' => $isPfp,
|
||||
'is_pfp_text' => $isPfp > 0 ? '已放行' : '未放行',
|
||||
'reasons' => $reasons,
|
||||
'reason_text' => $reasons === [] ? '无' : implode(' / ', array_column($reasons, 'label')),
|
||||
'config_mask' => $config['mask'],
|
||||
'config_source' => $config['source'],
|
||||
'config_source_text' => $this->configSourceText($config['source']),
|
||||
'config_options' => array_map(
|
||||
static fn (int $bit, string $label): array => ['bit' => $bit, 'label' => $label],
|
||||
array_keys($config['reasons']),
|
||||
array_values($config['reasons'])
|
||||
),
|
||||
'crm_available' => $crmAvailable,
|
||||
'crm_label_bit' => $crmLabelBit,
|
||||
'crm_label_bit_text' => $crmAvailable ? CaseLabelBit::toText($crmLabelBit) : '未知(CRM 库不可读)',
|
||||
'crm_ignored_reasons' => array_map(
|
||||
static fn (int $bit): array => [
|
||||
'bit' => $bit,
|
||||
'label' => CaseLabelBit::crmLabel($bit),
|
||||
'description' => CaseLabelBit::description($bit),
|
||||
],
|
||||
$ignoredBits
|
||||
),
|
||||
'expected_is_need_pfp' => $expectedNeedPfp,
|
||||
'sync_mismatch' => $expectedNeedPfp !== null && $expectedNeedPfp !== $isNeedPfp,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 把位图翻译成用户可读的进产原因
|
||||
*
|
||||
* @param array<int,string> $configReasons
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private function describeReasons(int $bitmap, array $configReasons): array
|
||||
{
|
||||
return array_map(
|
||||
static fn (int $bit): array => [
|
||||
'bit' => $bit,
|
||||
'label' => $configReasons[$bit] ?? CaseLabelBit::crmLabel($bit),
|
||||
'crm_label' => CaseLabelBit::crmLabel($bit),
|
||||
'description' => CaseLabelBit::description($bit),
|
||||
],
|
||||
CaseLabelBit::split($bitmap)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 agent-be configs 表中的 stuck_payment_reason
|
||||
*
|
||||
* 复现 ConfigService::getOne 的取值顺序:代理自身配置 → 全局配置 → 兜底默认值。
|
||||
*
|
||||
* @return array{reasons: array<int,string>, mask: int, source: string}
|
||||
*/
|
||||
private function stuckPaymentReasonConfig(string $operatorCode): array
|
||||
{
|
||||
try {
|
||||
$rows = DB::connection($this->connection)
|
||||
->table('configs')
|
||||
->where('key', self::STUCK_PAYMENT_REASON_KEY)
|
||||
->whereIn('agent_code', array_values(array_unique([$operatorCode, ''])))
|
||||
->get();
|
||||
|
||||
$row = $rows->firstWhere('agent_code', $operatorCode) ?: $rows->firstWhere('agent_code', '');
|
||||
$reasons = $this->parseStuckPaymentReasons($row->val ?? null);
|
||||
|
||||
if ($reasons !== []) {
|
||||
return [
|
||||
'reasons' => $reasons,
|
||||
'mask' => $this->maskOf($reasons),
|
||||
'source' => ((string) ($row->agent_code ?? '')) === '' ? 'global' : 'agent',
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('读取 stuck_payment_reason 配置失败,使用默认卡款原因。', ['exception' => $e]);
|
||||
}
|
||||
|
||||
return [
|
||||
'reasons' => self::DEFAULT_STUCK_PAYMENT_REASONS,
|
||||
'mask' => $this->maskOf(self::DEFAULT_STUCK_PAYMENT_REASONS),
|
||||
'source' => 'default',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* configs.val 形如 [{"key":2,"lable":"新病例进产"},{"key":4,"lable":"转产品"}]
|
||||
* 线上配置的 label 字段存在 lable 拼写,两种都兼容
|
||||
*
|
||||
* @return array<int,string>
|
||||
*/
|
||||
private function parseStuckPaymentReasons(mixed $val): array
|
||||
{
|
||||
if (is_string($val)) {
|
||||
$val = json_decode($val, true);
|
||||
}
|
||||
|
||||
if (! is_array($val)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$reasons = [];
|
||||
foreach ($val as $item) {
|
||||
$bit = (int) (is_array($item) ? ($item['key'] ?? 0) : 0);
|
||||
if ($bit <= 0) {
|
||||
continue;
|
||||
}
|
||||
$label = (string) ($item['lable'] ?? $item['label'] ?? '');
|
||||
$reasons[$bit] = $label !== '' ? $label : CaseLabelBit::crmLabel($bit);
|
||||
}
|
||||
|
||||
return $reasons;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复现 DebtEnum::needMoney - 所有配置项 key 的按位或
|
||||
*
|
||||
* @param array<int,string> $reasons
|
||||
*/
|
||||
private function maskOf(array $reasons): int
|
||||
{
|
||||
$mask = 0;
|
||||
foreach (array_keys($reasons) as $bit) {
|
||||
$mask |= $bit;
|
||||
}
|
||||
|
||||
return $mask;
|
||||
}
|
||||
|
||||
/**
|
||||
* 直查 CRM 库的 ea_case_cstm.label_bit,用于判断代理库是否同步到位
|
||||
*/
|
||||
private function crmLabelBit(string $caseCode): ?int
|
||||
{
|
||||
if ($caseCode === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$value = DB::connection($this->crmConnection)
|
||||
->table('ea_case as c')
|
||||
->join('ea_case_cstm as cc', 'cc.id_c', '=', 'c.id')
|
||||
->where('c.name', $caseCode)
|
||||
->where('c.deleted', 0)
|
||||
->value('cc.label_bit');
|
||||
|
||||
return $value === null ? null : (int) $value;
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('读取 CRM label_bit 失败,跳过同步比对。', ['case_code' => $caseCode, 'exception' => $e]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $ctx
|
||||
*/
|
||||
private function pfpDetail(array $ctx, bool $pass): string
|
||||
{
|
||||
if ($pass) {
|
||||
$detail = sprintf('病例因「%s」被卡在生产前,需要代理确认进产后才会放行。', $ctx['reason_text']);
|
||||
|
||||
if ($ctx['is_pfp'] > 0) {
|
||||
$detail .= '该病例已放行(is_pfp = 1)。';
|
||||
}
|
||||
|
||||
if ($ctx['sync_mismatch']) {
|
||||
$detail .= sprintf(
|
||||
'注意:CRM 当前 label_bit = %d,按配置应为 is_need_pfp = %d,与代理库不一致。',
|
||||
$ctx['crm_label_bit'],
|
||||
$ctx['expected_is_need_pfp']
|
||||
);
|
||||
}
|
||||
|
||||
return $detail;
|
||||
}
|
||||
|
||||
if ($ctx['sync_mismatch'] && $ctx['expected_is_need_pfp'] > 0) {
|
||||
return sprintf(
|
||||
'CRM 已标记「%s」,按配置应写入 is_need_pfp = %d,但代理库仍为 0,疑似 case_basic_info_change 事件未消费或延迟。',
|
||||
CaseLabelBit::toText($ctx['expected_is_need_pfp']),
|
||||
$ctx['expected_is_need_pfp']
|
||||
);
|
||||
}
|
||||
|
||||
if ($ctx['crm_ignored_reasons'] !== []) {
|
||||
return sprintf(
|
||||
'CRM 标记了「%s」,但该原因未纳入 stuck_payment_reason 配置(当前仅 %s),代理端不会产生进产原因。',
|
||||
implode(' / ', array_column($ctx['crm_ignored_reasons'], 'label')),
|
||||
$this->configOptionText($ctx)
|
||||
);
|
||||
}
|
||||
|
||||
if ($ctx['crm_available'] && $ctx['crm_label_bit'] === 0) {
|
||||
return '病例在 CRM 侧没有任何卡生产标记,属于正常病例,不需要也无法走代理进产流程。';
|
||||
}
|
||||
|
||||
return '病例没有卡生产原因(is_need_pfp = 0),不需要代理放行,进产流程不会对该病例生效。';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $ctx
|
||||
*/
|
||||
private function pfpHint(array $ctx): string
|
||||
{
|
||||
if ($ctx['sync_mismatch'] && $ctx['expected_is_need_pfp'] > 0) {
|
||||
return '检查 agent-be 是否正常消费 CRM 的病例变更事件,必要时重新推送该病例的 case_basic_info_change 消息';
|
||||
}
|
||||
|
||||
if ($ctx['crm_ignored_reasons'] !== []) {
|
||||
return '若该原因也需要代理放行,需在 agent-be configs 表的 stuck_payment_reason 中补充对应 key';
|
||||
}
|
||||
|
||||
if (! $ctx['crm_available']) {
|
||||
return '未能读取 CRM 的 ea_case_cstm.label_bit,可检查 crmslave 数据库配置后重新诊断';
|
||||
}
|
||||
|
||||
return '确认该病例是否确实需要卡款放行;正常病例由 CRM 直接进产,无需代理操作';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $ctx
|
||||
*/
|
||||
private function configOptionText(array $ctx): string
|
||||
{
|
||||
$options = array_map(
|
||||
static fn (array $option): string => sprintf('%s(%d)', $option['label'], $option['bit']),
|
||||
$ctx['config_options']
|
||||
);
|
||||
|
||||
return $options === [] ? '未配置任何卡款原因' : implode('、', $options);
|
||||
}
|
||||
|
||||
private function configSourceText(string $source): string
|
||||
{
|
||||
return match ($source) {
|
||||
'agent' => '代理级 configs 配置',
|
||||
'global' => '全局 configs 配置',
|
||||
default => '内置默认配置(未读到 configs 表)',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 归属代理检查 - 必须满足:
|
||||
* 1) entity.agent_code === operatorCode
|
||||
@@ -190,7 +482,7 @@ class ProductionDiagnosisService
|
||||
$detail = match (true) {
|
||||
$entityAgentCode === '' => '归属代理 agent_code 为空,无法定位操作代理',
|
||||
$entityAgentCode !== $operatorCode => '当前操作代理 '.$operatorCode.' 与单据归属代理 '.$entityAgentCode.' 不一致',
|
||||
!$exists => '结算代理表 '.$settlementTable.' 中未找到 code='.$entityCode.', agent_code='.$operatorCode.' 的有效记录',
|
||||
! $exists => '结算代理表 '.$settlementTable.' 中未找到 code='.$entityCode.', agent_code='.$operatorCode.' 的有效记录',
|
||||
default => '操作代理为归属代理,且在结算代理表中存在有效记录',
|
||||
};
|
||||
|
||||
@@ -219,7 +511,7 @@ class ProductionDiagnosisService
|
||||
{
|
||||
$productCode = (string) ($entity->product_code ?? '');
|
||||
|
||||
if (!$operatorAgent) {
|
||||
if (! $operatorAgent) {
|
||||
return [
|
||||
'key' => 'credit',
|
||||
'label' => '账期检查',
|
||||
@@ -249,7 +541,7 @@ class ProductionDiagnosisService
|
||||
->where('deleted', 0)
|
||||
->first();
|
||||
|
||||
if (!$relation) {
|
||||
if (! $relation) {
|
||||
return [
|
||||
'key' => 'credit',
|
||||
'label' => '账期检查',
|
||||
@@ -303,9 +595,9 @@ class ProductionDiagnosisService
|
||||
$pass = $lastCreditAgentCode !== '' && $lastCreditAgentCode === $operatorCode;
|
||||
|
||||
$detail = match (true) {
|
||||
!$crmConfigured => 'CRM 接口未配置(CRM_SERVICE_BASE_URI),一级代理账期视为「未知」,链路计算可能与生产不一致',
|
||||
! $crmConfigured => 'CRM 接口未配置(CRM_SERVICE_BASE_URI),一级代理账期视为「未知」,链路计算可能与生产不一致',
|
||||
$firstAgentCreditMap === null => '调用 CRM 一级代理详情失败,无法判断一级代理账期',
|
||||
!$firstAgentHasCredit => '一级代理 '.$rootAgentCode.' 在产品 '.$productCode.' 上无账期,AgentCredit 返回空,账期判断必然失败',
|
||||
! $firstAgentHasCredit => '一级代理 '.$rootAgentCode.' 在产品 '.$productCode.' 上无账期,AgentCredit 返回空,账期判断必然失败',
|
||||
$lastCreditAgentCode === '' => '账期链路计算结果为空',
|
||||
$pass => '最后一级有账期的代理 = 单据归属代理('.$operatorCode.')',
|
||||
default => '最后一级有账期的代理为 '.$lastCreditAgentCode.',与单据归属代理 '.$operatorCode.' 不一致',
|
||||
@@ -348,7 +640,7 @@ class ProductionDiagnosisService
|
||||
'credit_source' => 'crm:getAgentByCode',
|
||||
];
|
||||
|
||||
if (!$firstAgentHasCredit) {
|
||||
if (! $firstAgentHasCredit) {
|
||||
return [
|
||||
'last_credit_agent_code' => '',
|
||||
'chain' => $chainView,
|
||||
@@ -377,8 +669,9 @@ class ProductionDiagnosisService
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$hasCredit) {
|
||||
if (! $hasCredit) {
|
||||
$broken = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -520,7 +813,10 @@ class ProductionDiagnosisService
|
||||
return $type === self::TYPE_CASE ? (string) $entity->case_code : (string) $entity->code;
|
||||
}
|
||||
|
||||
private function normalizeEntity(string $type, object $entity): array
|
||||
/**
|
||||
* @param array<string,mixed>|null $pfpContext
|
||||
*/
|
||||
private function normalizeEntity(string $type, object $entity, ?array $pfpContext = null): array
|
||||
{
|
||||
$base = [
|
||||
'status' => (int) $entity->status,
|
||||
@@ -538,6 +834,9 @@ class ProductionDiagnosisService
|
||||
'patient_name' => (string) ($entity->patient_name ?? ''),
|
||||
'is_need_pfp' => (int) ($entity->is_need_pfp ?? 0),
|
||||
'is_pfp' => (int) ($entity->is_pfp ?? 0),
|
||||
'debt_reason_text' => $pfpContext['reason_text'] ?? '无',
|
||||
'is_pfp_text' => $pfpContext['is_pfp_text'] ?? ((int) ($entity->is_pfp ?? 0) > 0 ? '已放行' : '未放行'),
|
||||
'crm_label_bit_text' => $pfpContext['crm_label_bit_text'] ?? '未知',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -549,13 +848,13 @@ class ProductionDiagnosisService
|
||||
|
||||
private function creditHint(bool $crmConfigured, ?array $firstAgentCreditMap, bool $firstAgentHasCredit): string
|
||||
{
|
||||
if (!$crmConfigured) {
|
||||
if (! $crmConfigured) {
|
||||
return '配置 .env 中的 CRM_SERVICE_BASE_URI 后可获得准确的一级代理账期判断';
|
||||
}
|
||||
if ($firstAgentCreditMap === null) {
|
||||
return 'CRM 接口调用失败,可查看 laravel.log';
|
||||
}
|
||||
if (!$firstAgentHasCredit) {
|
||||
if (! $firstAgentHasCredit) {
|
||||
return '需在 CRM 「集团详情」productList 中确认该产品的 agentAccountingPeriod > 0';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user