*/ private const DEFAULT_STUCK_PAYMENT_REASONS = [ CaseLabelBit::APPLIANCE_NEED_MONEY => '新病例进产', CaseLabelBit::UPGRADE_NEED_MONEY => '转产品', ]; /** @var string agent-be 数据库连接名 */ private string $connection = 'agentslave'; /** @var string CRM 数据库连接名,用于回溯 label_bit 原始值 */ private string $crmConnection = 'crmslave'; 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 = []; $pfpContext = null; $checks['status'] = $this->checkStatus($type, $entity); if ($type === self::TYPE_CASE) { $pfpContext = $this->buildPfpContext($entity, $operatorCode); $checks['need_pfp'] = $this->checkNeedPfp($pfpContext); } $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, $pfpContext), '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 位图 * * is_need_pfp 来源于 CRM 的 ea_case_cstm.label_bit,经 stuck_payment_reason * 配置过滤后写入代理库;只有存在卡款原因的病例才会走代理端进产流程。 * * @param array $ctx buildPfpContext 的返回值 */ private function checkNeedPfp(array $ctx): array { $pass = $ctx['is_need_pfp'] > 0; $reasonText = $ctx['reason_text']; $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' => '进产原因检查', 'pass' => $pass, 'expected' => '病例存在卡生产原因('.$this->configOptionText($ctx).')', 'actual' => $actual, 'detail' => $this->pfpDetail($ctx, $pass), 'hint' => $pass ? null : $this->pfpHint($ctx), ] + $ctx; } /** * 汇总进产原因所需的全部上下文:代理库位图、配置项、CRM 原始 label_bit * * @return array */ 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 $configReasons * @return array> */ 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, 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 */ 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 $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 $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 $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 $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 * 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>} */ 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; } /** * @param array|null $pfpContext */ private function normalizeEntity(string $type, object $entity, ?array $pfpContext = null): 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), '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'] ?? '未知', ]; } 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 是否启用了对应产品账期'; } }