Compare commits

...

1 Commits

Author SHA1 Message Date
b3b4846c6e 聚水潭订单标签明细:新增「是否种菜订单」列(按下单时间后面的位置)
- 新表 jst_order_label_planting_rules(公司 + 标签 → 是/否)+ 模型 + 缓存服务 JstOrderLabelPlantingRules
- 新增导入命令 jst-order-labels:import-planting-rules,读取财务「判断种菜单.xlsx」(表头:标签 / 是否种菜订单),按 --company 导入并清缓存
- 页面:下单时间后面新增「是否种菜订单」列,逐行用(公司 + 标签)匹配映射表,命中为是、未命中为否
- 导出:CSV 同步新增该列(页面/导出同口径)
- 本地:build-frontend.ps1 通过;6 个改动/新增 PHP 文件 php -l 通过
2026-10-07 11:13:53 +08:00
6 changed files with 281 additions and 4 deletions

View File

@ -0,0 +1,67 @@
<?php
namespace App\Business;
use App\Models\Business\JstOrderLabelPlantingRule;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use Throwable;
/**
* 聚水潭订单标签明细的「是否种菜订单」判断:按(公司 + 标签)匹配映射表。
*
* 映射表由财务维护(判断种菜单.xlsx)并按公司缓存,页面与导出共用同一份结果;
* 映射表缺失或未导入时不报错,统一按「否」展示。
*/
class JstOrderLabelPlantingRules
{
private const TTL_HOURS = 24;
/** @return array<string,string> [标签 => 是/否] */
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}";
}
}

View File

@ -0,0 +1,146 @@
<?php
namespace App\Console\Commands;
use App\Business\JstOrderLabelPlantingRules;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use PhpOffice\PhpSpreadsheet\IOFactory;
use Throwable;
/**
* 导入「标签 | 是否种菜订单」映射表,供聚水潭订单标签明细的「是否种菜订单」列使用。
*/
class ImportJstOrderLabelPlantingRules extends Command
{
protected $signature = 'jst-order-labels:import-planting-rules
{path : xlsx/csv 文件}
{--company= : 公司ID(默认使用当前公司或 1)}
{--sheet=1 : 工作表序号(从 1 开始)或名称}
{--replace : 先清空该公司已有映射再导入(默认按标签覆盖)}';
protected $description = '导入聚水潭订单标签明细的「是否种菜订单」映射表';
public function handle(JstOrderLabelPlantingRules $rules): int
{
$path = (string) $this->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<array{label:string,is_planting:string}>
*/
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;
}
}

View File

@ -1823,6 +1823,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,
@ -1900,21 +1908,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,
]);
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Models\Business;
use App\Abstracts\Model;
/**
* 聚水潭订单标签明细 › 「是否种菜订单」映射规则(标签 → 是/否)。
*/
class JstOrderLabelPlantingRule extends Model
{
protected $table = 'jst_order_label_planting_rules';
protected $fillable = [
'company_id',
'label',
'is_planting',
];
}

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* 聚水潭订单标签明细「是否种菜订单」映射表(标签 → 是/否)。
*
* 来源:财务维护的「判断种菜单.xlsx」(标签 / 是否种菜订单),由
* jst-order-labels:import-planting-rules 导入;页面只读匹配,不改动订单标签数据。
*/
return new class extends Migration
{
public function up(): void
{
Schema::create('jst_order_label_planting_rules', function (Blueprint $table): void {
$table->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');
}
};

View File

@ -125,6 +125,7 @@
<th scope="col">线上订单号</th>
<th scope="col">店铺名称</th>
<th scope="col">下单时间</th>
<th scope="col">是否种菜订单</th>
<th scope="col">标签</th>
</tr>
</thead>
@ -135,11 +136,12 @@
<td>{{ $record->order_no ?: '-' }}</td>
<td>{{ $record->shop_name ?: '-' }}</td>
<td>{{ $record->ordered_at?->format('Y-m-d H:i:s') ?? '-' }}</td>
<td>{{ $record->is_planting_order ?? '否' }}</td>
<td>{{ $record->label ?: '-' }}</td>
</tr>
@empty
<tr class="is-empty">
<td colspan="5">
<td colspan="6">
{{ $totalRows === 0 ? '暂无数据,请先用 jst-order-labels:import 导入订单标签明细。' : '当前筛选条件下没有记录,请调整筛选条件后重试。' }}
</td>
</tr>