load(); \support\App::loadAllConfig(['route']); date_default_timezone_set('Asia/Shanghai'); use support\Db; use plugin\saiadmin\app\model\water\WaterModel; use plugin\saiadmin\app\service\WaterBusiness; use plugin\saiadmin\app\service\WaterLedger; use plugin\saiadmin\app\controller\water\WaterController; use plugin\saiadmin\exception\ApiException; $checks = 0; function check(bool $condition, string $message): void { global $checks; ++$checks; if (!$condition) throw new RuntimeException($message); } function rejects(callable $callback, string $message): void { try { $callback(); } catch (ApiException $e) { check(true,$message); return; } throw new RuntimeException($message); } function workbook(string $content): array { $path = tempnam(sys_get_temp_dir(),'ledger-test-'); $reader = new \OpenSpout\Reader\XLSX\Reader(); try { file_put_contents($path,$content); $reader->open($path); $rows = []; foreach ($reader->getSheetIterator() as $sheet) { foreach ($sheet->getRowIterator() as $row) $rows[] = $row->toArray(); } return $rows; } finally { $reader->close(); unlink($path); } } $ledger = new WaterLedger(); $business = new WaterBusiness(); $tag = 'LEDGER-TEST-'.bin2hex(random_bytes(5)); $before = \plugin\saiadmin\app\model\water\WaterLedger::query()->count(); Db::beginTransaction(); try { $enterprise = $business->save('enterprise',['name'=>$tag.'企业','contact'=>'测试','phone'=>'TEST','address'=>'测试地址'],1)['id']; $sku = $business->save('sku',['name'=>$tag.'商品','liters'=>18.9],1)['id']; $ids = []; for ($i=0; $i<25; $i++) { $ids[] = (int)\plugin\saiadmin\app\model\water\WaterLedger::query()->insertGetId([ 'enterprise_id'=>$enterprise, 'sku_id'=>$sku, 'kind'=>$i%2 ? 'site_count' : 'inbound', 'quantity'=>$i===24 ? -3 : $i, 'reference'=>$tag.'-'.$i, 'note'=>$i===24 ? '=1+1' : '查说明-'.$i, 'created_by'=>1, 'created_at'=>'2026-09-'.($i<20 ? '09' : '10').' '.($i===24 ? '23:59:59' : '00:00:00'), ]); } $first = $business->listing('ledger',['keyword'=>' '.$tag.' ','page'=>1,'limit'=>10]); $second = $ledger->listing(['keyword'=>$tag,'page'=>2,'limit'=>10]); $last = $ledger->listing(['keyword'=>$tag,'page'=>3,'limit'=>10]); check($first['total']===25 && count($first['data'])===10 && count($second['data'])===10 && count($last['data'])===5,'10/10/5 pagination'); check(array_column(array_merge($first['data'],$second['data'],$last['data']),'id')===array_reverse($ids),'stable descending IDs without duplicate or skipped rows'); check(count($ledger->listing(['keyword'=>$tag,'page'=>4,'limit'=>10])['data'])===0,'out of range page'); check($ledger->listing(['keyword'=>$tag.'企业'])['total']===25,'enterprise keyword'); check($ledger->listing(['keyword'=>$tag.'商品'])['total']===25,'SKU keyword'); check($ledger->listing(['keyword'=>$tag.'-24'])['total']===1,'reference keyword'); check($ledger->listing(['keyword'=>'查说明-23'])['total']===1,'note keyword'); check($ledger->listing(['keyword'=>$tag,'kind'=>'inbound'])['total']===13,'kind filter'); $filter = ['keyword'=>$tag,'kind'=>'inbound','start_date'=>'2026-09-10','end_date'=>'2026-09-10']; $filtered = $ledger->listing($filter); check($filtered['total']===3 && $filtered['data'][0]['created_at']==='2026-09-10 23:59:59','combined filters include final second'); check($ledger->listing(['keyword'=>$tag,'end_date'=>'2026-09-09'])['total']===20,'end date boundary'); check(count($first['kinds'])===12 && $first['data'][0]['kind_label']==='仓库入库','localized kinds include fulfillment movements'); foreach ([['kind'=>'purchase'],['start_date'=>'2026-02-30'],['end_date'=>'invalid'],['start_date'=>'2026-09-10','end_date'=>'2026-09-09'],['keyword'=>str_repeat('字',121)]] as $invalid) { rejects(fn()=>$ledger->listing($invalid),'invalid list filters rejected'); rejects(fn()=>$ledger->export($invalid),'invalid export filters rejected'); } $exported = workbook($ledger->export(['keyword'=>$tag,'page'=>2,'limit'=>1])); check(count($exported)===26,'export includes all 25 matching rows regardless of page'); check(array_column(array_slice($exported,1),0)===array_reverse($ids),'export order equals list'); check($exported[1][4]===-3 && $exported[1][6]==="'=1+1",'negative quantity stays numeric and formula-like note safe'); $filteredExport = workbook($ledger->export($filter)); check(count($filteredExport)===4 && array_column(array_slice($filteredExport,1),0)===array_column($filtered['data'],'id'),'filtered export equals list'); check(count(workbook($ledger->export(['keyword'=>$tag.'-missing'])))===1,'empty export has headers only'); $reflection = new ReflectionClass(WaterController::class); $controller = $reflection->newInstanceWithoutConstructor(); $reflection->getProperty('adminId')->setValue($controller,0); $request = new \support\Request("GET /core/water/ledger HTTP/1.1\r\nHost: localhost\r\n\r\n"); rejects(fn()=>$controller->ledger($request),'list rejects unauthorized user'); rejects(fn()=>$controller->exportLedger($request),'export rejects unauthorized user'); $reflection->getProperty('adminId')->setValue($controller,1); $body = json_encode(['keyword'=>$tag,'kind'=>'inbound']); $request = new \support\Request("POST /core/water/ledger/export HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: ".strlen($body)."\r\n\r\n".$body); $response = $controller->exportLedger($request); check($response->getStatusCode()===200 && count(workbook($response->rawBody()))===14,'authorized JSON export controller'); } finally { Db::rollback(); } check(\plugin\saiadmin\app\model\water\WaterLedger::query()->count()===$before,'fixtures rolled back'); echo "PASS: $checks water ledger checks; all test records rolled back.\n";