种菜明细表:新增按月从聚水潭种菜报销工单同步的命令(带自动排重)

每个月都要手工整理「中心明细表」,改成一条命令:planting-details:sync-work-orders --month=YYYY-MM。

- 口径用 1~8 月已入库的 7 万多行反推验证过:只取「工单类型=种菜报销、状态=已完成」的工单,明细表「日期」= 工单完成日 - 1 天(历史命中 100%),所以某月数据 = 完成时间落在该月次日到下月次日的工单。
- 字段映射(聚水潭只返回 fieldId,不给字段名,这批 id 是拿历史数据逐列对撞出来的):财务售价/财务运费/财务佣金/京喜差价/扣款/财务空包费/财务打款金额 分别对应工单的 任务付款/任务快递费/任务佣金/京喜差价/任务扣款/任务空包费/最终报销金额;负责人、账户、类型、标签、创建人、平台商品ID 也各有对应字段。
- 自动排重:写入前先删掉「本月 + 本次订单号」的已有行,重复执行不会翻倍;--force-clear 才整月覆盖,--dry-run 只看不写。
- 所属利润中心没有工单来源,用表里已有的 店铺→利润中心 映射回填,命中不了的店铺在命令输出里逐个提示。
- 补测试:字段映射 + 日期口径 + 去重 + 利润中心回填 + force-clear。
This commit is contained in:
qiuxiang 2026-10-08 09:35:34 +08:00
parent 636187a9d8
commit ef62cac0d9
3 changed files with 612 additions and 0 deletions

View File

