```
feat(split-order): 完善分账订单管理功能 - 添加订单关联关系,支持查询原始订单信息 - 实现分账订单列表的搜索过滤功能,支持买家、订单号、状态等多维度筛选 - 优化前端页面展示,添加分页、样式美化和交互提示 - 增加重试分账和查询状态的操作确认提示 - 格式化显示买家信息、金额和时间等数据字段 - 支持汇付和微信两种分账通道的状态显示 ```
这个提交包含在:
@@ -15,4 +15,9 @@ class SplitOrder extends Model
|
||||
{
|
||||
return $this->hasMany(SplitOrderReceiver::class, 'split_order_id', 'id');
|
||||
}
|
||||
|
||||
public function order()
|
||||
{
|
||||
return $this->belongsTo(\app\admin\model\shopro\order\Order::class, 'order_id', 'id');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,21 @@ use app\admin\controller\shopro\Base;
|
||||
use addons\shopro\library\split\Config as SplitConfig;
|
||||
use addons\shopro\library\split\SplitExecutor;
|
||||
use addons\shopro\library\split\ProfitSharingService;
|
||||
use app\admin\model\shopro\order\Order as ShopOrder;
|
||||
use app\admin\model\shopro\user\User as ShopUser;
|
||||
|
||||
class Order extends Base
|
||||
{
|
||||
protected $model = null;
|
||||
|
||||
protected $statusMap = [
|
||||
0 => '待分账',
|
||||
1 => '分账中',
|
||||
2 => '成功',
|
||||
3 => '部分成功',
|
||||
4 => '失败',
|
||||
];
|
||||
|
||||
public function _initialize()
|
||||
{
|
||||
parent::_initialize();
|
||||
@@ -21,14 +31,143 @@ class Order extends Base
|
||||
{
|
||||
$this->request->filter(['strip_tags']);
|
||||
if ($this->request->isAjax()) {
|
||||
list($where, $sort, $order, $offset, $limit) = $this->custombuildparams();
|
||||
$total = $this->model->where($where)->count();
|
||||
$list = $this->model->with('receivers')->where($where)->order($sort, $order)->limit($offset, $limit)->select();
|
||||
$nobuildfields = ['buyer_keyword', 'order_sn', 'status', 'channel', 'createtime'];
|
||||
list($where, $sort, $order, $offset, $limit) = $this->custombuildparams(null, $nobuildfields);
|
||||
|
||||
$query = $this->buildSearch();
|
||||
$total = $query->where($where)->count();
|
||||
$list = $this->buildSearch()
|
||||
->with([
|
||||
'receivers',
|
||||
'order' => function ($q) {
|
||||
$q->field('id,order_sn,user_id,pay_fee,paytime,status');
|
||||
},
|
||||
'order.user' => function ($q) {
|
||||
$q->field('id,nickname,mobile');
|
||||
},
|
||||
])
|
||||
->where($where)
|
||||
->order($sort, $order)
|
||||
->limit($offset, $limit)
|
||||
->select();
|
||||
|
||||
$list = $this->formatSplitOrderList($list);
|
||||
|
||||
return $this->success('操作成功', null, ['total' => $total, 'rows' => $list]);
|
||||
}
|
||||
$this->assignconfig('splitHuifuOnly', (new SplitConfig())->isHuifuMode());
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
protected function buildSearch()
|
||||
{
|
||||
$filter = (array) json_decode($this->request->get('filter', ''), true);
|
||||
$query = $this->model;
|
||||
|
||||
if (!empty($filter['order_sn'])) {
|
||||
$orderIds = ShopOrder::where('order_sn', 'like', '%' . trim($filter['order_sn']) . '%')->column('id');
|
||||
$query = $query->where('order_id', 'in', $orderIds ?: [0]);
|
||||
}
|
||||
if (!empty($filter['buyer_keyword'])) {
|
||||
$kw = trim($filter['buyer_keyword']);
|
||||
$userIds = ShopUser::where('nickname|mobile', 'like', '%' . $kw . '%')->column('id');
|
||||
$orderIds = $userIds
|
||||
? ShopOrder::where('user_id', 'in', $userIds)->column('id')
|
||||
: [];
|
||||
$query = $query->where('order_id', 'in', $orderIds ?: [0]);
|
||||
}
|
||||
if (isset($filter['status']) && $filter['status'] !== '') {
|
||||
$query = $query->where('status', intval($filter['status']));
|
||||
}
|
||||
if (!empty($filter['channel'])) {
|
||||
$query = $query->where('channel', $filter['channel']);
|
||||
}
|
||||
if (!empty($filter['createtime'])) {
|
||||
$parts = explode(' - ', $filter['createtime']);
|
||||
if (count($parts) === 2) {
|
||||
$start = strtotime($parts[0]);
|
||||
$end = strtotime($parts[1] . ' 23:59:59');
|
||||
if ($start && $end) {
|
||||
$query = $query->where('createtime', 'between', [$start, $end]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function formatSplitOrderList($list)
|
||||
{
|
||||
$rows = [];
|
||||
foreach ($list as $item) {
|
||||
$data = is_array($item) ? $item : $item->toArray();
|
||||
|
||||
$order = $data['order'] ?? null;
|
||||
$user = is_array($order) ? ($order['user'] ?? null) : null;
|
||||
|
||||
$status = intval($data['status'] ?? 0);
|
||||
$channel = $data['channel'] ?? 'wechat';
|
||||
$buyerNickname = $user['nickname'] ?? '';
|
||||
$buyerMobile = $user['mobile'] ?? '';
|
||||
|
||||
$receivers = $data['receivers'] ?? [];
|
||||
$lines = [];
|
||||
foreach ($receivers as $recv) {
|
||||
$r = is_array($recv) ? $recv : (method_exists($recv, 'toArray') ? $recv->toArray() : []);
|
||||
$lines[] = sprintf(
|
||||
'%s ¥%s',
|
||||
$this->roleTypeLabel($r['role_type'] ?? ''),
|
||||
number_format(floatval($r['amount'] ?? 0), 2, '.', '')
|
||||
);
|
||||
}
|
||||
|
||||
$data['buyer_nickname'] = $buyerNickname;
|
||||
$data['buyer_mobile'] = $buyerMobile;
|
||||
$data['buyer_display'] = $this->formatBuyerDisplay(
|
||||
$buyerNickname,
|
||||
$buyerMobile,
|
||||
is_array($order) ? intval($order['user_id'] ?? 0) : 0
|
||||
);
|
||||
$data['status_text'] = $this->statusMap[$status] ?? '-';
|
||||
$data['channel_text'] = $channel === 'huifu' ? '汇付' : '微信';
|
||||
$data['createtime_text'] = !empty($data['createtime'])
|
||||
? date('Y-m-d H:i:s', $data['createtime'])
|
||||
: '-';
|
||||
$data['order_paytime_text'] = (is_array($order) && !empty($order['paytime']))
|
||||
? date('Y-m-d H:i:s', $order['paytime'])
|
||||
: '-';
|
||||
$data['receiver_summary'] = $lines ? implode(';', $lines) : '—';
|
||||
|
||||
unset($data['order']);
|
||||
$rows[] = $data;
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
protected function formatBuyerDisplay($nickname, $mobile, $userId)
|
||||
{
|
||||
$nickname = trim((string) $nickname);
|
||||
$mobile = trim((string) $mobile);
|
||||
if ($nickname !== '') {
|
||||
return $mobile !== '' ? ($nickname . '(' . $mobile . ')') : $nickname;
|
||||
}
|
||||
if ($mobile !== '') {
|
||||
return $mobile;
|
||||
}
|
||||
return $userId ? ('用户#' . $userId) : '—';
|
||||
}
|
||||
|
||||
protected function roleTypeLabel($type)
|
||||
{
|
||||
$map = [
|
||||
'platform' => '平台',
|
||||
'promoter' => '推广员',
|
||||
'regional_agent' => '区域公司',
|
||||
];
|
||||
return $map[$type] ?? ($type ?: '接收方');
|
||||
}
|
||||
|
||||
public function retry($ids = null)
|
||||
{
|
||||
$id = $ids ?: $this->request->post('id');
|
||||
|
||||
@@ -1,19 +1,154 @@
|
||||
<link rel="stylesheet" href="__CDN__/assets/addons/shopro/libs/element/element.css">
|
||||
<div id="splitOrderIndex" v-cloak style="padding:20px;">
|
||||
<el-table :data="listData" border v-loading="loading">
|
||||
<el-table-column prop="out_order_no" label="分账单号" min-width="160"></el-table-column>
|
||||
<el-table-column prop="order_sn" label="订单号" min-width="160"></el-table-column>
|
||||
<el-table-column prop="total_amount" label="订单金额" width="100"></el-table-column>
|
||||
<el-table-column prop="split_amount" label="分账金额" width="100"></el-table-column>
|
||||
<el-table-column label="状态" width="100"><template slot-scope="s">{{ statusText(s.row.status) }}</template></el-table-column>
|
||||
<el-table-column prop="retry_count" label="重试" width="70"></el-table-column>
|
||||
<el-table-column label="操作" width="160">
|
||||
<template slot-scope="s">
|
||||
<el-button type="text" v-if="s.row.status==4" @click="retry(s.row.id)">重试</el-button>
|
||||
<el-button type="text" @click="query(s.row.id)">查询</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<link rel="stylesheet" href="__CDN__/assets/addons/shopro/libs/common.css">
|
||||
<style>
|
||||
#splitOrderIndex {
|
||||
padding: 0 20px 30px;
|
||||
background: #f5f7fa;
|
||||
min-height: 100%;
|
||||
}
|
||||
.page-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.page-intro {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
line-height: 1.7;
|
||||
margin: 0;
|
||||
}
|
||||
.page-intro strong { color: #303133; font-weight: 600; }
|
||||
.toolbar-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
.source-main {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.source-sub {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.6;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.link-text {
|
||||
color: #7438D5;
|
||||
cursor: pointer;
|
||||
}
|
||||
.link-text:hover { opacity: 0.85; }
|
||||
.money-text { color: #e1212b; font-weight: 600; }
|
||||
.status-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.status-tag.s0 { background: #f4f4f5; color: #909399; }
|
||||
.status-tag.s1 { background: #ecf5ff; color: #409eff; }
|
||||
.status-tag.s2 { background: #f0f9eb; color: #67c23a; }
|
||||
.status-tag.s3 { background: #fdf6ec; color: #e6a23c; }
|
||||
.status-tag.s4 { background: #fef0f0; color: #f56c6c; }
|
||||
.op-hint {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-left: 4px;
|
||||
}
|
||||
</style>
|
||||
<div id="splitOrderIndex" v-cloak>
|
||||
<div class="page-card">
|
||||
<p class="page-intro">
|
||||
<strong>分账记录</strong>展示向支付通道(微信/汇付)发起的<strong>网关分账</strong>结果,与「分销佣金」「区域差价」钱包入账是不同链路。
|
||||
仅<strong>已支付且触发分账</strong>的订单会出现记录;列表为空时请先确认分账已开启并完成一笔测试支付。
|
||||
</p>
|
||||
</div>
|
||||
<div class="page-card">
|
||||
<div class="toolbar-row">
|
||||
<el-input v-model="filters.buyer_keyword" placeholder="买家昵称/手机号" size="small" style="width:160px;" clearable></el-input>
|
||||
<el-input v-model="filters.order_sn" placeholder="订单号" size="small" style="width:170px;" clearable></el-input>
|
||||
<el-select v-model="filters.status" placeholder="分账状态" size="small" style="width:120px;" clearable>
|
||||
<el-option label="待分账" value="0"></el-option>
|
||||
<el-option label="分账中" value="1"></el-option>
|
||||
<el-option label="成功" value="2"></el-option>
|
||||
<el-option label="部分成功" value="3"></el-option>
|
||||
<el-option label="失败" value="4"></el-option>
|
||||
</el-select>
|
||||
<el-select v-model="filters.channel" placeholder="通道" size="small" style="width:100px;" clearable>
|
||||
<el-option label="汇付" value="huifu"></el-option>
|
||||
<el-option label="微信" value="wechat"></el-option>
|
||||
</el-select>
|
||||
<el-date-picker v-model="dateRange" type="daterange" size="small" range-separator="至"
|
||||
start-placeholder="记录开始" end-placeholder="记录结束" value-format="yyyy-MM-dd"></el-date-picker>
|
||||
<el-button type="primary" size="small" @click="search">搜索</el-button>
|
||||
<el-button size="small" @click="resetFilters">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-card" style="padding-bottom: 8px;">
|
||||
<el-table :data="listData" border stripe v-loading="loading"
|
||||
empty-text="暂无分账记录。请确认已开启分账,并完成支付/确认收货后再查看。">
|
||||
<el-table-column label="记录时间" prop="createtime_text" width="158"></el-table-column>
|
||||
<el-table-column label="买家" min-width="140">
|
||||
<template slot-scope="s">
|
||||
<div class="source-main">{{ s.row.buyer_display || '—' }}</div>
|
||||
<div class="source-sub" v-if="s.row.buyer_nickname && s.row.buyer_mobile">手机 {{ s.row.buyer_mobile }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="订单 / 分账" min-width="220">
|
||||
<template slot-scope="s">
|
||||
<div class="source-main">
|
||||
<span class="link-text" @click="openOrder(s.row.order_id)">{{ s.row.order_sn || '—' }}</span>
|
||||
<span class="source-sub" style="display:inline;margin-left:6px;">{{ s.row.channel_text }}</span>
|
||||
</div>
|
||||
<div class="source-sub">分账单号 {{ s.row.out_order_no || '—' }}</div>
|
||||
<div class="source-sub" v-if="s.row.order_paytime_text !== '-'">支付 {{ s.row.order_paytime_text }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="118" align="right">
|
||||
<template slot-scope="s">
|
||||
<div>订单 <span class="money-text">¥{{ formatMoney(s.row.total_amount) }}</span></div>
|
||||
<div class="source-sub">分账 ¥{{ formatMoney(s.row.split_amount) }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="接收方明细" min-width="160" show-overflow-tooltip>
|
||||
<template slot-scope="s">
|
||||
<span class="source-sub">{{ s.row.receiver_summary || '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="96" align="center">
|
||||
<template slot-scope="s">
|
||||
<span :class="'status-tag s' + s.row.status">{{ s.row.status_text }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="重试" width="64" align="center" prop="retry_count"></el-table-column>
|
||||
<el-table-column label="操作" width="168" fixed="right">
|
||||
<template slot-scope="s">
|
||||
<el-button type="text" v-if="s.row.status == 4" @click="retry(s.row)">重试分账</el-button>
|
||||
<el-button type="text" @click="query(s.row.id)">同步通道状态</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div style="margin-top: 14px; text-align: right;">
|
||||
<el-pagination
|
||||
background
|
||||
layout="total, prev, pager, next, sizes"
|
||||
:current-page.sync="currentPage"
|
||||
:page-size.sync="limit"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:total="total"
|
||||
@current-change="getList"
|
||||
@size-change="onSizeChange">
|
||||
</el-pagination>
|
||||
</div>
|
||||
<p class="op-hint" style="margin-top:10px;">
|
||||
「重试分账」仅对<strong>失败</strong>记录有效,会向通道重新发起分账;「同步通道状态」只拉取最新结果并更新本页,不会重复扣款。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="__CDN__/assets/addons/shopro/libs/vue.js"></script>
|
||||
<script src="__CDN__/assets/addons/shopro/libs/element/element.js"></script>
|
||||
|
||||
@@ -3,20 +3,108 @@ define(['jquery', 'bootstrap', 'backend', 'form'], function ($, undefined, Backe
|
||||
index: function () {
|
||||
new Vue({
|
||||
el: '#splitOrderIndex',
|
||||
data: { listData: [], loading: false },
|
||||
mounted() { this.getList(); },
|
||||
data() {
|
||||
return {
|
||||
listData: [],
|
||||
loading: false,
|
||||
currentPage: 1,
|
||||
limit: 20,
|
||||
total: 0,
|
||||
dateRange: [],
|
||||
filters: {
|
||||
buyer_keyword: '',
|
||||
order_sn: '',
|
||||
status: '',
|
||||
channel: ''
|
||||
},
|
||||
splitHuifuOnly: !!(typeof Config !== 'undefined' && Config.splitHuifuOnly)
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.getList();
|
||||
},
|
||||
methods: {
|
||||
buildFilter() {
|
||||
var filter = Object.assign({}, this.filters);
|
||||
if (this.dateRange && this.dateRange.length === 2) {
|
||||
filter.createtime = this.dateRange[0] + ' - ' + this.dateRange[1];
|
||||
}
|
||||
return filter;
|
||||
},
|
||||
search() {
|
||||
this.currentPage = 1;
|
||||
this.getList();
|
||||
},
|
||||
resetFilters() {
|
||||
this.filters = { buyer_keyword: '', order_sn: '', status: '', channel: '' };
|
||||
this.dateRange = [];
|
||||
this.currentPage = 1;
|
||||
this.getList();
|
||||
},
|
||||
onSizeChange() {
|
||||
this.currentPage = 1;
|
||||
this.getList();
|
||||
},
|
||||
getList() {
|
||||
this.loading = true;
|
||||
Fast.api.ajax({ url: 'shopro/split/order/index', type: 'GET', data: { limit: 50, sort: 'id', order: 'desc' } }, (ret, res) => {
|
||||
this.listData = (res.data && res.data.rows) || [];
|
||||
Fast.api.ajax({
|
||||
url: 'shopro/split/order/index',
|
||||
type: 'GET',
|
||||
data: {
|
||||
offset: (this.currentPage - 1) * this.limit,
|
||||
limit: this.limit,
|
||||
sort: 'id',
|
||||
order: 'desc',
|
||||
filter: JSON.stringify(this.buildFilter()),
|
||||
op: JSON.stringify({})
|
||||
}
|
||||
}, (ret, res) => {
|
||||
const data = res.data || {};
|
||||
this.listData = data.rows || [];
|
||||
this.total = data.total || 0;
|
||||
this.loading = false;
|
||||
return false;
|
||||
}, () => { this.loading = false; });
|
||||
},
|
||||
retry(id) { Fast.api.ajax({ url: 'shopro/split/order/retry/ids/' + id, type: 'POST' }, () => this.getList()); },
|
||||
query(id) { Fast.api.ajax({ url: 'shopro/split/order/query/ids/' + id, type: 'POST' }, () => this.getList()); },
|
||||
statusText(s) { return { 0: '待分账', 1: '分账中', 2: '成功', 3: '部分成功', 4: '失败' }[s] || '-'; }
|
||||
formatMoney(v) {
|
||||
var n = parseFloat(v);
|
||||
return isNaN(n) ? '0.00' : n.toFixed(2);
|
||||
},
|
||||
openOrder(orderId) {
|
||||
if (!orderId) return;
|
||||
Fast.api.open('shopro/order/order/detail?id=' + orderId, '订单详情');
|
||||
},
|
||||
retry(row) {
|
||||
var tip = this.splitHuifuOnly
|
||||
? '将向汇付重新发起该笔分账。若订单资金仍被冻结,请先发货/确认收货或按文档处理 FREEZE。'
|
||||
: '将向微信/汇付重新发起该笔分账,请确认该订单尚未分账成功。';
|
||||
Layer.confirm(
|
||||
'订单 ' + (row.order_sn || '') + '(' + (row.buyer_display || '买家') + ')<br>' + tip,
|
||||
{ title: '确认重试分账', btn: ['重试', '取消'] },
|
||||
() => {
|
||||
Fast.api.ajax({ url: 'shopro/split/order/retry/ids/' + row.id, type: 'POST' }, () => {
|
||||
this.getList();
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
query(id) {
|
||||
Fast.api.ajax({ url: 'shopro/split/order/query/ids/' + id, type: 'POST' }, (ret, res) => {
|
||||
var hint = '已同步通道状态并更新记录';
|
||||
if (res.data) {
|
||||
try {
|
||||
var d = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
|
||||
var parts = [d.resp_desc, d.resp_code, d.bank_message, d.status].filter(Boolean);
|
||||
if (parts.length) {
|
||||
hint += ':' + parts.join(' ');
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
Layer.msg(hint, { time: 4000 });
|
||||
this.getList();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户