feat : coupon

This commit is contained in:
2025-03-21 15:14:43 +08:00
parent a0dd51fc68
commit 11a36ece80
14 changed files with 644 additions and 16 deletions

View File

@@ -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));
}

View File

@@ -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;
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Controller\AbstractController;
use App\Middleware\Admin\JwtAuthMiddleware;
use App\Service\Admin\Order\OrderInfoService;
use App\Service\Admin\Order\OrderListService;
use App\Service\Admin\Order\RefundService;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\Middlewares;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\Validation\Annotation\Scene;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
#[Controller(prefix: "admin/order")]
#[Middlewares([
JwtAuthMiddleware::class,
])]
class OrderController extends AbstractController
{
#[RequestMapping(path: "list", methods: "GET")]
#[Scene(scene: "list")]
public function list()
{
//todo
return (new OrderListService())->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();
}
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -0,0 +1,22 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Admin\Order;
use App\Service\Admin\BaseService;
class OrderInfoService extends BaseService
{
public function handle()
{
//todo Write logic
return $this->return->success();
}
}

View File

@@ -0,0 +1,22 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Admin\Order;
use App\Service\Admin\BaseService;
class OrderListService extends BaseService
{
public function handle()
{
//todo Write logic
return $this->return->success();
}
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Admin\Order;
use App\Constants\Common\RefundCode;
use App\Exception\ErrException;
use App\Service\Admin\BaseService;
use App\Service\Amqp\Refund\FullRefundOrderService;
use App\Service\Amqp\Refund\PartialRefundOrderService;
use Exception;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class RefundService extends BaseService
{
/**
* @return array
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function handle(): array
{
try {
$orderId = (int)$this->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();
}
}

View File

@@ -0,0 +1,301 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Amqp\Refund;
use App\Constants\Common\OrderCode;
use App\Constants\Common\PayCode;
use App\Constants\Common\RefundCode;
use App\Model\Order;
use App\Model\OrderGood;
use App\Model\PayOrder;
use App\Model\RefundOrder;
use App\Model\UserCoupon;
use App\Service\Common\Pay\Wx\WxJsRechargeOrderService;
use App\Service\ServiceTrait\Api\OrderTrait;
use Exception;
use Hyperf\Di\Annotation\Inject;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
abstract class BaseRefundOrderService
{
use OrderTrait;
/**
* @var int
*/
public int $orderId;
/**
* @var string
*/
public string $reason;
/**
* @var int
*/
public int $type;
/**
* @var array
*/
public array $orderGoodIds = [];
/**
* @var Order
*/
#[Inject]
protected Order $orderModel;
/**
* @var PayOrder
*/
#[Inject]
protected PayOrder $payOrderModel;
/**
* @var Order
*/
protected Order $orderInfo;
/**
* @var PayOrder
*/
protected PayOrder $payInfo;
/**
* @var float
*/
public float $refundAmount;
abstract function handle();
/**
* @return void
*/
protected function getOrderInfo(): void
{
$this->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();
}
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Amqp\Refund;
use App\Constants\Common\OrderCode;
use Exception;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class FullRefundOrderService extends BaseRefundOrderService
{
/**
* @var int
*/
protected int $orderType = OrderCode::ORDER_TYPE_GOOD;
/**
* @return void
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function handle(): void
{
try {
$this->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());
}
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Amqp\Refund;
use App\Constants\Common\OrderCode;
use App\Model\OrderGood;
use Exception;
use Hyperf\Di\Annotation\Inject;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class PartialRefundOrderService extends BaseRefundOrderService
{
/**
* @var int
*/
protected int $orderType = OrderCode::ORDER_TYPE_GOOD;
/**
* @var OrderGood
*/
#[Inject]
protected OrderGood $orderGoodModel;
/**
* @return void
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function handle(): void
{
try {
$this->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());
}
}
}

View File

@@ -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();
}

View File

@@ -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();

View File

@@ -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);