Files
datahub/backend/app/Platform/Tmall/EntityParse/Order.php
T
2026-02-09 12:57:43 +08:00

460 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace App\Platform\Tmall\EntityParse;
use App\Constants\OrderStatus;
use App\Model\Company;
use App\Model\Model as Entity;
use App\Model\Product;
use App\Model\Store;
use App\Entity\Parse\EntityParse;
use App\Constants\PaymentMethod;
use App\Constants\OrderType;
use Carbon\Carbon;
use Hyperf\Collection\LazyCollection;
use InvalidArgumentException;
/**
* 天猫订单解析器示例
*
* 展示如何继承 EntityParse 实现自定义解析逻辑
*/
class Order extends EntityParse
{
/**
* 公司作用域匹配
*
* 从 metadata 中提取 company_id,然后查询数据库获取公司对象
*
* @param array $metadata
* @return Company
* @throws InvalidArgumentException
*/
public function companyScopeMatch(array $metadata): Company
{
// 验证必需字段
if (!isset($metadata['company_id'])) {
throw new InvalidArgumentException('company_id is required in metadata');
}
$company_id = $metadata['company_id'];
$company = Company::find($company_id);
if (!$company) {
throw new InvalidArgumentException("Company with ID {$company_id} not found");
}
return $company;
}
/**
* 店铺作用域匹配
*
* 从 metadata 中提取 store_id,然后查询数据库获取店铺对象
*
* @param array $metadata
* @return Store
* @throws InvalidArgumentException
*/
public function storeScopeMatch(array $metadata): Store
{
// 验证必需字段
if (!isset($metadata['store_id'])) {
throw new InvalidArgumentException('store_id is required in metadata');
}
$store_id = $metadata['store_id'];
$store = Store::find($store_id);
if (!$store) {
throw new InvalidArgumentException("Store with ID {$store_id} not found");
}
return $store;
}
/**
* 实体数据映射
*
* 将原始数据映射为可供 Model 使用的数组集合
*
* @param array $raw_data 原始数据数组,通常来自 $data['raw_data']
* @return LazyCollection 返回 LazyCollection,每个元素为可供 Model::fill() 使用的数组
*/
public function entityMap(array $raw_data): LazyCollection
{
// 使用 LazyCollection 进行延迟处理,提高性能
return LazyCollection::make(function () use ($raw_data) {
// 如果 raw_data 是单个记录,转换为数组
$records = isset($raw_data[0]) ? $raw_data : [$raw_data];
foreach ($records as $record) {
$raw = \json_encode($record);
// 映射每条原始数据到 Model 可用的数组格式
yield [
'company_id' => $this->getCompany()->id,
'platform_id' => $this->getPlatform()->id,
'store_id' => $this->getStore()->id,
'order_status_id' => $this->getOrderStatusId($record['status']),
'platform_order_id' => $record['tid'],
'payment_method_id' => $this->getPaymentMethodId(),
'presale' => false,
'total_amount' => $record['total_fee'],
'total_paid' => $record['payment'],
'total_discount' => $record['discount_fee'],
// 实际收到的金额,如果平台有商家补贴,此金额需要重新计算 - 部分退款订单也影响此字段的值
'total_received' => $record['received_payment'],
'freight_fee' => 0,
'tax_fee' => 0,
'discount_fee' => 0,
'commission_fee' => 0,
'coupon_amount' => 0,
'voucher_amount' => 0,
'order_type_id' => OrderType::Normor_Order->value,
'created_date' => Carbon::parse($record['created'], $this->getStore()->getTimezoneString())->format('Y-m-d H:i:sP'),
'updated_date' => isset($record['modified']) ? Carbon::parse($record['modified'], $this->getStore()->getTimezoneString())->format('Y-m-d H:i:sP') : null,
'paid_date' => isset($record['pay_time']) ? Carbon::parse($record['pay_time'], $this->getStore()->getTimezoneString())->format('Y-m-d H:i:sP') : null,
'shipping_date' => isset($record['consign_time']) ? Carbon::parse($record['consign_time'], $this->getStore()->getTimezoneString())->format('Y-m-d H:i:sP') : null,
'zipcode' => $record['receiver_zip'] ?? '',
'city' => $record['receiver_city'] ?? '',
'province' => $record['receiver_state'] ?? '',
'country' => 'CN',
'raw' => $raw,
'ext' => null,
'buyer_user_id' => $record['buyer_open_uid'],
'hash' => \md5($raw),
'created_at' => Carbon::now()->format('Y-m-d H:i:sP'),
'updated_at' => Carbon::now()->format('Y-m-d H:i:sP'),
];
}
});
}
/**
* 获取实体的唯一键字段(用于 upsert 的 uniqueBy 参数)
*
* 定义订单的唯一约束字段组合
*
* @return array 唯一键字段名数组
*/
public function getUniqueBy(): array
{
// 天猫订单的唯一性由店铺 + 平台订单号确定(实际上由平台订单号就已经足够)
// @attention create_date 为数据库强制要求,必需携带
return ['store_id', 'platform_order_id', 'created_date'];
}
/**
* 获取可更新的字段列表(用于 upsert 的 update 参数)
*
* 明确定义哪些字段在更新时可以被修改
* 排除:主键、唯一键、创建时间、关联 ID 等不应变更的字段
*
* @return array 可更新字段名数组
*/
public function getUpdateFields(): array
{
// 方案1:手动指定可更新字段(推荐,最明确)
// 根据实际业务需求添加其他可更新字段
return [
'company_id',
'platform_id',
'store_id',
'order_status_id',
'platform_order_id',
'payment_method_id',
'presale',
'total_amount',
'total_paid',
'total_discount',
'total_received',
'freight_fee',
'tax_fee',
'discount_fee',
'commission_fee',
'coupon_amount',
'voucher_amount',
'order_type_id',
'created_date',
'updated_date',
'paid_date',
'shipping_date',
'zipcode',
'city',
'province',
'country',
'raw',
'ext',
'buyer_user_id',
'hash',
'updated_at', // 更新时刷新,created_at 不包含以保留原始创建时间
];
// 方案2:动态计算(如果字段较多且经常变动)
// $entity = $this->entityMatch($this->getData());
// $excludeFields = array_merge(
// ['id', 'created_at', 'created_date', 'company_id', 'platform_id'],
// $this->getUniqueBy()
// );
// return $this->getTableColumnsExcept($entity, $excludeFields);
}
/**
* 根据淘宝订单状态确定对应的 Tools 订单状态
*
* @param string $platform_order_status 淘宝平台订单状态
* @return int Tools 订单状态 ID
*/
public function getOrderStatusId(string $platform_order_status): int
{
$status_map = $this->orderStatusMap();
return $status_map[$platform_order_status] ?? OrderStatus::PAYMENT_COMPLETE->value;
}
/**
* 淘宝订单状态到 Tools 订单状态的映射
*
* @return array<string, int>
*/
private function orderStatusMap(): array
{
return [
// Payment pending (1)
'TRADE_NO_CREATE_PAY' => OrderStatus::PAYMENT_PENDING->value, // 没有创建支付宝交易
'WAIT_BUYER_PAY' => OrderStatus::PAYMENT_PENDING->value, // 等待买家付款
'PAY_PENDING' => OrderStatus::PAYMENT_PENDING->value, // 国际信用卡支付付款确认中
'WAIT_PRE_AUTH_CONFIRM' => OrderStatus::PAYMENT_PENDING->value, // 0元购合约中
// Payment fail (2)
'TRADE_CLOSED_BY_TAOBAO' => OrderStatus::PAYMENT_FAIL->value, // 付款以前,卖家或买家主动关闭交易
// Payment complete (3)
'SELLER_CONSIGNED_PART' => OrderStatus::PAYMENT_COMPLETE->value, // 卖家部分发货
'WAIT_SELLER_SEND_GOODS' => OrderStatus::PAYMENT_COMPLETE->value, // 等待卖家发货,即:买家已付款
// Awaiting shipment (4)
'PAID_FORBID_CONSIGN' => OrderStatus::AWAITING_SHIPMENT->value, // 拼团中订单或者发货强管控的订单,已付款但禁止发货
// Shipped (5)
'WAIT_BUYER_CONFIRM_GOODS' => OrderStatus::SHIPPED->value, // 等待买家确认收货,即:卖家已发货
'TRADE_BUYER_SIGNED' => OrderStatus::SHIPPED->value, // 买家已签收,货到付款专用
// Finished (8)
'TRADE_FINISHED' => OrderStatus::FINISHED->value, // 交易成功
// Cancel before shipping (9)
'TRADE_CLOSED' => OrderStatus::CANCEL_BEFORE_SHIPPING->value, // 付款以后用户退款成功,交易自动关闭
];
}
public function getPaymentMethodId() : int
{
// @attention 暂时固定返回支付方式为支付宝, 其他平台需要定义付款方式映射
return PaymentMethod::ALIPAY_CN->value;
}
/**
*
* orderItems 的输出结果为 \App\Model\OrderItem::fill() 可以直接使用的数据格式
*
* @param array $raw_data
* @param array $platform_orders_id_to_local_db_order_id_map
* @return array
*/
public function formatOrderItemsFromRaw(array $raw_data, array $platform_orders_id_to_local_db_order_id_map): array
{
// 由于 $raw_data 是批量数据,包含多条订单结果,因此订单子项需要从集合中提取
// Tmall 平台响应体的数据格式为: collection->payload->orders
// $platform_orders_id_to_local_db_order_id_map 内部元素数据格式为 [platform_order_id => local db order id]
$items = [];
$product_keys = []; // 索引 => 查询键
foreach ($raw_data as $raw_orders) {
$parent_order_created_date = Carbon::parse($raw_orders['created']);
foreach ($raw_orders['orders']['order'] as $raw_order_item) {
// 数据库中父订单 id
$db_order_id = $platform_orders_id_to_local_db_order_id_map[$raw_orders['tid']];
$platform_order_id = strval($raw_orders['tid']);
// 记录当前索引
$index = count($items);
// 先添加 itemproduct_id 初始为 0
$items[] = $this->tmallOrderItemMap($raw_order_item, $platform_order_id, $db_order_id, $parent_order_created_date, 0);
// 记录该索引对应的产品查询键 (Tmall 使用 num_iid 作为 item_idsku_id 作为 model_id)
$model_id = !empty($raw_order_item['sku_id']) ? (string)$raw_order_item['sku_id'] : null;
$product_keys[$index] = $this->buildProductMapKey((string)$raw_order_item['num_iid'], $model_id);
}
}
// 批量查询产品 并使用匹配到的产品 覆写 $items product_id 字段的值
if (!empty($product_keys)) {
$products_id_map = $this->batchQueryProducts($product_keys);
foreach ($product_keys as $index => $key) {
if (isset($products_id_map[$key])) {
$items[$index]['product_id'] = $products_id_map[$key];
}
}
}
return $items;
}
/**
* 构建产品映射键
*
* @param string $item_id 平台商品 ID
* @param string|null $model_id 平台 SKU ID
* @return string
*/
private function buildProductMapKey(string $item_id, ?string $model_id): string
{
return $item_id . ':' . ($model_id ?? 'null');
}
/**
* 批量查询产品 ID
*
* 使用 PostgreSQL VALUES + JOIN 语法优化,避免生成冗长的 OR 条件链
*
* @param array $product_keys 索引 => 查询键 的映射
* @return array 查询键 => 产品 ID 的映射
*/
private function batchQueryProducts(array $product_keys): array
{
$store_id = $this->getStore()->id;
$unique_keys = array_unique(array_values($product_keys));
if (empty($unique_keys)) {
return [];
}
// 构建 VALUES 子句和绑定参数
$values = [];
$bindings = [];
foreach ($unique_keys as $key) {
[$item_id, $model_id] = explode(':', $key, 2);
$values[] = '(?::text, ?::text)';
$bindings[] = $item_id;
$bindings[] = $model_id === 'null' ? null : $model_id;
}
$valuesClause = implode(', ', $values);
$bindings[] = $store_id;
// PostgreSQL 原生 SQLVALUES 作为驱动表(小表在左侧)
// store_id 条件移入 JOIN ON 子句,让索引过滤更早介入
$sql = "
SELECT p.id, p.platform_item_id, p.platform_model_id
FROM (VALUES {$valuesClause}) AS v(item_id, model_id)
INNER JOIN products p
ON p.store_id = ?
AND p.platform_item_id = v.item_id
AND (p.platform_model_id = v.model_id OR (v.model_id IS NULL AND p.platform_model_id IS NULL))
";
$products = \Hyperf\DbConnection\Db::select($sql, $bindings);
$map = [];
foreach ($products as $product) {
$key = $this->buildProductMapKey($product->platform_item_id, $product->platform_model_id);
$map[$key] = $product->id;
}
return $map;
}
/**
* Tmall 订单子项映射转换
* @return void
*/
private function tmallOrderItemMap(array $item, string $platform_order_id, int $parent_order_id, Carbon $parent_order_created_date): array
{
// $item = [
// "adjust_fee" => "0.00",
// "buyer_rate" => false,
// "cid" => 50026872,
// "consign_due_time" => "2_5",
// "discount_fee" => "231.98",
// "divide_order_fee" => "504.00",
// "is_bybt_order" => false,
// "is_daixiao" => false,
// "is_idle" => "0",
// "is_jzfx" => false,
// "is_oversold" => false,
// "nr_outer_iid" => "768990037900",
// "num" => 1,
// "num_iid" => 637901668632,
// "oid" => 4932549972242808538,
// "oid_str" => "4932549972242808538",
// "order_attr" => "{"esDate":"2025-12-14","pushTime":"2025-12-09 11:30:00"}",
// "order_from" => "WAP,WAP",
// "outer_iid" => "768990037900",
// "outer_sku_id" => "768990037900",
// "part_mjz_discount" => "15.00",
// "payment" => "504.00",
// "pic_path" => "https://img.alicdn.com/bao/uploaded/i4/2413132940/O1CN01duCukP1XaZLX2jVQy_!!2413132940.jpg",
// "price" => "693.00",
// "refund_status" => "NO_REFUND",
// "s_tariff_fee" => "0.00",
// "seller_rate" => false,
// "seller_type" => "B",
// "sku_id" => "6123547511321",
// "sku_properties_name" => "颜色分类:高倍Omega鱼油软胶囊 180粒/瓶",
// "snapshot_url" => "za:4932549972242808538_1",
// "status" => "WAIT_SELLER_SEND_GOODS",
// "store_code" => "STORE_11388627",
// "sub_order_tax_fee" => "57.98",
// "sub_order_tax_promotion_fee" => "0.00",
// "sub_order_tax_rate" => "0",
// "tax_coupon_discount" => "57.98",
// "tax_free" => true,
// "title" => "【优惠价】NordicNaturals挪威小鱼dha深海鱼油Omega3成人rTG高纯度epa胶囊",
// "total_fee" => "461.02",
// ]
return [
'company_id' => $this->getCompany()->id,
'platform_id' => $this->getPlatform()->id,
'store_id' => $this->getStore()->id,
'order_id' => $parent_order_id,
'platform_order_id' => strval($platform_order_id),
'sub_order_id' => $item['oid'],
// @attention sku 的处理需要仅以规范和约束,确保 sku 准确
'sub_order_type_id' => null,
// @attention 值为 0 表示未找到产品 id
'product_id' => 0, // 值为 0 表示未找到产品 id, 之后会被批量查询覆盖
'platform_product_id' => $item['num_iid'],
// @attention @TODO 需要对 运营侧的产品维护做进一步规范
'product_sku' => $item['outer_sku_id'] ?? null,
// @attention @TODO 需要对 运营侧的产品维护做进一步规范
'product_barcode' => null,
'unit_price' => $item['price'],
'quantity' => $item['num'],
'discount' => $item['discount_fee'],
'total' => $item['divide_order_fee'],
// @attention 扩展字段,用来存储其他信息
'ext' => null,
'created_date' => $parent_order_created_date,
];
}
}