Merge pull request 'feat(orders): order detail modal, list actions, use configured prefix' (#309) from worktree-order-center-list into main

Reviewed-on: CYL/wordpress-theme-aksrt#309
This commit is contained in:
2026-07-27 13:13:15 +08:00
5 changed files with 492 additions and 46 deletions
+153
View File
@@ -521,6 +521,159 @@
font-variant-numeric: tabular-nums;
}
/* Order detail modal content */
.aksrt-order-detail-dialog {
width: min(560px, 100%);
}
.aksrt-order-detail__list {
margin: 0;
display: grid;
gap: 10px;
}
.aksrt-order-detail__row {
display: grid;
grid-template-columns: 96px minmax(0, 1fr);
gap: 10px 14px;
align-items: start;
font-size: 13.5px;
}
.aksrt-order-detail__row dt {
margin: 0;
color: var(--color-ink-3, #8b8f97);
font-weight: 500;
}
.aksrt-order-detail__row dd {
margin: 0;
color: var(--color-ink, #1f2329);
font-weight: 600;
word-break: break-all;
font-variant-numeric: tabular-nums;
}
.aksrt-order-detail__items,
.aksrt-order-detail__timeline {
margin-top: 16px;
padding-top: 14px;
border-top: 1px dashed color-mix(in srgb, var(--color-line, #e5e7eb) 85%, transparent);
}
.aksrt-order-detail__items h4,
.aksrt-order-detail__timeline h4 {
margin: 0 0 10px;
font-size: 13.5px;
font-weight: 700;
color: var(--color-ink, #1f2329);
}
.aksrt-order-detail__items ul,
.aksrt-order-detail__timeline ol {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: 8px;
}
.aksrt-order-detail__items li {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
gap: 10px;
align-items: center;
font-size: 13px;
color: var(--color-ink-2, #4b5563);
}
.aksrt-order-detail__items li em {
font-style: normal;
color: var(--color-ink-3, #8b8f97);
}
.aksrt-order-detail__items li strong {
font-weight: 700;
color: var(--color-ink, #1f2329);
font-variant-numeric: tabular-nums;
}
.aksrt-order-detail__timeline li {
display: grid;
grid-template-columns: 140px minmax(0, 1fr);
gap: 10px;
font-size: 12.5px;
color: var(--color-ink-2, #4b5563);
}
.aksrt-order-detail__timeline time {
color: var(--color-ink-3, #8b8f97);
font-variant-numeric: tabular-nums;
}
.aksrt-modal__footer .btn-primary,
.aksrt-modal__footer .btn-outline,
.aksrt-modal__footer .btn-ghost,
.aksrt-modal__footer a.btn-ghost,
.aksrt-modal__footer a.btn-outline {
min-height: 34px;
padding: 0 14px;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
line-height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
cursor: pointer;
}
.aksrt-modal__footer .btn-primary {
border: 1px solid transparent;
background: var(--color-primary, #3b82f6);
color: var(--color-on-primary, #fff);
}
.aksrt-modal__footer .btn-primary.c-green {
background: var(--color-success, #16a34a);
}
.aksrt-modal__footer .btn-outline {
border: 1px solid var(--color-line, #e5e7eb);
background: var(--surface-1, #fff);
color: var(--color-ink-2, #4b5563);
}
.aksrt-modal__footer .btn-ghost {
border: 1px solid transparent;
background: transparent;
color: var(--color-ink-3, #8b8f97);
}
@media (max-width: 767px) {
.aksrt-order-detail__row {
grid-template-columns: 1fr;
gap: 2px;
}
.aksrt-order-detail__timeline li {
grid-template-columns: 1fr;
gap: 2px;
}
.aksrt-modal__footer {
flex-wrap: wrap;
}
.aksrt-modal__footer .btn-primary,
.aksrt-modal__footer .btn-outline,
.aksrt-modal__footer .btn-ghost,
.aksrt-modal__footer a {
flex: 1 1 auto;
}
}
/* legacy item classes (if any remain) */
.aksrt-orders-page__item {
background: var(--surface-1);
+221 -4
View File
@@ -396,8 +396,39 @@
return page.querySelector('[data-aksrt-order-card="' + orderNumber + '"]');
}
function escapeHtml(value) {
return String(value == null ? "" : value)
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function formatMoney(order) {
var amount = Number(order && order.amount != null ? order.amount : 0);
if (order && order.is_points) {
var symbol = order.currency_symbol || "积分";
return escapeHtml(symbol) + " " + Math.round(amount);
}
return "¥" + amount.toFixed(2);
}
/** Plain money for table cells (formatMoney already escapes). */
function formatMoneyText(order) {
var amount = Number(order && order.amount != null ? order.amount : 0);
if (order && order.is_points) {
var symbol = order.currency_symbol || "积分";
return String(symbol) + " " + Math.round(amount);
}
return "¥" + amount.toFixed(2);
}
function updateCardStatus(card, newStatus, statusLabel) {
if (!card) return;
if (newStatus) {
card.setAttribute("data-order-status", newStatus);
}
var badge = card.querySelector(".aksrt-orders-page__card-status, .aksrt-orders-page__item-badge");
if (badge) {
badge.textContent = statusLabel || newStatus;
@@ -414,6 +445,162 @@
}
}
/* ── Order detail modal ── */
var detailModal = page.querySelector("[data-aksrt-order-detail-modal]");
var detailBody = page.querySelector("[data-aksrt-order-detail-body]");
var detailFooter = page.querySelector("[data-aksrt-order-detail-footer]");
var detailOrderNumber = "";
function closeOrderDetailModal() {
if (detailModal) detailModal.hidden = true;
detailOrderNumber = "";
document.documentElement.classList.remove("aksrt-modal-open");
}
document.addEventListener("keydown", function (event) {
if (event.key === "Escape" && detailModal && !detailModal.hidden) {
closeOrderDetailModal();
}
});
function openOrderDetailModal(orderNumber) {
if (!detailModal || !detailBody || !detailFooter) {
showToast("订单详情组件未加载");
return;
}
detailOrderNumber = orderNumber || "";
detailBody.innerHTML = '<p class="muted-2-color">加载中…</p>';
detailFooter.innerHTML = "";
detailModal.hidden = false;
document.documentElement.classList.add("aksrt-modal-open");
request(orderPath(orderNumber), undefined, "GET").then(function (result) {
var order = null;
if (result && result.data && result.data.order_number) {
order = result.data;
} else if (result && result.data && result.data.data && result.data.data.order_number) {
order = result.data.data;
} else if (result && result.order_number) {
order = result;
}
if (result && result.success === false) {
detailBody.innerHTML = '<p class="c-red">' + escapeHtml((result && result.message) || "加载失败") + "</p>";
return;
}
if (!order || !order.order_number) {
detailBody.innerHTML = '<p class="c-red">订单不存在</p>';
return;
}
renderOrderDetail(order);
}).catch(function () {
detailBody.innerHTML = '<p class="c-red">网络错误,请稍后重试</p>';
});
}
function renderOrderDetail(order) {
if (!detailBody || !detailFooter) return;
var actions = (order && order.actions) || {};
var payment = order.payment_method || {};
var paymentLabel = payment.label || payment.value || "—";
var qty = order.quantity || 1;
if ((!qty || qty < 1) && order.items && order.items.length) {
qty = 0;
order.items.forEach(function (item) {
qty += Math.max(1, parseInt(item.quantity || item.qty || 1, 10) || 1);
});
}
if (!qty || qty < 1) qty = 1;
var rows = [
["订单状态", order.status_label || order.status || "—"],
["订单名称", order.product_name || (order.product && order.product.title) || "—"],
["订单类型", order.product_type_label || order.product_type || "—"],
["订单数量", String(qty)],
["应付金额", formatMoneyText(order)],
["订单编号", order.order_number || "—"],
["创建时间", order.created_at || "—"],
["支付时间", order.paid_at || "—"],
["支付方式", paymentLabel]
];
if (Number(order.shipping_fee || 0) > 0) {
rows.push(["运费", "¥" + Number(order.shipping_fee || 0).toFixed(2)]);
}
if (Number(order.discount || 0) > 0) {
rows.push(["优惠", "¥" + Number(order.discount || 0).toFixed(2)]);
}
if (order.customer_note) {
rows.push(["备注", order.customer_note]);
}
var html = '<dl class="aksrt-order-detail__list">';
rows.forEach(function (row) {
html += '<div class="aksrt-order-detail__row">';
html += '<dt>' + escapeHtml(row[0]) + '</dt>';
// Money strings already composed; still escape all cells for safety.
html += '<dd>' + escapeHtml(row[1]) + '</dd>';
html += '</div>';
});
html += "</dl>";
if (order.items && order.items.length) {
html += '<div class="aksrt-order-detail__items"><h4>商品明细</h4><ul>';
order.items.forEach(function (item) {
var title = item.product_title || item.title || order.product_name || "商品";
var itemQty = item.quantity || item.qty || 1;
var price = item.pay_price != null ? item.pay_price : (item.unit_price || 0);
html += '<li><span>' + escapeHtml(title) + '</span>';
html += '<em>×' + escapeHtml(String(itemQty)) + '</em>';
if (order.is_points) {
html += '<strong>' + escapeHtml(order.currency_symbol || "积分") + " " + Math.round(Number(price || 0)) + "</strong>";
} else {
html += '<strong>¥' + Number(price || 0).toFixed(2) + "</strong>";
}
html += "</li>";
});
html += "</ul></div>";
}
if (order.timeline && order.timeline.length) {
html += '<div class="aksrt-order-detail__timeline"><h4>订单进度</h4><ol>';
order.timeline.forEach(function (tl) {
html += '<li><time>' + escapeHtml(tl.time || "") + "</time><span>" + escapeHtml(tl.label || "") + "</span></li>";
});
html += "</ol></div>";
}
detailBody.innerHTML = html;
var footer = "";
// Status-aware footer actions.
if (actions.can_cancel) {
footer += '<button type="button" class="btn-ghost" data-aksrt-order-cancel="' + escapeHtml(order.order_number) + '">关闭订单</button>';
}
if (actions.can_pay) {
footer += '<button type="button" class="btn-primary" data-aksrt-order-pay="' + escapeHtml(order.order_number) + '" data-aksrt-order-method="' + escapeHtml((payment && payment.value) || "") + '">立即支付</button>';
}
if (actions.can_receipt) {
footer += '<button type="button" class="btn-primary" data-aksrt-order-receipt="' + escapeHtml(order.order_number) + '">确认收货</button>';
}
if (actions.can_review) {
footer += '<button type="button" class="btn-primary c-green" data-aksrt-order-review="' + escapeHtml(order.order_number) + '">评价</button>';
}
if (actions.can_after_sale) {
footer += '<button type="button" class="btn-outline" data-aksrt-order-after-sale="' + escapeHtml(order.order_number) + '">申请售后</button>';
}
if (actions.can_rebuy) {
footer += '<button type="button" class="btn-outline" data-aksrt-order-rebuy="' + escapeHtml(order.order_number) + '">再次购买</button>';
}
if (actions.can_view_product && order.permalink) {
footer += '<a class="btn-ghost" href="' + escapeHtml(order.permalink) + '">查看商品</a>';
}
if (order.secondary_url && order.secondary_label) {
footer += '<a class="btn-ghost" href="' + escapeHtml(order.secondary_url) + '">' + escapeHtml(order.secondary_label) + "</a>";
}
footer += '<button type="button" class="btn-outline" data-aksrt-order-detail-close>关闭</button>';
detailFooter.innerHTML = footer;
}
/* ── After-Sale Modal ── */
var afterSaleModal = page.querySelector("[data-aksrt-aftersale-modal]");
var afterSaleForm = page.querySelector("[data-aksrt-aftersale-form]");
@@ -496,6 +683,20 @@
/* ── Event delegation ── */
page.addEventListener("click", function (event) {
// Order detail open
var detailBtn = event.target.closest("[data-aksrt-order-detail]");
if (detailBtn && !event.target.closest("[data-aksrt-order-detail-modal]")) {
var detailOrder = detailBtn.getAttribute("data-aksrt-order-detail");
if (detailOrder) openOrderDetailModal(detailOrder);
return;
}
// Order detail close
if (event.target.closest("[data-aksrt-order-detail-close]")) {
closeOrderDetailModal();
return;
}
// Timeline toggle
var timelineToggle = event.target.closest("[data-aksrt-toggle-timeline]");
if (timelineToggle) {
@@ -521,6 +722,9 @@
}
showToast("已确认收货");
updateCardStatus(findOrderCard(receiptOrder), "completed", "已完成");
if (detailOrderNumber === receiptOrder) {
closeOrderDetailModal();
}
})
.catch(function () {
receiptBtn.disabled = false;
@@ -557,6 +761,7 @@
if (reviewBtn) {
var reviewOrder = reviewBtn.getAttribute("data-aksrt-order-review");
if (!reviewOrder) return;
closeOrderDetailModal();
openOrderReviewModal(reviewOrder, reviewBtn);
return;
}
@@ -568,13 +773,14 @@
// 支付方式取订单创建时选定的方式(按钮 data 属性可覆盖);
// 留空则由服务端使用订单存储的 payment_method。
var payMethod = payButton.getAttribute("data-aksrt-order-method") || "";
var payLabel = payButton.textContent || "去支付";
payButton.disabled = true;
payButton.textContent = "处理中...";
request(orderPath(payOrderNumber, "/pay"), payMethod ? { payment_method: payMethod } : {})
.then(function (result) {
if (!result.success) {
payButton.disabled = false;
payButton.textContent = "立即支付";
payButton.textContent = payLabel;
showToast(result.message || "支付失败。");
return;
}
@@ -600,10 +806,13 @@
showToast("支付成功!");
var card = findOrderCard(payOrderNumber);
updateCardStatus(card, "paid", "已支付");
if (detailOrderNumber === payOrderNumber) {
closeOrderDetailModal();
}
})
.catch(function () {
payButton.disabled = false;
payButton.textContent = "立即支付";
payButton.textContent = payLabel;
showToast("网络错误,请稍后重试。");
});
return;
@@ -613,23 +822,30 @@
var cancelButton = event.target.closest("[data-aksrt-order-cancel]");
if (cancelButton) {
var cancelOrderNumber = cancelButton.getAttribute("data-aksrt-order-cancel");
if (!window.confirm("确认关闭该订单?关闭后不可再支付。")) {
return;
}
var cancelLabel = cancelButton.textContent || "取消";
cancelButton.disabled = true;
cancelButton.textContent = "取消中...";
request(orderPath(cancelOrderNumber, "/cancel"))
.then(function (result) {
if (!result.success) {
cancelButton.disabled = false;
cancelButton.textContent = "取消订单";
cancelButton.textContent = cancelLabel;
showToast(result.message || "取消失败。");
return;
}
showToast("订单已取消。");
var card = findOrderCard(cancelOrderNumber);
updateCardStatus(card, "cancelled", "已取消");
if (detailOrderNumber === cancelOrderNumber) {
closeOrderDetailModal();
}
})
.catch(function () {
cancelButton.disabled = false;
cancelButton.textContent = "取消订单";
cancelButton.textContent = cancelLabel;
showToast("网络错误,请稍后重试。");
});
return;
@@ -639,6 +855,7 @@
var afterSaleButton = event.target.closest("[data-aksrt-order-after-sale]");
if (afterSaleButton) {
var afterSaleOrderNumber = afterSaleButton.getAttribute("data-aksrt-order-after-sale");
closeOrderDetailModal();
openAfterSaleModal(afterSaleOrderNumber);
return;
}
@@ -209,9 +209,11 @@ $pending_count = (int) ($stats['pending'] ?? 0);
? esc_html($symbol) . ' ' . esc_html((string) (int) round((float) ($order['amount'] ?? 0)))
: '¥' . esc_html(number_format((float) ($order['amount'] ?? 0), 2));
?>
<article class="aksrt-orders-page__card" data-aksrt-order-card="<?php echo esc_attr($on); ?>" data-product-type="<?php echo esc_attr($ptype); ?>">
<article class="aksrt-orders-page__card" data-aksrt-order-card="<?php echo esc_attr($on); ?>" data-product-type="<?php echo esc_attr($ptype); ?>" data-order-status="<?php echo esc_attr($os); ?>">
<div class="aksrt-orders-page__card-head">
<span class="aksrt-orders-page__card-type"><?php echo esc_html((string) ($order['product_type_label'] ?? '')); ?></span>
<?php if ((string) ($order['product_type_label'] ?? '') !== '') : ?>
<span class="aksrt-orders-page__card-type"><?php echo esc_html((string) $order['product_type_label']); ?></span>
<?php endif; ?>
<span class="aksrt-orders-page__card-number"><?php echo esc_html($on); ?></span>
<time class="aksrt-orders-page__card-time"><?php echo esc_html((string) ($order['created_at'] ?? '')); ?></time>
<span class="aksrt-orders-page__card-status"><?php echo esc_html((string) ($order['status_label'] ?? '')); ?></span>
@@ -290,34 +292,6 @@ $pending_count = (int) ($stats['pending'] ?? 0);
<?php endif; ?>
<div class="aksrt-orders-page__card-actions">
<?php if (!empty($actions['can_pay'])) : ?>
<button type="button" class="btn-primary btn-sm" data-aksrt-order-pay="<?php echo esc_attr($on); ?>" data-aksrt-order-method="<?php echo esc_attr((string) ($order['payment_method']['value'] ?? '')); ?>"><?php esc_html_e('去支付', 'aksrt'); ?></button>
<?php endif; ?>
<?php if (!empty($actions['can_receipt'])) : ?>
<button type="button" class="btn-primary btn-sm" data-aksrt-order-receipt="<?php echo esc_attr($on); ?>"><?php esc_html_e('确认收货', 'aksrt'); ?></button>
<?php endif; ?>
<?php if (!empty($actions['can_review']) || (class_exists('\AKSRT\Payment\Order') && $os === \AKSRT\Payment\Order::STATUS_PENDING_REVIEW)) : ?>
<button type="button" class="btn-primary btn-sm c-green" data-aksrt-order-review="<?php echo esc_attr($on); ?>"><?php esc_html_e('评价', 'aksrt'); ?></button>
<?php endif; ?>
<?php if (!empty($actions['can_cancel'])) : ?>
<button type="button" class="btn-ghost btn-sm" data-aksrt-order-cancel="<?php echo esc_attr($on); ?>"><?php esc_html_e('取消', 'aksrt'); ?></button>
<?php endif; ?>
<?php if (!empty($actions['can_rebuy'])) : ?>
<button type="button" class="btn-outline btn-sm" data-aksrt-order-rebuy="<?php echo esc_attr($on); ?>"><?php esc_html_e('再次购买', 'aksrt'); ?></button>
<?php endif; ?>
<?php if (!empty($actions['can_after_sale'])) : ?>
<button type="button" class="btn-outline btn-sm" data-aksrt-order-after-sale="<?php echo esc_attr($on); ?>"><?php esc_html_e('售后', 'aksrt'); ?></button>
<?php endif; ?>
<?php if (!empty($actions['can_view']) && !empty($order['permalink'])) : ?>
<a class="btn-outline btn-sm" href="<?php echo esc_url((string) $order['permalink']); ?>"><?php
echo (class_exists('\AKSRT\Core\DomainModel') && $ptype === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE)
? esc_html__('去查看', 'aksrt')
: esc_html__('查看', 'aksrt');
?></a>
<?php endif; ?>
<?php if (!empty($order['secondary_url']) && !empty($order['secondary_label'])) : ?>
<a class="btn-ghost btn-sm" href="<?php echo esc_url((string) $order['secondary_url']); ?>"><?php echo esc_html((string) $order['secondary_label']); ?></a>
<?php endif; ?>
<?php if (!empty($order['time_left'])) : ?>
<span class="aksrt-orders-page__countdown" data-aksrt-order-countdown="<?php echo esc_attr((string) $order['time_left']); ?>">
<?php
@@ -329,6 +303,32 @@ $pending_count = (int) ($stats['pending'] ?? 0);
?>
</span>
<?php endif; ?>
<button type="button" class="btn-outline btn-sm" data-aksrt-order-detail="<?php echo esc_attr($on); ?>"><?php esc_html_e('查看', 'aksrt'); ?></button>
<?php if (!empty($actions['can_cancel'])) : ?>
<button type="button" class="btn-ghost btn-sm" data-aksrt-order-cancel="<?php echo esc_attr($on); ?>"><?php esc_html_e('取消', 'aksrt'); ?></button>
<?php endif; ?>
<?php if (!empty($actions['can_pay'])) : ?>
<button type="button" class="btn-primary btn-sm" data-aksrt-order-pay="<?php echo esc_attr($on); ?>" data-aksrt-order-method="<?php echo esc_attr((string) ($order['payment_method']['value'] ?? '')); ?>"><?php esc_html_e('去支付', 'aksrt'); ?></button>
<?php endif; ?>
<?php if (!empty($actions['can_receipt'])) : ?>
<button type="button" class="btn-primary btn-sm" data-aksrt-order-receipt="<?php echo esc_attr($on); ?>"><?php esc_html_e('确认收货', 'aksrt'); ?></button>
<?php endif; ?>
<?php if (!empty($actions['can_review']) || (class_exists('\AKSRT\Payment\Order') && $os === \AKSRT\Payment\Order::STATUS_PENDING_REVIEW)) : ?>
<button type="button" class="btn-primary btn-sm c-green" data-aksrt-order-review="<?php echo esc_attr($on); ?>"><?php esc_html_e('评价', 'aksrt'); ?></button>
<?php endif; ?>
<?php if (!empty($actions['can_rebuy'])) : ?>
<button type="button" class="btn-outline btn-sm" data-aksrt-order-rebuy="<?php echo esc_attr($on); ?>"><?php esc_html_e('再次购买', 'aksrt'); ?></button>
<?php endif; ?>
<?php if (!empty($actions['can_after_sale'])) : ?>
<button type="button" class="btn-outline btn-sm" data-aksrt-order-after-sale="<?php echo esc_attr($on); ?>"><?php esc_html_e('售后', 'aksrt'); ?></button>
<?php endif; ?>
<?php if (!empty($actions['can_view_product']) && !empty($order['permalink'])) : ?>
<a class="btn-ghost btn-sm" href="<?php echo esc_url((string) $order['permalink']); ?>"><?php
echo (class_exists('\AKSRT\Core\DomainModel') && $ptype === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE)
? esc_html__('去内容', 'aksrt')
: esc_html__('商品页', 'aksrt');
?></a>
<?php endif; ?>
</div>
</article>
<?php endforeach; ?>
@@ -358,6 +358,21 @@ $pending_count = (int) ($stats['pending'] ?? 0);
</div>
<?php endif; ?>
<!-- Order detail modal -->
<div class="aksrt-modal" data-aksrt-order-detail-modal hidden>
<div class="aksrt-modal__overlay" data-aksrt-order-detail-close></div>
<div class="aksrt-modal__dialog aksrt-order-detail-dialog" role="dialog" aria-modal="true" aria-labelledby="aksrt-order-detail-title">
<div class="aksrt-modal__header">
<h3 id="aksrt-order-detail-title"><?php esc_html_e('订单详情', 'aksrt'); ?></h3>
<button type="button" class="aksrt-modal__close" data-aksrt-order-detail-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>">&times;</button>
</div>
<div class="aksrt-modal__body" data-aksrt-order-detail-body>
<p class="muted-2-color"><?php esc_html_e('加载中…', 'aksrt'); ?></p>
</div>
<div class="aksrt-modal__footer" data-aksrt-order-detail-footer></div>
</div>
</div>
<!-- After-sale modal -->
<div class="aksrt-modal" data-aksrt-aftersale-modal hidden>
<div class="aksrt-modal__overlay"></div>
+40 -4
View File
@@ -41,7 +41,7 @@ trait OrderRepositoryCore {
$note = sanitize_textarea_field($note_raw);
}
$wpdb->insert($this->table(), [
$row = [
'order_number' => $order_number,
'user_id' => (int) ($data['user_id'] ?? 0),
'product_type' => sanitize_key((string) ($data['product_type'] ?? '')),
@@ -53,9 +53,17 @@ trait OrderRepositoryCore {
'payment_transaction_id' => sanitize_text_field((string) ($data['payment_transaction_id'] ?? '')),
'created_at' => $data['created_at'] ?? current_time('mysql'),
'paid_at' => $data['paid_at'] ?? null,
'expires_at' => $data['expires_at'] ?? null,
'note' => $note,
], ['%s', '%d', '%s', '%d', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']);
];
$formats = ['%s', '%d', '%s', '%d', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%s'];
// Honour configured expiry when callers pass expires_at (shop checkout etc.).
if (array_key_exists('expires_at', $data) && $data['expires_at'] !== null && $data['expires_at'] !== '') {
$row['expires_at'] = (string) $data['expires_at'];
$formats[] = '%s';
}
$wpdb->insert($this->table(), $row, $formats);
if ($wpdb->insert_id) {
return $order_number;
@@ -506,8 +514,36 @@ trait OrderRepositoryCore {
/**
* Build a unique order number using the admin-configured prefix.
*
* Column + major gateways (WeChat out_trade_no) cap at 32 chars, so the
* prefix is trimmed to leave room for {YmdHis}{4 digits} (=18).
*/
private function generate_order_number(): string {
return 'AKS' . date('YmdHis') . str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT);
$prefix = 'AKSRT';
if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_order_settings')) {
$settings = \AKSRT\Settings\Settings::get_order_settings();
$configured = sanitize_text_field((string) ($settings['prefix'] ?? ''));
if ($configured !== '') {
$prefix = $configured;
}
}
// Keep only gateway-safe chars (REST + WeChat/Alipay out_trade_no).
$prefix = preg_replace('/[^A-Za-z0-9_-]/', '', $prefix);
if (!is_string($prefix) || $prefix === '') {
$prefix = 'AKSRT';
}
$body = date('YmdHis') . str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT);
// Reserve 18 for body; leave ≥1 char of prefix so numbers stay human-readable.
$max_prefix = max(1, 32 - strlen($body));
if (strlen($prefix) > $max_prefix) {
$prefix = substr($prefix, 0, $max_prefix);
}
return $prefix . $body;
}
/**
+33 -8
View File
@@ -185,7 +185,9 @@ class UserOrders {
$can_receipt = $is_shop && $status === Order::STATUS_DELIVERED;
$can_review = $is_shop && $status === Order::STATUS_PENDING_REVIEW;
$can_rebuy = $is_shop && in_array($status, [Order::STATUS_COMPLETED, Order::STATUS_PENDING_REVIEW], true);
$can_view = $permalink !== '' && in_array($status, [
// Order detail modal is always available; product permalink is optional.
$can_view = true;
$can_view_product = $permalink !== '' && in_array($status, [
Order::STATUS_PAID,
Order::STATUS_DELIVERED,
Order::STATUS_COMPLETED,
@@ -232,14 +234,16 @@ class UserOrders {
'shipping_records' => $shipping_records,
'pay_url' => '#',
'actions' => [
'can_pay' => $can_pay,
'can_cancel' => $can_cancel,
'can_after_sale' => $can_after_sale,
'can_receipt' => $can_receipt,
'can_review' => $can_review,
'can_rebuy' => $can_rebuy,
'can_view' => $can_view,
'can_pay' => $can_pay,
'can_cancel' => $can_cancel,
'can_after_sale' => $can_after_sale,
'can_receipt' => $can_receipt,
'can_review' => $can_review,
'can_rebuy' => $can_rebuy,
'can_view' => $can_view,
'can_view_product' => $can_view_product,
],
'quantity' => $this->resolve_order_quantity($order_items, $meta),
'time_left' => $this->get_order_countdown($expires_at, $status),
'secondary_url' => $this->resolve_secondary_url($product_type),
'secondary_label' => $this->resolve_secondary_label($product_type),
@@ -534,4 +538,25 @@ class UserOrders {
$left = strtotime($expires_at) - time();
return max(0, (int) $left);
}
/**
* Sum line quantities for display (shop multi-item or single meta item).
*
* @param array<int, array<string, mixed>> $order_items
* @param array<string, mixed> $meta
*/
private function resolve_order_quantity(array $order_items, array $meta): int {
$qty = 0;
$source = !empty($order_items) ? $order_items : (is_array($meta['items'] ?? null) ? $meta['items'] : []);
foreach ($source as $row) {
if (!is_array($row)) {
continue;
}
$qty += max(1, (int) ($row['quantity'] ?? $row['qty'] ?? 1));
}
if ($qty <= 0) {
$qty = max(1, (int) ($meta['quantity'] ?? $meta['qty'] ?? 1));
}
return $qty;
}
}