Quellcode durchsuchen

feat(lakala): 实现分账订单创建与申请功能

- 新增分账订单数据库迁移文件,增加 mer_cup_no 字段
- 实现分账订单创建逻辑,包括金额校验、余额检查和事务处理
- 添加分账订单计算方法,支持手续费扣除和金额转换
- 更新分账接收方绑定逻辑,记录商户 cupNo 信息
- 引入分账命令类型枚举,支持分账、撤销和回退操作
- 修改查询商户分账信息接口,支持指定商户号查询
- 增加前端分账申请接口调用,完善页面交互逻辑
- 添加分账申请表单验证和错误提示功能
- 实现分账申请成功后的弹窗关闭与状态反馈
- 补充相关模型字段映射和类型定义配置
runphp vor 4 Monaten
Ursprung
Commit
4cdca617b4

+ 26 - 0
database/migrations/20251128113903_profit_share_receiver_mer_cup_no.php

@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+use Phinx\Migration\AbstractMigration;
+
+final class ProfitShareReceiverMerCupNo extends AbstractMigration
+{
+    /**
+     * Change Method.
+     *
+     * Write your reversible migrations using this method.
+     *
+     * More information on writing migrations is available here:
+     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
+     *
+     * Remember to call "create()" or "update()" and NOT "save()" when working
+     * with the Table class.
+     */
+    public function change(): void
+    {
+        $this->table('profit_share_receiver')
+            ->addColumn('mer_cup_no', 'string', ['limit' => 32, 'null' => true, 'comment' => '商户cupNo', 'after' => 'receiver_no'])
+            ->update();
+    }
+}

+ 38 - 1
src/Controller/Api/ProfitShareOrderController.php

@@ -3,14 +3,51 @@ declare(strict_types=1);
 
 namespace SixShop\Lakala\Controller\Api;
 
+use app\model\User;
+use SixShop\Balpay\Entity\ExtensionBalpayLogEntity;
+use SixShop\Balpay\Enum\BalpayLogTypeEnum;
 use SixShop\Core\Request;
+use SixShop\Lakala\Config;
 use SixShop\Lakala\Entity\ProfitShareOrderEntity;
 use think\Response;
+use function SixShop\Core\success_response;
+use function SixShop\Core\throw_logic_exception;
 
 class ProfitShareOrderController
 {
-    public function save(Request $request, ProfitShareOrderEntity $profitShareOrderEntity): Response
+    public function save(
+        Request $request,
+        ProfitShareOrderEntity $profitShareOrderEntity,
+        ExtensionBalpayLogEntity $extensionBalpayLogEntity,
+        Config $config,
+    ): Response
     {
+        $userID = (int)$request->userID;
+        $amount = $request->post('amount/f', 0);
+        if ($amount < $config->profit_share_min) {
+            throw_logic_exception('分账金额不能低于' . $config->profit_share_min);
+        }
+        if ($amount > $config->profit_share_max) {
+            throw_logic_exception('分账金额不能高于' . $config->profit_share_max);
+        }
+        $balance = round((float)User::where('id', $userID)->value('balance'), 2);
+        if ($balance < $amount) {
+            throw_logic_exception('余额不足');
+        }
+        $receiverID = $request->post('receiver_id/d', 0);
+        $result = $profitShareOrderEntity->transaction(function () use (
+            $profitShareOrderEntity,
+            $extensionBalpayLogEntity,
+            $userID,
+            $receiverID,
+            $amount
+        ){
+            $profitShareOrder = $profitShareOrderEntity->createOrder($userID, $receiverID, $amount);
+            $extensionBalpayLogEntity->change($userID, $amount, BalpayLogTypeEnum::PROFIT_SHARE, '分账', $profitShareOrder->id);
 
+            return $profitShareOrder;
+        });
+
+        return success_response($result);
     }
 }

+ 68 - 2
src/Entity/ProfitShareOrderEntity.php

@@ -1,20 +1,86 @@
 <?php
 declare(strict_types=1);
+
 namespace SixShop\Lakala\Entity;
 
 use SixShop\Core\Entity\BaseEntity;
+use SixShop\Lakala\Enum\ProfitShareOrderCMDTypeEnum;
+use SixShop\Lakala\Enum\ProfitShareOrderStatusEnum;
+use SixShop\Lakala\Enum\ReceiverStatusEnum;
+use SixShop\Lakala\Facade\Config;
+use SixShop\Lakala\Facade\LedgerService;
 use SixShop\Lakala\Model\ProfitShareOrderModel;
+use SixShop\Lakala\Model\ProfitShareReceiverModel;
 use think\Paginator;
