'工单编号', 'online_order_no' => '线上订单号', 'tracking_no' => '物流单号', 'shop_name' => '店铺名称', 'supplier' => '供应商', 'product_code' => '商品编码', ]; public const COLUMNS = [ 'started_at' => '发起时间', 'work_order_no' => '工单编号', 'initiator' => '发起人', 'bookkeeping_reason' => '记账原因', 'reason_level_1' => '记账原因-一级选项', 'reason_level_2' => '记账原因-二级选项', 'bookkeeping_type' => '记账类型', 'executor' => '执行人', 'source' => '记账来源', 'amount' => '记账金额', 'deduction_status' => '厂家扣款判定', 'supplier_config' => '厂家记账配置', 'deduction_basis' => '判定说明', 'remark' => '记账备注说明', 'attachment' => '上传附件', 'online_order_no' => '线上订单号', 'internal_order_no' => '内部单号', 'actual_paid_amount' => '实付金额', 'carrier' => '物流公司', 'tracking_no' => '物流单号', 'shop_name' => '店铺名称', 'platform' => '平台', 'shipping_warehouse' => '订单发货仓库', 'status' => '工单状态', 'processing_status' => '记账处理状态', 'counterparty_agreed' => '被记账方是否同意记账', 'counterparty_note' => '被记账方处理说明', 'specialist_result' => '专员记账判定结果', 'specialist_note' => '专员处理说明', 'updated_at' => '修改时间', 'work_order_type' => '工单类型', 'product_code' => '商品编码', 'style_code' => '款式编码', 'other_attribute_6' => '其它属性6', 'product_name' => '商品名称', 'platform_sku_id' => '平台商品skuId', 'platform_product_id' => '平台商品id', 'product_quantity' => '商品数量', 'product_price' => '商品价格', 'product_amount' => '商品金额', 'buyer_paid_amount' => '买家实付', 'supplier' => '供应商', 'accepting_department' => '受理部门', 'order_shipped_at' => '订单发货时间', ]; public const MONEY_COLUMNS = ['amount', 'actual_paid_amount']; public function __construct( private readonly Repository $repository, private readonly SupplierBookkeepingConfigStore $configs, ) {} /** @return array */ public function filters(): array { $filters = []; foreach (['started_from', 'started_to'] as $key) { $value = request()->query($key, ''); abort_unless(is_string($value) && ($value === '' || preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)), 422, '发起日期格式无效。'); $filters[$key] = $value; } $reason = request()->query('reason', ''); abort_unless(is_string($reason) && ($reason === '' || in_array($reason, Repository::reasonPaths(), true) || array_key_exists($reason, Repository::REASONS)), 422, '记账原因无效。'); $filters['reason'] = $reason; $status = request()->query('status', ''); abort_unless(is_string($status) && ($status === '' || in_array($status, Repository::STATUS_LABELS, true)), 422, '工单状态无效。'); $filters['status'] = $status; $deduction = request()->query('deduction', ''); abort_unless(is_string($deduction) && ($deduction === '' || in_array($deduction, Rule::STATUSES, true)), 422, '扣款判定无效。'); $filters['deduction'] = $deduction; foreach (['bookkeeping_type' => 50, ...array_fill_keys(array_keys(self::TEXT_FILTERS), 200)] as $key => $max) { $value = request()->query($key, ''); abort_unless(is_string($value) && mb_strlen($value) <= $max, 422, '筛选条件格式无效。'); $filters[$key] = trim($value); } return $filters; } /** @return array 可导出列(按请求顺序) */ public function exportColumns(): array { $requested = request()->query('columns'); if ($requested === null || $requested === '') { return self::COLUMNS; } abort_unless(is_string($requested) && strlen($requested) <= 1000, 422, '导出列无效。'); $keys = explode(',', $requested); abort_unless(count($keys) === count(array_unique($keys)) && array_diff($keys, array_keys(self::COLUMNS)) === [], 422, '导出列无效。'); return array_replace(array_flip($keys), array_intersect_key(self::COLUMNS, array_flip($keys))); } /** * @param array $filters * @param list>|null $rows * @return \Generator> */ public function rows(array $filters, ?array $rows = null): \Generator { $from = $filters['started_from'] !== '' ? $filters['started_from'] . ' 00:00:00' : ''; $to = $filters['started_to'] !== '' ? $filters['started_to'] . ' 23:59:59' : ''; $deduction = $filters['deduction'] ?? ''; $configs = $this->configs->activeConfigs(); foreach ($rows ?? $this->repository->rows() as $row) { // 配置随时可改,判定不进镜像结果缓存,每次按当前配置计算。 $row = Rule::decide($row, $configs) + $row; if ($from !== '' && $row['started_at'] < $from) continue; if ($to !== '' && $row['started_at'] > $to) continue; if ($filters['reason'] !== '' && $row['bookkeeping_reason'] !== $filters['reason'] && $row['reason_level_1'] !== $filters['reason']) continue; if ($filters['status'] !== '' && $row['status'] !== $filters['status']) continue; if ($filters['bookkeeping_type'] !== '' && $row['bookkeeping_type'] !== $filters['bookkeeping_type']) continue; if ($deduction !== '' && $row['deduction_status'] !== $deduction) continue; foreach (self::TEXT_FILTERS as $key => $label) { if ($filters[$key] !== '' && mb_stripos((string) ($row[$key] ?? ''), $filters[$key]) === false) continue 2; } yield $row; } } public static function cents(mixed $amount): int { if ($amount === null || ! preg_match('/^(-?)(\d+)\.(\d{2})$/', (string) $amount, $m)) { return 0; } $value = ((int) $m[2]) * 100 + (int) $m[3]; return $m[1] === '-' ? -$value : $value; } public function page(): array { if (request()->query('bk_view') === 'config') { return app(SupplierBookkeepingConfigPageData::class)->page(); } $filters = $this->filters(); $page = max(1, (int) request('page', 1)); $sync = $this->repository->syncStatus(); $summary = ['count' => 0, 'amount_cents' => 0, 'supplier_reason' => 0, 'warehouse_reason' => 0, 'deduction_count' => 0, 'deduction_cents' => 0, 'review' => 0]; $items = []; $types = []; $error = null; if (! in_array($sync['code'], ['ready', 'failed'], true)) { $error = $sync['code'] === 'initial_failed' ? '聚工单工单镜像首次同步失败,暂不能展示厂家记账,请联系管理员重新同步。' : '聚工单工单镜像尚未完成首次同步,完成后自动展示厂家记账;当前为空不代表没有记账工单。'; } else { try { $all = $this->repository->rows(); foreach ($all as $row) { if ($row['bookkeeping_type'] !== '') $types[$row['bookkeeping_type']] = true; } foreach ($this->rows($filters, $all) as $row) { if ($summary['count'] >= ($page - 1) * self::PER_PAGE && count($items) < self::PER_PAGE) $items[] = $row; $summary['count']++; $summary['amount_cents'] += self::cents($row['amount']); $summary[$row['reason_level_1'] === '厂家记账' ? 'supplier_reason' : 'warehouse_reason']++; if ($row['deduction_status'] === Rule::STATUS_DEDUCT) { $summary['deduction_count']++; $summary['deduction_cents'] += self::cents($row['amount']); } elseif ($row['deduction_status'] === Rule::STATUS_REVIEW) { $summary['review']++; } } } catch (\Throwable $exception) { report($exception); $error = '厂家记账数据暂时无法读取,请稍后重试。'; $summary = array_fill_keys(array_keys($summary), 0); $items = []; } } ksort($types); return [ 'view' => 'business.pages.supplier-payables.manufacturer-bookkeeping', 'bookkeepingView' => 'details', 'bookkeepingFilters' => $filters, 'bookkeepingSummary' => $summary, 'bookkeepingSync' => $sync, 'bookkeepingError' => $error, 'bookkeepingTypes' => array_keys($types), 'bookkeepingRows' => new LengthAwarePaginator($items, $summary['count'], self::PER_PAGE, $page, [ 'path' => request()->url(), 'query' => request()->except('page'), ]), ]; } }