| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104 |
- <?php
- /**
- * 用途:商城顾客售后申请(落 xhHdRefund 待审核)
- * 谁用:app-mall RefundController 提交/列表/撤销、app-hd RefundController 审核通过/驳回
- * 解决:申请只落待审核记录不动资金库存;审核通过时复用 HdRefundService 执行真实退款
- */
- namespace bizHd\refund\classes;
- use bizHd\custom\classes\CustomClass;
- use bizHd\custom\classes\HdClass;
- use bizHd\hb\classes\HbClass;
- use bizHd\order\classes\OrderClass;
- use bizHd\order\classes\OrderGoodsClass;
- use bizHd\order\classes\OrderItemClass;
- use bizHd\refund\services\HdRefundService;
- use bizHd\shop\classes\ShopClass;
- use biz\shop\classes\MainClass;
- use biz\shop\classes\ShopClass as SharedShopClass;
- use common\components\imgUtil;
- use common\components\orderSn;
- use common\components\util;
- class HdRefundMallClass
- {
- /**
- * 创建售后申请(仅落库待审核,不触达资金/库存)
- * 若该订单最新顾客申请为已取消,则复用该记录把状态改回待审核
- * @param array $post 含 refundType/product/price/remark 等
- * @param object $order xhOrder 对象
- * @param array $extra 含 userId/hdId/customId/shopId/mainId/sjId 等上下文
- * @return object
- */
- public static function createApply($post, $order, $extra = [])
- {
- $refundType = (int)($post['refundType'] ?? HdRefundClass::REFUND_TYPE_MONEY_GOOD);
- if (!in_array($refundType, [HdRefundClass::REFUND_TYPE_MONEY_GOOD, HdRefundClass::REFUND_TYPE_MONEY], true)) {
- util::fail('退款方式不正确');
- }
- $productList = [];
- if ($refundType === HdRefundClass::REFUND_TYPE_MONEY_GOOD) {
- $productJson = $post['product'] ?? '';
- if (is_string($productJson)) {
- $productList = json_decode($productJson, true);
- } elseif (is_array($productJson)) {
- $productList = $productJson;
- }
- if (empty($productList) || !is_array($productList)) {
- util::fail('请选择要退的商品');
- }
- $productList = self::validateAndEnrichProduct($productList, $order);
- // 退货并退款由服务端按秒杀/团购活动价 + 普通商品分摊优惠重算,不信任前端 price
- $refundPrice = self::calcReturnRefundPrice($productList, $order);
- } else {
- $refundPrice = $post['price'] ?? 0;
- }
- $couldRefund = self::remainRefundCash($order);
- // 红包全额抵扣后实付为 0:允许提交 0 元申请,审核通过后退回红包;其它 0 元仍拦截
- if (!is_numeric($refundPrice) || bccomp((string)$refundPrice, '0', 2) < 0) {
- util::fail('退款金额不正确');
- }
- if (bccomp((string)$refundPrice, '0', 2) === 0 && !self::allowZeroCashRefundByHb($order)) {
- util::fail('退款金额必须大于0');
- }
- if (bccomp((string)$refundPrice, $couldRefund, 2) === 1) {
- util::fail('退款金额超过可退金额');
- }
- // 申请时写入原单支付快照,待审核详情/列表即可展示退回方式;通过时 addRefund 会再按原单刷新
- $snap = HdNextDayRefundClass::buildRefundPaySnapshot($order, true);
- // 预判当天/封账退款口径,便于待审核展示;最终以审核通过时 mustUseNextDay 为准
- $mainId = (int)($extra['mainId'] ?? $order->mainId ?? 0);
- $main = $mainId > 0 ? MainClass::getById($mainId, true) : null;
- $sameDay = HdNextDayRefundClass::mustUseNextDay($order, $main)
- ? HdRefundClass::SAME_DAY_NO
- : HdRefundClass::SAME_DAY_YES;
- $data = [
- 'mainId' => $mainId,
- 'shopId' => (int)($extra['shopId'] ?? $order->shopId ?? 0),
- 'sjId' => (int)($extra['sjId'] ?? $order->sjId ?? 0),
- 'hdId' => (int)($extra['hdId'] ?? $order->hdId ?? 0),
- 'customId' => self::resolveApplyCustomId($extra, $order),
- 'customName' => $order->customName ?? '',
- 'userId' => (int)($extra['userId'] ?? 0),
- 'orderId' => (int)($order->id ?? 0),
- 'orderSn' => $order->orderSn ?? '',
- 'refundSn' => orderSn::getHdRefundSn(),
- 'refundType' => $refundType,
- 'product' => !empty($productList) ? json_encode($productList, JSON_UNESCAPED_UNICODE) : '',
- // 顾客上传售后图短路径 JSON,审核页展开为 smallImgList
- 'imgList' => self::normalizeImgListJson($post['imgList'] ?? ''),
- 'refundPrice' => round((float)$refundPrice, 2),
- 'remark' => trim((string)($post['remark'] ?? '')),
- 'status' => HdRefundClass::STATUS_PENDING,
- 'shopAdminId' => 0,
- 'shopAdminName' => '',
- 'passTime' => null,
- 'rejectReason' => '',
- 'sameDay' => $sameDay,
- 'onlinePay' => $snap['onlinePay'],
- 'payWay' => $snap['payWay'],
- 'hasReturn' => $snap['hasReturn'],
- 'returnBalance' => $snap['returnBalance'],
- 'couldReturn' => $snap['couldReturn'],
- ];
- // 撤销后重新申请:复用已取消记录,避免同一订单堆多条取消单
- $latest = HdRefundClass::getLatestByOrderId((int)($order->id ?? 0));
- if (!empty($latest) && (int)($latest['status'] ?? -1) === HdRefundClass::STATUS_CANCEL) {
- unset($data['refundSn']);
- return self::reopenCancelledApply((int)$latest['id'], $data);
- }
- return HdRefundClass::add($data, true);
- }
- /**
- * 规范化售后图片字段:只保留短路径字符串列表再 JSON 编码
- * @param string|array $imgList 前端 JSON 或数组
- * @return string 空串或 JSON 数组字符串
- */
- public static function normalizeImgListJson($imgList)
- {
- $arr = [];
- if (is_string($imgList)) {
- $imgList = trim($imgList);
- if ($imgList === '') {
- return '';
- }
- $decoded = json_decode($imgList, true);
- $arr = is_array($decoded) ? $decoded : [];
- } elseif (is_array($imgList)) {
- $arr = $imgList;
- }
- $paths = [];
- foreach ($arr as $item) {
- if (!is_string($item)) {
- continue;
- }
- $path = trim($item);
- // 若误传完整 URL,尽量截成短路径
- if (strpos($path, 'http://') === 0 || strpos($path, 'https://') === 0) {
- $parts = parse_url($path);
- $path = ltrim((string)($parts['path'] ?? ''), '/');
- }
- if ($path === '') {
- continue;
- }
- $paths[] = $path;
- if (count($paths) >= 6) {
- break;
- }
- }
- if (empty($paths)) {
- return '';
- }
- return json_encode($paths, JSON_UNESCAPED_UNICODE);
- }
- /**
- * 给售后行挂上可预览的缩略图/大图列表(对齐 ghs refundDetail)
- * @param array $row
- * @return array
- */
- public static function attachImgDisplayFields($row)
- {
- if (!is_array($row)) {
- return $row;
- }
- $imgString = $row['imgList'] ?? '';
- $imgArr = [];
- if (is_string($imgString) && $imgString !== '') {
- $decoded = json_decode($imgString, true);
- if (is_array($decoded)) {
- $imgArr = $decoded;
- }
- } elseif (is_array($imgString)) {
- $imgArr = $imgString;
- }
- $smallImgList = [];
- $bigImgList = [];
- foreach ($imgArr as $image) {
- if (!is_string($image) || trim($image) === '') {
- continue;
- }
- $short = trim($image);
- $smallImgList[] = imgUtil::groupImg($short) . '?x-oss-process=image/resize,m_fill,h_130,w_130';
- $bigImgList[] = imgUtil::groupImg($short) . '?x-oss-process=image/resize,m_fill,h_700,w_700';
- }
- $row['imgList'] = $imgArr;
- $row['smallImgList'] = $smallImgList;
- $row['bigImgList'] = $bigImgList;
- return $row;
- }
- /**
- * 将已取消申请改回待审核
- * 顾客撤销后再次提交时复用同一条记录,刷新申请内容并清掉审核痕迹
- * @param int $id 已取消申请 id
- * @param array $data 新的申请字段
- * @return object 更新后的售后对象
- */
- protected static function reopenCancelledApply($id, $data)
- {
- $refund = HdRefundClass::getById($id, true);
- if (empty($refund) || (int)$refund->status !== HdRefundClass::STATUS_CANCEL) {
- util::fail('申请状态已变化,请刷新后重试');
- }
- foreach ($data as $key => $val) {
- if ($key === 'id') {
- continue;
- }
- $refund->$key = $val;
- }
- $refund->status = HdRefundClass::STATUS_PENDING;
- $refund->rejectReason = '';
- $refund->shopAdminId = 0;
- $refund->shopAdminName = '';
- $refund->passTime = null;
- $refund->addTime = date('Y-m-d H:i:s');
- if (!$refund->save()) {
- throw new \Exception('重新申请失败');
- }
- return $refund;
- }
- /**
- * 申请客户 id:请求上下文 customId 为 0 时回退到原订单,保证联系客服能进会话
- * @param array $extra
- * @param object $order
- * @return int
- */
- protected static function resolveApplyCustomId($extra, $order)
- {
- $customId = (int)($extra['customId'] ?? 0);
- if ($customId > 0) {
- return $customId;
- }
- return (int)($order->customId ?? 0);
- }
- /**
- * 校验退货数量不超过可退数量,并补齐商品名、订单行单价快照
- * @param array $productList [{productId,num,unitPrice,property,name?}] property 0商品 1花材
- * @param object $order
- * @return array 补齐 name/unitPrice 后的列表
- */
- protected static function validateAndEnrichProduct($productList, $order)
- {
- $orderSn = $order->orderSn ?? '';
- $goodsMap = OrderGoodsClass::getAllByCondition(
- ['orderSn' => $orderSn],
- null,
- '*',
- 'goodsId',
- true
- );
- $itemMap = OrderItemClass::getAllByCondition(
- ['orderSn' => $orderSn],
- null,
- '*',
- 'itemId',
- true
- );
- $enriched = [];
- foreach ($productList as $row) {
- $num = (float)($row['num'] ?? 0);
- if ($num <= 0) {
- util::fail('退货数量必须大于0');
- }
- $productId = (int)($row['productId'] ?? 0);
- $property = (int)($row['property'] ?? 0);
- if ($property === 0) {
- $line = $goodsMap[$productId] ?? null;
- if (empty($line)) {
- util::fail('没有找到退款商品');
- }
- $remain = bcsub((string)($line->num ?? 0), (string)($line->refundNum ?? 0), 2);
- if (bccomp((string)$num, $remain, 2) === 1) {
- $name = $line->name ?? '商品';
- util::fail("{$name}超过可退数量");
- }
- $row['name'] = $row['name'] ?? ($line->name ?? '');
- $row['unitPrice'] = $line->unitPrice ?? ($row['unitPrice'] ?? 0);
- $row['seckillGoodsId'] = (int)($line->seckillGoodsId ?? 0);
- if (empty($row['cover'])) {
- $row['cover'] = $line->cover ?? '';
- }
- if (empty($row['styleName'])) {
- $row['styleName'] = $line->styleName ?? '';
- }
- } else {
- $line = $itemMap[$productId] ?? null;
- if (empty($line)) {
- util::fail('没有找到退款花材');
- }
- $remain = bcsub((string)($line->num ?? 0), (string)($line->refundNum ?? 0), 2);
- if (bccomp((string)$num, $remain, 2) === 1) {
- $name = $line->name ?? '花材';
- util::fail("{$name}超过可退数量");
- }
- $row['name'] = $row['name'] ?? ($line->name ?? '');
- $row['unitPrice'] = $line->unitPrice ?? ($row['unitPrice'] ?? 0);
- if (empty($row['unitName'])) {
- $row['unitName'] = $line->unitName ?? '';
- }
- if (empty($row['cover'])) {
- $row['cover'] = $line->cover ?? '';
- }
- }
- $enriched[] = $row;
- }
- return $enriched;
- }
- /**
- * 退货并退款应退金额:秒杀/团购按行上活动单价;普通商品再按整单优惠占比扣除会员折、满减、红包
- * @param array $productList 已用订单行单价、seckillGoodsId 补齐的勾选商品
- * @param object $order xhOrder
- * @return float 不超过可退上限的应退金额
- */
- protected static function calcReturnRefundPrice($productList, $order)
- {
- $isGroupBuy = (int)($order->groupBuyId ?? 0) > 0;
- $selectedActivity = 0.0;
- $selectedNormal = 0.0;
- foreach ($productList as $row) {
- $num = $row['num'] ?? 0;
- $unitPrice = $row['unitPrice'] ?? 0;
- if (!is_numeric($num) || !is_numeric($unitPrice)) {
- continue;
- }
- $line = (float)$num * (float)$unitPrice;
- if ($isGroupBuy || (int)($row['seckillGoodsId'] ?? 0) > 0) {
- $selectedActivity += $line;
- } else {
- $selectedNormal += $line;
- }
- }
- $hbAmount = self::toMoney($order->hbAmount ?? 0);
- $prePrice = self::toMoney($order->prePrice ?? 0);
- if (bccomp($prePrice, '0', 2) > 0) {
- $allocBase = bcadd($prePrice, $hbAmount, 2);
- } else {
- $allocBase = self::toMoney($order->goodsPrice ?? 0);
- $allocBase = bcadd($allocBase, self::toMoney($order->sendCost ?? 0), 2);
- $allocBase = bcadd($allocBase, self::toMoney($order->packingFee ?? 0), 2);
- $allocBase = bcadd($allocBase, self::toMoney($order->labourCost ?? 0), 2);
- $allocBase = bcadd($allocBase, self::toMoney($order->serviceFee ?? 0), 2);
- }
- $activityOriginal = self::sumOrderActivityOriginal($order, $isGroupBuy);
- $normalBase = (float)$allocBase - (float)$activityOriginal;
- if ($normalBase < 0) {
- $normalBase = 0;
- }
- $discountTotal = self::toMoney($order->discountAmount ?? 0);
- $discountTotal = bcadd($discountTotal, self::toMoney($order->orderReachDiscountPrice ?? 0), 2);
- $discountTotal = bcadd($discountTotal, $hbAmount, 2);
- $normalPrice = $selectedNormal;
- if ($selectedNormal > 0 && $normalBase > 0 && (float)$discountTotal > 0) {
- $normalPrice = $selectedNormal - ($selectedNormal * (float)$discountTotal / $normalBase);
- }
- $price = number_format(round($selectedActivity + $normalPrice, 2), 2, '.', '');
- if (bccomp($price, '0', 2) < 0) {
- $price = '0.00';
- }
- $couldRefund = self::remainRefundCash($order);
- if (bccomp($price, $couldRefund, 2) === 1) {
- $price = $couldRefund;
- }
- return round((float)$price, 2);
- }
- /**
- * 整单秒杀/团购商品原价合计,供从优惠分摊基数中剔除
- * @param object $order
- * @param bool $isGroupBuy 拼团整单则商品总额都是活动价
- * @return float
- */
- protected static function sumOrderActivityOriginal($order, $isGroupBuy)
- {
- if ($isGroupBuy) {
- return (float)self::toMoney($order->goodsPrice ?? 0);
- }
- $orderSn = $order->orderSn ?? '';
- if ($orderSn === '') {
- return 0.0;
- }
- $goodsRows = OrderGoodsClass::getAllByCondition(['orderSn' => $orderSn]) ?: [];
- $total = 0.0;
- foreach ($goodsRows as $goods) {
- $seckillGoodsId = is_array($goods)
- ? (int)($goods['seckillGoodsId'] ?? 0)
- : (int)($goods->seckillGoodsId ?? 0);
- if ($seckillGoodsId <= 0) {
- continue;
- }
- $num = is_array($goods) ? ($goods['num'] ?? 0) : ($goods->num ?? 0);
- $unitPrice = is_array($goods) ? ($goods['unitPrice'] ?? 0) : ($goods->unitPrice ?? 0);
- if (!is_numeric($num) || !is_numeric($unitPrice)) {
- continue;
- }
- $total += (float)$num * (float)$unitPrice;
- }
- return round($total, 2);
- }
- /**
- * 金额转两位小数字符串,供 bcmath 分摊,避免浮点误差
- * @param mixed $value
- * @return string
- */
- protected static function toMoney($value)
- {
- if ($value === null || $value === '' || !is_numeric($value)) {
- return '0.00';
- }
- return number_format((float)$value, 2, '.', '');
- }
- /**
- * 从订单数组或对象取字段;详情接口和申请页传入形态不同
- * @param array|object $order
- * @param string $key
- * @param mixed $default
- * @return mixed
- */
- protected static function orderField($order, $key, $default = 0)
- {
- if (is_object($order)) {
- return $order->$key ?? $default;
- }
- if (is_array($order)) {
- return $order[$key] ?? $default;
- }
- return $default;
- }
- /**
- * 剩余可退现金:实付合计 orderPrice 减当天已退 tkPrice 与封账退款 nextDayTkPrice
- * 红包抵扣已计入 orderPrice,所以红包全额单这里为 0
- * @param array|object $order
- * @return string 两位小数字符串,不会为负
- */
- public static function remainRefundCash($order)
- {
- $already = bcadd(
- self::toMoney(self::orderField($order, 'tkPrice', 0)),
- self::toMoney(self::orderField($order, 'nextDayTkPrice', 0)),
- 2
- );
- $could = bcsub(
- self::toMoney(self::orderField($order, 'orderPrice', 0)),
- $already,
- 2
- );
- if (bccomp($could, '0', 2) < 0) {
- return '0.00';
- }
- return $could;
- }
- /**
- * 红包是否仍占用本单未退回:下单写正数 hbId,退回后改成负数
- * @param array|object $order
- * @return bool
- */
- public static function hasUnreturnedHb($order)
- {
- return (int)self::orderField($order, 'hbId', 0) > 0;
- }
- /**
- * 订单实付现金:优先 mainPay(详情「订单合计」),否则 actPrice / orderPrice
- * @param array|object $order
- * @return string 两位小数字符串
- */
- public static function orderPayAmount($order)
- {
- foreach (['mainPay', 'actPrice', 'orderPrice'] as $key) {
- if (self::orderFieldExists($order, $key)) {
- return self::toMoney(self::orderField($order, $key, 0));
- }
- }
- return '0.00';
- }
- /**
- * 字段是否存在于订单数组或对象上;mainPay=0 也算有值,不能当缺失
- * @param array|object $order
- * @param string $key
- * @return bool
- */
- protected static function orderFieldExists($order, $key)
- {
- if (is_object($order)) {
- return isset($order->$key);
- }
- if (is_array($order)) {
- return array_key_exists($key, $order);
- }
- return false;
- }
- /**
- * 现金为 0 时是否仍可申请:红包未退回,且红包金额大于订单实付
- * 红包把合计抵成 0 时 hbAmount > mainPay;红包已退回或金额不足以覆盖实付则不允许
- * @param array|object $order
- * @return bool
- */
- public static function allowZeroCashRefundByHb($order)
- {
- if (!self::hasUnreturnedHb($order)) {
- return false;
- }
- $hbAmount = self::toMoney(self::orderField($order, 'hbAmount', 0));
- $payAmount = self::orderPayAmount($order);
- return bccomp($hbAmount, $payAmount, 2) === 1;
- }
- /**
- * 是否还有可售后标的:剩余现金>0,或现金为 0 但红包未退回且红包金额大于实付
- * @param array|object $order
- * @return bool
- */
- public static function hasRefundableAmount($order)
- {
- if (bccomp(self::remainRefundCash($order), '0', 2) > 0) {
- return true;
- }
- return self::allowZeroCashRefundByHb($order);
- }
- /**
- * 商家审核通过:复用已有待审核行执行真实退款 + 红包按过期退回 + 消费回退
- * @param object $refund xhHdRefund 待审核对象
- * @param object $order xhOrder(需可写)
- * @param array $shopAdmin 审核人信息 [id, name, shopId, sjId, mainId]
- * @return object 更新后的售后单
- */
- public static function approve($refund, $order, $shopAdmin = [])
- {
- if ((int)$refund->status !== HdRefundClass::STATUS_PENDING) {
- util::fail('仅待审核申请可通过');
- }
- $productList = [];
- if (!empty($refund->product)) {
- $decoded = json_decode($refund->product, true);
- if (is_array($decoded)) {
- $productList = $decoded;
- }
- }
- $post = [
- 'price' => $refund->refundPrice,
- 'refundType' => (int)$refund->refundType,
- 'product' => $productList,
- 'remark' => $refund->remark ?? '',
- 'shopId' => (int)($shopAdmin['shopId'] ?? $refund->shopId),
- 'sjId' => (int)($shopAdmin['sjId'] ?? $order->sjId ?? 0),
- 'mainId' => (int)($shopAdmin['mainId'] ?? $refund->mainId),
- 'shopAdminId' => (int)($shopAdmin['id'] ?? 0),
- 'shopAdminName' => $shopAdmin['name'] ?? '',
- ];
- // 复用待审核行写明细/回库/资金,避免再插一条
- $refund = HdRefundService::addRefund($post, $order, $refund);
- // 当天售后才在此回退累计消费;封账/隔天已在 applyHdAmountAndFund 扣过,避免重复
- $sameDay = intval($refund->sameDay ?? HdRefundClass::SAME_DAY_YES);
- if ($sameDay === HdRefundClass::SAME_DAY_YES) {
- $custom = CustomClass::getLockById($order->customId);
- if (!empty($custom)) {
- $custom->buyAmount = bcsub((string)$custom->buyAmount, (string)$refund->refundPrice, 2);
- $customName = $custom->name ?? $order->customName ?? '';
- $orderSnText = $order->orderSn ?? '';
- $eventPrefix = $customName !== '' ? $customName : '客户';
- $custom->eventRemark = $eventPrefix . '商城售后' . $orderSnText;
- $custom->relateId = $order->id;
- $custom->relateType = 1;
- $custom->staffId = (int)($shopAdmin['id'] ?? 0);
- $custom->staffName = $shopAdmin['name'] ?? '';
- $custom->mainId = (int)($shopAdmin['mainId'] ?? $refund->mainId);
- $custom->save();
- $hd = HdClass::getLockById($custom->hdId);
- if (!empty($hd)) {
- $hd->expendAmount = bcsub((string)$hd->expendAmount, (string)$refund->refundPrice, 2);
- $hd->save();
- }
- }
- }
- $updatedOrder = OrderClass::getById($order->id, true, 'id, actPrice, hbId, hbAmount');
- if (!empty($updatedOrder) && (int)$updatedOrder->hbId > 0 && bccomp((string)$updatedOrder->actPrice, '0', 2) <= 0) {
- HbClass::hbBackIfNotExpired($updatedOrder);
- }
- return $refund;
- }
- /**
- * 商家驳回申请
- * @param object $refund
- * @param string $reason
- * @param array $shopAdmin
- * @return object
- */
- public static function reject($refund, $reason, $shopAdmin = [])
- {
- if ((int)$refund->status !== HdRefundClass::STATUS_PENDING) {
- util::fail('仅待审核申请可驳回');
- }
- $reason = trim((string)$reason);
- if ($reason === '') {
- util::fail('请填写驳回原因');
- }
- $refund->status = HdRefundClass::STATUS_REJECT;
- $refund->rejectReason = mb_substr($reason, 0, 250);
- $refund->shopAdminId = (int)($shopAdmin['id'] ?? 0);
- $refund->shopAdminName = $shopAdmin['name'] ?? '';
- $refund->passTime = date('Y-m-d H:i:s');
- $refund->save();
- return $refund;
- }
- /**
- * 顾客撤销待审核申请
- * @param object $refund
- * @return object
- */
- public static function cancel($refund)
- {
- if ((int)$refund->status !== HdRefundClass::STATUS_PENDING) {
- util::fail('仅待审核申请可撤销');
- }
- $refund->status = HdRefundClass::STATUS_CANCEL;
- $refund->save();
- return $refund;
- }
- /**
- * 顾客端售后列表(mallApp 退款售后页,跨店;可按订单收窄)
- * @param int $userId 当前登录商城用户
- * @param string $tab all|pending|0|refunded|1|closed|2|3
- * @param int $orderId 可选,>0 时只返回该订单的顾客申请(订单详情「申请记录」)
- * @return array 分页 list + pendingCount
- */
- public static function getCustomApplyList($userId, $tab = 'all', $orderId = 0)
- {
- $userId = (int)$userId;
- $orderId = (int)$orderId;
- $where = ['userId' => $userId];
- // 订单详情进入时只看本单,避免混入其它订单售后
- if ($orderId > 0) {
- $where['orderId'] = $orderId;
- }
- $tab = (string)$tab;
- if ($tab === 'pending' || $tab === '0') {
- $where['status'] = HdRefundClass::STATUS_PENDING;
- } elseif ($tab === 'refunded' || $tab === '1') {
- $where['status'] = HdRefundClass::STATUS_PASS;
- } elseif ($tab === 'closed') {
- $where['status'] = ['in', [HdRefundClass::STATUS_REJECT, HdRefundClass::STATUS_CANCEL]];
- } elseif ($tab !== '' && $tab !== 'all' && is_numeric($tab)) {
- $where['status'] = (int)$tab;
- }
- $data = HdRefundClass::getList('*', $where, 'addTime DESC');
- $pendingWhere = [
- 'userId' => $userId,
- 'status' => HdRefundClass::STATUS_PENDING,
- ];
- if ($orderId > 0) {
- $pendingWhere['orderId'] = $orderId;
- }
- $data['pendingCount'] = (int)HdRefundClass::getCount($pendingWhere);
- if (!empty($data['list'])) {
- $data['list'] = self::formatCustomList($data['list']);
- } else {
- $data['list'] = [];
- }
- return $data;
- }
- /**
- * 顾客端售后详情:单条申请补齐与列表相同的展示字段
- * @param array $row xhHdRefund 行
- * @return array
- */
- public static function formatCustomDetail($row)
- {
- $list = self::formatCustomList([$row]);
- $detail = $list[0] ?? $row;
- // 详情再补图片预览字段,列表可不带大图
- return self::attachImgDisplayFields($detail);
- }
- /**
- * 状态短文案(审核语义)
- * @param int $status
- * @return string
- */
- public static function statusText($status)
- {
- $map = [
- HdRefundClass::STATUS_PENDING => '待审核',
- HdRefundClass::STATUS_PASS => '已通过',
- HdRefundClass::STATUS_REJECT => '已驳回',
- HdRefundClass::STATUS_CANCEL => '已取消',
- ];
- return $map[(int)$status] ?? '';
- }
- /**
- * 列表卡片主状态文案,对齐设计稿「退款中·待商家处理 / 退款成功」
- * 已返充时用「退款成功·已返充」,列表右上角也能看清
- * @param int $status
- * @param int $refundType 1退货退款 2仅退款
- * @param array $row 可选,用于通过后按资金态细化标题
- * @return string
- */
- public static function displayStatusTitle($status, $refundType, $row = [])
- {
- $status = (int)$status;
- if ($status === HdRefundClass::STATUS_PENDING) {
- return (int)$refundType === HdRefundClass::REFUND_TYPE_MONEY_GOOD
- ? '退货中·待商家处理'
- : '退款中·待商家处理';
- }
- if ($status === HdRefundClass::STATUS_PASS) {
- if ((int)($row['returnBalance'] ?? 0) === HdNextDayRefundClass::FLAG_YES) {
- return '退款成功·已返充';
- }
- if ((int)($row['hasReturn'] ?? 0) === HdNextDayRefundClass::FLAG_YES) {
- return '退款成功';
- }
- if ((int)($row['couldReturn'] ?? 0) === HdNextDayRefundClass::FLAG_YES) {
- return '退款成功·待退还';
- }
- return '退款成功';
- }
- if ($status === HdRefundClass::STATUS_REJECT) {
- return '已驳回';
- }
- if ($status === HdRefundClass::STATUS_CANCEL) {
- return '已取消';
- }
- return self::statusText($status);
- }
- /**
- * 资金落地文案:返充 / 原路 / 待商家退还(供列表 tip 与详情「退回方式」)
- * @param array $row
- * @return string
- */
- public static function fundStatusText($row)
- {
- $status = (int)($row['status'] ?? 0);
- if ($status !== HdRefundClass::STATUS_PASS) {
- return '';
- }
- if ((int)($row['returnBalance'] ?? 0) === HdNextDayRefundClass::FLAG_YES) {
- return '已返充到余额';
- }
- if ((int)($row['hasReturn'] ?? 0) === HdNextDayRefundClass::FLAG_YES) {
- return '已原路退回';
- }
- // 审核通过但线下等尚未选手选落地
- if ((int)($row['couldReturn'] ?? 0) === HdNextDayRefundClass::FLAG_YES) {
- return '等待商家退还';
- }
- return '退款处理中';
- }
- /**
- * 卡片底部提示:处理中给审核时效;通过后按返充/原路/待退还区分,避免一律写「原路退回」
- * @param array $row
- * @return string
- */
- public static function displayStatusTip($row)
- {
- $status = (int)($row['status'] ?? 0);
- if ($status === HdRefundClass::STATUS_PENDING) {
- return '商家审核中·预计 24 小时内处理';
- }
- if ($status === HdRefundClass::STATUS_PASS) {
- $fundText = self::fundStatusText($row);
- $time = trim((string)($row['passTime'] ?? ''));
- $timeOk = $time !== '' && $time !== '0000-00-00 00:00:00';
- if ((int)($row['returnBalance'] ?? 0) === HdNextDayRefundClass::FLAG_YES) {
- return $timeOk
- ? ('退款已到账·已返充到余额 ' . $time)
- : '退款已到账·已返充到余额';
- }
- if ((int)($row['hasReturn'] ?? 0) === HdNextDayRefundClass::FLAG_YES) {
- return $timeOk
- ? ('退款已到账·原路退回 ' . $time)
- : '退款已到账·原路退回';
- }
- if ($fundText !== '') {
- return $timeOk ? ($fundText . ' ' . $time) : $fundText;
- }
- return '退款已通过';
- }
- if ($status === HdRefundClass::STATUS_REJECT) {
- $reason = trim((string)($row['rejectReason'] ?? ''));
- return $reason !== '' ? ('商家已驳回·' . $reason) : '商家已驳回';
- }
- if ($status === HdRefundClass::STATUS_CANCEL) {
- return '申请已取消';
- }
- return '';
- }
- /**
- * 退款类型标签:1退货退款 2仅退款
- * @param int $refundType
- * @return string
- */
- public static function refundTypeText($refundType)
- {
- return (int)$refundType === HdRefundClass::REFUND_TYPE_MONEY_GOOD ? '退货退款' : '仅退款';
- }
- /**
- * 批量补齐店铺名、商品封面/规格,避免列表 N+1
- * @param array $list
- * @return array
- */
- public static function formatCustomList($list)
- {
- if (empty($list) || !is_array($list)) {
- return [];
- }
- $shopIds = [];
- $hdIds = [];
- $orderSns = [];
- foreach ($list as $row) {
- $shopId = (int)($row['shopId'] ?? 0);
- if ($shopId > 0) {
- $shopIds[$shopId] = $shopId;
- }
- $hdId = (int)($row['hdId'] ?? 0);
- if ($hdId > 0) {
- $hdIds[$hdId] = $hdId;
- }
- $sn = trim((string)($row['orderSn'] ?? ''));
- if ($sn !== '') {
- $orderSns[$sn] = $sn;
- }
- }
- $shopMap = [];
- if (!empty($shopIds)) {
- $shopMap = ShopClass::getByIds(array_values($shopIds), null, 'id', 'id,shopName,merchantName,avatar') ?: [];
- }
- $hdCustomMap = [];
- if (!empty($hdIds)) {
- $hdRows = HdClass::getByIds(array_values($hdIds), null, 'id', 'id,customId') ?: [];
- foreach ($hdRows as $id => $hd) {
- $hdCustomMap[(int)$id] = (int)($hd['customId'] ?? 0);
- }
- }
- $orderCustomMap = [];
- $goodsBySn = [];
- $itemBySn = [];
- $firstGoodsBySn = [];
- if (!empty($orderSns)) {
- $snList = array_values($orderSns);
- $orderRows = OrderClass::getAllByCondition(['orderSn' => ['in', $snList]], null, 'orderSn,customId') ?: [];
- foreach ($orderRows as $order) {
- $sn = $order['orderSn'] ?? '';
- $cid = (int)($order['customId'] ?? 0);
- if ($sn !== '' && $cid > 0) {
- $orderCustomMap[$sn] = $cid;
- }
- }
- $goodsRows = OrderGoodsClass::getAllByCondition(['orderSn' => ['in', $snList]]) ?: [];
- foreach ($goodsRows as $goods) {
- $sn = $goods['orderSn'] ?? '';
- $gid = (int)($goods['goodsId'] ?? 0);
- if ($sn === '') {
- continue;
- }
- $goodsBySn[$sn][$gid] = $goods;
- if (!isset($firstGoodsBySn[$sn])) {
- $firstGoodsBySn[$sn] = $goods;
- }
- }
- $itemRows = OrderItemClass::getAllByCondition(['orderSn' => ['in', $snList]]) ?: [];
- foreach ($itemRows as $item) {
- $sn = $item['orderSn'] ?? '';
- $itemId = (int)($item['itemId'] ?? 0);
- if ($sn === '' || $itemId <= 0) {
- continue;
- }
- $itemBySn[$sn][$itemId] = $item;
- }
- }
- $formatted = [];
- foreach ($list as $row) {
- $formatted[] = self::formatCustomRow($row, $shopMap, $goodsBySn, $itemBySn, $firstGoodsBySn, $hdCustomMap, $orderCustomMap);
- }
- return $formatted;
- }
- /**
- * 单条申请转列表/详情展示结构
- * @param array $row
- * @param array $shopMap
- * @param array $goodsBySn
- * @param array $itemBySn
- * @param array $firstGoodsBySn
- * @param array $hdCustomMap
- * @param array $orderCustomMap
- * @return array
- */
- public static function formatCustomRow($row, $shopMap = [], $goodsBySn = [], $itemBySn = [], $firstGoodsBySn = [], $hdCustomMap = [], $orderCustomMap = [])
- {
- $status = (int)($row['status'] ?? 0);
- $refundType = (int)($row['refundType'] ?? HdRefundClass::REFUND_TYPE_MONEY);
- $shopId = (int)($row['shopId'] ?? 0);
- $orderSn = trim((string)($row['orderSn'] ?? ''));
- $shop = $shopMap[$shopId] ?? [];
- $row['customId'] = self::resolveChatCustomId($row, $hdCustomMap, $orderCustomMap);
- $productList = [];
- if (!empty($row['product'])) {
- $decoded = is_array($row['product']) ? $row['product'] : json_decode($row['product'], true);
- if (is_array($decoded)) {
- $productList = $decoded;
- }
- }
- $goodsList = [];
- foreach ($productList as $product) {
- $property = (int)($product['property'] ?? 0);
- $productId = (int)($product['productId'] ?? 0);
- $line = [];
- if ($property === 1) {
- $line = $itemBySn[$orderSn][$productId] ?? [];
- } else {
- $line = $goodsBySn[$orderSn][$productId] ?? [];
- }
- $goodsList[] = self::buildGoodsView($product, $line);
- }
- if (empty($goodsList) && isset($firstGoodsBySn[$orderSn])) {
- $goodsList[] = self::buildGoodsView([], $firstGoodsBySn[$orderSn]);
- }
- $row['productList'] = $productList;
- $row['goodsList'] = $goodsList;
- $row['goods'] = $goodsList[0] ?? self::emptyGoodsView();
- $displayName = SharedShopClass::formatDisplayShopName($shop);
- $row['hdName'] = $displayName !== '' ? $displayName : '花店';
- $row['hdAvatar'] = self::coverUrl($shop['avatar'] ?? '');
- $row['statusText'] = self::statusText($status);
- $row['statusTitle'] = self::displayStatusTitle($status, $refundType, $row);
- $row['statusTip'] = self::displayStatusTip($row);
- $row['fundStatusText'] = self::fundStatusText($row);
- $row['statusTone'] = self::statusTone($status);
- $row['refundTypeText'] = self::refundTypeText($refundType);
- $row['reasonText'] = self::reasonText($row);
- $row['canModify'] = $status === HdRefundClass::STATUS_PENDING ? 1 : 0;
- $row['refundPrice'] = round((float)($row['refundPrice'] ?? 0), 2);
- // 详情页「审核时间」读 auditTime;库字段是 passTime
- $passTime = trim((string)($row['passTime'] ?? ''));
- $row['auditTime'] = ($passTime !== '' && $passTime !== '0000-00-00 00:00:00') ? $passTime : '';
- // 资金三态显式转 int,前端判断返充/原路更稳
- $row['returnBalance'] = (int)($row['returnBalance'] ?? 0);
- $row['hasReturn'] = (int)($row['hasReturn'] ?? 0);
- $row['couldReturn'] = (int)($row['couldReturn'] ?? 0);
- return $row;
- }
- /**
- * 联系客服用的 customId:申请表可能为 0,依次用申请本身、xhHd、原订单补齐
- * @param array $row
- * @param array $hdCustomMap
- * @param array $orderCustomMap
- * @return int
- */
- protected static function resolveChatCustomId($row, $hdCustomMap, $orderCustomMap)
- {
- $applyCustomId = (int)($row['customId'] ?? 0);
- if ($applyCustomId > 0) {
- return $applyCustomId;
- }
- $hdId = (int)($row['hdId'] ?? 0);
- if ($hdId > 0 && !empty($hdCustomMap[$hdId])) {
- return (int)$hdCustomMap[$hdId];
- }
- $orderSn = trim((string)($row['orderSn'] ?? ''));
- if ($orderSn !== '' && !empty($orderCustomMap[$orderSn])) {
- return (int)$orderCustomMap[$orderSn];
- }
- return 0;
- }
- /**
- * 处理中橙色、成功绿色、关闭灰色,供前端绑 class
- * @param int $status
- * @return string pending|success|closed
- */
- protected static function statusTone($status)
- {
- $status = (int)$status;
- if ($status === HdRefundClass::STATUS_PENDING) {
- return 'pending';
- }
- if ($status === HdRefundClass::STATUS_PASS) {
- return 'success';
- }
- return 'closed';
- }
- /**
- * 售后原因:优先顾客备注
- * @param array $row
- * @return string
- */
- protected static function reasonText($row)
- {
- $remark = trim((string)($row['remark'] ?? ''));
- if ($remark !== '') {
- return $remark;
- }
- return '顾客申请售后';
- }
- /**
- * 把申请商品快照与订单行封面/规格拼成卡片商品
- * @param array $product
- * @param array $line
- * @return array
- */
- protected static function buildGoodsView($product, $line)
- {
- $name = $product['name'] ?? ($line['name'] ?? '商品');
- $cover = $product['cover'] ?? ($line['cover'] ?? '');
- $spec = $product['styleName'] ?? ($product['unitName'] ?? '');
- if ($spec === '') {
- $spec = $line['styleName'] ?? ($line['unitName'] ?? '');
- }
- $price = $product['unitPrice'] ?? ($line['unitPrice'] ?? 0);
- $num = $product['num'] ?? ($line['num'] ?? 1);
- return [
- 'name' => $name,
- 'cover' => self::coverUrl($cover),
- 'spec' => $spec,
- 'price' => round((float)$price, 2),
- 'num' => $num,
- ];
- }
- /**
- * 无商品时的占位,避免前端取 goods 空指针
- * @return array
- */
- protected static function emptyGoodsView()
- {
- return [
- 'name' => '售后商品',
- 'cover' => self::coverUrl(''),
- 'spec' => '',
- 'price' => 0,
- 'num' => 1,
- ];
- }
- /**
- * 相对路径转可访问封面;已是 http(s) 则原样返回
- * @param string $short
- * @return string
- */
- protected static function coverUrl($short)
- {
- $short = trim((string)$short);
- if ($short === '') {
- return imgUtil::groupImg('retail/default-img.png');
- }
- if (strpos($short, 'http://') === 0 || strpos($short, 'https://') === 0) {
- return $short;
- }
- return imgUtil::groupImg($short) . '?x-oss-process=image/resize,m_fill,h_200,w_200';
- }
- }
|