Explorar el Código

feat(lakala): 新增分账订单管理功能

- 创建分账订单数据表 profit_share_order 及其字段定义
- 新增分账订单前端页面 ProfitShareOrder.vue 并集成到菜单导航- 实现分账订单列表展示、搜索及分页功能
- 支持查看分账订单详情信息
- 更新分账接收方状态枚举值及字段标签优化- 添加分账订单相关后端控制器与实体类
- 注册分账订单路由并应用中间件权限控制- 扩展编号生成枚举以支持分账订单号生成
runphp hace 4 meses
padre
commit
f441d2f61e

+ 41 - 0
database/migrations/20251113020336_profit_share_order.php

@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+use Phinx\Migration\AbstractMigration;
+
+final class ProfitShareOrder 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_order', ['comment' => '分润订单表'])
+            ->addColumn('out_separate_no', 'string', ['length' => 32,'comment' => '商户分账指令流水号'])
+            ->addColumn('merchant_no', 'string', ['length' => 32,'comment' => '商户号'])
+            ->addColumn('user_id', 'integer', ['signed' => false,'comment' => '用户id'])
+            ->addColumn('total_amt', 'integer', ['signed' => false,'comment' => '订单金额(分)'])
+            ->addColumn('lkl_org_no', 'string', ['length' => 16,'comment' => '拉卡拉机构编号'])
+            ->addColumn('cal_type', 'integer', ['signed' => false,'comment' => '分账计算类型 0- 按照指定金额,1- 按照指定比例。默认 0'])
+            ->addColumn('recv_merchant_no', 'string', ['length' => 32,'comment' => '接收方商户号'])
+            ->addColumn('recv_no', 'string', ['length' => 32,'comment' => '分账接收方编号'])
+            ->addColumn('separate_value', 'decimal', ['precision' => 10, 'scale' => 2,'comment' => '分账数值 calType为0时,按照固定金额分账,单位:分 calType为1时,按照比例分账,单位:百分比的小数值,比如0.55 (55%)'])
+            ->addColumn('separate_no', 'string', ['length' => 32,'comment' => '分账指令流水号'])
+            ->addColumn('status', 'enum', ['values' => ['PENDING','PROCESSING', 'ACCEPTED', 'SUCCESS', 'FAIL'], 'comment' => '分账状态 待处理: PENDING 处理中:PROCESSING, 已受理:ACCEPTED, 成功:SUCCESS, 失败:FAIL'])
+            ->addColumn('fee_amt', 'integer', ['signed' => false,'comment' => '分账手续费(分)'])
+            ->addColumn('fail_reason', 'string', ['length' => 256,'comment' => '分账失败原因'])
+            ->addColumn('cmd_type', 'enum', ['values' => ['SEPARATE', 'CANCEL', 'FALLBACK'], 'comment' => '指令类型'])
+            ->addTimestamps('create_time', 'update_time')
+            ->addIndex(['out_separate_no'], ['unique' => true])
+            ->create();
+    }
+}

+ 0 - 14
resource/admin/ProfitShareApply.vue

@@ -1,14 +0,0 @@
-<template>
-  <div>
-    分账申请记录
-  </div>
-</template>
-
-<script>
-export default {
-  name: "ProfitShareApply"
-}
-</script>
-
-<style scoped>
-</style>

+ 302 - 0
resource/admin/ProfitShareOrder.vue