+use function SixShop\Core\throw_logic_exception;
 
 /**
  * @mixin ProfitShareOrderModel
  */
 class ProfitShareOrderEntity extends BaseEntity
 {
-    public function getOrderList(array $params, array $pageAndLimit):Paginator
+    public function getOrderList(array $params, array $pageAndLimit): Paginator
     {
-        return $this->withSearch(['out_separate_no', 'status'],$params)
+        return $this->withSearch(['out_separate_no', 'status'], $params)
             ->append(['status_text'])
             ->paginate($pageAndLimit);
     }
+
+    /**
+     * @param int $userID
+     * @param int $receiverID
+     * @param float $amount
+     * @return self
+     */
+    public function createOrder(int $userID, int $receiverID, float $amount): self
+    {
+        $receiver = ProfitShareReceiverModel::where([
+            'id' => $receiverID,
+            'user_id' => $userID,
+            'status' => ReceiverStatusEnum::BOUND,
+        ])->findOrEmpty();
+        if ($receiver->isEmpty()) {
+            throw_logic_exception('分账接收方不存在或未绑定');
+        }
+        $merInfo = LedgerService::queryLedgerMer($receiver->mer_cup_no, $receiver->org_id);
+        $calculateData = $this->calculateOrder($amount, $merInfo->splitLowestRatio);
+        $this->save([
+            'merchant_no' => $receiver->mer_cup_no,
+            'user_id' => $userID,
+            'total_amt' => $calculateData['total_amt'],
+            'lkl_org_no' => $receiver->org_id,
+            'cal_type' => 0, // 按照指定金额
+            'recv_merchant_no' => $receiver->mer_cup_no,
+            'recv_no' => $receiver->receiver_no,
+            'separate_value' => $calculateData['separate_value'],
+            'status' => ProfitShareOrderStatusEnum::PENDING,
+            'fee_amt' => $calculateData['fee_amt'],
+            'cmd_type' => ProfitShareOrderCMDTypeEnum::SEPARATE,
+        ]);
+        return $this;
+    }
+
+    /**
+     * 计算分账数据
+     *
+     * @param float $amount
+     * @return array{total_amt:int, fee_amt:int, separate_value:int}
+     */
+    private function calculateOrder(float $amount, float $splitLowestRatio): array
+    {
+        $fee = Config::getConfig('profit_share_fee');
+        $feeAmt = bcmul((string)$amount, (string)$fee, 2);
+        if (bccomp($feeAmt, '1') == -1) {
+            $feeAmt = 1;
+        }
+        $seprateValue = bcmul((string)$amount, '100', 2);
+        $seprateValue = bcsub($seprateValue, $feeAmt);
+        $totalAmout = bcmul($seprateValue, '100', 2);
+        $totalAmout = bcdiv($totalAmout, (string)$splitLowestRatio);
+        return [
+            'total_amt' => $totalAmout,
+            'fee_amt' => (int)$feeAmt,
+            'separate_value' => (int)$seprateValue,
+        ];
+    }
 }

+ 2 - 1
src/Entity/ProfitShareReceiverEntity.php

@@ -165,13 +165,14 @@ class ProfitShareReceiverEntity extends BaseEntity
             $entity->entrust_file_path = $response->attFileId;
             $entity->save();
         }
-        LedgerService::applyBind([
+        $respData = LedgerService::applyBind([
             'orderNo' => $entity->order_no,
             'orgCode' => $entity->org_code,
             'receiverNo' => $entity->receiver_no,
             'entrustFileName' => $entity->entrust_file_name,
             'entrustFilePath' => $entity->entrust_file_path,
         ]);
+        $entity->mer_cup_no = $respData['merCupNo'];
         $entity->status = ReceiverStatusEnum::BINDING;
         $entity->save();
         return $entity;

+ 19 - 0
src/Enum/ProfitShareOrderCMDTypeEnum.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace SixShop\Lakala\Enum;
+
+enum ProfitShareOrderCMDTypeEnum:string
+{
+    case SEPARATE = 'SEPARATE';
+    case CANCEL = 'CANCEL';
+    case FALLBACK = 'FALLBACK';
+
+    public function toString()
+    {
+        return match ($this) {
+            self::SEPARATE => '分账',
+            self::CANCEL => '分账撤销',
+            self::FALLBACK => '分账回退',
+        };
+    }
+}

+ 3 - 1
src/Model/ProfitShareOrderModel.php

@@ -2,6 +2,7 @@
 declare(strict_types=1);
 namespace SixShop\Lakala\Model;
 
+use SixShop\Lakala\Enum\ProfitShareOrderCMDTypeEnum;
 use SixShop\Lakala\Enum\ProfitShareOrderStatusEnum;
 use SixShop\Lakala\Enum\ReceiverStatusEnum;
 use SixShop\Payment\Enum\NumberBizEnum;
@@ -14,7 +15,8 @@ class ProfitShareOrderModel extends Model
         return [
             'name' => 'profit_share_order',
             'type' => [
-                'status' => ProfitShareOrderStatusEnum::class
+                'status' => ProfitShareOrderStatusEnum::class,
+                'cmd_type' => ProfitShareOrderCMDTypeEnum::class,
             ],
             'insert' => ['out_separate_no'],
             'readonly' => ['user_id', 'out_separate_no'],

+ 2 - 2
src/Service/LedgerService.php

@@ -154,14 +154,14 @@ class LedgerService
      *
      * @link https://o.lakala.com/#/home/document/detail?id=381
      */
-    public function queryLedgerMer(string $orgCode = '1', string $version = '1.0'):object
+    public function queryLedgerMer(string $merchantNo, string $orgCode = '1', string $version = '1.0'):object
     {
         $request = new V2ModelRequest();
         $request->setReqData([
             'version' => $version,
             'orderNo' => generate_number(NumberBizEnum::ORDER_NO, 5),
             'orgCode' => $orgCode,
-            'merCupNo' => $this->config->merchant_no,
+            'merCupNo' => $merchantNo,
         ]);
         $response = $this->v2LakalaApi->tradeApi('/api/v2/mms/openApi/ledger/queryLedgerMer', $request);
         if ($response->getRetCode() == '000000') {

+ 1 - 1
tests/Service/LedgerServiceTest.php

@@ -79,7 +79,7 @@ class LedgerServiceTest extends TestCase
     #[Test]
     public function queryLedgerMer():void
     {
-        $response = $this->ledgerService->queryLedgerMer(orgCode: '903119',);
+        $response = $this->ledgerService->queryLedgerMer(merchantNo: '822451048160BXH', orgCode: '903119',);
         dump($response);
     }