feat(shopro_wechat_trade): 增加微信发货/确认收货同步页面功能

增加统计信息显示、筛选搜索功能和详细信息弹窗,优化用户体验

- 添加buildQuery方法用于构建查询条件,支持状态、同步类型和订单号筛选
- 添加getStats方法获取各状态统计数据并在页面显示统计栏
- 在index页面增加类型筛选、订单号搜索、重置和仅看失败按钮
- 实现detail方法用于获取同步记录详情
- 增加详细的CSS样式和前端交互逻辑
- 添加防止重复重试成功记录的验证
- 格式化JSON数据显示和错误信息展示
```
这个提交包含在:
可乐加冰
2026-10-08 15:44:59 +08:00
父节点 2e1e17ba02
当前提交 35762c67e6
共有 5 个文件被更改,包括 561 次插入 和 42 次删除

查看文件

@@ -8,25 +8,68 @@ use think\Db;
class Sync extends Backend
{
protected function buildQuery($status = '', $syncType = '', $orderSn = '')
{
$query = Db::name('shopro_wechat_trade_sync');
if ($status !== '') {
$query->where('status', $status);
}
if ($syncType !== '') {
$query->where('sync_type', $syncType);
}
if ($orderSn !== '') {
$query->where('order_sn', 'like', '%' . $orderSn . '%');
}
return $query;
}
protected function getStats()
{
$rows = Db::name('shopro_wechat_trade_sync')
->field('status, COUNT(*) AS cnt')
->group('status')
->select();
$stats = ['success' => 0, 'pending' => 0, 'failed' => 0, 'total' => 0];
foreach ($rows as $row) {
$key = $row['status'] ?? '';
if (isset($stats[$key])) {
$stats[$key] = intval($row['cnt']);
}
$stats['total'] += intval($row['cnt']);
}
return $stats;
}
public function index()
{
if ($this->request->isAjax()) {
$page = max(1, intval($this->request->get('page', 1)));
$limit = max(1, min(100, intval($this->request->get('limit', 20))));
$status = trim($this->request->get('status', ''));
$syncType = trim($this->request->get('sync_type', ''));
$orderSn = trim($this->request->get('order_sn', ''));
$query = Db::name('shopro_wechat_trade_sync')->order('id', 'desc');
if ($status !== '') {
$query->where('status', $status);
}
$total = $query->count();
$query = $this->buildQuery($status, $syncType, $orderSn)->order('id', 'desc');
$total = (clone $query)->count();
$list = $query->page($page, $limit)->select();
return json(['total' => $total, 'rows' => $list]);
}
$this->view->assign('stats', $this->getStats());
return $this->view->fetch();
}
public function detail()
{
$id = intval($this->request->get('id'));
$row = Db::name('shopro_wechat_trade_sync')->where('id', $id)->find();
if (!$row) {
$this->error('记录不存在');
}
$this->success('', null, $row);
}
public function retry()
{
$id = intval($this->request->post('id'));
@@ -34,6 +77,9 @@ class Sync extends Backend
if (!$row) {
$this->error('记录不存在');
}
if ($row['status'] === 'success') {
$this->error('该记录已同步成功,无需重试');
}
try {
$service = new WxShippingService();
if ($row['sync_type'] === 'confirm') {

查看文件

@@ -1,14 +1,122 @@
<div class="panel panel-default panel-intro">
<div class="panel-heading">微信发货/确认收货同步日志</div>
<style>
.wx-sync-page .page-tip {
margin-bottom: 14px;
padding: 12px 14px;
background: #f4f8ff;
border: 1px solid #d9e8ff;
border-radius: 4px;
color: #606266;
font-size: 13px;
line-height: 1.7;
}
.wx-sync-page .page-tip strong { color: #303133; }
.wx-sync-page .stats-bar {
margin-bottom: 12px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.wx-sync-page .stat-item {
display: inline-flex;
align-items: center;
padding: 6px 12px;
border-radius: 4px;
font-size: 13px;
background: #f5f7fa;
color: #606266;
}
.wx-sync-page .stat-item .num {
font-weight: 600;
margin-left: 4px;
}
.wx-sync-page .stat-success { background: #f0f9eb; color: #67c23a; }
.wx-sync-page .stat-pending { background: #fdf6ec; color: #e6a23c; }
.wx-sync-page .stat-failed { background: #fef0f0; color: #f56c6c; }
.wx-sync-page .toolbar-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.wx-sync-page .toolbar-row .form-control { width: auto; min-width: 140px; }
.wx-sync-page .toolbar-row .order-sn-input { min-width: 220px; }
.wx-sync-detail pre {
max-height: 280px;
overflow: auto;
background: #f5f7fa;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 10px;
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
}
.wx-sync-detail .detail-row {
margin-bottom: 10px;
font-size: 13px;
line-height: 1.6;
}
.wx-sync-detail .detail-label {
color: #909399;
margin-right: 6px;
}
</style>
<div class="panel panel-default panel-intro wx-sync-page">
<div class="panel-heading">
微信发货/确认收货同步日志
<span class="text-muted" style="font-size:12px;margin-left:10px;">记录向微信平台同步的操作结果,供排查与手动重试</span>
</div>
<div class="panel-body">
<div id="toolbar" class="toolbar">
<select id="status-filter" class="form-control" style="width:160px;display:inline-block;">
<div class="page-tip">
<strong>说明:</strong>本页展示商城向微信「发货信息管理服务」同步的记录,包括
<strong>发货同步</strong>(upload_shipping_info)与<strong>确认收货同步</strong>(notify_confirm_receive)。
「同步成功」仅表示微信接口返回成功,<strong>不代表</strong>商城订单状态;订单实际状态请到「订单管理」查看。
失败记录会按配置自动重试,也可在此手动重试。
</div>
<div class="stats-bar">
<span class="stat-item">全部<span class="num">{$stats.total|default=0}</span></span>
<span class="stat-item stat-success">成功<span class="num">{$stats.success|default=0}</span></span>
<span class="stat-item stat-pending">待重试/处理中<span class="num">{$stats.pending|default=0}</span></span>
<span class="stat-item stat-failed">失败<span class="num">{$stats.failed|default=0}</span></span>
</div>
<div id="toolbar" class="toolbar toolbar-row">
<select id="status-filter" class="form-control">
<option value="">全部状态</option>
<option value="pending">pending</option>
<option value="success">success</option>
<option value="failed">failed</option>
<option value="success">成功</option>
<option value="pending">待重试/处理中</option>
<option value="failed">失败</option>
</select>
<select id="type-filter" class="form-control">
<option value="">全部类型</option>
<option value="shipping">发货同步</option>
<option value="confirm">确认收货同步</option>
</select>
<input type="text" id="order-sn-filter" class="form-control order-sn-input" placeholder="搜索订单号">
<button type="button" class="btn btn-primary btn-search"><i class="fa fa-search"></i> 搜索</button>
<button type="button" class="btn btn-default btn-reset">重置</button>
<button type="button" class="btn btn-warning btn-filter-failed">仅看失败</button>
</div>
<table id="table" class="table table-striped table-bordered table-hover" width="100%"></table>
</div>
</div>
<div id="sync-detail-template" class="hide wx-sync-detail">
<div style="padding:16px;">
<div class="detail-row"><span class="detail-label">记录ID:</span><span class="detail-id"></span></div>
<div class="detail-row"><span class="detail-label">订单号:</span><span class="detail-order-sn"></span></div>
<div class="detail-row"><span class="detail-label">同步类型:</span><span class="detail-sync-type"></span></div>
<div class="detail-row"><span class="detail-label">同步状态:</span><span class="detail-status"></span></div>
<div class="detail-row detail-delivery-mode-row"><span class="detail-label">发货模式:</span><span class="detail-delivery-mode"></span></div>
<div class="detail-row"><span class="detail-label">重试次数:</span><span class="detail-retry"></span></div>
<div class="detail-row detail-error-row"><span class="detail-label">错误信息:</span><span class="detail-error"></span></div>
<div class="detail-row"><span class="detail-label">创建时间:</span><span class="detail-create"></span></div>
<div class="detail-row"><span class="detail-label">更新时间:</span><span class="detail-update"></span></div>
<div class="detail-row"><span class="detail-label">请求内容:</span></div>
<pre class="detail-request"></pre>
<div class="detail-row"><span class="detail-label">响应内容:</span></div>
<pre class="detail-response"></pre>
</div>
</div>

查看文件

@@ -1,4 +1,84 @@
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
var SYNC_TYPE_MAP = {
shipping: '发货同步',
confirm: '确认收货同步'
};
var DELIVERY_MODE_MAP = {
1: '统一发货',
2: '分拆发货'
};
function escapeHtml(text) {
return $('<div/>').text(text == null ? '' : text).html();
}
function formatSyncType(value) {
var text = SYNC_TYPE_MAP[value] || value || '-';
if (value === 'shipping') {
return '<span class="label label-info">' + text + '</span>';
}
if (value === 'confirm') {
return '<span class="label label-primary">' + text + '</span>';
}
return text;
}
function formatStatus(row) {
var status = row.status;
var retryCount = parseInt(row.retry_count, 10) || 0;
if (status === 'success') {
return '<span class="label label-success">成功</span>';
}
if (status === 'failed') {
return '<span class="label label-danger">失败</span>';
}
if (status === 'pending') {
if (retryCount > 0) {
return '<span class="label label-warning">待重试</span>';
}
return '<span class="label label-default">处理中</span>';
}
return status || '-';
}
function formatDeliveryMode(value, row) {
if (row.sync_type !== 'shipping') {
return '<span class="text-muted">-</span>';
}
var text = DELIVERY_MODE_MAP[value] || ('模式' + value);
return text;
}
function formatErrorInfo(value, row) {
if (row.status === 'success') {
return '<span class="text-muted">-</span>';
}
var parts = [];
if (row.errcode !== null && row.errcode !== undefined && row.errcode !== '') {
parts.push('[' + row.errcode + ']');
}
if (row.errmsg) {
parts.push(row.errmsg);
}
return parts.length ? escapeHtml(parts.join(' ')) : '<span class="text-muted">-</span>';
}
function formatJsonText(raw) {
if (!raw) {
return '(无)';
}
try {
return JSON.stringify(JSON.parse(raw), null, 2);
} catch (e) {
return raw;
}
}
function formatDatetime(value) {
return value ? Table.api.formatter.datetime(value) : '-';
}
var Controller = {
index: function () {
Table.api.init({
@@ -9,55 +89,186 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
});
var table = $("#table");
table.bootstrapTable({
url: $.fn.bootstrapTable.defaults.extend.index_url,
pk: 'id',
sortName: 'id',
sortOrder: 'desc',
search: false,
commonSearch: false,
showExport: false,
queryParams: function (params) {
params.status = $('#status-filter').val();
params.sync_type = $('#type-filter').val();
params.order_sn = $.trim($('#order-sn-filter').val());
return params;
},
columns: [
{ field: 'id', title: 'ID' },
{ field: 'order_sn', title: '订单号' },
{ field: 'sync_type', title: '类型' },
{ field: 'status', title: '状态' },
{ field: 'delivery_mode', title: '发货模式' },
{ field: 'errcode', title: 'errcode' },
{ field: 'errmsg', title: 'errmsg' },
{ field: 'retry_count', title: '重试次数' },
{ field: 'id', title: 'ID', width: 70 },
{
field: 'order_sn',
title: '订单号',
formatter: function (value, row) {
if (!value) {
return '-';
}
if (row.order_id) {
return '<a href="javascript:;" class="btn-link btn-order" data-id="' + row.order_id + '">' + escapeHtml(value) + '</a>';
}
return escapeHtml(value);
}
},
{
field: 'sync_type',
title: '同步类型',
formatter: function (value) {
return formatSyncType(value);
}
},
{
field: 'status',
title: '同步状态',
formatter: function (value, row) {
return formatStatus(row);
}
},
{
field: 'delivery_mode',
title: '发货模式',
formatter: function (value, row) {
return formatDeliveryMode(value, row);
}
},
{
field: 'errmsg',
title: '错误信息',
formatter: function (value, row) {
return formatErrorInfo(value, row);
}
},
{
field: 'retry_count',
title: '重试次数',
width: 90,
formatter: function (value, row) {
if (row.status === 'success') {
return '<span class="text-muted">-</span>';
}
return value || 0;
}
},
{
field: 'createtime',
title: '创建时间',
formatter: function (value) {
return value ? Table.api.formatter.datetime(value) : '';
}
formatter: formatDatetime
},
{
field: 'operate',
title: '操作',
width: 140,
table: table,
events: Table.api.events.operate,
formatter: function (value, row) {
if (row.status === 'success') {
return '-';
var html = [];
html.push('<a href="javascript:;" class="btn btn-xs btn-info btn-detail" data-id="' + row.id + '">详情</a>');
if (row.status !== 'success') {
html.push(' <a href="javascript:;" class="btn btn-xs btn-warning btn-retry" data-id="' + row.id + '">重试</a>');
}
return '<a href="javascript:;" class="btn btn-xs btn-warning btn-retry" data-id="' + row.id + '">重试</a>';
return html.join('');
}
}
]
});
$('#status-filter').on('change', function () {
function refreshTable() {
table.bootstrapTable('refresh');
}
$('#status-filter, #type-filter').on('change', refreshTable);
$('.btn-search').on('click', refreshTable);
$('#order-sn-filter').on('keypress', function (e) {
if (e.which === 13) {
refreshTable();
}
});
$('.btn-reset').on('click', function () {
$('#status-filter').val('');
$('#type-filter').val('');
$('#order-sn-filter').val('');
refreshTable();
});
$('.btn-filter-failed').on('click', function () {
$('#status-filter').val('failed');
$('#type-filter').val('');
refreshTable();
});
table.on('click', '.btn-order', function () {
var orderId = $(this).data('id');
Fast.api.open('shopro/order/order/detail?id=' + orderId, '订单详情');
});
table.on('click', '.btn-retry', function () {
var id = $(this).data('id');
$.post('shopro_wechat_trade/sync/retry', { id: id }, function (ret) {
Backend.api.toastr[ret.code === 1 ? 'success' : 'error'](ret.msg);
table.bootstrapTable('refresh');
Layer.confirm('将重新向微信发起同步请求,是否继续?', { icon: 3, title: '确认重试' }, function (index) {
Layer.close(index);
$.post('shopro_wechat_trade/sync/retry', { id: id }, function (ret) {
Backend.api.toastr[ret.code === 1 ? 'success' : 'error'](ret.msg);
refreshTable();
});
});
});
table.on('click', '.btn-detail', function () {
var id = $(this).data('id');
$.get('shopro_wechat_trade/sync/detail', { id: id }, function (ret) {
if (ret.code !== 1) {
Backend.api.toastr.error(ret.msg || '加载失败');
return;
}
var row = ret.data || {};
var $tpl = $('#sync-detail-template').clone().removeAttr('id').removeClass('hide');
$tpl.find('.detail-id').text(row.id || '-');
$tpl.find('.detail-order-sn').text(row.order_sn || '-');
$tpl.find('.detail-sync-type').html(formatSyncType(row.sync_type));
$tpl.find('.detail-status').html(formatStatus(row));
if (row.sync_type === 'shipping') {
$tpl.find('.detail-delivery-mode').text(DELIVERY_MODE_MAP[row.delivery_mode] || ('模式' + row.delivery_mode));
$tpl.find('.detail-delivery-mode-row').show();
} else {
$tpl.find('.detail-delivery-mode-row').hide();
}
$tpl.find('.detail-retry').text(row.retry_count || 0);
if (row.status === 'success') {
$tpl.find('.detail-error-row').hide();
} else {
var errParts = [];
if (row.errcode !== null && row.errcode !== undefined && row.errcode !== '') {
errParts.push('[' + row.errcode + ']');
}
if (row.errmsg) {
errParts.push(row.errmsg);
}
$tpl.find('.detail-error').text(errParts.join(' ') || '-');
$tpl.find('.detail-error-row').show();
}
$tpl.find('.detail-create').text(formatDatetime(row.createtime));
$tpl.find('.detail-update').text(formatDatetime(row.updatetime));
$tpl.find('.detail-request').text(formatJsonText(row.request_payload));
$tpl.find('.detail-response').text(formatJsonText(row.response_payload));
Layer.open({
type: 1,
title: '同步记录详情 #' + row.id,
area: ['720px', '80%'],
shadeClose: true,
content: $tpl.prop('outerHTML')
});
});
});