update tmall refund item attribute parse
This commit is contained in:
@@ -302,9 +302,9 @@ class Refund extends EntityParse
|
|||||||
return RefundType::RETURN_AND_REFUND->value;
|
return RefundType::RETURN_AND_REFUND->value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 买家未收到货 → 仅退款
|
// 买家未收到货 → 未发货前退款
|
||||||
if ($good_status === 'BUYER_NOT_RECEIVED') {
|
if ($good_status === 'BUYER_NOT_RECEIVED') {
|
||||||
return RefundType::REFUND_ONLY->value;
|
return RefundType::RETURN_BEFORE_SHIPPING->value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 买家已收货但无须退货 → 退款无须退货
|
// 买家已收货但无须退货 → 退款无须退货
|
||||||
@@ -312,6 +312,283 @@ class Refund extends EntityParse
|
|||||||
return RefundType::REFUND_WITHOUT_RETURN->value;
|
return RefundType::REFUND_WITHOUT_RETURN->value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return RefundType::REFUND_ONLY->value;
|
return RefundType::RETURN_BEFORE_SHIPPING->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Tmall 判定为 "未发货0秒退"
|
||||||
|
// Tmall 平台 0秒 退款的业务处理
|
||||||
|
// 买家下单后立刻反悔,申请退款或者凑单秒退的情况
|
||||||
|
// 此类订单 Tmall 不会推送到原始订单数据库中,因此 API 无法抽取到此类数据,需要重建订单信息
|
||||||
|
// 判定规则为 refund_item.attribute. clj_zero_second_refund
|
||||||
|
public function fixInstantRefundOrders() : void
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析退款子项的 attribute 字段为结构化数据
|
||||||
|
*
|
||||||
|
* Tmall attribute 原始格式: ;key:value;key:value;...
|
||||||
|
* 编码规则: #3B → :(冒号) #3A → ;(分号)
|
||||||
|
*
|
||||||
|
* @param string $attribute 原始 attribute 字符串
|
||||||
|
* @return array 结构化属性数据
|
||||||
|
*/
|
||||||
|
public function parseRefundItemAttribute(string $attribute): array
|
||||||
|
{
|
||||||
|
$pairs = $this->parseAttributePairs($attribute);
|
||||||
|
|
||||||
|
if (empty($pairs)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
// ── 基本信息 ──
|
||||||
|
'biz_code' => $pairs['bizCode'] ?? null, // 业务编码 (tmall.hk.refund=天猫国际退款)
|
||||||
|
'sdk_code' => $pairs['sdkCode'] ?? null, // SDK来源
|
||||||
|
'shop_name' => $pairs['shop_name'] ?? null, // 店铺名称
|
||||||
|
'workflow_name' => $pairs['workflowName'] ?? null, // 工作流
|
||||||
|
'op_role' => $pairs['opRole'] ?? null, // 操作角色 (daemon=系统自动)
|
||||||
|
|
||||||
|
// ── 商品信息 ──
|
||||||
|
'sku' => $this->parseSkuAttribute($pairs['sku'] ?? ''), // SKU ID + 规格属性
|
||||||
|
'item_buy_amount' => (int) ($pairs['itemBuyAmount'] ?? 0), // 购买数量
|
||||||
|
'item_price' => (int) ($pairs['itemPrice'] ?? 0), // 商品单价 (分)
|
||||||
|
'leaves_cat' => $pairs['leavesCat'] ?? null, // 叶子类目ID
|
||||||
|
'root_cat' => $pairs['rootCat'] ?? null, // 根类目ID
|
||||||
|
'is_virtual' => (bool) ($pairs['isVirtual'] ?? 0), // 是否虚拟商品
|
||||||
|
|
||||||
|
// ── 退款金额 (单位: 分) ──
|
||||||
|
'apply_init_refund_fee' => (int) ($pairs['apply_init_refund_fee'] ?? 0), // 申请退款金额
|
||||||
|
'ex_max_refund_fee' => (int) ($pairs['EXmrf'] ?? 0), // 最大可退金额
|
||||||
|
'online_refund_fee' => (int) ($pairs['ol_tf'] ?? 0), // 在线退款金额
|
||||||
|
'refund_post_fee' => (int) ($pairs['refundPostFee'] ?? 0), // 退款运费
|
||||||
|
'main_order_post_fee' => (int) ($pairs['mainOrderPostFee'] ?? 0), // 主单运费
|
||||||
|
'to_seller_fee' => (int) ($pairs['toSellerFee'] ?? 0), // 卖家应付金额
|
||||||
|
|
||||||
|
// ── 退款原因与类型 ──
|
||||||
|
'apply_reason_text' => $pairs['apply_reason_text'] ?? null, // 退款原因文本
|
||||||
|
'apply_text_id' => $pairs['apply_text_id'] ?? null, // 退款原因ID
|
||||||
|
'dispute_request' => (int) ($pairs['disputeRequest'] ?? 0), // 是否有纠纷
|
||||||
|
'dispute_trade_status' => (int) ($pairs['disputeTradeStatus'] ?? 0), // 纠纷交易状态
|
||||||
|
'dbt' => $pairs['DBT'] ?? null, // 退款业务类型 (InterceptUnconsignRefund=拦截未发货退款)
|
||||||
|
|
||||||
|
// ── 退款特征标识 ──
|
||||||
|
'clj_zero_second_refund' => (bool) ($pairs['clj_zero_second_refund'] ?? 0), // 0秒闪电退款 (为 true 时需重建订单, 见 fixInstantRefundOrders)
|
||||||
|
'tmg_simple_zero_refund' => (bool) ($pairs['tmgSimpleZeroRefund'] ?? 0), // 简易0秒退款
|
||||||
|
'warehouse_refund' => (bool) ($pairs['warehouseRefund'] ?? 0), // 仓库退款
|
||||||
|
'part_refund' => (bool) ($pairs['part_refund'] ?? 0), // 部分退款
|
||||||
|
'percent_refund' => (bool) ($pairs['percent_refund'] ?? 0), // 比例退款
|
||||||
|
'products' => $pairs['products'] ?? null, // 退款产品标识 (timeoutrefund=超时退款)
|
||||||
|
|
||||||
|
// ── 资金流 ──
|
||||||
|
'fund_flow' => $this->parseFundFlowAttribute($pairs['fundFlowInfo'] ?? ''), // 资金渠道明细 [{type, group, name, amount}]
|
||||||
|
'pay_mode' => $pairs['payMode'] ?? null, // 支付方式
|
||||||
|
'pay_lock' => $pairs['pay_lock'] ?? null, // 支付锁定方
|
||||||
|
|
||||||
|
// ── 优惠信息 ──
|
||||||
|
'promotions' => $this->parsePromotionsAttribute($pairs['pmtR'] ?? ''), // 优惠明细 [{type, amount}]
|
||||||
|
'has_threshold_coupon' => (bool) ($pairs['has_threshold_coupon'] ?? 0), // 是否有门槛券
|
||||||
|
'threshold_instruction' => $this->decodeAttributeJson($pairs['threshold_instruction'] ?? null), // 门槛券指令
|
||||||
|
|
||||||
|
// ── 价保 ──
|
||||||
|
'price_protection' => $this->parsePriceProtectionAttribute($pairs['price_protection'] ?? ''), // 价保时间范围 {start, end}
|
||||||
|
|
||||||
|
// ── 物流拦截 ──
|
||||||
|
'intercept_type' => $pairs['interceptType'] ?? null, // 拦截类型 (clj=菜鸟)
|
||||||
|
'intercept_status' => (int) ($pairs['interceptStatus'] ?? 0), // 拦截状态
|
||||||
|
'intercept_items' => $this->decodeAttributeJson($pairs['interceptItemListResult'] ?? null), // 拦截结果明细
|
||||||
|
'ability_template_code' => $pairs['abilityTemplateCode'] ?? null, // 能力模板编码
|
||||||
|
'ability_success_flag' => (bool) ($pairs['abilitySuccessFlag'] ?? 0), // 能力执行是否成功
|
||||||
|
'warehouse_intercept_op' => (int) ($pairs['sellerOpAbWarehouseIntercept'] ?? 0), // 卖家仓库拦截操作
|
||||||
|
|
||||||
|
// ── 卖家/买家 ──
|
||||||
|
'seller_batch' => (bool) ($pairs['seller_batch'] ?? 0), // 是否批量处理
|
||||||
|
'seller_audit' => (int) ($pairs['seller_audit'] ?? 0), // 卖家审核状态
|
||||||
|
'has_seller_memo' => (bool) ($pairs['hasSellerMemo'] ?? 0), // 是否有卖家备注
|
||||||
|
'user_credit' => (int) ($pairs['userCredit'] ?? 0), // 买家信用等级
|
||||||
|
'agree_source' => $pairs['agreeSource'] ?? null, // 同意来源 (clj=菜鸟自动同意)
|
||||||
|
|
||||||
|
// ── 其他 ──
|
||||||
|
'channel_sub_order_id' => $pairs['chnlObn'] ?? null, // 渠道子订单编号
|
||||||
|
'last_order' => (bool) ($pairs['lastOrder'] ?? 0), // 是否最后一笔子单
|
||||||
|
'b2c' => (bool) ($pairs['b2c'] ?? 0), // 是否B2C
|
||||||
|
'bgmtc' => $pairs['bgmtc'] ?? null, // 退款后台创建时间
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 attribute 原始字符串拆分为 key => value 键值对
|
||||||
|
*
|
||||||
|
* 格式: ;key:value;key:value;...
|
||||||
|
* 解码: #3B → :(冒号) #3A → ;(分号)
|
||||||
|
*
|
||||||
|
* @param string $attribute
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
private function parseAttributePairs(string $attribute): array
|
||||||
|
{
|
||||||
|
$pairs = [];
|
||||||
|
$segments = explode(';', $attribute);
|
||||||
|
|
||||||
|
foreach ($segments as $segment) {
|
||||||
|
$segment = trim($segment);
|
||||||
|
if ($segment === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$colonPos = strpos($segment, ':');
|
||||||
|
if ($colonPos === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = substr($segment, 0, $colonPos);
|
||||||
|
$value = substr($segment, $colonPos + 1);
|
||||||
|
|
||||||
|
// #3B → :(冒号) #3A → ;(分号)
|
||||||
|
$value = str_replace(['#3B', '#3A'], [':', ';'], $value);
|
||||||
|
|
||||||
|
$pairs[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $pairs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 SKU 属性
|
||||||
|
*
|
||||||
|
* 格式: SKU_ID|属性名:属性值;属性名:属性值
|
||||||
|
* 示例: 5085821404607|口味:橘子味;颜色分类:维C 250mg
|
||||||
|
*
|
||||||
|
* @param string $value 已解码的 SKU 字符串
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
private function parseSkuAttribute(string $value): ?array
|
||||||
|
{
|
||||||
|
if ($value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = explode('|', $value, 2);
|
||||||
|
|
||||||
|
$result = [
|
||||||
|
'sku_id' => $parts[0],
|
||||||
|
'properties' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isset($parts[1])) {
|
||||||
|
foreach (explode(';', $parts[1]) as $prop) {
|
||||||
|
$kv = explode(':', $prop, 2);
|
||||||
|
if (count($kv) === 2) {
|
||||||
|
$result['properties'][$kv[0]] = $kv[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析资金流信息
|
||||||
|
*
|
||||||
|
* 格式: TYPE^GROUP^NAME^AMOUNT|TYPE^GROUP^NAME^AMOUNT
|
||||||
|
* 示例: CASH^ALIPAY_FUND^支付宝^15489|OTHER_ASSETS^OTHER_ASSETS_GROUP^优惠^2211
|
||||||
|
*
|
||||||
|
* @param string $value
|
||||||
|
* @return array [{type, group, name, amount}]
|
||||||
|
*/
|
||||||
|
private function parseFundFlowAttribute(string $value): array
|
||||||
|
{
|
||||||
|
if ($value === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$flows = [];
|
||||||
|
|
||||||
|
foreach (array_filter(explode('|', $value)) as $item) {
|
||||||
|
$parts = explode('^', $item);
|
||||||
|
if (count($parts) >= 4) {
|
||||||
|
$flows[] = [
|
||||||
|
'type' => $parts[0], // 资金类型 (CASH=现金 / OTHER_ASSETS=其他资产)
|
||||||
|
'group' => $parts[1], // 资金组 (ALIPAY_FUND=支付宝 / OTHER_ASSETS_GROUP=优惠资产)
|
||||||
|
'name' => $parts[2], // 显示名称
|
||||||
|
'amount' => (int) $parts[3], // 金额 (分)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $flows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析优惠明细
|
||||||
|
*
|
||||||
|
* 格式: TYPE^AMOUNT|TYPE^AMOUNT|
|
||||||
|
* 示例: TAPP_USERCOUPON_SP^2211|
|
||||||
|
*
|
||||||
|
* @param string $value
|
||||||
|
* @return array [{type, amount}]
|
||||||
|
*/
|
||||||
|
private function parsePromotionsAttribute(string $value): array
|
||||||
|
{
|
||||||
|
if ($value === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$promotions = [];
|
||||||
|
|
||||||
|
foreach (array_filter(explode('|', $value)) as $item) {
|
||||||
|
$parts = explode('^', $item);
|
||||||
|
if (count($parts) >= 2) {
|
||||||
|
$promotions[] = [
|
||||||
|
'type' => $parts[0], // 优惠类型 (TAPP_USERCOUPON_SP=平台优惠券)
|
||||||
|
'amount' => (int) $parts[1], // 优惠金额 (分)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $promotions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析价保时间范围
|
||||||
|
*
|
||||||
|
* 格式: START_TIME~END_TIME
|
||||||
|
* 示例: 2026-02-24 13:47:45~2026-03-16 23:59:59
|
||||||
|
*
|
||||||
|
* @param string $value
|
||||||
|
* @return array|null {start, end}
|
||||||
|
*/
|
||||||
|
private function parsePriceProtectionAttribute(string $value): ?array
|
||||||
|
{
|
||||||
|
if ($value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = explode('~', $value, 2);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'start' => $parts[0] ?? null,
|
||||||
|
'end' => $parts[1] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解码 attribute 中内嵌的 JSON 字符串
|
||||||
|
*
|
||||||
|
* @param string|null $value
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
private function decodeAttributeJson(?string $value): mixed
|
||||||
|
{
|
||||||
|
if ($value === null || $value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// attribute 中的 JSON 使用 \" 转义双引号,需先还原
|
||||||
|
return json_decode(stripslashes($value), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user