From 11a36ece8017e4e33ce2933305d9d1f9d091d1a5 Mon Sep 17 00:00:00 2001 From: ctexthuang Date: Fri, 21 Mar 2025 15:14:43 +0800 Subject: [PATCH] feat : coupon --- app/Amqp/Consumer/RefundOrderConsumer.php | 19 +- app/Constants/Common/RefundCode.php | 7 + app/Controller/Admin/OrderController.php | 72 +++++ app/Model/PayOrder.php | 4 +- app/Model/RefundOrder.php | 2 +- app/Service/Admin/Order/OrderInfoService.php | 22 ++ app/Service/Admin/Order/OrderListService.php | 22 ++ app/Service/Admin/Order/RefundService.php | 83 +++++ .../Amqp/Refund/BaseRefundOrderService.php | 301 ++++++++++++++++++ .../Amqp/Refund/FullRefundOrderService.php | 54 ++++ .../Amqp/Refund/PartialRefundOrderService.php | 62 ++++ app/Service/Api/Order/RefundOrderService.php | 3 +- .../Pay/Wx/WxJsRechargeOrderService.php | 3 +- .../Common/OrderChangeStatusTrait.php | 6 +- 14 files changed, 644 insertions(+), 16 deletions(-) create mode 100644 app/Controller/Admin/OrderController.php create mode 100644 app/Service/Admin/Order/OrderInfoService.php create mode 100644 app/Service/Admin/Order/OrderListService.php create mode 100644 app/Service/Admin/Order/RefundService.php create mode 100644 app/Service/Amqp/Refund/BaseRefundOrderService.php create mode 100644 app/Service/Amqp/Refund/FullRefundOrderService.php create mode 100644 app/Service/Amqp/Refund/PartialRefundOrderService.php diff --git a/app/Amqp/Consumer/RefundOrderConsumer.php b/app/Amqp/Consumer/RefundOrderConsumer.php index 3058cf2..48aecaa 100644 --- a/app/Amqp/Consumer/RefundOrderConsumer.php +++ b/app/Amqp/Consumer/RefundOrderConsumer.php @@ -4,8 +4,11 @@ declare(strict_types=1); namespace App\Amqp\Consumer; +use App\Constants\Common\RefundCode; use App\Lib\Log; use App\Model\Order; +use App\Service\Amqp\Refund\FullRefundOrderService; +use App\Service\Amqp\Refund\PartialRefundOrderService; use App\Service\Amqp\Refund\RefundService; use App\Service\ServiceTrait\Api\OrderTrait; use Exception; @@ -41,7 +44,6 @@ class RefundOrderConsumer extends ConsumerMessage #[Inject] protected Order $orderModel; - /** * @param $data * @param AMQPMessage $message @@ -51,21 +53,24 @@ class RefundOrderConsumer extends ConsumerMessage */ public function consumeMessage($data, AMQPMessage $message): Result { - if (!$data['order_id'] || !$data['type'] || !$data['amount'] || !$data['reason']) { - $this->log->error('CancelOrderConsumer:error:NoData:'.json_encode($data)); + if (!$data['order_id'] || !$data['type'] || !$data['reason']) { + $this->log->error('RefundOrderConsumer:error:NoData:'.json_encode($data)); return Result::ACK; } try { - $service = new RefundService(); + $service = match ($data['type']) { + RefundCode::FULL_GOOD_REFUND => new FullRefundOrderService(), +// RefundCode::PARTIAL_GOOD_REFUND => new PartialRefundOrderService(), + RefundCode::PARTIAL_GOOD_REFUND => throw new Exception('部分退款直接调用后台退款接口'), +// RefundCode::BALANCE_REFUND => $service = new RefundService(), + }; $service->orderId = (int)$data['order_id']; - $service->type = (int)$data['type']; - $service->refundAmount = $data['amount']; $service->reason = $data['reason']; + $service->type = $data['type']; $service->handle(); - } catch (Exception $e) { $this->log->error('RefundOrderConsumer:error:'.$e->getMessage().':data:'.json_encode($data)); } diff --git a/app/Constants/Common/RefundCode.php b/app/Constants/Common/RefundCode.php index f6d262d..ba0b680 100644 --- a/app/Constants/Common/RefundCode.php +++ b/app/Constants/Common/RefundCode.php @@ -30,4 +30,11 @@ class RefundCode */ const string ORDER_TYPE_GOOD_PREFIX = 'RG'; const string ORDER_TYPE_BALANCE_PREFIX = 'RB'; + + /** + * @var int 退款类型 1=全部退款 2=部分退款 11=余额退款 + */ + CONST INT FULL_GOOD_REFUND = 1; + CONST INT PARTIAL_GOOD_REFUND = 2; + CONST INT BALANCE_REFUND = 11; } \ No newline at end of file diff --git a/app/Controller/Admin/OrderController.php b/app/Controller/Admin/OrderController.php new file mode 100644 index 0000000..591ae80 --- /dev/null +++ b/app/Controller/Admin/OrderController.php @@ -0,0 +1,72 @@ +handle(); + } + + #[RequestMapping(path: "info", methods: "GET")] + #[Scene(scene: "info")] + public function info() + { + //todo + return (new OrderInfoService())->handle(); + } + + /** + * @return array + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + #[RequestMapping(path: "refund_all", methods: "POST")] + #[Scene(scene: "refund_all")] + public function refundAll() + { + return (new RefundService)->handle(); + } + + /** + * @return array + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + #[RequestMapping(path: "refund_partial", methods: "GET")] + #[Scene(scene: "refund_partial")] + public function refundPartial() + { + return (new RefundService)->refundPartial(); + } + + #[RequestMapping(path: "refund_batch", methods: "GET")] + #[Scene(scene: "refund_batch")] + public function batchRefund() + { + // todo + return (new RefundService)->refundBatch(); + } +} diff --git a/app/Model/PayOrder.php b/app/Model/PayOrder.php index a8a08d5..65d03ba 100644 --- a/app/Model/PayOrder.php +++ b/app/Model/PayOrder.php @@ -48,9 +48,9 @@ class PayOrder extends Model /** * @param int $id * @param int $type - * @return \Hyperf\Database\Model\Model|null + * @return \Hyperf\Database\Model\Model|PayOrder|null */ - public function getInfoByOrderIdAndType(int $id,int $type): \Hyperf\Database\Model\Model|null + public function getInfoByOrderIdAndType(int $id,int $type): \Hyperf\Database\Model\Model|null|PayOrder { return $this->where('order_id',$id)->where('order_type',$type)->where('status',PayCode::FINISH_PAY)->first(); } diff --git a/app/Model/RefundOrder.php b/app/Model/RefundOrder.php index bd16c00..15d4e2c 100644 --- a/app/Model/RefundOrder.php +++ b/app/Model/RefundOrder.php @@ -62,7 +62,7 @@ class RefundOrder extends Model ->where('order_id', $orderId) ->where('order_type', $type) ->whereIn('refund_status',[ - RefundCode::WAIT_REFUND, + RefundCode::WAIT_BY_PAY_TOOL, RefundCode::REFUND_SUCCESS ])->sum('refund_money') ?? 0; } diff --git a/app/Service/Admin/Order/OrderInfoService.php b/app/Service/Admin/Order/OrderInfoService.php new file mode 100644 index 0000000..14e8e33 --- /dev/null +++ b/app/Service/Admin/Order/OrderInfoService.php @@ -0,0 +1,22 @@ +return->success(); + } +} \ No newline at end of file diff --git a/app/Service/Admin/Order/OrderListService.php b/app/Service/Admin/Order/OrderListService.php new file mode 100644 index 0000000..d87e34c --- /dev/null +++ b/app/Service/Admin/Order/OrderListService.php @@ -0,0 +1,22 @@ +return->success(); + } +} \ No newline at end of file diff --git a/app/Service/Admin/Order/RefundService.php b/app/Service/Admin/Order/RefundService.php new file mode 100644 index 0000000..b7754a3 --- /dev/null +++ b/app/Service/Admin/Order/RefundService.php @@ -0,0 +1,83 @@ +request->input('order_id'); + + $service = new FullRefundOrderService(); + $service->orderId = $orderId; + $service->reason = $this->request->input('reason',''); + $service->type = RefundCode::FULL_GOOD_REFUND; + + $service->handle(); + + return $this->return->success(); + }catch (Exception $e) { + throw new ErrException($e->getMessage()); + } + } + + /** + * @return array + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function refundPartial(): array + { + try { + $orderId = (int)$this->request->input('order_id'); + $orderGoodIds = $this->request->input('order_good_ids'); + if (empty($orderGoodIds)) throw new ErrException('请选择商品再退款'); + + $service = new PartialRefundOrderService(); + $service->orderId = $orderId; + $service->reason = $this->request->input('reason',''); + $service->orderGoodIds = $orderGoodIds; + $service->type = RefundCode::PARTIAL_GOOD_REFUND; + + $service->handle(); + + return $this->return->success(); + }catch (Exception $e) { + throw new ErrException($e->getMessage()); + } + } + + /** + * @return array + */ + public function refundBatch(): array + { + $siteId = $this->request->input('site_id'); + $cycleId = (int)$this->request->input('cycle_id'); + + return $this->return->success(); + } +} \ No newline at end of file diff --git a/app/Service/Amqp/Refund/BaseRefundOrderService.php b/app/Service/Amqp/Refund/BaseRefundOrderService.php new file mode 100644 index 0000000..16a82c7 --- /dev/null +++ b/app/Service/Amqp/Refund/BaseRefundOrderService.php @@ -0,0 +1,301 @@ +orderInfo = match ($this->orderType) { + OrderCode::ORDER_TYPE_GOOD => $this->orderModel->getInfoById($this->orderId), +// OrderCode::ORDER_TYPE_BALANCE => $this->orderModel->getInfoById($this->orderId), + default => null, + }; + } + + /** + * @var array + */ + protected array $orderGoodList; + + /** + * @var string + */ + protected string $totalGoodsPrice = '0'; + + + /** + * @return void + * @throws Exception + */ + protected function checkGoodOrder(): void + { + if (empty($this->orderInfo)) throw new Exception('订单信息不存在:'.json_encode(['order_id' => $this->orderId,'order_type' => $this->type])); + + if (!in_array($this->orderInfo->status,OrderCode::CAN_REFUND_STATUS)) throw new Exception('订单状态不能退款:'.json_encode($this->orderInfo->toArray())); + + //余额订单必须退款全部金额 +// if ($this->type == OrderCode::ORDER_TYPE_BALANCE && $this->refundAmount != $this->orderInfo->actual_price) throw new Exception('余额订单必须退款全部金额:'.json_encode($this->orderInfo->toArray())); + + if ($this->type != RefundCode::PARTIAL_GOOD_REFUND) return; + + if (empty($this->orderGoodIds)) throw new Exception('商品信息不存在:'.json_encode(['order_id' => $this->orderId,'order_type' => $this->type,'goods_list' => $this->orderGoodIds])); + + $orderGoodList = $this->orderGoodModel->whereIn('id',$this->orderGoodIds)->where('order_id',$this->orderId)->get(); + if ($orderGoodList->isEmpty()) throw new Exception('该订单没有这些商品信息:'.json_encode(['order_id' => $this->orderId,'order_type' => $this->type,'goods_list' => $this->orderGoodIds])); + $this->orderGoodList = $orderGoodList->toArray(); + unset($orderGoodList); + foreach ($this->orderGoodList as $orderGood) { + if (!in_array($orderGood['status'],OrderCode::CAN_REFUND_STATUS)) throw new Exception('商品状态不能退款:'.json_encode(['order_info' => $this->orderInfo->toArray(),'order_good' => $orderGood,'goods_list' => $this->orderGoodIds])); + + $oneGoodTotalPrice = bcmul((string)$orderGood['unit_price'],(string)$orderGood['quantity'],2); + $this->totalGoodsPrice = bcadd($this->totalGoodsPrice, $oneGoodTotalPrice,2) ; + } + } + + /** + * @return void + * @throws Exception + */ + protected function getPayOrder(): void + { + $this->payInfo = $this->payOrderModel->getInfoByOrderIdAndType($this->orderId, $this->orderType); + + if (empty($this->payInfo)) throw new Exception('支付信息不存在'); + } + + /** + * @return void + * @throws Exception + */ + protected function getRefundAmount(): void + { + $this->refundAmount = match ($this->type) { + RefundCode::BALANCE_REFUND => $this->getBalanceRefundAmount(), + RefundCode::FULL_GOOD_REFUND => $this->getFullGoodRefundAmount(), + RefundCode::PARTIAL_GOOD_REFUND => $this->getPartialGoodAmount(), + default => throw new Exception('退款类型错误'), + }; + + if ($this->refundAmount <= 0) throw new Exception('退款金额不能为0'); + } + + /** + * @var RefundOrder + */ + #[Inject] + protected RefundOrder $refundOrderModel; + + private function getBalanceRefundAmount(): float + { + $isRefundMoney = $this->refundOrderModel->getAllMoneyByOrderId($this->refundInfo->order_id,$this->refundInfo->order_type); + + return $this->payInfo->pay_money - $isRefundMoney; + } + + /** + * @return float + */ + private function getFullGoodRefundAmount(): float + { + $isRefundMoney = $this->refundOrderModel->getAllMoneyByOrderId($this->refundInfo->order_id,$this->refundInfo->order_type); + + // 部分退款会验证 pay_money != order_money throw error 所以不需要判断 + return $this->payInfo->pay_money - $isRefundMoney; + } + + + protected UserCoupon $userCouponModel; + + /** + * @return float + * @throws Exception + */ + private function getPartialGoodAmount(): float + { + if ($this->totalGoodsPrice <= 0) throw new Exception('商品退款价格不能为0'); + // 部分退款 不支持 价格不一致 + if ($this->payInfo->pay_money != $this->orderInfo->actual_price) throw new Exception('价格不一致的订单无法部分退款'); + + $isRefundMoney = $this->refundOrderModel->getSuccessMoneyByOrderId($this->refundInfo->order_id,$this->refundInfo->order_type); + + return floatval(min($this->payInfo->pay_money - $isRefundMoney,$this->totalGoodsPrice)); + } + + /** + * @var RefundOrder + */ + protected RefundOrder $refundInfo; + + /** + * @return void + * @throws Exception + */ + protected function insertRefundOrder(): void + { + $this->refundInfo = new RefundOrder(); + + $this->refundInfo->user_id = $this->orderInfo->user_id; + $this->refundInfo->order_type = $this->type; + $this->refundInfo->order_id = $this->orderId; + $this->refundInfo->pay_id = $this->payInfo->id; + $this->refundInfo->refund_status = RefundCode::WAIT_REFUND; + $this->refundInfo->refund_money = $this->refundAmount; + $this->refundInfo->refund_type = $this->payInfo->recharge_type; + $this->refundInfo->refund_order_sno = $this->generateRefundOrderNo($this->type, $this->orderInfo->user_id); + $this->refundInfo->reason = $this->reason; + + if (!$this->refundInfo->save()) throw new Exception('退款订单创建失败'); + } + + + /** + * @return void + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + protected function refund(): void + { + $rechargeService = match ($this->refundInfo->refund_type) + { + PayCode::ALIPAY => $this->setAliPayRefund(), + PayCode::WECHAT_PAY => $this->setWechatPayRefund(), + }; + + $rechargeService->setConfig(); + $rechargeService->setRefundNotify(); + + $rechargeService->refund( + $this->refundAmount, + (float)$this->payInfo->pay_money, + $this->orderInfo->order_sno, + $this->refundInfo->refund_order_sno, + ); + } + + /** + * @return void + * @throws Exception + */ + protected function updateRefund(): void + { + $status = RefundCode::WAIT_BY_PAY_TOOL; + $errMsg = '等待支付工具退款'; + + $this->refundInfo->refund_status = $status; + $this->refundInfo->remark = $errMsg; + + if (!$this->refundInfo->save()) throw new Exception('退款订单更新失败'); + } + + /** + * @param string $errMsg + * @return void + * @throws Exception + */ + protected function updateError(string $errMsg = ''): void + { + if (empty($this->refundInfo)) return; + + $status = RefundCode::REFUND_FAIL; + $errMsg = empty($errMsg) ? '未知错误' : $errMsg; + + $this->refundInfo->refund_status = $status; + $this->refundInfo->remark = $errMsg; + + if (!$this->refundInfo->save()) throw new Exception('退款订单更新失败'); + } + + /** + * @return null + */ + protected function setAliPayRefund(): null + { + return null; + } + + /** + * @return WxJsRechargeOrderService + */ + protected function setWechatPayRefund(): WxJsRechargeOrderService + { + return new WxJsRechargeOrderService(); + } +} \ No newline at end of file diff --git a/app/Service/Amqp/Refund/FullRefundOrderService.php b/app/Service/Amqp/Refund/FullRefundOrderService.php new file mode 100644 index 0000000..1732558 --- /dev/null +++ b/app/Service/Amqp/Refund/FullRefundOrderService.php @@ -0,0 +1,54 @@ +getOrderInfo(); + + $this->checkGoodOrder(); + + $this->getPayOrder(); + + $this->getRefundAmount(); + + $this->insertRefundOrder(); + + $this->refund(); + + $this->updateRefund(); + } catch (Exception $e) { + $errArr = explode(":", $e->getMessage()); + $errMsg = $errArr[0]; + $this->updateError($errMsg); + throw new Exception($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/Service/Amqp/Refund/PartialRefundOrderService.php b/app/Service/Amqp/Refund/PartialRefundOrderService.php new file mode 100644 index 0000000..d464386 --- /dev/null +++ b/app/Service/Amqp/Refund/PartialRefundOrderService.php @@ -0,0 +1,62 @@ +getOrderInfo(); + + $this->checkGoodOrder(); + + $this->getPayOrder(); + + $this->getRefundAmount(); + + $this->insertRefundOrder(); + + $this->refund(); + + $this->updateRefund(); + } catch (Exception $e) { + $errArr = explode(":", $e->getMessage()); + $errMsg = $errArr[0]; + $this->updateError($errMsg); + throw new Exception($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/Service/Api/Order/RefundOrderService.php b/app/Service/Api/Order/RefundOrderService.php index 33f493e..5c03e89 100644 --- a/app/Service/Api/Order/RefundOrderService.php +++ b/app/Service/Api/Order/RefundOrderService.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace App\Service\Api\Order; use App\Constants\Common\OrderCode; +use App\Constants\Common\RefundCode; use App\Exception\ErrException; use App\Model\Order; use App\Model\PayOrder; @@ -55,7 +56,7 @@ class RefundOrderService extends BaseService if (empty($payInfo)) throw new ErrException('订单支付信息不存在'); //立即取消 - $this->joinRefundQueue($orderId, $type, (float)$payInfo->pay_money, '用户主动取消订单'); + $this->joinRefundQueue($orderId, RefundCode::FULL_GOOD_REFUND, '用户主动取消订单'); return $this->return->success(); } diff --git a/app/Service/Common/Pay/Wx/WxJsRechargeOrderService.php b/app/Service/Common/Pay/Wx/WxJsRechargeOrderService.php index 81ea3b1..3ceab8e 100644 --- a/app/Service/Common/Pay/Wx/WxJsRechargeOrderService.php +++ b/app/Service/Common/Pay/Wx/WxJsRechargeOrderService.php @@ -4,6 +4,7 @@ namespace App\Service\Common\Pay\Wx; use App\Constants\Common\OrderCode; use App\Constants\Common\PayCode; +use App\Constants\Common\RefundCode; use App\Service\ServiceTrait\Api\CateringTrait; use App\Service\ServiceTrait\Api\CheckOrderCallBackTrait; use App\Service\ServiceTrait\Api\CouponTrait; @@ -90,7 +91,7 @@ class WxJsRechargeOrderService extends WxJsRechargeBaseService }); //已经截单 自动退款 - if (!$this->isCatering) $this->joinRefundQueue($this->orderId,OrderCode::ORDER_TYPE_GOOD,$this->orderInfo->actual_price,'已截单,系统自动退款'); + if (!$this->isCatering) $this->joinRefundQueue($this->orderId,RefundCode::FULL_GOOD_REFUND,'已截单,系统自动退款'); //todo 发送订阅通知 // $this->sendGainCoinMsg(); diff --git a/app/Service/ServiceTrait/Common/OrderChangeStatusTrait.php b/app/Service/ServiceTrait/Common/OrderChangeStatusTrait.php index 6688f44..42d3e62 100644 --- a/app/Service/ServiceTrait/Common/OrderChangeStatusTrait.php +++ b/app/Service/ServiceTrait/Common/OrderChangeStatusTrait.php @@ -44,19 +44,17 @@ trait OrderChangeStatusTrait /** * @param int $orderId - * @param int $type - * @param float|int $amount amount = 0 全部 amount > 0 部分 + * @param int $type refundCode * @param string $reason * @return void * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ - protected function joinRefundQueue(int $orderId, int $type, float|int $amount = 0, string $reason = '系统自动'): void + protected function joinRefundQueue(int $orderId, int $type, string $reason = '系统自动'): void { $message = new RefundOrderProducer([ 'order_id' => $orderId, 'type' => $type, - 'amount' => $amount, 'reason' => $reason ]); $producer = ApplicationContext::getContainer()->get(Producer::class);