318 lines
11 KiB
PHP
318 lines
11 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Platform\Tmall\EntityParse;
|
||
|
||
use App\Constants\RefundStatus;
|
||
use App\Constants\RefundType;
|
||
use App\Platform\Tmall\Constants\RefundStatus as TmallRefundStatus;
|
||
use App\Model\Company;
|
||
use App\Model\Order;
|
||
use App\Model\Store;
|
||
use App\Entity\Parse\EntityParse;
|
||
use Carbon\Carbon;
|
||
use Hyperf\Collection\LazyCollection;
|
||
use InvalidArgumentException;
|
||
|
||
/**
|
||
* 天猫退款解析器
|
||
*
|
||
* Tmall 没有主退款和子退款的概念,每个售后单对应一个订单子项。
|
||
* 每个售后单同时写入 refunds(主记录)和 refund_items(明细)两张表。
|
||
*/
|
||
class Refund extends EntityParse
|
||
{
|
||
/**
|
||
* 公司作用域匹配
|
||
*
|
||
* @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 = Company::find($metadata['company_id']);
|
||
|
||
if (!$company) {
|
||
throw new InvalidArgumentException("Company with ID {$metadata['company_id']} not found");
|
||
}
|
||
|
||
return $company;
|
||
}
|
||
|
||
/**
|
||
* 店铺作用域匹配
|
||
*
|
||
* @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 = Store::find($metadata['store_id']);
|
||
|
||
if (!$store) {
|
||
throw new InvalidArgumentException("Store with ID {$metadata['store_id']} not found");
|
||
}
|
||
|
||
return $store;
|
||
}
|
||
|
||
/**
|
||
* 实体数据映射 - 映射到 refunds 表
|
||
*
|
||
* Tmall 不存在父退款订单,因此此方法返回空值
|
||
*
|
||
* @param array $raw_data 原始数据数组
|
||
* @return LazyCollection 每个元素为可供 Refund Model::fill() 使用的数组
|
||
*/
|
||
public function entityMap(array $raw_data): LazyCollection
|
||
{
|
||
return LazyCollection::make();
|
||
}
|
||
|
||
/**
|
||
* 获取实体的唯一键字段(用于 upsert 的 uniqueBy 参数)
|
||
*
|
||
* @return array
|
||
*/
|
||
public function getUniqueBy(): array
|
||
{
|
||
return ['store_id', 'platform_refund_id'];
|
||
}
|
||
|
||
/**
|
||
* 获取可更新的字段列表(用于 upsert 的 update 参数)
|
||
*
|
||
* @return array
|
||
*/
|
||
public function getUpdateFields(): array
|
||
{
|
||
return [
|
||
'order_id',
|
||
'refund_status_id',
|
||
'refund_type_id',
|
||
'reason',
|
||
'refund_amount',
|
||
'freight_refund',
|
||
'refund_total',
|
||
'buyer_user_id',
|
||
'updated_date',
|
||
'completed_date',
|
||
'raw',
|
||
'ext',
|
||
'hash',
|
||
'updated_at',
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Tmall 没有父级退款单
|
||
* @return bool
|
||
*/
|
||
public function hasParentRefund(): bool
|
||
{
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 格式化退款子项数据 - 映射到 refund_items 表
|
||
*
|
||
* Tmall 每个售后单同时作为一条 refund_item 记录
|
||
* 需要在 refunds upsert 完成后调用,以获取 refund_id 映射
|
||
*
|
||
* @param array $raw_data 原始数据数组
|
||
* @param array $platform_refund_id_to_local_refund_id_map [platform_refund_id => local db refund id]
|
||
* @return array
|
||
*/
|
||
public function formatRefundItemsFromRaw(array $raw_data, array| null $platform_refund_id_to_local_refund_id_map = null): array
|
||
{
|
||
$records = isset($raw_data[0]) ? $raw_data : [$raw_data];
|
||
$items = [];
|
||
|
||
foreach ($records as $record) {
|
||
$platform_refund_id = strval($record['refund_id']);
|
||
$tz = $this->getStore()->getTimezoneString();
|
||
$raw = \json_encode($record);
|
||
|
||
$items[] = [
|
||
'company_id' => $this->getCompany()->id,
|
||
'platform_id' => $this->getPlatform()->id,
|
||
'store_id' => $this->getStore()->id,
|
||
'refund_id' => 0,
|
||
'platform_parent_refund_id' => '',
|
||
'platform_refund_id' => $platform_refund_id,
|
||
'refund_status_id' => $this->getRefundStatusId($record['status']),
|
||
'refund_type_id' => $this->getRefundTypeId($record),
|
||
'reason' => $record['reason'] ?? null,
|
||
'currency' => 'CNY',
|
||
'buyer_user_id' => $record['buyer_open_uid'] ?? null,
|
||
'platform_order_id' => strval($record['tid']),
|
||
'platform_sub_order_id' => strval($record['oid']),
|
||
'platform_product_id' => isset($record['num_iid']) ? strval($record['num_iid']) : null,
|
||
'quantity' => $record['num'] ?? 0,
|
||
'refund_amount' => (float) ($record['refund_fee'] ?? 0),
|
||
'order_created_date' => null, // @attention 暂时设置为 null, 后续会批量更新
|
||
'order_paid_date' => null, // @attention 暂时设置为 null, 后续会批量更新
|
||
'created_date' => Carbon::parse($record['created'], $tz)->format('Y-m-d H:i:sP'),
|
||
'updated_date' => isset($record['modified']) ? Carbon::parse($record['modified'], $tz)->format('Y-m-d H:i:sP') : null,
|
||
'completed_date' => isset($record['end_time']) ? Carbon::parse($record['end_time'], $tz)->format('Y-m-d H:i:sP') : null,
|
||
'raw' => $raw,
|
||
'ext' => null,
|
||
'created_at' => Carbon::now()->format('Y-m-d H:i:sP'),
|
||
'updated_at' => Carbon::now()->format('Y-m-d H:i:sP'),
|
||
];
|
||
}
|
||
|
||
return $this->fillOrderDates($items);
|
||
}
|
||
|
||
/**
|
||
* 根据退款子项的 store_id + platform_order_id 查询订单,
|
||
* 将 order_created_date 和 order_paid_date 填入 $items 数组
|
||
*
|
||
* 订单搜索范围:[退款 created_date - 3个月, 退款 created_date]
|
||
*
|
||
* @param array $items 退款子项数据数组
|
||
* @return array 填充了订单日期的退款子项数组
|
||
*/
|
||
protected function fillOrderDates(array $items): array
|
||
{
|
||
$lookups = [];
|
||
foreach ($items as $item) {
|
||
$key = $item['store_id'] . ':' . $item['platform_order_id'];
|
||
if (!isset($lookups[$key])) {
|
||
$lookups[$key] = [
|
||
'store_id' => $item['store_id'],
|
||
'platform_order_id' => $item['platform_order_id'],
|
||
'created_date' => $item['created_date'],
|
||
];
|
||
}
|
||
}
|
||
|
||
if (empty($lookups)) {
|
||
return $items;
|
||
}
|
||
|
||
$orderDatesMap = $this->queryOrderDates($lookups);
|
||
|
||
dump("Matched " . count($orderDatesMap) . " orders for " . count($lookups) . " refund item lookups");
|
||
|
||
foreach ($items as &$item) {
|
||
$key = $item['store_id'] . ':' . $item['platform_order_id'];
|
||
if (isset($orderDatesMap[$key])) {
|
||
$item['order_created_date'] = $orderDatesMap[$key]['order_created_date'];
|
||
$item['order_paid_date'] = $orderDatesMap[$key]['order_paid_date'];
|
||
}
|
||
}
|
||
unset($item);
|
||
|
||
return $items;
|
||
}
|
||
|
||
/**
|
||
* 根据 store_id + platform_order_id 批量查询订单的创建时间和付款时间
|
||
*
|
||
* 订单搜索范围:[退款 created_date - 3个月, 退款 created_date]
|
||
*
|
||
* @param array $lookups [key => ['store_id', 'platform_order_id', 'created_date']]
|
||
* @return array [store_id:platform_order_id => ['order_created_date' => ..., 'order_paid_date' => ...]]
|
||
*/
|
||
protected function queryOrderDates(array $lookups): array
|
||
{
|
||
$orders = Order::query()
|
||
->where(function ($query) use ($lookups) {
|
||
foreach ($lookups as $lookup) {
|
||
$createdDate = Carbon::parse($lookup['created_date']);
|
||
$query->orWhere(function ($q) use ($lookup, $createdDate) {
|
||
$q->where('store_id', $lookup['store_id'])
|
||
->where('platform_order_id', $lookup['platform_order_id'])
|
||
->whereBetween('created_date', [
|
||
$createdDate->copy()->subMonths(3),
|
||
$createdDate,
|
||
]);
|
||
});
|
||
}
|
||
})
|
||
->get(['store_id', 'platform_order_id', 'created_date', 'paid_date']);
|
||
|
||
$map = [];
|
||
foreach ($orders as $order) {
|
||
$key = $order->store_id . ':' . $order->platform_order_id;
|
||
$map[$key] = [
|
||
'order_created_date' => $order->created_date,
|
||
'order_paid_date' => $order->paid_date,
|
||
];
|
||
}
|
||
|
||
return $map;
|
||
}
|
||
|
||
/**
|
||
* 将 Tmall 退款状态映射为系统退款状态 ID
|
||
*
|
||
* @param string $platformStatus
|
||
* @return int
|
||
*/
|
||
public function getRefundStatusId(string $platform_status): int
|
||
{
|
||
$map = $this->refundStatusMap();
|
||
return $map[$platform_status] ?? RefundStatus::APPLIED->value;
|
||
}
|
||
|
||
/**
|
||
* Tmall 退款状态 → 系统退款状态映射
|
||
*
|
||
* @return array<string, int>
|
||
*/
|
||
private function refundStatusMap(): array
|
||
{
|
||
return [
|
||
TmallRefundStatus::WAIT_SELLER_AGREE->value => RefundStatus::APPLIED->value,
|
||
TmallRefundStatus::WAIT_BUYER_RETURN_GOODS->value => RefundStatus::SELLER_ACCEPTED->value,
|
||
TmallRefundStatus::WAIT_SELLER_CONFIRM_GOODS->value => RefundStatus::SELLER_ACCEPTED->value,
|
||
TmallRefundStatus::SELLER_REFUSE_BUYER->value => RefundStatus::REFUSED->value,
|
||
TmallRefundStatus::SUCCESS->value => RefundStatus::SUCCESS->value,
|
||
TmallRefundStatus::CLOSED->value => RefundStatus::CLOSED->value,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 根据 Tmall 售后单数据判断退款类型
|
||
*
|
||
* @param array $record 单条售后单原始数据
|
||
* @return int
|
||
*/
|
||
public function getRefundTypeId(array $record): int
|
||
{
|
||
$has_good_return = $record['has_good_return'] ?? false;
|
||
$good_status = $record['good_status'] ?? '';
|
||
|
||
// 买家已退货或等待退货 → 退货退款
|
||
if ($has_good_return) {
|
||
return RefundType::RETURN_AND_REFUND->value;
|
||
}
|
||
|
||
// 买家未收到货 → 仅退款
|
||
if ($good_status === 'BUYER_NOT_RECEIVED') {
|
||
return RefundType::REFUND_ONLY->value;
|
||
}
|
||
|
||
// 买家已收货但无须退货 → 退款无须退货
|
||
if ($good_status === 'BUYER_RECEIVED') {
|
||
return RefundType::REFUND_WITHOUT_RETURN->value;
|
||
}
|
||
|
||
return RefundType::REFUND_ONLY->value;
|
||
}
|
||
}
|