|
|
@@ -0,0 +1,260 @@
|
|
|
+<?php
|
|
|
+/**
|
|
|
+ * 用途:商城顾客售后申请服务
|
|
|
+ * 谁用:app-mall 提交/撤销申请、app-hd 审核通过/驳回
|
|
|
+ * 解决:申请只落待审核记录;审核通过时复用 HdRefundService 执行真实退款,并处理红包与分销重算
|
|
|
+ */
|
|
|
+
|
|
|
+namespace bizHd\refund\services;
|
|
|
+
|
|
|
+use bizHd\base\services\BaseService;
|
|
|
+use bizHd\custom\classes\CustomClass;
|
|
|
+use bizHd\custom\classes\HdClass;
|
|
|
+use bizHd\hb\classes\HbClass;
|
|
|
+use bizHd\order\classes\OrderClass;
|
|
|
+use bizHd\refund\classes\MallRefundApplyClass;
|
|
|
+use common\components\util;
|
|
|
+
|
|
|
+class MallRefundApplyService extends BaseService
|
|
|
+{
|
|
|
+ public static $baseFile = '\bizHd\refund\classes\MallRefundApplyClass';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建售后申请(仅落库待审核,不触达资金)
|
|
|
+ * @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'] ?? MallRefundApplyClass::REFUND_TYPE_MONEY_GOOD);
|
|
|
+ if (!in_array($refundType, [MallRefundApplyClass::REFUND_TYPE_MONEY_GOOD, MallRefundApplyClass::REFUND_TYPE_MONEY], true)) {
|
|
|
+ util::fail('退款方式不正确');
|
|
|
+ }
|
|
|
+
|
|
|
+ $refundPrice = $post['price'] ?? 0;
|
|
|
+ if (!is_numeric($refundPrice) || bccomp((string)$refundPrice, '0', 2) <= 0) {
|
|
|
+ util::fail('退款金额必须大于0');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 可退上限:订单金额 - 已退累计
|
|
|
+ $couldRefund = bcsub((string)($order->orderPrice ?? 0), (string)($order->tkPrice ?? 0), 2);
|
|
|
+ if (bccomp((string)$refundPrice, $couldRefund, 2) === 1) {
|
|
|
+ util::fail('退款金额超过可退金额');
|
|
|
+ }
|
|
|
+
|
|
|
+ $productList = [];
|
|
|
+ if ($refundType === MallRefundApplyClass::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);
|
|
|
+ }
|
|
|
+
|
|
|
+ $data = [
|
|
|
+ 'mainId' => (int)($extra['mainId'] ?? $order->mainId ?? 0),
|
|
|
+ 'shopId' => (int)($extra['shopId'] ?? $order->shopId ?? 0),
|
|
|
+ 'hdId' => (int)($extra['hdId'] ?? $order->hdId ?? 0),
|
|
|
+ 'customId' => (int)($extra['customId'] ?? $order->customId ?? 0),
|
|
|
+ 'userId' => (int)($extra['userId'] ?? 0),
|
|
|
+ 'orderId' => (int)($order->id ?? 0),
|
|
|
+ 'orderSn' => $order->orderSn ?? '',
|
|
|
+ 'refundType' => $refundType,
|
|
|
+ 'product' => !empty($productList) ? json_encode($productList, JSON_UNESCAPED_UNICODE) : '',
|
|
|
+ 'refundPrice' => round((float)$refundPrice, 2),
|
|
|
+ 'remark' => trim((string)($post['remark'] ?? '')),
|
|
|
+ 'hbId' => (int)($order->hbId ?? 0) > 0 ? (int)$order->hbId : 0,
|
|
|
+ 'status' => MallRefundApplyClass::STATUS_PENDING,
|
|
|
+ ];
|
|
|
+
|
|
|
+ return MallRefundApplyClass::add($data, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 校验退货数量不超过可退数量,并补齐商品名快照
|
|
|
+ * @param array $productList [{productId,num,unitPrice,property,name?}] property 0商品 1花材
|
|
|
+ * @param object $order
|
|
|
+ * @return array 补齐 name 后的列表
|
|
|
+ */
|
|
|
+ protected static function validateAndEnrichProduct($productList, $order)
|
|
|
+ {
|
|
|
+ $orderSn = $order->orderSn ?? '';
|
|
|
+ $goodsMap = \bizHd\order\classes\OrderGoodsClass::getAllByCondition(
|
|
|
+ ['orderSn' => $orderSn],
|
|
|
+ null,
|
|
|
+ '*',
|
|
|
+ 'goodsId',
|
|
|
+ true
|
|
|
+ );
|
|
|
+ $itemMap = \bizHd\order\classes\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 ?? '');
|
|
|
+ } 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 ?? '');
|
|
|
+ if (empty($row['unitName'])) {
|
|
|
+ $row['unitName'] = $line->unitName ?? '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $enriched[] = $row;
|
|
|
+ }
|
|
|
+ return $enriched;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 商家审核通过:执行真实退款 + 红包按过期退回 + 消费回退 + 分销重算
|
|
|
+ * @param object $apply MallRefundApply
|
|
|
+ * @param object $order xhOrder(需可写)
|
|
|
+ * @param array $shopAdmin 审核人信息 [id, name, shopId, sjId, mainId]
|
|
|
+ * @return object 更新后的申请
|
|
|
+ */
|
|
|
+ public static function approve($apply, $order, $shopAdmin = [])
|
|
|
+ {
|
|
|
+ if ((int)$apply->status !== MallRefundApplyClass::STATUS_PENDING) {
|
|
|
+ util::fail('仅待审核申请可通过');
|
|
|
+ }
|
|
|
+
|
|
|
+ $productList = [];
|
|
|
+ if (!empty($apply->product)) {
|
|
|
+ $decoded = json_decode($apply->product, true);
|
|
|
+ if (is_array($decoded)) {
|
|
|
+ $productList = $decoded;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $post = [
|
|
|
+ 'price' => $apply->refundPrice,
|
|
|
+ 'refundType' => (int)$apply->refundType,
|
|
|
+ 'product' => $productList,
|
|
|
+ 'remark' => $apply->remark ?? '',
|
|
|
+ 'shopId' => (int)($shopAdmin['shopId'] ?? $apply->shopId),
|
|
|
+ 'sjId' => (int)($shopAdmin['sjId'] ?? $order->sjId ?? 0),
|
|
|
+ 'mainId' => (int)($shopAdmin['mainId'] ?? $apply->mainId),
|
|
|
+ 'shopAdminId' => (int)($shopAdmin['id'] ?? 0),
|
|
|
+ 'shopAdminName' => $shopAdmin['name'] ?? '',
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 复用现有即时退款资金处理
|
|
|
+ $refund = HdRefundService::addRefund($post, $order);
|
|
|
+ $refundId = (int)($refund->id ?? 0);
|
|
|
+
|
|
|
+ // 累计消费、花店支出回退(与 RefundController::actionCreateOrder 保持一致)
|
|
|
+ $custom = CustomClass::getLockById($order->customId);
|
|
|
+ if (!empty($custom)) {
|
|
|
+ $custom->buyAmount = bcsub((string)$custom->buyAmount, (string)$apply->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'] ?? $apply->mainId);
|
|
|
+ $custom->save();
|
|
|
+
|
|
|
+ $hd = HdClass::getLockById($custom->hdId);
|
|
|
+ if (!empty($hd)) {
|
|
|
+ $hd->expendAmount = bcsub((string)$hd->expendAmount, (string)$apply->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);
|
|
|
+ }
|
|
|
+
|
|
|
+ $apply->status = MallRefundApplyClass::STATUS_PASS;
|
|
|
+ $apply->hdRefundId = $refundId;
|
|
|
+ $apply->auditAdminId = (int)($shopAdmin['id'] ?? 0);
|
|
|
+ $apply->auditAdminName = $shopAdmin['name'] ?? '';
|
|
|
+ $apply->auditTime = date('Y-m-d H:i:s');
|
|
|
+ $apply->save();
|
|
|
+
|
|
|
+ // TODO 分销重算由 Controller 在事务 commit 后调用 tryCalcDistributionAfterPay,避免嵌套事务
|
|
|
+
|
|
|
+ return $apply;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 商家驳回申请
|
|
|
+ * @param object $apply
|
|
|
+ * @param string $reason
|
|
|
+ * @param array $shopAdmin
|
|
|
+ * @return object
|
|
|
+ */
|
|
|
+ public static function reject($apply, $reason, $shopAdmin = [])
|
|
|
+ {
|
|
|
+ if ((int)$apply->status !== MallRefundApplyClass::STATUS_PENDING) {
|
|
|
+ util::fail('仅待审核申请可驳回');
|
|
|
+ }
|
|
|
+ $reason = trim((string)$reason);
|
|
|
+ if ($reason === '') {
|
|
|
+ util::fail('请填写驳回原因');
|
|
|
+ }
|
|
|
+
|
|
|
+ $apply->status = MallRefundApplyClass::STATUS_REJECT;
|
|
|
+ $apply->rejectReason = mb_substr($reason, 0, 250);
|
|
|
+ $apply->auditAdminId = (int)($shopAdmin['id'] ?? 0);
|
|
|
+ $apply->auditAdminName = $shopAdmin['name'] ?? '';
|
|
|
+ $apply->auditTime = date('Y-m-d H:i:s');
|
|
|
+ $apply->save();
|
|
|
+ return $apply;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 顾客撤销待审核申请
|
|
|
+ * @param object $apply
|
|
|
+ * @return object
|
|
|
+ */
|
|
|
+ public static function cancel($apply)
|
|
|
+ {
|
|
|
+ if ((int)$apply->status !== MallRefundApplyClass::STATUS_PENDING) {
|
|
|
+ util::fail('仅待审核申请可撤销');
|
|
|
+ }
|
|
|
+ $apply->status = MallRefundApplyClass::STATUS_CANCEL;
|
|
|
+ $apply->save();
|
|
|
+ return $apply;
|
|
|
+ }
|
|
|
+}
|