@@ -0,0 +1,302 @@
+<template>
+  <div class="profit-share-apply">
+    <!-- 搜索条件 -->
+    <el-card class="search-card">
+      <el-form :model="searchForm" label-width="140px" inline>
+        <el-form-item label="商户分账指令流水号">
+          <el-input 
+            v-model="searchForm.out_separate_no" 
+            placeholder="请输入商户分账指令流水号" 
+            clearable
+            @keyup.enter="handleSearch"
+          />
+        </el-form-item>
+        <el-form-item label="分账状态">
+          <el-select v-model="searchForm.status" placeholder="请选择分账状态" clearable>
+            <el-option label="全部" value="" />
+            <el-option label="待处理" value="PENDING" />
+            <el-option label="处理中" value="PROCESSING" />
+            <el-option label="已受理" value="ACCEPTED" />
+            <el-option label="成功" value="SUCCESS" />
+            <el-option label="失败" value="FAIL" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleSearch">查询</el-button>
+          <el-button @click="handleReset">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- 列表数据 -->
+    <el-card class="table-card">
+      <template #header>
+        <div class="table-header">
+          <div class="header-title">分账申请记录列表</div>
+        </div>
+      </template>
+      
+      <el-table
+        v-loading="loading"
+        :data="tableData"
+        border
+        stripe
+      >
+        <el-table-column prop="out_separate_no" label="商户分账指令流水号" min-width="200" />
+        <el-table-column prop="user_id" label="用户ID" width="100" />
+        <el-table-column prop="total_amt" label="订单金额(分)" width="120">
+          <template #default="{ row }">
+            {{ formatAmount(row.total_amt) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="recv_no" label="分账接收方编号" width="150" />
+        <el-table-column prop="status" label="分账状态" width="100">
+          <template #default="{ row }">
+            {{ getStatusText(row.status) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="fail_reason" label="分账失败原因" min-width="150" />
+        <el-table-column prop="create_time" label="创建时间" width="180" />
+        <el-table-column prop="update_time" label="更新时间" width="180" />
+        <el-table-column label="操作" width="150" fixed="right">
+          <template #default="{ row }">
+            <el-button type="primary" size="small" @click="handleView(row)">详情</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      
+      <!-- 分页 -->
+      <el-pagination
+        v-model:current-page="pagination.page"
+        v-model:page-size="pagination.limit"
+        :total="pagination.total"
+        :page-sizes="[10, 20, 50, 100]"
+        layout="total, sizes, prev, pager, next, jumper"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+        class="pagination"
+      />
+    </el-card>
+
+    <!-- 详情对话框 -->
+    <el-dialog
+      v-model="detailDialogVisible"
+      title="分账申请详情"
+      width="600px"
+      :before-close="handleCloseDetailDialog"
+    >
+      <el-form
+        v-loading="detailLoading"
+        :model="detailData"
+        label-width="160px"
+        label-position="left"
+      >
+        <el-form-item label="商户分账指令流水号:">
+          <span>{{ detailData.out_separate_no }}</span>
+        </el-form-item>
+        <el-form-item label="用户ID:">
+          <span>{{ detailData.user_id }}</span>
+        </el-form-item>
+        <el-form-item label="订单金额(分):">
+          <span>{{ formatAmount(detailData.total_amt) }}</span>
+        </el-form-item>
+        <el-form-item label="分账接收方编号:">
+          <span>{{ detailData.recv_no }}</span>
+        </el-form-item>
+        <el-form-item label="分账状态:">
+          <span>{{ getStatusText(detailData.status) }}</span>
+        </el-form-item>
+        <el-form-item v-if="detailData.fail_reason" label="分账失败原因:">
+          <span>{{ detailData.fail_reason }}</span>
+        </el-form-item>
+        <el-form-item label="创建时间:">
+          <span>{{ detailData.create_time }}</span>
+        </el-form-item>
+        <el-form-item label="更新时间:">
+          <span>{{ detailData.update_time }}</span>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="handleCloseDetailDialog">关闭</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "ProfitShareOrder",
+  props: {
+    axiosInstance: {
+      type: Object,
+      default: null
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      detailLoading: false,
+      searchForm: {
+        out_separate_no: '',
+        status: ''
+      },
+      tableData: [],
+      pagination: {
+        page: 1,
+        limit: 10,
+        total: 0
+      },
+      detailDialogVisible: false,
+      detailData: {}
+    }
+  },
+  async created() {
+    this.fetchData()
+  },
+  methods: {
+    // 格式化金额显示
+    formatAmount(amount) {
+      return amount || amount === 0 ? amount : '-';
+    },
+
+    // 获取分账状态文本
+    getStatusText(status) {
+      const statusMap = {
+        'PENDING': '待处理',
+        'PROCESSING': '处理中',
+        'ACCEPTED': '已受理',
+        'SUCCESS': '成功',
+        'FAIL': '失败'
+      }
+      return statusMap[status] || status
+    },
+
+    // 获取列表数据
+    async fetchData() {
+      if (!this.axiosInstance) {
+        this.$message.error('无法获取请求实例')
+        return
+      }
+      
+      this.loading = true
+      try {
+        const params = {
+          page: this.pagination.page,
+          limit: this.pagination.limit,
+          out_separate_no: this.searchForm.out_separate_no,
+          status: this.searchForm.status
+        }
+        
+        const res = await this.axiosInstance.get('/lakala/profit_share_order', { params })
+        if (res.code === 200) {
+          this.tableData = res.page.data
+          this.pagination.total = res.page.total
+          this.pagination.limit = res.page.per_page
+        } else {
+          this.$message.error(res.msg || res.message || '获取数据失败')
+        }
+      } catch (error) {
+        console.error('获取分账申请记录列表失败:', error)
+        this.$message.error('获取数据失败: ' + (error.message || '未知错误'))
+      } finally {
+        this.loading = false
+      }
+    },
+
+    // 获取详情数据
+    async fetchDetail(id) {
+      if (!this.axiosInstance) {
+        this.$message.error('无法获取请求实例')
+        return
+      }
+
+      this.detailLoading = true
+      try {
+        const res = await this.axiosInstance.get(`/lakala/profit_share_order/${id}`)
+        if (res.code === 200) {
+          this.detailData = res.data
+        } else {
+          this.$message.error(res.msg || res.message || '获取详情失败')
+        }
+      } catch (error) {
+        console.error('获取分账申请详情失败:', error)
+        this.$message.error('获取详情失败: ' + (error.message || '未知错误'))
+      } finally {
+        this.detailLoading = false
+      }
+    },
+    
+    // 查询
+    handleSearch() {
+      this.pagination.page = 1
+      this.fetchData()
+    },
+    
+    // 重置
+    handleReset() {
+      this.searchForm = {
+        out_separate_no: '',
+        status: ''
+      }
+      this.pagination.page = 1
+      this.fetchData()
+    },
+    
+    // 查看详情
+    async handleView(row) {
+      await this.fetchDetail(row.id)
+      this.detailDialogVisible = true
+    },
+
+    // 关闭详情对话框
+    handleCloseDetailDialog() {
+      this.detailDialogVisible = false
+      this.detailData = {}
+    },
+    
+    // 分页相关
+    handleSizeChange(val) {
+      this.pagination.limit = val
+      this.pagination.page = 1
+      this.fetchData()
+    },
+    
+    handleCurrentChange(val) {
+      this.pagination.page = val
+      this.fetchData()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.profit-share-apply {
+  padding: 20px;
+}
+
+.search-card {
+  margin-bottom: 20px;
+}
+
+.table-card {
+  margin-bottom: 20px;
+}
+
+.table-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.header-title {
+  font-size: 16px;
+  font-weight: bold;
+}
+
+.pagination {
+  margin-top: 20px;
+  text-align: right;
+}
+</style>

+ 130 - 12
resource/admin/ProfitShareReceiver.vue

@@ -15,8 +15,12 @@
           <el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
             <el-option label="全部" value="" />
             <el-option label="待审核" value="1" />
-            <el-option label="已通过" value="2" />
-            <el-option label="已拒绝" value="3" />
+            <el-option label="提交中" value="2" />
+            <el-option label="验证通过" value="3" />
+            <el-option label="验证失败" value="4" />
+            <el-option label="绑定中" value="5" />
+            <el-option label="绑定成功" value="6" />
+            <el-option label="绑定失败" value="7" />
           </el-select>
         </el-form-item>
         <el-form-item>
@@ -41,18 +45,19 @@
         stripe
       >
         <el-table-column prop="id" label="ID" width="80" />
-        <el-table-column prop="order_no" label="订单号" min-width="180" />
+        <el-table-column prop="order_no" label="订单号" min-width="180" />
         <el-table-column prop="receiver_name" label="接收方名称" min-width="120" />
-        <el-table-column prop="acct_name" label="账户名称" min-width="120" />
-        <el-table-column prop="acct_no" label="账户号" min-width="180" />
-        <el-table-column prop="acct_type_code" label="账户类型" width="100">
+        <el-table-column prop="acct_name" label="收款账户名称" min-width="120" />
+        <el-table-column prop="acct_no" label="收款账户号" min-width="180" />
+        <el-table-column prop="acct_type_code" label="收款账户账户类型" width="120">
           <template #default="{ row }">
             {{ getAccountTypeText(row.acct_type_code) }}
           </template>
         </el-table-column>
-        <el-table-column prop="contact_mobile" label="联系电话" width="120" />
+        <el-table-column prop="contact_mobile" label="联系手机号" width="120" />
         <el-table-column prop="status_text" label="状态" width="100" />
         <el-table-column prop="create_time" label="申请时间" width="180" />
+        <el-table-column prop="fail_reason" label="失败原因" min-width="150" />
         <el-table-column label="操作" width="150" fixed="right">
           <template #default="{ row }">
             <el-button type="primary" size="small" @click="handleView(row)">详情</el-button>
@@ -72,6 +77,66 @@
         class="pagination"
       />
     </el-card>
+
+    <!-- 详情对话框 -->
+    <el-dialog
+      v-model="detailDialogVisible"
+      title="分账接收方详情"
+      width="600px"
+      :before-close="handleCloseDetailDialog"
+    >
+      <el-form
+        v-loading="detailLoading"
+        :model="detailData"
+        label-width="140px"
+        label-position="left"
+      >
+        <el-form-item label="订单编号:">
+          <span>{{ detailData.order_no }}</span>
+        </el-form-item>
+        <el-form-item label="分账接收方名称:">
+          <span>{{ detailData.receiver_name }}</span>
+        </el-form-item>
+        <el-form-item label="联系手机号:">
+          <span>{{ detailData.contact_mobile }}</span>
+        </el-form-item>
+        <el-form-item label="收款账户名称:">
+          <span>{{ detailData.acct_name }}</span>
+        </el-form-item>
+        <el-form-item label="收款账户卡号:">
+          <span>{{ detailData.acct_no }}</span>
+        </el-form-item>
+        <el-form-item label="收款账户账户类型:">
+          <span>{{ getAccountTypeText(detailData.acct_type_code) }}</span>
+        </el-form-item>
+        <el-form-item label="收款账户证件类型:">
+          <span>{{ getCertificateTypeText(detailData.acct_certificate_type) }}</span>
+        </el-form-item>
+        <el-form-item label="收款账户证件号:">
+          <span>{{ detailData.acct_certificate_no }}</span>
+        </el-form-item>
+        <el-form-item label="收款账户开户行名称:">
+          <span>{{ detailData.acct_open_bank_name }}</span>
+        </el-form-item>
+        <el-form-item label="提款类型:">
+          <span>{{ getSettleTypeText(detailData.settle_type) }}</span>
+        </el-form-item>
+        <el-form-item label="状态:">
+          <span>{{ detailData.status_text }}</span>
+        </el-form-item>
+        <el-form-item v-if="detailData.fail_reason" label="失败原因:">
+          <span>{{ detailData.fail_reason }}</span>
+        </el-form-item>
+        <el-form-item label="申请时间:">
+          <span>{{ detailData.create_time }}</span>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="handleCloseDetailDialog">关闭</el-button>
+        </span>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
@@ -87,6 +152,7 @@ export default {
   data() {
     return {
       loading: false,
+      detailLoading: false,
       searchForm: {
         order_no: '',
         status: ''
@@ -96,7 +162,9 @@ export default {
         page: 1,
         limit: 10,
         total: 0
-      }
+      },
+      detailDialogVisible: false,
+      detailData: {}
     }
   },
   async created() {
@@ -106,11 +174,31 @@ export default {
     // 获取账户类型文本
     getAccountTypeText(code) {
       const accountTypes = {
-        '58': '对私',
-        '59': '对公'
+        '57': '对公',
+        '58': '对私'
       }
       return accountTypes[code] || code
     },
+
+    // 获取证件类型文本
+    getCertificateTypeText(code) {
+      const certificateTypes = {
+        '17': '身份证',
+        '18': '护照',
+        '19': '港澳居民来往内地通行证',
+        '20': '台湾居民来往内地通行证'
+      }
+      return certificateTypes[code] || code
+    },
+
+    // 获取提款类型文本
+    getSettleTypeText(code) {
+      const settleTypes = {
+        '01': '主动提款',
+        '03': '交易自动结算'
+      }
+      return settleTypes[code] || code
+    },
     
     // 获取列表数据
     async fetchData() {
@@ -145,6 +233,29 @@ export default {
         this.loading = false
       }
     },
+
+    // 获取详情数据
+    async fetchDetail(id) {
+      if (!this.axiosInstance) {
+        this.$message.error('无法获取请求实例')
+        return
+      }
+
+      this.detailLoading = true
+      try {
+        const res = await this.axiosInstance.get(`/lakala/profit_share_receiver/${id}`)
+        if (res.code === 200) {
+          this.detailData = res.data
+        } else {
+          this.$message.error(res.msg || res.message || '获取详情失败')
+        }
+      } catch (error) {
+        console.error('获取分账接收方详情失败:', error)
+        this.$message.error('获取详情失败: ' + (error.message || '未知错误'))
+      } finally {
+        this.detailLoading = false
+      }
+    },
     
     // 查询
     handleSearch() {
@@ -163,8 +274,15 @@ export default {
     },
     
     // 查看详情
-    handleView(row) {
-      this.$message.info('查看详情功能待实现')
+    async handleView(row) {
+      await this.fetchDetail(row.id)
+      this.detailDialogVisible = true
+    },
+
+    // 关闭详情对话框
+    handleCloseDetailDialog() {
+      this.detailDialogVisible = false
+      this.detailData = {}
     },
     
     // 分页相关

+ 5 - 5
resource/admin/index.vue

@@ -11,7 +11,7 @@
           <el-menu-item index="profitShareReceiver">
             <span>分账接收方申请</span>
           </el-menu-item>
-          <el-menu-item index="profitShareApply">
+          <el-menu-item index="profitShareOrder">
             <span>分帐申请</span>
           </el-menu-item>
         </el-menu>
@@ -29,13 +29,13 @@
 
 <script>
 import ProfitShareReceiver from './ProfitShareReceiver.vue'
-import ProfitShareApply from './ProfitShareApply.vue'
+import ProfitShareOrder from './ProfitShareOrder.vue'
 
 export default {
   name: 'LakalaAdmin',
   components: {
     ProfitShareReceiver,
-    ProfitShareApply
+    ProfitShareOrder
   },
   props: {
     axiosInstance: {
@@ -52,8 +52,8 @@ export default {
     currentComponent() {
       if (this.activeMenu === 'profitShareReceiver') {
         return 'ProfitShareReceiver'
-      } else if (this.activeMenu === 'profitShareApply') {
-        return 'ProfitShareApply'
+      } else if (this.activeMenu === 'profitShareOrder') {
+        return 'ProfitShareOrder'
       }
       return 'ProfitShareReceiver'
     }

+ 7 - 1
route/admin.php

@@ -2,7 +2,10 @@
 declare(strict_types=1);
 
 use think\facade\Route;
-use SixShop\Lakala\Controller\Admin\ProfitShareReceiverController;
+use SixShop\Lakala\Controller\Admin\{
+    ProfitShareReceiverController,
+    ProfitShareOrderController
+};
 use SixShop\System\Middleware\MacroPageMiddleware;
 // Admin路由
 // 路由前缀: /admin/lakala
@@ -11,4 +14,7 @@ use SixShop\System\Middleware\MacroPageMiddleware;
 // ->middleware(['auth'])
 
 Route::resource('profit_share_receiver', ProfitShareReceiverController::class)
+    ->middleware(['auth', MacroPageMiddleware::class]);
+
+Route::resource('profit_share_order', ProfitShareOrderController::class)
     ->middleware(['auth', MacroPageMiddleware::class]);

+ 20 - 0
src/Controller/Admin/ProfitShareOrderController.php

@@ -0,0 +1,20 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\Lakala\Controller\Admin;
+
+use SixShop\Core\Request;
+use SixShop\Lakala\Entity\ProfitShareOrderEntity;
+use think\Response;
+use function SixShop\Core\page_response;
+
+class ProfitShareOrderController
+{
+    public function index(Request $request, ProfitShareOrderEntity $entity): Response
+    {
+        $params = $request->get([
+            'status/d' => 0,
+            'out_separate_no' => '',
+        ]);
+        return page_response(page: $entity->getOrderList($params, $request->pageAndLimit()));
+    }
+}

+ 20 - 0
src/Entity/ProfitShareOrderEntity.php

@@ -0,0 +1,20 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\Lakala\Entity;
+
+use SixShop\Core\Entity\BaseEntity;
+use SixShop\Lakala\Model\ProfitShareOrderModel;
+use think\Paginator;
+
+/**
+ * @mixin ProfitShareOrderModel
+ */
+class ProfitShareOrderEntity extends BaseEntity
+{
+    public function getOrderList(array $params, array $pageAndLimit):Paginator
+    {
+        return $this->withSearch(['out_separate_no', 'status'],$params)
+            ->append(['status_text'])
+            ->paginate($pageAndLimit);
+    }
+}

+ 22 - 0
src/Enum/ProfitShareOrderStatusEnum.php

@@ -0,0 +1,22 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\Lakala\Enum;
+
+enum ProfitShareOrderStatusEnum:string
+{
+    case PENDING = 'PENDING';
+    case PROCESSING = 'PROCESSING';
+    case ACCEPTED = 'ACCEPTED';
+    case SUCCESS = 'SUCCESS';
+    case FAIL = 'FAIL';
+    public function toString(): string
+    {
+        return match ($this) {
+            self::PENDING => '待处理',
+            self::PROCESSING => '处理中',
+            self::ACCEPTED => '已受理',
+            self::SUCCESS => '成功',
+            self::FAIL => '失败',
+        };
+    }
+}

+ 31 - 0
src/Model/ProfitShareOrderModel.php

@@ -0,0 +1,31 @@
+<?php
+declare(strict_types=1);
+namespace SixShop\Lakala\Model;
+
+use SixShop\Lakala\Enum\ProfitShareOrderStatusEnum;
+use SixShop\Lakala\Enum\ReceiverStatusEnum;
+use SixShop\Payment\Enum\NumberBizEnum;
+use think\Model;
+
+class ProfitShareOrderModel extends Model
+{
+    protected function getOptions(): array
+    {
+        return [
+            'name' => 'profit_share_order',
+            'type' => [
+                'status' => ProfitShareOrderStatusEnum::class
+            ],
+            'insert' => ['out_separate_no'],
+            'readonly' => ['user_id', 'out_separate_no'],
+        ];
+    }
+    public function getStatusTextAttr($value, $data)
+    {
+        return $data['status']->toString();
+    }
+    protected function setOutSeparateNoAttr($value, $data): string
+    {
+        return generate_number(NumberBizEnum::PROFIT_SHARE_ORDER, 5);
+    }
+}