feat(after-sale): 售后处理记录改为每次提交留痕并新增时间线
工单行内新增 handling_log 列,用户申请、用户撤销与后台每次改状态都落一条
「状态 + 备注 + 来源 + 时间」,按时间正序存放、上限 50 条(对齐
MetaAssetService::MAX_RECORDS)。set_status 因此变成读-改-写,补上
GET_LOCK/RELEASE_LOCK 包裹,并在锁内重读工单快照——锁外取快照仍会被并发
追加覆盖。
三处界面由「只显示最后一条」改为回放整条处理链路:
- 后台订单明细「售后记录」按工单渲染头部(状态/金额/申请时间)+ 原因 +
完整处理记录
- 售后面板手风琴「处理备注」单独占整行并置于「申请说明」上方,删掉已无
意义的「审核时间」
- 前台售后详细弹窗与订单详情弹窗的售后记录改为时间线:左列绳结状态条
(节点 + 状态 + 时间),右列处理备注;样式只在 modal-runtime.css 定义
一份,两个弹窗共用
清理随之失效的死代码:工单的 admin_note / reviewed_at / completed_at,以及
全仓库无写入方的 gateway_request_id / gateway_refund_id / gateway_result;
连带删掉 gatewayResultText()、uninstall 里从未存在过的 aksrt_after_sale
表项,并把两处后台页面重复的状态标签数组收成 after_sale_status_labels()、
重复的 .aksrt-as-log 样式收成 after_sale_log_css()。
顺带修复 88669f69 重构退款流水线时误删 refund_amount()、却仍在 create()
里调用的问题——此前任何用户提交售后都会直接 fatal。
验证:tests/widget-system/ 462 项、tests/admin/ 76 项通过;新增
after-sale-log-harness.php 驱动真实 AfterSaleService,覆盖首条日志、空备注
也留痕、撤销记录、50 条上限与畸形行规范化;两个后台页面经 dev harness
渲染校验「处理备注」宽行位于「申请说明」之前;未做浏览器视觉验收。
This commit is contained in:
1 parent
31e6ba00af
commit
26ce9a5904
15 files changed
+644
-57
No files matched your search
@@ -351,6 +351,13 @@ class PayOrders {
|
||||
|
||||
echo '<p><a class="button" href="' . esc_url($this->page_url('aksrt_orders')) . '">← ' . esc_html__('返回订单列表', 'aksrt') . '</a></p>';
|
||||
|
||||
// 售后记录区块:工单卡片 + 处理记录列表(与售后面板共用 .aksrt-as-log 样式)。
|
||||
echo '<style>'
|
||||
. '.aksrt-as-ticket{padding:10px 0;border-bottom:1px solid #f0f0f1;}'
|
||||
. '.aksrt-as-ticket:last-child{padding-bottom:0;border-bottom:0;}'
|
||||
. '.aksrt-as-ticket__head{margin:0 0 4px;}'
|
||||
. $this->after_sale_log_css()
|
||||
. '</style>';
|
||||
echo '<table class="widefat striped" style="max-width:960px;"><tbody>';
|
||||
$rows = [
|
||||
__('订单号', 'aksrt') => $order_number,
|
||||
@@ -371,12 +378,17 @@ class PayOrders {
|
||||
$after_sales = \AKSRT\Payment\AfterSaleService::get_instance()->list_for_order($order_number);
|
||||
if ($after_sales) {
|
||||
echo '<tr><th>' . esc_html__('售后记录', 'aksrt') . '</th><td>';
|
||||
$status_labels = ['pending' => '待审核', 'processing' => '处理中', 'completed' => '已完成', 'rejected' => '已拒绝', 'cancelled' => '已取消'];
|
||||
$status_labels = $this->after_sale_status_labels();
|
||||
foreach ($after_sales as $application) {
|
||||
echo '<p><strong>' . esc_html($status_labels[(string) ($application['status'] ?? '')] ?? (string) ($application['status'] ?? '')) . '</strong> · ' . esc_html((string) ($application['refund_amount'] ?? '')) . '<br>' . esc_html((string) ($application['reason'] ?? ''));
|
||||
if (!empty($application['admin_note'])) echo '<br>' . esc_html__('处理备注:', 'aksrt') . esc_html((string) $application['admin_note']);
|
||||
if (!empty($application['gateway_result'])) echo '<br><code>' . esc_html((string) $application['gateway_result']) . '</code>';
|
||||
echo '</p>';
|
||||
$as_status = (string) ($application['status'] ?? '');
|
||||
echo '<div class="aksrt-as-ticket is-' . esc_attr($as_status) . '">';
|
||||
echo '<p class="aksrt-as-ticket__head"><strong>' . esc_html($status_labels[$as_status] ?? $as_status) . '</strong>'
|
||||
. ' <span class="description">' . esc_html((string) ($application['refund_amount'] ?? '')) . '</span>'
|
||||
. ' <span class="description">' . esc_html((string) ($application['created_at'] ?? '')) . '</span></p>';
|
||||
echo '<p>' . nl2br(esc_html((string) ($application['reason'] ?? ''))) . '</p>';
|
||||
// 处理记录:后台每一次提交都留一条(状态 + 备注 + 时间),与售后面板同款标记。
|
||||
echo $this->render_after_sale_log((array) ($application['handling_log'] ?? []), $status_labels); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo '</div>';
|
||||
}
|
||||
echo '</td></tr>';
|
||||
}
|
||||
@@ -429,7 +441,7 @@ class PayOrders {
|
||||
|
||||
echo '<form method="get"><input type="hidden" name="page" value="' . esc_attr(self::PAGE_AFTER_SALES_SLUG) . '" />';
|
||||
echo '<select name="after_sale_status"><option value="">' . esc_html__('全部售后状态', 'aksrt') . '</option>';
|
||||
foreach ([\AKSRT\Payment\AfterSaleService::STATUS_PENDING => '待审核', \AKSRT\Payment\AfterSaleService::STATUS_PROCESSING => '处理中', \AKSRT\Payment\AfterSaleService::STATUS_COMPLETED => '已完成', \AKSRT\Payment\AfterSaleService::STATUS_REJECTED => '已拒绝', \AKSRT\Payment\AfterSaleService::STATUS_CANCELLED => '已取消'] as $key => $label) echo '<option value="' . esc_attr($key) . '"' . selected($status, $key, false) . '>' . esc_html($label) . '</option>';
|
||||
foreach ($this->after_sale_status_labels() as $key => $label) echo '<option value="' . esc_attr($key) . '"' . selected($status, $key, false) . '>' . esc_html($label) . '</option>';
|
||||
echo '</select> <label>' . esc_html__('开始日期', 'aksrt') . ' <input type="date" name="date_from" value="' . esc_attr($date_from) . '" /></label> <label>' . esc_html__('结束日期', 'aksrt') . ' <input type="date" name="date_to" value="' . esc_attr($date_to) . '" /></label> ';
|
||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
@@ -452,12 +464,13 @@ class PayOrders {
|
||||
. '.aksrt-as-review textarea{flex:1 1 240px;min-width:0;box-sizing:border-box;margin:0;}'
|
||||
. '.aksrt-as-review-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end;margin-left:auto;}'
|
||||
. '.aksrt-as-field--wide{grid-column:1/-1;}'
|
||||
. $this->after_sale_log_css()
|
||||
. '@media (min-width:1500px){.aksrt-as-detail-grid{grid-template-columns:repeat(5,minmax(0,1fr));}}'
|
||||
. '@media (max-width:600px){.aksrt-as-detail-grid{grid-template-columns:minmax(0,1fr);}}'
|
||||
. '</style>';
|
||||
echo '<table class="widefat striped"><thead><tr><th>ID</th><th>订单号</th><th>用户</th><th>原因</th><th>申请资料</th><th>申请说明</th><th>退款金额</th><th>状态</th><th>操作 / 处理记录</th></tr></thead><tbody>';
|
||||
if (!$items) echo '<tr><td colspan="9">' . esc_html__('暂无售后申请', 'aksrt') . '</td></tr>';
|
||||
$status_labels = [\AKSRT\Payment\AfterSaleService::STATUS_PENDING => '待审核', \AKSRT\Payment\AfterSaleService::STATUS_PROCESSING => '处理中', \AKSRT\Payment\AfterSaleService::STATUS_COMPLETED => '已完成', \AKSRT\Payment\AfterSaleService::STATUS_REJECTED => '已拒绝', \AKSRT\Payment\AfterSaleService::STATUS_CANCELLED => '已取消'];
|
||||
$status_labels = $this->after_sale_status_labels();
|
||||
foreach ($items as $item) {
|
||||
$id = (int) $item['id']; $number = (string) $item['order_number']; $uid = (int) $item['user_id'];
|
||||
$user = $uid > 0 ? get_userdata($uid) : null;
|
||||
@@ -496,7 +509,6 @@ class PayOrders {
|
||||
$desc_cell = ($desc_text !== '' ? nl2br(esc_html($desc_text)) : '') . ($thumbs ? '<div class="aksrt-as-desc-thumbs" style="margin-top:6px;">' . implode('', $thumbs) . '</div>' : '');
|
||||
if ($desc_cell === '') $desc_cell = '—';
|
||||
$status_label = $status_labels[(string) $item['status']] ?? (string) $item['status'];
|
||||
$admin_note = (string) ($item['admin_note'] ?? '');
|
||||
$order_refunded = is_array($order) && (string) ($order['status'] ?? '') === \AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDED;
|
||||
// 摘要行 + 手风琴详情行:点击「操作」展开整条记录的完整信息与状态面板
|
||||
echo '<tr class="aksrt-as-row">';
|
||||
@@ -520,13 +532,15 @@ class PayOrders {
|
||||
__('当前状态', 'aksrt') => esc_html($status_label),
|
||||
__('售后原因', 'aksrt') => $reason_cell,
|
||||
__('申请资料', 'aksrt') => $materials_cell,
|
||||
__('处理备注', 'aksrt') => $admin_note !== '' ? esc_html($admin_note) : '—',
|
||||
__('审核时间', 'aksrt') => !empty($item['reviewed_at']) ? esc_html((string) $item['reviewed_at']) : '—',
|
||||
];
|
||||
echo '<div class="aksrt-as-detail"><div class="aksrt-as-detail-grid">';
|
||||
foreach ($detail_fields as $field_label => $field_value) {
|
||||
echo '<span class="aksrt-as-field"><span class="aksrt-as-label">' . esc_html($field_label) . '</span><span class="aksrt-as-value">' . $field_value . '</span></span>';
|
||||
}
|
||||
// 处理备注:单独占整行,放在申请说明上方;每次提交一条,含状态与时间。
|
||||
echo '<span class="aksrt-as-field aksrt-as-field--wide"><span class="aksrt-as-label">' . esc_html__('处理备注', 'aksrt') . '</span><span class="aksrt-as-value">'
|
||||
. $this->render_after_sale_log((array) ($item['handling_log'] ?? []), $status_labels) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
. '</span></span>';
|
||||
// 举证图片并入申请说明,单独占整行展示。
|
||||
echo '<span class="aksrt-as-field aksrt-as-field--wide"><span class="aksrt-as-label">' . esc_html__('申请说明', 'aksrt') . '</span><span class="aksrt-as-value">' . $desc_cell . '</span></span>';
|
||||
echo '</div>';
|
||||
@@ -619,6 +633,58 @@ class PayOrders {
|
||||
];
|
||||
}
|
||||
|
||||
/** 售后状态中文标签:订单明细页与售后面板(含筛选下拉)共用,取代两处内联数组。 */
|
||||
/** 处理记录列表样式:两个后台页面各自输出同一套规则,不依赖对方加载。 */
|
||||
private function after_sale_log_css(): string {
|
||||
return '.aksrt-as-log{display:grid;gap:5px;margin:6px 0 0;padding:0;list-style:none;}'
|
||||
. '.aksrt-as-log__item{display:flex;flex-wrap:wrap;gap:3px 10px;align-items:baseline;font-size:12px;line-height:1.7;}'
|
||||
. '.aksrt-as-log__time{color:#646970;font-variant-numeric:tabular-nums;}'
|
||||
. '.aksrt-as-log__status{font-weight:600;}'
|
||||
. '.aksrt-as-log__item.is-pending .aksrt-as-log__status{color:#b26200;}'
|
||||
. '.aksrt-as-log__item.is-processing .aksrt-as-log__status{color:#2271b1;}'
|
||||
. '.aksrt-as-log__item.is-completed .aksrt-as-log__status{color:#007017;}'
|
||||
. '.aksrt-as-log__item.is-rejected .aksrt-as-log__status,.aksrt-as-log__item.is-cancelled .aksrt-as-log__status{color:#b32d2e;}'
|
||||
. '.aksrt-as-log__note{flex:1 1 100%;color:#3c434a;}';
|
||||
}
|
||||
|
||||
private function after_sale_status_labels(): array {
|
||||
return [
|
||||
\AKSRT\Payment\AfterSaleService::STATUS_PENDING => '待审核',
|
||||
\AKSRT\Payment\AfterSaleService::STATUS_PROCESSING => '处理中',
|
||||
\AKSRT\Payment\AfterSaleService::STATUS_COMPLETED => '已完成',
|
||||
\AKSRT\Payment\AfterSaleService::STATUS_REJECTED => '已拒绝',
|
||||
\AKSRT\Payment\AfterSaleService::STATUS_CANCELLED => '已取消',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理记录列表:每一次状态提交一条「时间 + 状态 + 备注」,备注为空也留痕。
|
||||
* 后台订单明细页与售后面板共用同一套标记与样式。
|
||||
*
|
||||
* @param list<array<string,mixed>> $log
|
||||
* @param array<string,string> $status_labels
|
||||
*/
|
||||
private function render_after_sale_log(array $log, array $status_labels): string {
|
||||
if (!$log) {
|
||||
return '<p class="description">' . esc_html__('暂无处理记录', 'aksrt') . '</p>';
|
||||
}
|
||||
$service = \AKSRT\Payment\AfterSaleService::class;
|
||||
$html = '<ul class="aksrt-as-log">';
|
||||
foreach ($log as $entry) {
|
||||
$status = (string) ($entry['status'] ?? '');
|
||||
$label = (string) ($entry['source'] ?? '') === $service::SOURCE_USER
|
||||
? ($status === $service::STATUS_CANCELLED ? esc_html__('用户撤销售后', 'aksrt') : esc_html__('用户提交申请', 'aksrt'))
|
||||
: ($status_labels[$status] ?? $status);
|
||||
$note = (string) ($entry['note'] ?? '');
|
||||
$html .= '<li class="aksrt-as-log__item is-' . esc_attr($status) . '">'
|
||||
. '<span class="aksrt-as-log__time">' . esc_html((string) ($entry['created_at'] ?? '')) . '</span>'
|
||||
. '<span class="aksrt-as-log__status">' . esc_html($label) . '</span>'
|
||||
. '<span class="aksrt-as-log__note">' . ($note !== '' ? nl2br(esc_html($note)) : '—') . '</span>'
|
||||
. '</li>';
|
||||
}
|
||||
return $html . '</ul>';
|
||||
}
|
||||
|
||||
public function handle_after_sale_status(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
$id = isset($_POST['after_sale_id']) ? absint($_POST['after_sale_id']) : 0;
|
||||
|
||||
Reference in new issue
Block a user