@ -0,0 +1,352 @@
<?php
namespace App\Business;
use App\Models\Business\PlantingDetail;
use Illuminate\Database\DatabaseManager;
use InvalidArgumentException;
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
use RuntimeException;
/**
* 聚水潭「种菜报销」协同工单 → 种菜明细表(账单流水数据 › 种菜数据汇总 › 种菜明细表)月度同步。
*
* 口径(用 1~8 月已入库的 7 万多行反推、命中率 100%):
* - 只取「工单类型 = 种菜报销、工单状态 = 已完成」的工单;
* - 明细表「日期」= 工单完成日 − 1 天。所以某个月的数据 = 完成时间落在该月次日到下月次日的工单;
* - 一张工单一行,金额列与工单表单字段一一对应(见 FIELD_* 常量)。
*
* 排重:写入前先删掉「本月 + 本次订单号」的已有行,所以同一个月重复执行不会翻倍;
* 想整月覆盖(连没有对应工单的人工行一起清掉)用 --force-clear。
*
* 「所属利润中心」没有工单来源,用表里已有的 店铺→利润中心 历史映射回填,命中不了就留空。
*/
class PlantingWorkOrderSynchronizer
{
/** 聚水潭协同工单镜像(jiansun.dbo.jst_workOrderInfo → js_jst_work_order_info)。 */
public const WORK_ORDER_DATASET = 'jiansun.dbo.jst_workOrderInfo';
/** 「种菜报销」这个工单类型的固定 id。 */
public const WORK_TYPE_ID = '6F03128403F2442CABCF283B59FAD88D';
/** 已完成的工单状态。 */
private const STATUS_CONFIRMED = 'CONFIRMED';
public const SOURCE = 'work-order-sync';
/** 工单表单字段(聚水潭只给 fieldId,不给字段名,这里的对应关系是拿历史数据逐列对撞出来的)。 */
private const FIELD_SELLING_PRICE = 'NUMBERField1413856620250924131615'; // 财务售价 = 任务付款金额
private const FIELD_FREIGHT = 'NUMBERField1413856620251113150537'; // 财务运费 = 任务快递费
private const FIELD_COMMISSION = 'NUMBERField1413856620251230154457'; // 财务佣金 = 任务佣金
private const FIELD_JINGXI_DIFF = 'NUMBERField1413856620250924131623'; // 京喜差价
private const FIELD_DEDUCTION = 'NUMBERField1413856620251113150612'; // 扣款 = 任务扣款费用
private const FIELD_EMPTY_PACKAGE = 'NUMBERField1413856620251113145620'; // 财务空包费 = 任务空包费
private const FIELD_PAYMENT = 'NUMBERField1413856620251117101250'; // 财务打款金额 = 最终报销金额
private const FIELD_OWNER = 'TEXTField1413856620251117180931'; // 负责人(收款人姓名)
private const FIELD_ACCOUNT = 'TEXTField1413856620251117113451'; // 账户(打款账号)
private const FIELD_TYPE = 'TEXTField1413856620251113150546'; // 类型(任务备注)
private const FIELD_TAG = 'businessRemarkField'; // 标签(业务备注)
private const FIELD_ORDER_NO = 'orderField'; // 订单编号(线上订单号)
private const FIELD_STORE = 'shopField'; // 正确店铺(店铺名称)
/** 金额列,统一按 4 位小数落库。 */
private const MONEY_COLUMNS = [
'finance_selling_price',
'finance_freight',
'finance_commission',
'jingxi_price_diff',
'deduction',
'finance_empty_package_fee',
'finance_payment_amount',
];
private const INSERT_CHUNK = 1000;
private const DELETE_CHUNK = 500;
public function __construct(
private readonly DatabaseManager $databases,
private readonly BusinessMirrorDatasetRegistry $datasets,
) {}
/**
* @return array{
* from:string,to:string,orders:int,inserted:int,deleted:int,
* selling_price:string,payment_amount:string,date_min:?string,date_max:?string,
* unmatched_stores:array<string,int>,dry_run:bool
* }
*/
public function sync(
string $from,
string $to,
int $companyId = 1,
bool $dryRun = false,
bool $forceClear = false,
): array {
$from = $this->normalizeDate($from, '起始日期');
$to = $this->normalizeDate($to, '结束日期');
if ($from > $to) {
throw new InvalidArgumentException('起始日期不能晚于结束日期。');
}
// 「日期」= 工单完成日 − 1,所以完成时间窗口整体后移一天。
$completeFrom = date('Y-m-d', strtotime($from . ' +1 day')) . ' 00:00:00';
$completeTo = date('Y-m-d', strtotime($to . ' +1 day')) . ' 00:00:00';
$target = $this->databases->connection();
$mirror = $this->databases->connection((string) config('upstream_mirrors.connection', 'business_mirror'));
$table = $this->datasets->get(self::WORK_ORDER_DATASET)->targetTable();
if (! $mirror->getSchemaBuilder()->hasTable($table)) {
throw new RuntimeException('聚水潭协同工单镜像尚未建立(' . $table . ')。');
}
$storeToProfitCenter = $this->storeToProfitCenter($companyId);
$now = now();
$rows = [];
$orders = 0;
$unmatchedStores = [];
$sellingPrice = 0.0;
$paymentAmount = 0.0;
$dateMin = null;
$dateMax = null;
$records = $mirror->table($table)
->where('woTypeId', self::WORK_TYPE_ID)
->where('status', self::STATUS_CONFIRMED)
->where('completeTime', '>=', $completeFrom)
->where('completeTime', '<', $completeTo)
->orderBy('completeTime')
->orderBy('woNo')
->get(['woNo', 'createby', 'completeTime', 'businessFieldList']);
foreach ($records as $record) {
$fields = $this->fields((string) $record->businessFieldList);
$orderNo = $this->text($fields[self::FIELD_ORDER_NO] ?? null);
if ($orderNo === null) {
continue;
}
$billDate = date('Y-m-d', strtotime((string) $record->completeTime . ' -1 day'));
if ($billDate < $from || $billDate > $to) {
continue;
}
$store = $this->text($fields[self::FIELD_STORE . '#label'] ?? null);
$profitCenter = $store === null ? null : ($storeToProfitCenter[$store] ?? null);
if ($store !== null && $profitCenter === null) {
$unmatchedStores[$store] = ($unmatchedStores[$store] ?? 0) + 1;
}
$row = [
'company_id' => $companyId,
'bill_date' => $billDate,
'order_no' => mb_substr($orderNo, 0, 100),
'pieces' => 1,
'remark' => null,
'store' => $store,
'owner' => $this->text($fields[self::FIELD_OWNER] ?? null),
'account' => $this->text($fields[self::FIELD_ACCOUNT] ?? null),
'type' => $this->text($fields[self::FIELD_TYPE] ?? null),
'creator' => $this->text($record->createby),
'tag' => $this->text($fields[self::FIELD_TAG] ?? null),
'profit_center' => $profitCenter,
'responsible_platform' => null,
'external_id' => $this->text($this->platformItemId($fields), 100),
'created_from' => self::SOURCE,
'created_by' => null,
'created_at' => $now,
'updated_at' => $now,
];
foreach (self::MONEY_COLUMNS as $column) {
$row[$column] = number_format(0.0, 4, '.', '');
}
foreach ([
'finance_selling_price' => self::FIELD_SELLING_PRICE,
'finance_freight' => self::FIELD_FREIGHT,
'finance_commission' => self::FIELD_COMMISSION,
'jingxi_price_diff' => self::FIELD_JINGXI_DIFF,
'deduction' => self::FIELD_DEDUCTION,
'finance_empty_package_fee' => self::FIELD_EMPTY_PACKAGE,
'finance_payment_amount' => self::FIELD_PAYMENT,
] as $column => $field) {
$row[$column] = number_format($this->number($fields[$field] ?? null), 4, '.', '');
}
$orders++;
$sellingPrice += (float) $row['finance_selling_price'];
$paymentAmount += (float) $row['finance_payment_amount'];
$dateMin = $dateMin === null || $billDate < $dateMin ? $billDate : $dateMin;
$dateMax = $dateMax === null || $billDate > $dateMax ? $billDate : $dateMax;
$rows[] = $row;
}
$orderNumbers = array_values(array_unique(array_column($rows, 'order_no')));
$deleted = 0;
if (! $dryRun) {
$deleted = $this->clear($target, $companyId, $from, $to, $orderNumbers, $forceClear);
foreach (array_chunk($rows, self::INSERT_CHUNK) as $chunk) {
$target->table('planting_details')->insert($chunk);
}
}
arsort($unmatchedStores);
return [
'from' => $from,
'to' => $to,
'orders' => $orders,
'inserted' => $dryRun ? 0 : count($rows),
'deleted' => $deleted,
'selling_price' => number_format($sellingPrice, 4, '.', ''),
'payment_amount' => number_format($paymentAmount, 4, '.', ''),
'date_min' => $dateMin,
'date_max' => $dateMax,
'unmatched_stores' => $unmatchedStores,
'dry_run' => $dryRun,
];
}
/**
* 排重:删掉本月内、这次要写入的那批订单号对应的已有行。
*
* @param list<string> $orderNumbers
*/
private function clear(
\Illuminate\Database\ConnectionInterface $target,
int $companyId,
string $from,
string $to,
array $orderNumbers,
bool $forceClear,
): int {
$base = static fn () => $target->table('planting_details')
->where('company_id', $companyId)
->where('bill_date', '>=', $from)
->where('bill_date', '<=', $to);
if ($forceClear) {
return (int) $base()->delete();
}
$deleted = 0;
foreach (array_chunk($orderNumbers, self::DELETE_CHUNK) as $chunk) {
$deleted += (int) $base()->whereIn('order_no', $chunk)->delete();
}
return $deleted;
}
/** @return array<string,string> 店铺 → 利润中心(同店铺多个取值时取出现次数最多的) */
private function storeToProfitCenter(int $companyId): array
{
$counts = [];
$rows = PlantingDetail::query()
->where('company_id', $companyId)
->whereNotNull('store')
->whereNotNull('profit_center')
->selectRaw('store, profit_center, COUNT(*) AS total')
->groupBy('store', 'profit_center')
->get();
foreach ($rows as $row) {
$store = (string) $row->store;
$counts[$store][(string) $row->profit_center] = (int) $row->total;
}
$map = [];
foreach ($counts as $store => $values) {
arsort($values);
$map[$store] = (string) array_key_first($values);
}
return $map;
}
/** @return array<string,mixed> fieldId → value(带 valueLabel 的另存为 fieldId#label) */
private function fields(string $businessFieldList): array
{
$fields = [];
$items = json_decode($businessFieldList, true);
if (! is_array($items)) {
return $fields;
}
foreach ($items as $item) {
if (! is_array($item) || ! isset($item['fieldId'])) {
continue;
}
if ($item['fieldId'] === 'itemInfo') {
// 商品明细是嵌套结构,原样留着给 platformItemId 取平台商品 ID。
$fields['itemInfo'] = $item;
continue;
}
$value = $item['value'] ?? null;
if (is_array($value)) {
$value = implode(',', array_map('strval', $value));
}
$fields[(string) $item['fieldId']] = $value;
if (($item['valueLabel'] ?? null) !== null) {
$fields[(string) $item['fieldId'] . '#label'] = $item['valueLabel'];
}
}
return $fields;
}
/** @param array<string,mixed> $fields */
private function platformItemId(array $fields): ?string
{
$itemInfo = $fields['itemInfo'] ?? null;
if (! is_array($itemInfo)) {
return null;
}
foreach ((array) ($itemInfo['childFieldList'] ?? []) as $childRow) {
foreach ((array) $childRow as $child) {
if (is_array($child) && ($child['fieldId'] ?? null) === 'platformItemId') {
return $this->text($child['value'] ?? null, 100);
}
}
}
return null;
}
private function text(mixed $value, int $limit = 191): ?string
{
if ($value === null) {
return null;
}
$text = trim((string) (is_scalar($value) ? $value : ''));
return $text === '' ? null : mb_substr($text, 0, $limit);
}
private function number(mixed $value): float
{
if ($value === null || $value === '') {
return 0.0;
}
return is_numeric($value) ? (float) $value : 0.0;
}
private function normalizeDate(string $value, string $label): string
{
$value = trim($value);
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) !== 1) {
throw new InvalidArgumentException($label . '必须是 YYYY-MM-DD。');
}
[$year, $month, $day] = array_map('intval', explode('-', $value));
if (! checkdate($month, $day, $year)) {
throw new InvalidArgumentException($label . '不是有效日期。');
}
return $value;
}
}

