diff --git a/app/Business/JstOrderLabelPlantingRules.php b/app/Business/JstOrderLabelPlantingRules.php new file mode 100644 index 00000000..c280c6fa --- /dev/null +++ b/app/Business/JstOrderLabelPlantingRules.php @@ -0,0 +1,67 @@ + [标签 => 是/否] */ + public function map(int $companyId): array + { + return Cache::remember($this->key($companyId), now()->addHours(self::TTL_HOURS), function () use ($companyId): array { + try { + if (! Schema::hasTable('jst_order_label_planting_rules')) { + return []; + } + + $map = []; + foreach (JstOrderLabelPlantingRule::query() + ->where('company_id', $companyId) + ->get(['label', 'is_planting']) as $row) { + $label = trim((string) $row->label); + if ($label === '') { + continue; + } + $map[$label] = trim((string) $row->is_planting) === '否' ? '否' : '是'; + } + + return $map; + } catch (Throwable) { + return []; + } + }); + } + + /** 单个标签的判断结果:命中映射表取映射值(默认是),未命中为否。 */ + public function flag(int $companyId, ?string $label): string + { + $key = trim((string) $label); + if ($key === '') { + return '否'; + } + + return $this->map($companyId)[$key] ?? '否'; + } + + public function forget(int $companyId): void + { + Cache::forget($this->key($companyId)); + } + + private function key(int $companyId): string + { + return "jst-order-labels:planting-rules:v1:{$companyId}"; + } +} diff --git a/app/Console/Commands/ImportJstOrderLabelPlantingRules.php b/app/Console/Commands/ImportJstOrderLabelPlantingRules.php new file mode 100644 index 00000000..4afc02ca --- /dev/null +++ b/app/Console/Commands/ImportJstOrderLabelPlantingRules.php @@ -0,0 +1,146 @@ +argument('path'); + if (! is_file($path)) { + $this->error('文件不存在:' . $path); + + return self::FAILURE; + } + + $companyId = (int) ($this->option('company') ?: (function_exists('company_id') && company_id() ? company_id() : 1)); + if ($companyId <= 0) { + $this->error('公司ID无效,请用 --company 指定。'); + + return self::FAILURE; + } + + try { + $rows = $this->readRows($path); + } catch (Throwable $e) { + $this->error('读取失败:' . $e->getMessage()); + + return self::FAILURE; + } + + if ($rows === []) { + $this->error('没有解析到有效数据(需要表头:标签 / 是否种菜订单)。'); + + return self::FAILURE; + } + + $now = now(); + DB::transaction(function () use ($rows, $now, $companyId): void { + if ($this->option('replace')) { + DB::table('jst_order_label_planting_rules')->where('company_id', $companyId)->delete(); + } + + foreach (array_chunk($rows, 500) as $chunk) { + DB::table('jst_order_label_planting_rules')->upsert( + array_map(static fn (array $row): array => $row + [ + 'company_id' => $companyId, + 'created_at' => $now, + 'updated_at' => $now, + ], $chunk), + ['company_id', 'label'], + ['is_planting', 'updated_at'], + ); + } + }); + + $rules->forget($companyId); + + $planting = count(array_filter($rows, static fn (array $row): bool => $row['is_planting'] === '是')); + + $this->info(sprintf('已导入 %d 条「是否种菜订单」映射(公司 %d):种菜 %d 条,非种菜 %d 条。', count($rows), $companyId, $planting, count($rows) - $planting)); + + return self::SUCCESS; + } + + /** + * @return list + */ + private function readRows(string $path): array + { + $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION)); + $readerType = match ($extension) { + 'xlsx' => 'Xlsx', + 'xls' => 'Xls', + 'csv' => 'Csv', + default => throw new \InvalidArgumentException('只支持 xlsx / xls / csv 文件。'), + }; + + $reader = IOFactory::createReader($readerType); + $reader->setReadDataOnly(true); + if ($readerType === 'Csv') { + $reader->setInputEncoding('UTF-8'); + } + + $book = $reader->load($path); + $sheetOption = trim((string) $this->option('sheet')); + if ($sheetOption !== '' && ctype_digit($sheetOption)) { + $index = (int) $sheetOption - 1; + if ($index < 0 || $index >= $book->getSheetCount()) { + throw new \InvalidArgumentException('工作表序号超出范围:' . $sheetOption); + } + $sheet = $book->getSheet($index); + } else { + $sheet = $sheetOption === '' ? $book->getActiveSheet() : $book->getSheetByName($sheetOption); + if ($sheet === null) { + throw new \InvalidArgumentException('找不到工作表:' . $sheetOption); + } + } + + $rows = []; + $header = null; + foreach ($sheet->getRowIterator() as $row) { + $cells = []; + $cellIterator = $row->getCellIterator(); + $cellIterator->setIterateOnlyExistingCells(false); + foreach ($cellIterator as $cell) { + $cells[] = trim((string) $cell->getValue()); + } + + if ($header === null) { + $cells[0] = preg_replace('/^\xEF\xBB\xBF/', '', $cells[0] ?? ''); + $header = $cells; + continue; + } + + $label = trim($cells[0] ?? ''); + $flag = trim($cells[1] ?? ''); + if ($label === '') { + continue; + } + + $rows[] = [ + 'label' => mb_substr($label, 0, 191), + 'is_planting' => in_array(mb_strtolower($flag), ['否', 'n', 'no', '0', 'false'], true) ? '否' : '是', + ]; + } + + return $rows; + } +} diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index 2e2ce0a6..3959b2a7 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -1829,6 +1829,14 @@ class Pages extends Controller ['path' => $request->url(), 'query' => $request->query()], ); + // 「是否种菜订单」:按(公司 + 标签)匹配财务维护的映射表,命中为是、未命中为否 + $plantingRules = app(\App\Business\JstOrderLabelPlantingRules::class); + $records->setCollection($records->getCollection()->map(static function ($record) use ($plantingRules) { + $record->setAttribute('is_planting_order', $plantingRules->flag((int) company_id(), $record->label)); + + return $record; + })); + return [ 'view' => 'business.pages.billing-transactions.jst-order-labels', 'records' => $records, @@ -1906,21 +1914,24 @@ class Pages extends Controller public function exportJstOrderLabels(Request $request): StreamedResponse { $query = $this->jstOrderLabelQuery($this->jstOrderLabelFilters($request)); + $plantingRules = app(\App\Business\JstOrderLabelPlantingRules::class); + $companyId = (int) company_id(); - return response()->streamDownload(function () use ($query): void { + return response()->streamDownload(function () use ($query, $plantingRules, $companyId): void { $csv = fopen('php://output', 'w'); if ($csv === false) { throw new \RuntimeException('无法创建聚水潭订单标签明细导出流。'); } fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF)); - fputcsv($csv, ['线上订单号', '店铺名称', '下单时间', '标签']); + fputcsv($csv, ['线上订单号', '店铺名称', '下单时间', '是否种菜订单', '标签']); - $query->orderBy('id')->chunkById(2000, function ($rows) use ($csv): void { + $query->orderBy('id')->chunkById(2000, function ($rows) use ($csv, $plantingRules, $companyId): void { foreach ($rows as $row) { fputcsv($csv, [ $row->order_no, $row->shop_name, $row->ordered_at?->format('Y-m-d H:i:s'), + $plantingRules->flag($companyId, $row->label), $row->label, ]); } diff --git a/app/Models/Business/JstOrderLabelPlantingRule.php b/app/Models/Business/JstOrderLabelPlantingRule.php new file mode 100644 index 00000000..891dca45 --- /dev/null +++ b/app/Models/Business/JstOrderLabelPlantingRule.php @@ -0,0 +1,19 @@ +bigIncrements('id'); + $table->unsignedInteger('company_id'); + $table->string('label', 191)->comment('聚水潭订单标签'); + $table->string('is_planting', 8)->default('是')->comment('是否种菜订单:是/否'); + $table->timestamps(); + + $table->unique(['company_id', 'label'], 'jst_order_label_planting_unique'); + }); + } + + public function down(): void + { + Schema::dropIfExists('jst_order_label_planting_rules'); + } +}; diff --git a/resources/views/business/pages/billing-transactions/jst-order-labels.blade.php b/resources/views/business/pages/billing-transactions/jst-order-labels.blade.php index 0db97c8e..1007dbb7 100644 --- a/resources/views/business/pages/billing-transactions/jst-order-labels.blade.php +++ b/resources/views/business/pages/billing-transactions/jst-order-labels.blade.php @@ -125,6 +125,7 @@ 线上订单号 店铺名称 下单时间 + 是否种菜订单 标签 @@ -135,11 +136,12 @@ {{ $record->order_no ?: '-' }} {{ $record->shop_name ?: '-' }} {{ $record->ordered_at?->format('Y-m-d H:i:s') ?? '-' }} + {{ $record->is_planting_order ?? '否' }} {{ $record->label ?: '-' }} @empty - + {{ $totalRows === 0 ? '暂无数据,请先用 jst-order-labels:import 导入订单标签明细。' : '当前筛选条件下没有记录,请调整筛选条件后重试。' }}