Files
datahub/backend/app/Platform/Tmall/EntityParse/Refund.php
T
2026-02-11 10:10:21 +08:00

236 lines
7.7 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\RefundStatus;
use App\Constants\RefundType;
use App\Platform\Tmall\Constants\RefundStatus as TmallRefundStatus;
use App\Model\Company;
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 $items;
}
/**
* 将 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;
}
}