View File

@ -0,0 +1,78 @@
<?php
namespace App\Console\Commands;
use App\Business\PlantingWorkOrderSynchronizer;
use Illuminate\Console\Command;
use InvalidArgumentException;
use Throwable;
class SyncPlantingDetailsFromWorkOrders extends Command
{
protected $signature = 'planting-details:sync-work-orders
{--month= : 月份 YYYY-MM(与 --from/--to 二选一)}
{--from= : 起始日期 YYYY-MM-DD}
{--to= : 结束日期 YYYY-MM-DD}
{--company=1 : 公司 ID}
{--dry-run : 只统计不写库}
{--force-clear : 整月覆盖(连没有对应工单的人工行一起清掉)}';
protected $description = '把聚水潭「种菜报销」工单同步成种菜明细表(日期 = 工单完成日 − 1;按月重复执行不会重复写)';
public function handle(PlantingWorkOrderSynchronizer $synchronizer): int
{
try {
[$from, $to] = $this->window();
$result = $synchronizer->sync(
$from,
$to,
(int) $this->option('company'),
(bool) $this->option('dry-run'),
(bool) $this->option('force-clear'),
);
} catch (Throwable $exception) {
$this->error($exception->getMessage());
return self::FAILURE;
}
$unmatched = $result['unmatched_stores'];
$result['unmatched_stores'] = count($unmatched);
$this->info(json_encode($result, JSON_UNESCAPED_UNICODE));
foreach (array_slice($unmatched, 0, 20, true) as $store => $count) {
$this->warn(sprintf('未匹配利润中心:%s(%d 行)', $store, $count));
}
if (count($unmatched) > 20) {
$this->warn('……还有 ' . (count($unmatched) - 20) . ' 家店铺未匹配到利润中心。');
}
return self::SUCCESS;
}
/** @return array{0:string,1:string} */
private function window(): array
{
$from = trim((string) $this->option('from'));
$to = trim((string) $this->option('to'));
$month = trim((string) $this->option('month'));
if ($month !== '') {
if ($from !== '' || $to !== '') {
throw new InvalidArgumentException('--month 与 --from/--to 只能给一个。');
}
if (preg_match('/^\d{4}-\d{2}$/', $month) !== 1) {
throw new InvalidArgumentException('--month 必须是 YYYY-MM。');
}
return [$month . '-01', date('Y-m-t', strtotime($month . '-01'))];
}
if ($from === '' || $to === '') {
throw new InvalidArgumentException('请给 --month=YYYY-MM,或同时给 --from 与 --to。');
}
return [$from, $to];
}
}

