> $rules * @return array{issues:array>,summary:array} */ public function validate(array $rules): array { $issues = []; $dupIndex = []; foreach ($rules as $rule) { $dupIndex[implode('|', [ trim((string) ($rule['platform'] ?? '')), trim((string) ($rule['item'] ?? '')), ])][] = (int) $rule['id']; } // 同一「平台 + 账单项目」下,用区分条件(业务备注 + 商品名称)分组: // 条件相同的两条 = 真重复(提示); // 条件不同的多条 = 按条件区分(正常,不报)。 $conditionIndex = []; foreach ($rules as $rule) { $conditionIndex[$this->pairKey($rule)][$this->conditionKey($rule)][] = (int) $rule['id']; } foreach ($rules as $rule) { $id = (int) $rule['id']; $list = []; $list = array_merge($list, $this->accountIssues($rule, 'dAcct', 'dSub', '借方')); $list = array_merge($list, $this->accountIssues($rule, 'cAcct', 'cSub', '贷方')); // 方向基准 $basis = trim((string) ($rule['basis'] ?? '')); $category = trim((string) ($rule['category'] ?? '')); if ($basis === '') { $list[] = $this->issue('错误', 'basis_missing', '没有填方向基准,程序无法判断该照抄还是互换。'); } elseif (! in_array($basis, AccountMappingStore::BASISES, true)) { $list[] = $this->issue('错误', 'basis_invalid', "方向基准「{$basis}」不是有效值,只能是支出基准或收入基准。"); } elseif ($category !== '' && isset(AccountMappingStore::CATEGORY_BASIS[$category])) { $expected = AccountMappingStore::CATEGORY_BASIS[$category]; if ($expected !== $basis) { $list[] = $this->issue( '警告', 'basis_mismatch', "大类「{$category}」按口径应是「{$expected}」,这里填的是「{$basis}」,账单收支与基准不一致时方向会反。" ); } } if ($category === '') { $list[] = $this->issue('警告', 'category_missing', '没有填大类,方向基准失去参照,也无法按大类筛选。'); } // 票据口径 $invoice = trim((string) ($rule['invoice'] ?? '')); if ($invoice === '') { $list[] = $this->issue('提示', 'invoice_missing', '票据口径为空,汇算清缴时不好判断调增项。'); } elseif (! in_array($invoice, AccountMappingStore::INVOICES, true)) { $list[] = $this->issue('提示', 'invoice_unknown', "票据口径「{$invoice}」不在常用取值(0.06 / 不开票 / -)内。"); } // 借贷两边同一个科目 $d = trim((string) ($rule['dAcct'] ?? '')); $c = trim((string) ($rule['cAcct'] ?? '')); $ds = trim((string) ($rule['dSub'] ?? '')); $cs = trim((string) ($rule['cSub'] ?? '')); if ($d !== '' && $d === $c && $ds === $cs) { $list[] = $this->issue('警告', 'same_account', '借方和贷方是同一个科目,这笔业务不会有损益影响。'); } // 同平台同项目重复 $pairKey = trim((string) ($rule['platform'] ?? '')) . '|' . trim((string) ($rule['item'] ?? '')); if (count($dupIndex[$pairKey] ?? []) > 1) { $sameCondition = array_values(array_diff($conditionIndex[$pairKey][$this->conditionKey($rule)] ?? [], [$id])); if ($sameCondition !== []) { $list[] = $this->issue( '提示', 'duplicate_pair', '同一平台、同一账单项目、区分条件也相同的第 ' . implode('、', $sameCondition) . ' 条规则属于重复,建议合并。' ); } if (trim((string) ($rule['memo'] ?? '')) === '' && trim((string) ($rule['goods'] ?? '')) === '') { $list[] = $this->issue( '提示', 'fallback_rule', '这条没有区分条件,属于兜底规则:只有在同项目下其它带条件的规则都没命中时才会用它。' ); } } if ($list !== []) { $issues[$id] = $list; } } $errRows = 0; $warnRows = 0; $errCount = 0; $warnCount = 0; $tipCount = 0; foreach ($issues as $list) { $levels = array_column($list, 'level'); if (in_array('错误', $levels, true)) { $errRows++; } elseif (in_array('警告', $levels, true)) { $warnRows++; } foreach ($levels as $level) { if ($level === '错误') { $errCount++; } elseif ($level === '警告') { $warnCount++; } else { $tipCount++; } } } $total = count($rules); return [ 'issues' => $issues, 'summary' => [ 'total' => $total, 'error_rows' => $errRows, 'warning_rows' => $warnRows, 'ok_rows' => max($total - $errRows - $warnRows, 0), 'error_count' => $errCount, 'warning_count' => $warnCount, 'tip_count' => $tipCount, ], ]; } /** * @return array */ private function accountIssues(array $rule, string $acctField, string $subField, string $side): array { $list = []; $acct = trim((string) ($rule[$acctField] ?? '')); $sub = trim((string) ($rule[$subField] ?? '')); if ($acct === '') { $list[] = $this->issue('错误', 'account_missing', "{$side}科目为空,生成不了分录。"); return $list; } $base = $this->coa->toCode($acct); if ($base === null) { $anywhere = $this->coa->anywhereByName($acct); $hint = $anywhere === [] ? '账套里没有这个名称的科目,需要先建科目或改成账套已有的一级科目。' : '它只存在于 ' . implode('、', $anywhere) . ',不是一级科目,应放到对应的一级科目下面。'; $list[] = $this->issue('错误', 'account_unknown', "{$side}科目「{$acct}」在账套里找不到:{$hint}"); return $list; } if ($sub === '' || $sub === $acct) { return $list; } $hits = $this->coa->resolve($acct, $sub); if (count($hits) === 1) { return $list; } if ($hits === []) { $anywhere = $this->coa->anywhereByName($sub); $hint = $anywhere === [] ? '账套里没有这个名称,需要新建,或改用已有明细。' : '它在 ' . implode('、', $anywhere) . ',不挂在「' . $acct . '」下面。'; $list[] = $this->issue('警告', 'sub_unknown', "{$side}二级科目「{$sub}」找不到:{$hint}"); return $list; } $list[] = $this->issue( '警告', 'sub_ambiguous', "{$side}二级科目「{$sub}」在「{$acct}」下有 " . count($hits) . ' 个(' . implode('、', $hits) . '),程序无法自动选,建议改写为编码。' ); return $list; } /** @return array{level:string,code:string,message:string} */ private function issue(string $level, string $code, string $message): array { return ['level' => $level, 'code' => $code, 'message' => $message]; } /** 平台 + 账单项目 */ private function pairKey(array $rule): string { return trim((string) ($rule['platform'] ?? '')) . '|' . trim((string) ($rule['item'] ?? '')); } /** 区分条件:业务备注关键词 + 商品名称关键词 */ private function conditionKey(array $rule): string { return trim((string) ($rule['memo'] ?? '')) . '|' . trim((string) ($rule['goods'] ?? '')); } }