storageRoot = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'account-mapping-matcher-' . uniqid(); File::makeDirectory($this->storageRoot . '/app', 0777, true, true); $this->app->useStoragePath($this->storageRoot); $this->store = app(AccountMappingStore::class); $this->matcher = app(AccountMappingMatcher::class); } protected function tearDown(): void { if ($this->storageRoot !== '' && File::isDirectory($this->storageRoot)) { File::deleteDirectory($this->storageRoot); } parent::tearDown(); } /** @param array $values */ private function rule(array $values): array { return $this->store->create($values + [ 'platform' => '测试平台', 'item' => '测试项目', 'category' => '费用', 'dAcct' => '销售费用', 'dSub' => '运费', 'cAcct' => '其他货币资金', 'cSub' => '', 'basis' => '支出基准', ]); } /** 同项目两条规则:带业务备注的优先于不带条件的兜底规则 */ public function test_specific_rule_wins_over_fallback(): void { $fallback = $this->rule(['item' => '在线支付']); $erp = $this->rule(['item' => '在线支付', 'memo' => 'ERP软件费', 'dAcct' => '管理费用', 'dSub' => '软件服务费']); $hit = $this->matcher->match('测试平台', '在线支付', ['ERP软件费'], '聚水潭ERP-专业版'); $this->assertNotNull($hit); $this->assertTrue($hit['matched']); $this->assertSame($erp['id'], $hit['rule']['id']); $miss = $this->matcher->match('测试平台', '在线支付', ['推广充值'], 'P4P充值包'); $this->assertNotNull($miss); $this->assertFalse($miss['matched']); $this->assertSame($fallback['id'], $miss['rule']['id']); $this->assertStringContainsString('兜底', $miss['reason']); } /** 商品名称关键词比业务备注更具体:两个都命中时取商品名称那条 */ public function test_goods_keyword_is_more_specific_than_remark(): void { $byMemo = $this->rule(['item' => '在线支付', 'memo' => '推广充值']); $byGoods = $this->rule(['item' => '在线支付', 'memo' => '推广充值', 'goods' => 'P4P充值包']); $hit = $this->matcher->match('测试平台', '在线支付', ['推广充值'], 'P4P充值包-2026年7月'); $this->assertNotNull($hit); $this->assertSame($byGoods['id'], $hit['rule']['id']); $other = $this->matcher->match('测试平台', '在线支付', ['推广充值'], '万相台充值'); $this->assertNotNull($other); $this->assertSame($byMemo['id'], $other['rule']['id']); } /** 月份式备注(2607退款 / 202607订单)不参与匹配,否则规则会随月份膨胀 */ public function test_period_labels_do_not_match_business_remark(): void { $rule = $this->rule(['item' => '退款', 'memo' => '退款']); $this->assertTrue($this->matcher->isPeriodLabel('2607退款')); $this->assertTrue($this->matcher->isPeriodLabel('202607订单')); $period = $this->matcher->match('测试平台', '退款', ['2607退款']); $this->assertNotNull($period); $this->assertFalse($period['matched']); $business = $this->matcher->match('测试平台', '退款', ['退款']); $this->assertNotNull($business); $this->assertTrue($business['matched']); $this->assertSame($rule['id'], $business['rule']['id']); } /** 商品名称关键词支持空格分隔的多个词,全部命中才算 */ public function test_goods_keyword_supports_multiple_terms(): void { $rule = $this->rule(['item' => '在线支付', 'goods' => '聚水潭 ERP']); $hit = $this->matcher->match('测试平台', '在线支付', [], '聚水潭ERP-专业版-订单付款'); $this->assertNotNull($hit); $this->assertTrue($hit['matched']); $this->assertSame($rule['id'], $hit['rule']['id']); $miss = $this->matcher->match('测试平台', '在线支付', [], '聚水潭-专业版'); $this->assertNotNull($miss); $this->assertFalse($miss['matched']); } /** 校验:同项目、条件不同 = 正常;条件相同 = 才算重复 */ public function test_validator_flags_only_same_condition_pairs(): void { $first = $this->rule(['item' => '交易分账_货款', 'memo' => '订单收入']); $second = $this->rule(['item' => '交易分账_货款', 'memo' => '退款']); $this->assertNotContains('duplicate_pair', $this->issueCodes([$first['id'], $second['id']]), '条件不同的两条不该报重复'); $third = $this->rule(['item' => '交易分账_货款', 'memo' => '退款']); $this->assertContains('duplicate_pair', $this->issueCodes([$second['id'], $third['id']]), '条件相同的两条要报重复'); } /** 「-」表示不限:存进去按空处理,匹配时不会拿它当关键词去找 */ public function test_dash_means_no_condition(): void { $rule = $this->rule(['item' => '在线支付', 'memo' => '-', 'goods' => '-']); $this->assertSame('', $rule['memo']); $this->assertSame('', $rule['goods']); $hit = $this->matcher->match('测试平台', '在线支付', ['推广充值'], 'P4P充值包'); $this->assertNotNull($hit); $this->assertFalse($hit['matched']); $this->assertSame($rule['id'], $hit['rule']['id']); } /** 校验:同项目下有兜底规则时给出提示,提醒它只做兜底 */ public function test_validator_hints_fallback_rule(): void { $fallback = $this->rule(['item' => '在线支付']); $specific = $this->rule(['item' => '在线支付', 'memo' => 'ERP软件费']); $codes = $this->issueCodes([$fallback['id']]); $this->assertContains('fallback_rule', $codes); $this->assertNotContains('duplicate_pair', $codes); $this->assertNotContains('duplicate_pair', $this->issueCodes([$specific['id']])); } /** @param array $ids @return array */ private function issueCodes(array $ids): array { $result = app(AccountMappingValidator::class)->validate($this->store->all()); $codes = []; foreach ($ids as $id) { foreach ($result['issues'][$id] ?? [] as $issue) { $codes[] = $issue['code']; } } return $codes; } }