diff --git a/backend/app/Platform/Shopee/EntityParse/Order.php b/backend/app/Platform/Shopee/EntityParse/Order.php index cd36728..ce75e6d 100644 --- a/backend/app/Platform/Shopee/EntityParse/Order.php +++ b/backend/app/Platform/Shopee/EntityParse/Order.php @@ -9,7 +9,7 @@ use App\Model\Model as Entity; use App\Model\Store; use App\Platform\AbstractOrderParse; use App\Platform\Shopee\Constants\OrderStatus; -use App\Platform\Shopee\Constants\PaymentMethod; +use App\Constants\PaymentMethod; use Carbon\Carbon; use Hyperf\Collection\LazyCollection; use Hyperf\Context\ApplicationContext; @@ -40,12 +40,12 @@ class Order extends AbstractOrderParse throw new InvalidArgumentException('company_id is required in metadata'); } - $companyId = $metadata['company_id']; + $company_id = $metadata['company_id']; - $company = Company::find($companyId); + $company = Company::find($company_id); if (!$company) { - throw new InvalidArgumentException("Company with ID {$companyId} not found"); + throw new InvalidArgumentException("Company with ID {$company_id} not found"); } return $company; @@ -87,17 +87,26 @@ class Order extends AbstractOrderParse * * 将原始数据映射为可供 Model 使用的数组集合 * - * @param array $rawData 原始数据数组,通常来自 $data['raw_data'] + * @attention 当前返回格式为 ['order' => [...], 'items' => [...]] + * 但 OrderConsumer 第 153-157 行期望直接返回订单数据数组(参考 Tmall 实现) + * 需要调整返回格式以兼容 OrderConsumer 的 upsert 操作 + * + * @TODO 调整返回格式: + * - 方案1:改为直接返回订单数据数组(与 Tmall 一致) + * - 方案2:修改 OrderConsumer 以支持当前的嵌套结构 + * + * @param array $raw_data 原始数据数组,通常来自 $data['raw_data'] * @return LazyCollection 返回 LazyCollection,每个元素为可供 Model::fill() 使用的数组 */ - public function entityMap(array $rawData): LazyCollection + public function entityMap(array $raw_data): LazyCollection { // 使用 LazyCollection 进行延迟处理,提高性能 - return LazyCollection::make(function () use ($rawData) { - // 如果 rawData 是单个记录,转换为数组 - $records = isset($rawData[0]) ? $rawData : [$rawData]; + return LazyCollection::make(function () use ($raw_data) { + // 如果 raw_data 是单个记录,转换为数组 + $records = isset($raw_data[0]) ? $raw_data : [$raw_data]; foreach ($records as $record) { + $_origin_order_item = $record['order_list']; // 'order_list' 为来自 shopee 原始数据的 订单子项信息 // 解析订单子项数据,传入 platform_order_id(实现 OrderContract 契约方法) @@ -115,7 +124,7 @@ class Order extends AbstractOrderParse 'store_id' => $this->getStore()->id, 'order_status_id'=> OrderStatus::{$record['order_status']}->value, 'platform_order_id' => $record['order_sn'], - 'payment_method_id' => $this->parsePaymentMethod($record['payment_method']), + 'payment_method_id' => $this->getPaymentMethodId(), 'buyer_user_id' => $record['buyer_user_id'] ?? null, 'presale' => false, // shopee 平台暂不存类型为 presales 的订单 'total_amount' => $record['total_amount'] ?? 0, @@ -154,109 +163,43 @@ class Order extends AbstractOrderParse * * Shopee API 文档:https://open.shopee.com/documents/v2/v2.order.get_order_detail?module=94&type=1 * - * @param array $orderItems Shopee 原始订单子项数组 - * @param string $platformOrderId 平台订单 ID (order_sn) + * @param array $order_items Shopee 原始订单子项数组 + * @param string $platform_order_id 平台订单 ID (order_sn) * @return array 解析后的订单子项数据数组 */ - public function parseOrderItems(array $orderItems, string $platformOrderId): array + public function parseOrderItems(array $order_items, string $platform_order_id): array { - $parsedItems = []; + $parsed_items = []; - foreach ($orderItems as $item) { + foreach ($order_items as $item) { // 价格和数量 - $originalPrice = (float)($item['model_original_price'] ?? 0); - $discountedPrice = (float)($item['model_discounted_price'] ?? $originalPrice); + $original_price = (float)($item['model_original_price'] ?? 0); + $discounted_price = (float)($item['model_discounted_price'] ?? $original_price); $quantity = (int)($item['model_quantity_purchased'] ?? 1); // SKU: 优先使用 model_sku(变体SKU),如果为空则使用 item_sku(商品SKU) $sku = !empty($item['model_sku']) ? $item['model_sku'] : ($item['item_sku'] ?? ''); - $parsedItems[] = [ + $parsed_items[] = [ 'company_id' => $this->getCompany()->id, 'platform_id' => 25, 'store_id' => $this->getStore()->id, - 'platform_order_id' => $platformOrderId, // 平台订单 ID(从 order_sn 传入) + 'platform_order_id' => $platform_order_id, // 平台订单 ID(从 order_sn 传入) 'sub_order_id' => (string)$item['order_item_id'], // 使用 order_item_id 作为订单子项的唯一标识 'sub_order_type_id' => 0, 'product_id' => 0, // 需要根据 platform_product_id 匹配,暂设为 0 'platform_product_id' => (string)$item['item_id'], // Shopee 商品 ID 'product_sku' => $sku, 'product_barcode' => '', - 'unit_price' => $discountedPrice, // 实际成交单价(折扣后) + 'unit_price' => $discounted_price, // 实际成交单价(折扣后) 'quantity' => $quantity, - 'discount' => ($originalPrice - $discountedPrice) * $quantity, // 总折扣金额 - 'total' => $discountedPrice * $quantity, // 总金额 = 折扣价 × 数量 + 'discount' => ($original_price - $discounted_price) * $quantity, // 总折扣金额 + 'total' => $discounted_price * $quantity, // 总金额 = 折扣价 × 数量 'ext' => '', // 原始信息已在 Order.raw 中记录,此处留空 ]; } - return $parsedItems; - } - - /** - * 解析 Shopee 支付方式字符串到 PaymentMethod 枚举值 - * - * @param string $paymentMethodStr Shopee 返回的支付方式字符串 - * @return int PaymentMethod 枚举值 - */ - private function parsePaymentMethod(string $paymentMethodStr): int - { - // 支付方式映射表:Shopee 字符串 => PaymentMethod 枚举 - $paymentMap = [ - 'Credit Card/Debit Card' => PaymentMethod::CREDIT_CARD_DEBIT_CARD, - 'Cash on Delivery' => PaymentMethod::CASH_ON_DELIVERY, - 'PayNow' => PaymentMethod::PAYNOW, - 'ShopeePay' => PaymentMethod::SHOPEEPAY, - 'Apple Pay' => PaymentMethod::APPLE_PAY, - 'Google Pay' => PaymentMethod::GOOGLE_PAY, - 'DBS PayLah!' => PaymentMethod::DBS_PAYLAH, - 'JKO Pay' => PaymentMethod::JKO_PAY, - 'Bank Transfer' => PaymentMethod::BANK_TRANSFER, - 'SPayLater' => PaymentMethod::SPAYLATER, - 'Linked Bank Account' => PaymentMethod::LINKED_BANK_ACCOUNT, - 'Mari Savings Account' => PaymentMethod::MARI_SAVINGS_ACCOUNT, - 'Credit / Debit Card' => PaymentMethod::CREDIT_DEBIT_CARD, - 'Online / Offline Payment' => PaymentMethod::ONLINE_OFFLINE_PAYMENT, - 'BPI Online' => PaymentMethod::BPI_ONLINE, - 'Payment waived' => PaymentMethod::PAYMENT_WAIVED, - 'SeaBank' => PaymentMethod::SEABANK, - 'Online Payment' => PaymentMethod::ONLINE_PAYMENT, - 'Indomaret / i.Saku' => PaymentMethod::INDOMARET_ISAKU, - 'Credit Card Installment' => PaymentMethod::CREDIT_CARD_INSTALLMENT, - 'SeaBank Bayar Instan' => PaymentMethod::SEABANK_BAYAR_INSTAN, - 'Alfamart / Alfamidi / Dan+Dan' => PaymentMethod::ALFAMART_ALFAMIDI_DANDAN, - 'BCA OneKlik' => PaymentMethod::BCA_ONEKLIK, - 'Mobile Banking' => PaymentMethod::MOBILE_BANKING, - 'QR Promptpay' => PaymentMethod::QR_PROMPTPAY, - 'ShopeePay Linked Bank Account' => PaymentMethod::SHOPEEPAY_LINKED_BANK_ACCOUNT, - 'Online Banking' => PaymentMethod::ONLINE_BANKING, - 'Maybank2u' => PaymentMethod::MAYBANK2U, - 'Cash Payment at Convenience Stores' => PaymentMethod::CASH_PAYMENT_CONVENIENCE_STORES, - 'Mari Credit' => PaymentMethod::MARI_CREDIT, - 'Credit Card Points' => PaymentMethod::CREDIT_CARD_POINTS, - 'Mari Credit Card Instant Checkout' => PaymentMethod::MARI_CREDIT_CARD_INSTANT_CHECKOUT, - 'Shopee Wallet' => PaymentMethod::SHOPEE_WALLET, - 'Cash Payment at Physical Stores' => PaymentMethod::CASH_PAYMENT_PHYSICAL_STORES, - 'ShopeePay Balance' => PaymentMethod::SHOPEEPAY_BALANCE, - 'BPI for Payment' => PaymentMethod::BPI_FOR_PAYMENT, - 'MariBank' => PaymentMethod::MARIBANK, - 'Domestic NAPAS Card' => PaymentMethod::DOMESTIC_NAPAS_CARD, - ]; - - // 如果找到匹配的支付方式,返回对应的枚举值 - if (isset($paymentMap[$paymentMethodStr])) { - return $paymentMap[$paymentMethodStr]->value; - } - - // 未找到匹配,记录日志并返回 OTHER - $logger = ApplicationContext::getContainer()->get(LoggerInterface::class); - $logger->warning('Shopee unknown payment method detected', [ - 'payment_method' => $paymentMethodStr, - 'platform' => 'shopee', - 'parser' => self::class, - ]); - - return PaymentMethod::OTHER->value; + return $parsed_items; } /** @@ -322,4 +265,161 @@ class Order extends AbstractOrderParse // ); // return $this->getTableColumnsExcept($entity, $excludeFields); } + + /** + * 根据 Shopee 订单状态确定对应的本地订单状态 + * + * @required OrderConsumer 要求实现此方法(第 130-132 行) + * @see \App\Constants\OrderStatus + * + * @param string $platform_order_status Shopee 平台订单状态 + * @return int 本地订单状态 ID + */ + public function getOrderStatusId(string $platform_order_status): int + { + $status_map = $this->orderStatusMap(); + + return $status_map[$platform_order_status] ?? \App\Constants\OrderStatus::PAYMENT_COMPLETE->value; + } + + /** + * Shopee 订单状态到本地订单状态的映射表 + * + * @see \App\Platform\Shopee\Constants\OrderStatus Shopee 平台订单状态枚举 + * @see \App\Constants\OrderStatus 本地订单状态枚举 + * + * @return array + */ + private function orderStatusMap(): array + { + return [ + // 未付款 -> 等待付款 + 'UNPAID' => \App\Constants\OrderStatus::PAYMENT_PENDING->value, + + // 可发货(已付款,等待卖家安排发货)-> 付款完成 + 'READY_TO_SHIP' => \App\Constants\OrderStatus::PAYMENT_COMPLETE->value, + + // 已处理(卖家已安排发货并获取物流单号)-> 等待发货 + 'PROCESSED' => \App\Constants\OrderStatus::AWAITING_SHIPMENT->value, + + // 重新发货(3PL 取件失败,需要重新安排发货)-> 等待发货 + 'RETRY_SHIP' => \App\Constants\OrderStatus::AWAITING_SHIPMENT->value, + + // 已发货(包裹已交给 3PL 或已被 3PL 取走)-> 已发货 + 'SHIPPED' => \App\Constants\OrderStatus::SHIPPED->value, + + // 待确认收货(买家已收到订单)-> 已发货 + 'TO_CONFIRM_RECEIVE' => \App\Constants\OrderStatus::SHIPPED->value, + + // 取消中(订单取消正在处理)-> 取消请求中 + 'IN_CANCEL' => \App\Constants\OrderStatus::CANCEL_REQUESTED->value, + + // 已取消 -> 取消确认 + 'CANCELLED' => \App\Constants\OrderStatus::CANCEL_CONFIRMED->value, + + // 退货中(买家请求退货,退货正在处理)-> 取消请求中 + 'TO_RETURN' => \App\Constants\OrderStatus::CANCEL_REQUESTED->value, + + // 已完成 -> 完成 + 'COMPLETED' => \App\Constants\OrderStatus::FINISHED->value, + ]; + } + + /** + * 获取支付方式 ID + * + * @required OrderConsumer 要求实现此方法(第 136-138 行) + * @see \App\Constants\PaymentMethod + * + * // 支付方式映射表:Shopee 字符串 => PaymentMethod 枚举 + * 因为 shopee 官方文档 https://open.shopee.cn/developer-guide/31 “V2.0 Data Definition” + * 和接口数据返回的结果对不上,因此统一设置为在线支付 + * + * + * @return int 支付方式 ID + */ + public function getPaymentMethodId(): int + { + // 实现 Shopee 支付方式到本地 PaymentMethod 枚举的映射 + return PaymentMethod::ONLINE_PAYMENT->value; + } + + /** + * 从原始数据格式化订单子项 + * + * @required OrderConsumer 要求实现此方法(第 124-126 行,第 177 行调用) + * + * @param array $raw_data 原始数据数组 + * @param array $platform_orders_id_to_local_db_order_id_map 平台订单 ID 到本地数据库订单 ID 的映射 [platform_order_id => local_db_order_id] + * @return array 格式化后的订单子项数据,可供 OrderItem::fill() 使用 + */ + public function formatOrderItemsFromRaw(array $raw_data, array $platform_orders_id_to_local_db_order_id_map): array + { + // 1. 从 $rawData 中提取订单子项信息(Shopee 的 order_list 字段) + // 2. 使用 $platformOrderIdToLocalDbOrderIdMap 获取本地 order_id + // 3. 调用 shopeeOrderItemMap() 进行单个子项的映射转换 + // 4. 返回可供 OrderItem::fill() 使用的数组 + + // 由于 $rawData 是批量数据,包含多条订单结果,因此订单子项需要从集合中提取 + // Shopee 平台响应体的数据格式为: collection->payload->item_list + // $platform_orders_id_to_local_db_order_id_map 内部元素数据格式为 [platform_order_id => local db order id] + + $items = []; + + foreach ($raw_data as $raw_orders) { + foreach ($raw_orders['order_list'] as $raw_order_item) { + // 数据库中父订单 id + $db_order_id = $platform_orders_id_to_local_db_order_id_map[$raw_orders['tid']]; + $items[] = $this->shopeeOrderItemMap($raw_order_item, $raw_orders['tid'], $db_order_id, $raw_orders['created']); + } + } + + return $items; + } + + /** + * Shopee 订单子项映射转换 + * + * @param array $item 单个订单子项原始数据 + * @param string $platformOrderId 平台订单 ID + * @param int $parentOrderId 本地数据库父订单 ID + * @param string $parentOrderCreatedDate 父订单创建时间 + * @return array 映射后的订单子项数据 + */ + private function shopeeOrderItemMap(array $item, string $platform_order_id, int $parent_order_id, string $parent_order_created_date): array + { + // @TODO 实现 Shopee 订单子项字段映射 + // 参考现有的 parseOrderItems() 方法中的映射逻辑 + // 需要额外添加 order_id 和 created_date 字段 + + // @doc 见 https://open.shopee.com/documents/v2/v2.order.get_order_detail?module=94&type=1 文档 + + // The after-discount price of the item in the listing currency. + // If there is no discount, this value will be same as that of model_original_price. + // In case of bundle deal item, this value will return 0 as by design bundle deal discount will not be breakdown to item/model level. Due to technical restriction, the value will return the price before bundle deal if we don't configure it to 0. Please call GetEscrowDetails if you want to calculate item-level discounted price for bundle deal item. + + return [ + 'company_id' => $this->getCompany()->id, + 'platform_id' => $this->getPlatform()->id, + 'store_id' => $this->getStore()->id, + 'order_id' => $parent_order_id, + 'platform_order_id' => $platform_order_id, + 'sub_order_id' => $item['order_item_id'], + // @attention sku 的处理需要仅以规范和约束,确保 sku 准确 + 'sub_order_type_id' => '', + 'product_id' => '', + 'platform_product_id' =>$item['item_id'], + // @attention @TODO 需要对 运营侧的产品维护做进一步规范 + 'product_sku' => $item['model_sku'] ?? $item['item_sku'] ?? null, + // @attention @TODO 需要对 运营侧的产品维护做进一步规范 + 'product_barcode' => null, + 'unit_price' => $item['model_original_price'], + 'quantity' => $item['model_quantity_purchased'], + 'discount' => $item['model_original_price'] - $item['model_quantity_purchased'] ?? 0, + 'total' => $item['model_quantity_purchased'], + // @attention 扩展字段,用来存储其他信息 + 'ext' => '', + 'created_date' => $parent_order_created_date, + ]; + } }