View File

@ -3,10 +3,13 @@
namespace Tests\Feature\Business;
use App\Business\PlantingDetailImporter;
use App\Business\PlantingWorkOrderSynchronizer;
use App\Models\Auth\Permission;
use App\Models\Business\PlantingDetail;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Tests\Feature\FeatureTestCase;
/**
@ -129,6 +132,185 @@ class PlantingDetailsPageTest extends FeatureTestCase
$this->assertSame($longId, $record->external_id);
}
/**
* 种菜报销工单同步:字段映射、日期=完成日-1、利润中心回填、重复执行不翻倍。
*/
public function test_sync_from_work_orders_maps_fields_and_dedupes(): void
{
$this->allowPage();
$this->fakeWorkOrderMirror();
// 历史行提供 店铺 → 利润中心 映射
PlantingDetail::query()->create([
'company_id' => $this->company->id,
'bill_date' => '2026-06-01',
'order_no' => 'HISTORY-1',
'store' => '天猫-亿家达旗舰店',
'profit_center' => '运营中心A',
'finance_payment_amount' => '1.0000',
]);
// 本月里没有对应工单的人工行:默认不删
$manual = PlantingDetail::query()->create([
'company_id' => $this->company->id,
'bill_date' => '2026-09-10',
'order_no' => 'MANUAL-ONLY',
'store' => '天猫-亿家达旗舰店',
'finance_payment_amount' => '9.0000',
]);
$result = app(PlantingWorkOrderSynchronizer::class)->sync('2026-09-01', '2026-09-30', $this->company->id);
// 只认「种菜报销 + 已完成 + 完成时间在 9/2~10/1」的两张工单
$this->assertSame(2, $result['orders']);
$this->assertSame(2, $result['inserted']);
$this->assertSame(0, $result['deleted']);
$first = PlantingDetail::query()->where('company_id', $this->company->id)
->where('order_no', '3316392626048007769')->firstOrFail();
$this->assertSame('2026-09-01', $first->bill_date->toDateString()); // 完成日 9-02 → 日期 9-01
$this->assertSame('37.7900', $first->finance_selling_price);
$this->assertSame('2.0000', $first->finance_commission);
$this->assertSame('37.7900', $first->finance_payment_amount);
$this->assertSame('天猫-亿家达旗舰店', $first->store);
$this->assertSame('黄文梳', $first->owner);
$this->assertSame('6214830551373771', $first->account);
$this->assertSame('CCDD', $first->type);
$this->assertSame('CCDD', $first->tag);
$this->assertSame('黄文梳', $first->creator);
$this->assertSame('1074872715022', $first->external_id);
$this->assertSame('运营中心A', $first->profit_center); // 从历史行回填
$this->assertSame(1, $first->pieces);
$this->assertSame(PlantingWorkOrderSynchronizer::SOURCE, $first->created_from);
$second = PlantingDetail::query()->where('company_id', $this->company->id)
->where('order_no', '260901-000000000000001')->firstOrFail();
$this->assertSame('2026-09-29', $second->bill_date->toDateString()); // 完成日 9-30
// 再跑一遍:同月同订单号先删后写,不会翻倍
$again = app(PlantingWorkOrderSynchronizer::class)->sync('2026-09-01', '2026-09-30', $this->company->id);
$this->assertSame(2, $again['deleted']);
$this->assertSame(2, $again['inserted']);
$this->assertSame(1, PlantingDetail::query()->where('company_id', $this->company->id)
->where('order_no', '3316392626048007769')->count());
$this->assertSame(1, PlantingDetail::query()->where('company_id', $this->company->id)
->whereKey($manual->id)->count());
// dry-run 不写库
$dry = app(PlantingWorkOrderSynchronizer::class)->sync('2026-09-01', '2026-09-30', $this->company->id, true);
$this->assertTrue($dry['dry_run']);
$this->assertSame(2, $dry['orders']);
$this->assertSame(0, $dry['inserted']);
}
public function test_sync_force_clear_replaces_whole_month(): void
{
$this->allowPage();
$this->fakeWorkOrderMirror();
PlantingDetail::query()->create([
'company_id' => $this->company->id,
'bill_date' => '2026-09-10',
'order_no' => 'MANUAL-ONLY',
'finance_payment_amount' => '9.0000',
]);
$result = app(PlantingWorkOrderSynchronizer::class)
->sync('2026-09-01', '2026-09-30', $this->company->id, false, true);
$this->assertSame(1, $result['deleted']);
$this->assertSame(2, $result['inserted']);
$this->assertSame(0, PlantingDetail::query()->where('company_id', $this->company->id)
->where('order_no', 'MANUAL-ONLY')->count());
$this->assertSame(2, PlantingDetail::query()->where('company_id', $this->company->id)->count());
}
/** 造一个只有聚水潭协同工单镜像的 sqlite 业务镜像连接。 */
private function fakeWorkOrderMirror(): void
{
config(['upstream_mirrors.connection' => 'business_mirror']);
config(['database.connections.business_mirror' => [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]]);
DB::purge('business_mirror');
Schema::connection('business_mirror')->create('js_jst_work_order_info', function ($table): void {
$table->increments('Id');
$table->string('woNo')->nullable();
$table->string('woTypeId')->nullable();
$table->string('status')->nullable();
$table->string('createby')->nullable();
$table->string('completeTime')->nullable();
$table->text('businessFieldList')->nullable();
});
foreach ($this->workOrderFixtures() as $row) {
DB::connection('business_mirror')->table('js_jst_work_order_info')->insert($row);
}
}
/** @return list<array<string,mixed>> */
private function workOrderFixtures(): array
{
$planting = PlantingWorkOrderSynchronizer::WORK_TYPE_ID;
$fields = [
['fieldId' => 'orderField', 'type' => 'TEXT', 'value' => '3316392626048007769'],
['fieldId' => 'shopField', 'type' => 'SELECT', 'value' => '18501511', 'valueLabel' => '天猫-亿家达旗舰店'],
['fieldId' => 'NUMBERField1413856620250924131615', 'type' => 'NUMBER', 'value' => '37.79'],
['fieldId' => 'NUMBERField1413856620251230154457', 'type' => 'NUMBER', 'value' => '2'],
['fieldId' => 'NUMBERField1413856620251117101250', 'type' => 'NUMBER', 'value' => '37.79'],
['fieldId' => 'TEXTField1413856620251117180931', 'type' => 'TEXT', 'value' => '黄文梳'],
['fieldId' => 'TEXTField1413856620251117113451', 'type' => 'TEXT', 'value' => '6214830551373771'],
['fieldId' => 'TEXTField1413856620251113150546', 'type' => 'TEXT', 'value' => 'CCDD'],
['fieldId' => 'businessRemarkField', 'type' => 'TEXTAREA', 'value' => 'CCDD'],
['fieldId' => 'itemInfo', 'type' => 'FORM', 'childFieldList' => [[
['fieldId' => 'erpItemId', 'type' => 'TEXT', 'value' => '11961631'],
['fieldId' => 'platformItemId', 'type' => 'TEXT', 'value' => '1074872715022'],
]]],
];
$plain = [
['fieldId' => 'orderField', 'type' => 'TEXT', 'value' => '260901-000000000000001'],
['fieldId' => 'shopField', 'type' => 'SELECT', 'value' => '21355501', 'valueLabel' => '拼多多-归来舍下住宅家具旗舰店'],
['fieldId' => 'NUMBERField1413856620250924131615', 'type' => 'NUMBER', 'value' => '0.06'],
['fieldId' => 'NUMBERField1413856620251230154457', 'type' => 'NUMBER', 'value' => '0.5'],
['fieldId' => 'NUMBERField1413856620251117101250', 'type' => 'NUMBER', 'value' => '0.56'],
];
return [
[
'woNo' => 'GD565967381', 'woTypeId' => $planting, 'status' => 'CONFIRMED',
'createby' => '黄文梳', 'completeTime' => '2026-09-02 11:47:53',
'businessFieldList' => json_encode($fields, JSON_UNESCAPED_UNICODE),
],
[
'woNo' => 'GD999999999', 'woTypeId' => $planting, 'status' => 'CONFIRMED',
'createby' => '陆小凤', 'completeTime' => '2026-09-30 09:27:29',
'businessFieldList' => json_encode($plain, JSON_UNESCAPED_UNICODE),
],
// 未完成 → 忽略
[
'woNo' => 'GD888888888', 'woTypeId' => $planting, 'status' => 'WAIT_CONFIRM',
'createby' => '陆小凤', 'completeTime' => '2026-09-15 09:27:29',
'businessFieldList' => json_encode($plain, JSON_UNESCAPED_UNICODE),
],
// 别的工单类型 → 忽略
[
'woNo' => 'GD777777777', 'woTypeId' => 'ORDER_AFSALES', 'status' => 'CONFIRMED',
'createby' => '关雎', 'completeTime' => '2026-09-15 09:27:29',
'businessFieldList' => json_encode($plain, JSON_UNESCAPED_UNICODE),
],
// 完成时间 10/2 → 日期 10/1,不在 9 月窗口 → 忽略
[
'woNo' => 'GD666666666', 'woTypeId' => $planting, 'status' => 'CONFIRMED',
'createby' => '陆小凤', 'completeTime' => '2026-10-02 09:27:29',
'businessFieldList' => json_encode($plain, JSON_UNESCAPED_UNICODE),
],
];
}
private function csvFixture(): string
{
$path = $this->writeFile('planting.csv');