From eb9261ffa4856dbaeed7a74bcfed7616958fd0b5 Mon Sep 17 00:00:00 2001 From: Nick Zeng Date: Wed, 25 Feb 2026 14:34:33 +0800 Subject: [PATCH] update tmall refund item attribute parse --- .../app/Platform/Tmall/EntityParse/Refund.php | 283 +++++++++++++++++- 1 file changed, 280 insertions(+), 3 deletions(-) diff --git a/backend/app/Platform/Tmall/EntityParse/Refund.php b/backend/app/Platform/Tmall/EntityParse/Refund.php index 06201b9..16eace3 100644 --- a/backend/app/Platform/Tmall/EntityParse/Refund.php +++ b/backend/app/Platform/Tmall/EntityParse/Refund.php @@ -302,9 +302,9 @@ class Refund extends EntityParse return RefundType::RETURN_AND_REFUND->value; } - // 买家未收到货 → 仅退款 + // 买家未收到货 → 未发货前退款 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_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 + */ + 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); } }