From 1d2505356e15728eeef362d072876fd3ba692143 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 2 Aug 2026 21:25:52 +0800 Subject: [PATCH] feat: remove shop feature (Phase 2) Delete all shop domain classes, controllers, schema builders, templates, JS/CSS, admin SPA pages, and shop-only payment classes (Shipping, AfterSale, Discount). Remove shop-only DB table definitions (product_variant, after_sale, order_shipping, card_key) from migrator and add their DROP to uninstall.php. Clean up shared files: - Remove aksrt_product CPT + taxonomies from Setup.php - Remove shop rewrite rules/query vars/template redirect from SetupPart2.php - Remove shop widget areas from SetupPart1.php - Remove shop enqueues from ScriptsPart1b.php - Remove sepakerator-aksrt-shop from SetupPart2.php - Remove get_shop_settings facade (delete ForumShop.php, clean DomainGroups.php) - Remove ORDER_PRODUCT_SHOP constant from DomainModel.php - Remove Shop::class fallback branches from UserOrders.php, OrderCenterRest.php - Remove shipping/after-sale branches from OrderRepositoryAdmin.php, PayMenus.php, PayOrders.php - Remove shop product widgets from widgets-register.php - Remove cart page from template-registry.php, routing.php, ScriptsPart2.php - Remove cart UI from sidebar-nav.php, profile-orders.php, sidebar-orders.php - Clean up order-center.js (remove rebuy/cartUrl/receipt/after-sale handlers) - Remove aksrt_get_cart_page_url/product_page/product_edit from transactions.php - Clean up aksrt_product_cat references in Coupon.php, PayCoupons.php Shared payment/order/credits/content-pay stack fully retained. --- assets/admin/src/constants/sections/shop.js | 81 - assets/admin/src/pages/shop/ShopAfterSale.vue | 304 ---- assets/admin/src/pages/shop/ShopCoupons.vue | 215 --- assets/admin/src/pages/shop/ShopDashboard.vue | 180 --- assets/admin/src/pages/shop/ShopOrders.vue | 173 -- assets/admin/src/pages/shop/ShopProducts.vue | 147 -- assets/admin/src/pages/shop/ShopShipping.vue | 364 ----- .../admin/src/pages/shop/ShopWithdrawals.vue | 138 -- .../shop/components/AfterSaleDetailDrawer.vue | 283 ---- .../shop/components/OrderContextPanel.vue | 107 -- .../shop/components/OrderDetailDrawer.vue | 479 ------ .../shop/components/ProductEditorDrawer.vue | 523 ------- .../components/ShippingBatchActionDialog.vue | 94 -- .../shop/components/ShippingDetailDrawer.vue | 237 --- .../shop/components/WithdrawalReviewModal.vue | 68 - .../src/router/component-loaders/shop.js | 2 - assets/admin/src/router/index.js | 2 +- .../admin/src/schema/sections/shop/basic.js | 49 - .../admin/src/schema/sections/shop/contact.js | 32 - .../admin/src/schema/sections/shop/index.js | 21 - assets/admin/src/schema/sections/shop/list.js | 40 - .../schema/sections/shop/product-defaults.js | 54 - .../schema/sections/shop/shipping-config.js | 52 - .../admin/src/schema/sections/shop/single.js | 45 - .../admin/src/schema/sections/shop/timing.js | 54 - assets/css/frontend/product-archive.css | 755 --------- assets/css/routes/cart.css | 400 ----- assets/css/routes/product.css | 491 ------ assets/js/cart-checkout.js | 483 ------ assets/js/order-center.js | 227 +-- assets/js/product-archive.js | 295 ---- assets/js/product-purchase.js | 407 ----- functions.php | 36 +- inc/helpers/aksrt-commerce.php | 96 -- inc/helpers/aksrt-transactions.php | 41 +- .../user-center-renderers/profile-orders.php | 171 +- .../user-center-renderers/sidebar-nav.php | 7 - .../user-center-renderers/sidebar-orders.php | 24 - inc/modules/user-parts/template-registry.php | 11 - inc/modules/widgets-register.php | 7 +- src/AKSRT/Admin/Native/PayAfterSale.php | 357 ----- src/AKSRT/Admin/Native/PayCoupons.php | 2 +- src/AKSRT/Admin/Native/PayMenus.php | 46 +- src/AKSRT/Admin/Native/PayOrders.php | 112 -- src/AKSRT/Admin/Native/PayShipping.php | 507 ------ src/AKSRT/Admin/Schema/SchemaShop.php | 28 - src/AKSRT/Admin/Schema/SchemaShopBasic.php | 37 - src/AKSRT/Admin/Schema/SchemaShopContact.php | 30 - src/AKSRT/Admin/Schema/SchemaShopList.php | 51 - .../Schema/SchemaShopProductDefaults.php | 46 - .../Admin/Schema/SchemaShopShippingConfig.php | 41 - src/AKSRT/Admin/Schema/SchemaShopSingle.php | 39 - src/AKSRT/Admin/Schema/SchemaShopTiming.php | 38 - src/AKSRT/Admin/ShopController.php | 453 ------ .../ShopControllerCouponGatewayOperations.php | 97 -- .../ShopControllerFrontendFlowOperations.php | 717 --------- .../ShopControllerFulfillmentOperations.php | 353 ----- .../Admin/ShopControllerOrderOperations.php | 136 -- .../ShopControllerProductAdminColumns.php | 187 --- .../Admin/ShopControllerProductOperations.php | 499 ------ .../ShopControllerVariantExportOperations.php | 113 -- src/AKSRT/Core/DBMigratorPart2.php | 95 -- src/AKSRT/Core/DBMigratorPart3.php | 12 +- src/AKSRT/Core/DomainModel.php | 2 - src/AKSRT/Core/ScriptsPart1b.php | 93 +- src/AKSRT/Core/ScriptsPart2.php | 1 - src/AKSRT/Core/Setup.php | 105 -- src/AKSRT/Core/SetupPart1.php | 24 - src/AKSRT/Core/SetupPart2.php | 92 -- src/AKSRT/Payment/AfterSale.php | 1385 ----------------- src/AKSRT/Payment/Coupon.php | 8 +- src/AKSRT/Payment/Discount.php | 167 -- src/AKSRT/Payment/OrderCenterRest.php | 31 +- src/AKSRT/Payment/OrderRepositoryAdmin.php | 79 +- src/AKSRT/Payment/Shipping.php | 371 ----- src/AKSRT/Payment/ShippingPart1.php | 371 ----- src/AKSRT/Payment/ShippingPart2.php | 152 -- src/AKSRT/Payment/UserOrders.php | 44 +- src/AKSRT/Settings/DomainGroups.php | 9 - src/AKSRT/Settings/ForumShop.php | 185 --- src/AKSRT/Shop/AddressService.php | 204 --- src/AKSRT/Shop/CartService.php | 173 -- src/AKSRT/Shop/CheckoutService.php | 414 ----- src/AKSRT/Shop/Part1.php | 677 -------- src/AKSRT/Shop/Part2.php | 436 ------ src/AKSRT/Shop/Part3.php | 399 ----- src/AKSRT/Shop/ProductMetaBox.php | 341 ---- src/AKSRT/Shop/ProductReview.php | 512 ------ src/AKSRT/Shop/ProductService.php | 356 ----- src/AKSRT/Shop/ProductVariantsMetaBox.php | 188 --- src/AKSRT/Shop/Shop.php | 397 ----- src/AKSRT/Shop/TermMeta.php | 748 --------- src/AKSRT/Widgets/WidgetProductList.php | 401 ----- src/AKSRT/Widgets/WidgetProductOneline.php | 52 - src/AKSRT/Widgets/WidgetProductTab.php | 179 --- templates/pages/page-cart.php | 292 ---- templates/pages/page-product-edit.php | 218 --- templates/pages/page-product.php | 471 ------ templates/parts/shop/product-card.php | 59 - templates/parts/shop/product-cat-filter.php | 149 -- templates/shop/product-cat.php | 155 -- templates/shop/product-tag.php | 179 --- uninstall.php | 3 + 103 files changed, 46 insertions(+), 21267 deletions(-) delete mode 100644 assets/admin/src/constants/sections/shop.js delete mode 100644 assets/admin/src/pages/shop/ShopAfterSale.vue delete mode 100644 assets/admin/src/pages/shop/ShopCoupons.vue delete mode 100644 assets/admin/src/pages/shop/ShopDashboard.vue delete mode 100644 assets/admin/src/pages/shop/ShopOrders.vue delete mode 100644 assets/admin/src/pages/shop/ShopProducts.vue delete mode 100644 assets/admin/src/pages/shop/ShopShipping.vue delete mode 100644 assets/admin/src/pages/shop/ShopWithdrawals.vue delete mode 100644 assets/admin/src/pages/shop/components/AfterSaleDetailDrawer.vue delete mode 100644 assets/admin/src/pages/shop/components/OrderContextPanel.vue delete mode 100644 assets/admin/src/pages/shop/components/OrderDetailDrawer.vue delete mode 100644 assets/admin/src/pages/shop/components/ProductEditorDrawer.vue delete mode 100644 assets/admin/src/pages/shop/components/ShippingBatchActionDialog.vue delete mode 100644 assets/admin/src/pages/shop/components/ShippingDetailDrawer.vue delete mode 100644 assets/admin/src/pages/shop/components/WithdrawalReviewModal.vue delete mode 100644 assets/admin/src/router/component-loaders/shop.js delete mode 100644 assets/admin/src/schema/sections/shop/basic.js delete mode 100644 assets/admin/src/schema/sections/shop/contact.js delete mode 100644 assets/admin/src/schema/sections/shop/index.js delete mode 100644 assets/admin/src/schema/sections/shop/list.js delete mode 100644 assets/admin/src/schema/sections/shop/product-defaults.js delete mode 100644 assets/admin/src/schema/sections/shop/shipping-config.js delete mode 100644 assets/admin/src/schema/sections/shop/single.js delete mode 100644 assets/admin/src/schema/sections/shop/timing.js delete mode 100644 assets/css/frontend/product-archive.css delete mode 100644 assets/css/routes/cart.css delete mode 100644 assets/css/routes/product.css delete mode 100644 assets/js/cart-checkout.js delete mode 100644 assets/js/product-archive.js delete mode 100644 assets/js/product-purchase.js delete mode 100644 inc/helpers/aksrt-commerce.php delete mode 100644 src/AKSRT/Admin/Native/PayAfterSale.php delete mode 100644 src/AKSRT/Admin/Native/PayShipping.php delete mode 100644 src/AKSRT/Admin/Schema/SchemaShop.php delete mode 100644 src/AKSRT/Admin/Schema/SchemaShopBasic.php delete mode 100644 src/AKSRT/Admin/Schema/SchemaShopContact.php delete mode 100644 src/AKSRT/Admin/Schema/SchemaShopList.php delete mode 100644 src/AKSRT/Admin/Schema/SchemaShopProductDefaults.php delete mode 100644 src/AKSRT/Admin/Schema/SchemaShopShippingConfig.php delete mode 100644 src/AKSRT/Admin/Schema/SchemaShopSingle.php delete mode 100644 src/AKSRT/Admin/Schema/SchemaShopTiming.php delete mode 100644 src/AKSRT/Admin/ShopController.php delete mode 100644 src/AKSRT/Admin/ShopControllerCouponGatewayOperations.php delete mode 100644 src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php delete mode 100644 src/AKSRT/Admin/ShopControllerFulfillmentOperations.php delete mode 100644 src/AKSRT/Admin/ShopControllerOrderOperations.php delete mode 100644 src/AKSRT/Admin/ShopControllerProductAdminColumns.php delete mode 100644 src/AKSRT/Admin/ShopControllerProductOperations.php delete mode 100644 src/AKSRT/Admin/ShopControllerVariantExportOperations.php delete mode 100644 src/AKSRT/Payment/AfterSale.php delete mode 100644 src/AKSRT/Payment/Discount.php delete mode 100644 src/AKSRT/Payment/Shipping.php delete mode 100644 src/AKSRT/Payment/ShippingPart1.php delete mode 100644 src/AKSRT/Payment/ShippingPart2.php delete mode 100644 src/AKSRT/Settings/ForumShop.php delete mode 100644 src/AKSRT/Shop/AddressService.php delete mode 100644 src/AKSRT/Shop/CartService.php delete mode 100644 src/AKSRT/Shop/CheckoutService.php delete mode 100644 src/AKSRT/Shop/Part1.php delete mode 100644 src/AKSRT/Shop/Part2.php delete mode 100644 src/AKSRT/Shop/Part3.php delete mode 100644 src/AKSRT/Shop/ProductMetaBox.php delete mode 100644 src/AKSRT/Shop/ProductReview.php delete mode 100644 src/AKSRT/Shop/ProductService.php delete mode 100644 src/AKSRT/Shop/ProductVariantsMetaBox.php delete mode 100644 src/AKSRT/Shop/Shop.php delete mode 100644 src/AKSRT/Shop/TermMeta.php delete mode 100644 src/AKSRT/Widgets/WidgetProductList.php delete mode 100644 src/AKSRT/Widgets/WidgetProductOneline.php delete mode 100644 src/AKSRT/Widgets/WidgetProductTab.php delete mode 100644 templates/pages/page-cart.php delete mode 100644 templates/pages/page-product-edit.php delete mode 100644 templates/pages/page-product.php delete mode 100644 templates/parts/shop/product-card.php delete mode 100644 templates/parts/shop/product-cat-filter.php delete mode 100644 templates/shop/product-cat.php delete mode 100644 templates/shop/product-tag.php diff --git a/assets/admin/src/constants/sections/shop.js b/assets/admin/src/constants/sections/shop.js deleted file mode 100644 index edc6fb74..00000000 --- a/assets/admin/src/constants/sections/shop.js +++ /dev/null @@ -1,81 +0,0 @@ -import { renderFaIcon, schemaPage } from './helpers' - -// Shop SPA = config only. Ops moved to native WP menus (Phase 4). -export const shopSection = { - key: 'shop', - label: '商城&商品', - icon: renderFaIcon('fas fa-shopping-cart'), - route: '/shop', - children: [ - schemaPage({ - key: 'shop-basic', - name: 'ShopBasic', - label: '商城基础', - route: '/shop/basic', - title: '商城基础', - desc: '商城总开关、首页链接、面包屑和 URL 别名设置。', - canonicalDomain: 'shop', - section: 'basic', - }), - schemaPage({ - key: 'shop-list', - name: 'ShopList', - label: '商品列表', - route: '/shop/list', - title: '商品列表', - desc: '商品列表排序、分页、卡片样式和销量显示配置。', - canonicalDomain: 'shop', - section: 'list', - }), - schemaPage({ - key: 'shop-single', - name: 'ShopSingle', - label: '商品详情', - route: '/shop/single', - title: '商品详情', - desc: '商品详情页布局、相关推荐和底部内容设置。', - canonicalDomain: 'shop', - section: 'single', - }), - schemaPage({ - key: 'shop-product-defaults', - name: 'ShopProductDefaults', - label: '商品默认参数', - route: '/shop/product-defaults', - title: '商品默认参数', - desc: '主题级商品参数默认值,作为参数继承链的根。', - canonicalDomain: 'shop', - section: 'product-defaults', - }), - schemaPage({ - key: 'shop-contact', - name: 'ShopContact', - label: '客服配置', - route: '/shop/contact', - title: '客服配置', - desc: '商品详情页客服按钮和联系方式配置。', - canonicalDomain: 'shop', - section: 'contact', - }), - schemaPage({ - key: 'shop-shipping-config', - name: 'ShopShippingConfig', - label: '物流配置', - route: '/shop/shipping-config', - title: '物流配置', - desc: '快递查询接口、物流公司列表和收货地址设置。', - canonicalDomain: 'shop', - section: 'shipping-config', - }), - schemaPage({ - key: 'shop-timing', - name: 'ShopTiming', - label: '时效与售后原因', - route: '/shop/timing', - title: '时效与售后原因', - desc: '确认收货、评价、退货时效和售后原因配置。', - canonicalDomain: 'shop', - section: 'timing', - }), - ], -} diff --git a/assets/admin/src/pages/shop/ShopAfterSale.vue b/assets/admin/src/pages/shop/ShopAfterSale.vue deleted file mode 100644 index 816da3f7..00000000 --- a/assets/admin/src/pages/shop/ShopAfterSale.vue +++ /dev/null @@ -1,304 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/ShopCoupons.vue b/assets/admin/src/pages/shop/ShopCoupons.vue deleted file mode 100644 index 9b69ca6a..00000000 --- a/assets/admin/src/pages/shop/ShopCoupons.vue +++ /dev/null @@ -1,215 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/ShopDashboard.vue b/assets/admin/src/pages/shop/ShopDashboard.vue deleted file mode 100644 index 43a48f4b..00000000 --- a/assets/admin/src/pages/shop/ShopDashboard.vue +++ /dev/null @@ -1,180 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/ShopOrders.vue b/assets/admin/src/pages/shop/ShopOrders.vue deleted file mode 100644 index 7f14c2ba..00000000 --- a/assets/admin/src/pages/shop/ShopOrders.vue +++ /dev/null @@ -1,173 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/ShopProducts.vue b/assets/admin/src/pages/shop/ShopProducts.vue deleted file mode 100644 index 91c8040e..00000000 --- a/assets/admin/src/pages/shop/ShopProducts.vue +++ /dev/null @@ -1,147 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/ShopShipping.vue b/assets/admin/src/pages/shop/ShopShipping.vue deleted file mode 100644 index 58971363..00000000 --- a/assets/admin/src/pages/shop/ShopShipping.vue +++ /dev/null @@ -1,364 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/ShopWithdrawals.vue b/assets/admin/src/pages/shop/ShopWithdrawals.vue deleted file mode 100644 index d831e556..00000000 --- a/assets/admin/src/pages/shop/ShopWithdrawals.vue +++ /dev/null @@ -1,138 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/components/AfterSaleDetailDrawer.vue b/assets/admin/src/pages/shop/components/AfterSaleDetailDrawer.vue deleted file mode 100644 index 1d3b888a..00000000 --- a/assets/admin/src/pages/shop/components/AfterSaleDetailDrawer.vue +++ /dev/null @@ -1,283 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/components/OrderContextPanel.vue b/assets/admin/src/pages/shop/components/OrderContextPanel.vue deleted file mode 100644 index b5846b1b..00000000 --- a/assets/admin/src/pages/shop/components/OrderContextPanel.vue +++ /dev/null @@ -1,107 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/components/OrderDetailDrawer.vue b/assets/admin/src/pages/shop/components/OrderDetailDrawer.vue deleted file mode 100644 index 0e0d0652..00000000 --- a/assets/admin/src/pages/shop/components/OrderDetailDrawer.vue +++ /dev/null @@ -1,479 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/components/ProductEditorDrawer.vue b/assets/admin/src/pages/shop/components/ProductEditorDrawer.vue deleted file mode 100644 index e8d020cf..00000000 --- a/assets/admin/src/pages/shop/components/ProductEditorDrawer.vue +++ /dev/null @@ -1,523 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/components/ShippingBatchActionDialog.vue b/assets/admin/src/pages/shop/components/ShippingBatchActionDialog.vue deleted file mode 100644 index b94b8242..00000000 --- a/assets/admin/src/pages/shop/components/ShippingBatchActionDialog.vue +++ /dev/null @@ -1,94 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/components/ShippingDetailDrawer.vue b/assets/admin/src/pages/shop/components/ShippingDetailDrawer.vue deleted file mode 100644 index 1c135f56..00000000 --- a/assets/admin/src/pages/shop/components/ShippingDetailDrawer.vue +++ /dev/null @@ -1,237 +0,0 @@ - - - - - diff --git a/assets/admin/src/pages/shop/components/WithdrawalReviewModal.vue b/assets/admin/src/pages/shop/components/WithdrawalReviewModal.vue deleted file mode 100644 index ca23ce13..00000000 --- a/assets/admin/src/pages/shop/components/WithdrawalReviewModal.vue +++ /dev/null @@ -1,68 +0,0 @@ - - - diff --git a/assets/admin/src/router/component-loaders/shop.js b/assets/admin/src/router/component-loaders/shop.js deleted file mode 100644 index 1ec6c4e6..00000000 --- a/assets/admin/src/router/component-loaders/shop.js +++ /dev/null @@ -1,2 +0,0 @@ -// Shop ops Vue pages removed (Phase 4). Config uses SchemaPage. -export const shopPageLoaders = {} diff --git a/assets/admin/src/router/index.js b/assets/admin/src/router/index.js index 4645f8a6..0d6a8833 100644 --- a/assets/admin/src/router/index.js +++ b/assets/admin/src/router/index.js @@ -73,7 +73,7 @@ function createSectionRedirectRoutes() { /** * Old SPA ops routes → native WP admin pages (Phase 4 transition). - * Bookmarks under #/shop/products etc. land on NativeOpsRedirect. + * Bookmarks under order-related paths land on NativeOpsRedirect. */ const NATIVE_OPS_REDIRECTS = [ { path: '/payment/charge-card', title: '充值卡', nativeAdminPath: 'admin.php?page=aksrt_charge_cards' }, diff --git a/assets/admin/src/schema/sections/shop/basic.js b/assets/admin/src/schema/sections/shop/basic.js deleted file mode 100644 index 2d365640..00000000 --- a/assets/admin/src/schema/sections/shop/basic.js +++ /dev/null @@ -1,49 +0,0 @@ -export const basicSchema = { - key: 'shop.basic', - title: '商城基础', - icon: 'fas fa-store', - description: '商城总开关、首页链接、面包屑和 URL 别名设置。', - defaults: { - shop_s: true, - shop_home_url: '', - shop_breadcrumbs_s: true, - shop_breadcrumbs_home: true, - shop_breadcrumbs_shop_home: false, - shop_breadcrumbs_shop_home_name: '商城首页', - shop_product_rewrite_slug: 'product', - shop_cart_rewrite_slug: 'cart', - shop_rewrite_suffix_html_s: true, - }, - groups: [ - { - key: 'general', - title: '商城开关', - icon: 'fas fa-power-off', - fields: [ - { key: 'shop_s', label: '启用商城', type: 'switch', default: true }, - { key: 'shop_home_url', label: '商城首页链接', type: 'input', default: '', help: '留空使用默认商城页面', dependsOn: { key: 'shop_s', value: true } }, - ], - }, - { - key: 'breadcrumbs', - title: '面包屑', - icon: 'fas fa-angle-double-right', - fields: [ - { key: 'shop_breadcrumbs_s', label: '显示面包屑', type: 'switch', default: true, dependsOn: { key: 'shop_s', value: true } }, - { key: 'shop_breadcrumbs_home', label: '显示首页链接', type: 'switch', default: true, dependsOn: { key: 'shop_breadcrumbs_s', value: true } }, - { key: 'shop_breadcrumbs_shop_home', label: '显示商城首页', type: 'switch', default: false, dependsOn: { key: 'shop_breadcrumbs_s', value: true } }, - { key: 'shop_breadcrumbs_shop_home_name', label: '商城首页名称', type: 'input', default: '商城首页', dependsOn: { key: 'shop_breadcrumbs_s', value: true } }, - ], - }, - { - key: 'rewrite', - title: 'URL 别名', - icon: 'fas fa-link', - fields: [ - { key: 'shop_product_rewrite_slug', label: '商品 URL 别名', type: 'input', default: 'product', dependsOn: { key: 'shop_s', value: true } }, - { key: 'shop_cart_rewrite_slug', label: '购物车 URL 别名', type: 'input', default: 'cart', dependsOn: { key: 'shop_s', value: true } }, - { key: 'shop_rewrite_suffix_html_s', label: '.html 伪静态后缀', type: 'switch', default: true, dependsOn: { key: 'shop_s', value: true } }, - ], - }, - ], -} diff --git a/assets/admin/src/schema/sections/shop/contact.js b/assets/admin/src/schema/sections/shop/contact.js deleted file mode 100644 index 6a1fc171..00000000 --- a/assets/admin/src/schema/sections/shop/contact.js +++ /dev/null @@ -1,32 +0,0 @@ -export const contactSchema = { - key: 'shop.contact', - title: '客服配置', - icon: 'fas fa-headset', - description: '商品详情页客服按钮和联系方式配置。', - defaults: { - shop_contact_msg_s: true, - shop_contact_name: '联系客服', - shop_contact_note: '', - shop_contact_more: '', - }, - groups: [ - { - key: 'button', - title: '客服按钮', - icon: 'fas fa-comment-dots', - fields: [ - { key: 'shop_contact_msg_s', label: '显示客服按钮', type: 'switch', default: true }, - { key: 'shop_contact_name', label: '按钮文案', type: 'input', default: '联系客服', dependsOn: { key: 'shop_contact_msg_s', value: true } }, - { key: 'shop_contact_note', label: '客服备注', type: 'textarea', default: '', dependsOn: { key: 'shop_contact_msg_s', value: true } }, - ], - }, - { - key: 'channels', - title: '联系方式', - icon: 'fas fa-address-book', - fields: [ - { key: 'shop_contact_more', label: '多客服联系方式', type: 'textarea', default: '', help: '每行一个:名称|图标|链接|二维码图片 URL' }, - ], - }, - ], -} diff --git a/assets/admin/src/schema/sections/shop/index.js b/assets/admin/src/schema/sections/shop/index.js deleted file mode 100644 index eb8f945e..00000000 --- a/assets/admin/src/schema/sections/shop/index.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Shop & Product domain schemas. - */ - -import { basicSchema } from './basic.js' -import { listSchema } from './list.js' -import { contactSchema } from './contact.js' -import { singleSchema } from './single.js' -import { productDefaultsSchema } from './product-defaults.js' -import { shippingConfigSchema } from './shipping-config.js' -import { timingSchema } from './timing.js' - -export const shopSchemas = { - basic: basicSchema, - list: listSchema, - contact: contactSchema, - single: singleSchema, - productDefaults: productDefaultsSchema, - shippingConfig: shippingConfigSchema, - timing: timingSchema, -} diff --git a/assets/admin/src/schema/sections/shop/list.js b/assets/admin/src/schema/sections/shop/list.js deleted file mode 100644 index 6074ddd7..00000000 --- a/assets/admin/src/schema/sections/shop/list.js +++ /dev/null @@ -1,40 +0,0 @@ -export const listSchema = { - key: 'shop.list', - title: '商品列表', - icon: 'fas fa-th-list', - description: '商品列表排序、分页、卡片样式和销量显示配置。', - defaults: { - shop_list_orderby: 'date', - shop_list_count: 12, - shop_list_paging: 'number', - shop_list_ias_s: false, - shop_list_ias_max: 5, - shop_list_style: 'default', - shop_sales_show: 'show', - shop_sales_show_min: 10, - }, - groups: [ - { - key: 'pagination', - title: '列表分页', - icon: 'fas fa-layer-group', - fields: [ - { key: 'shop_list_orderby', label: '默认排序', type: 'select', default: 'date', options: [{ label: '最新发布', value: 'date' }, { label: '销量最高', value: 'sales' }, { label: '价格最低', value: 'price_asc' }, { label: '价格最高', value: 'price_desc' }, { label: '评价最多', value: 'comments' }] }, - { key: 'shop_list_count', label: '每页数量', type: 'number', default: 12, min: 4, max: 100 }, - { key: 'shop_list_paging', label: '翻页模式', type: 'radio', default: 'number', options: [{ label: '数字翻页', value: 'number' }, { label: 'AJAX 加载', value: 'ajax' }] }, - { key: 'shop_list_ias_s', label: '滚动自动加载', type: 'switch', default: false, dependsOn: { key: 'shop_list_paging', value: 'ajax' } }, - { key: 'shop_list_ias_max', label: '自动加载页数', type: 'number', default: 5, min: 1, max: 50, dependsOn: { key: 'shop_list_ias_s', value: true } }, - ], - }, - { - key: 'style', - title: '列表样式', - icon: 'fas fa-palette', - fields: [ - { key: 'shop_list_style', label: '卡片样式', type: 'select', default: 'default', options: [{ label: '默认', value: 'default' }, { label: '竖向大卡片', value: 'card-lg' }, { label: '横向小卡片', value: 'card-sm' }] }, - { key: 'shop_sales_show', label: '销量显示', type: 'select', default: 'show', options: [{ label: '显示', value: 'show' }, { label: '不显示', value: 'hide' }, { label: '超量显示', value: 'threshold' }] }, - { key: 'shop_sales_show_min', label: '起显阈值', type: 'number', default: 10, min: 0, max: 99999, help: '销量超过此数字才显示', dependsOn: { key: 'shop_sales_show', value: 'threshold' } }, - ], - }, - ], -} diff --git a/assets/admin/src/schema/sections/shop/product-defaults.js b/assets/admin/src/schema/sections/shop/product-defaults.js deleted file mode 100644 index 566b6bbc..00000000 --- a/assets/admin/src/schema/sections/shop/product-defaults.js +++ /dev/null @@ -1,54 +0,0 @@ -export const productDefaultsSchema = { - key: 'shop.product-defaults', - title: '商品默认参数', - icon: 'fas fa-sliders-h', - description: '主题级商品参数默认值,作为「商品→分类→主题设置」参数继承链的根。', - defaults: { - shop_limit_buy: -1, - shop_shipping_fee: 'free', - shop_guest_buy: false, - shop_email_fill: false, - shop_rebate: true, - shop_after_sale: 'refund_return', - shop_service: '', - shop_product_params_default: '', - }, - groups: [ - { - key: 'policy', - title: '购买策略', - icon: 'fas fa-shopping-bag', - fields: [ - { key: 'shop_limit_buy', label: '默认限购', type: 'number', default: -1, min: -1, max: 9999, help: '-1 不限,0 禁购,正数为限购数量' }, - { key: 'shop_guest_buy', label: '免登录购买', type: 'switch', default: false }, - { key: 'shop_email_fill', label: '邮箱必留', type: 'switch', default: false }, - { key: 'shop_rebate', label: '允许返佣', type: 'switch', default: true }, - ], - }, - { - key: 'shipping', - title: '运费默认', - icon: 'fas fa-truck', - fields: [ - { key: 'shop_shipping_fee', label: '默认运费类型', type: 'select', default: 'free', options: [{ label: '包邮', value: 'free' }, { label: '固定运费', value: 'fixed' }, { label: '按金额计算', value: 'amount' }] }, - ], - }, - { - key: 'afterSale', - title: '售后与服务', - icon: 'fas fa-undo', - fields: [ - { key: 'shop_after_sale', label: '默认售后政策', type: 'select', default: 'refund_return', options: [{ label: '仅退款', value: 'refund' }, { label: '退货退款', value: 'refund_return' }, { label: '不支持售后', value: 'none' }] }, - { key: 'shop_service', label: '服务保障', type: 'textarea', default: '', help: '每行一项:名称|说明|图标图片 URL' }, - ], - }, - { - key: 'params', - title: '默认参数模板', - icon: 'fas fa-list-alt', - fields: [ - { key: 'shop_product_params_default', label: '商品参数模板', type: 'textarea', default: '', help: '每行一个参数:参数名|参数值,新建商品自动带入' }, - ], - }, - ], -} diff --git a/assets/admin/src/schema/sections/shop/shipping-config.js b/assets/admin/src/schema/sections/shop/shipping-config.js deleted file mode 100644 index 9e41c82d..00000000 --- a/assets/admin/src/schema/sections/shop/shipping-config.js +++ /dev/null @@ -1,52 +0,0 @@ -export const shippingConfigSchema = { - key: 'shop.shipping-config', - title: '物流配置', - icon: 'fas fa-shipping-fast', - description: '快递查询接口、物流公司列表和收货地址设置。', - defaults: { - shop_manual_address: false, - shop_express_api_sdk: 'kuaidi100', - shop_express_query_interval: 60, - shop_express_kdniao_opt: '', - shop_express_kuaidi100_opt: '', - shop_express_aliyun_opt: '', - shop_shipping_company: '', - }, - groups: [ - { - key: 'address', - title: '收货地址', - icon: 'fas fa-map-marker-alt', - fields: [ - { key: 'shop_manual_address', label: '手动填写收货地址', type: 'switch', default: false, help: '开启后用户可手动输入地址而非选择地址簿' }, - ], - }, - { - key: 'express', - title: '快递查询', - icon: 'fas fa-search', - fields: [ - { key: 'shop_express_api_sdk', label: '快递查询接口', type: 'select', default: 'kuaidi100', options: [{ label: '快递100', value: 'kuaidi100' }, { label: '阿里云', value: 'aliyun' }, { label: '快递鸟', value: 'kdniao' }] }, - { key: 'shop_express_query_interval', label: '查询刷新间隔(分钟)', type: 'number', default: 60, min: 30, max: 240 }, - ], - }, - { - key: 'credentials', - title: '接口凭据', - icon: 'fas fa-key', - fields: [ - { key: 'shop_express_kdniao_opt', label: '快递鸟凭据', type: 'textarea', default: '', help: 'AppID|APIKey', dependsOn: { key: 'shop_express_api_sdk', value: 'kdniao' } }, - { key: 'shop_express_kuaidi100_opt', label: '快递100凭据', type: 'textarea', default: '', help: 'Key|Customer', dependsOn: { key: 'shop_express_api_sdk', value: 'kuaidi100' } }, - { key: 'shop_express_aliyun_opt', label: '阿里云凭据', type: 'textarea', default: '', help: 'AppCode', dependsOn: { key: 'shop_express_api_sdk', value: 'aliyun' } }, - ], - }, - { - key: 'companies', - title: '物流公司', - icon: 'fas fa-building', - fields: [ - { key: 'shop_shipping_company', label: '物流公司列表', type: 'textarea', default: '', help: '每行一个:编码|名称,如 SF|顺丰速运' }, - ], - }, - ], -} diff --git a/assets/admin/src/schema/sections/shop/single.js b/assets/admin/src/schema/sections/shop/single.js deleted file mode 100644 index e2056c55..00000000 --- a/assets/admin/src/schema/sections/shop/single.js +++ /dev/null @@ -1,45 +0,0 @@ -export const singleSchema = { - key: 'shop.single', - title: '商品详情', - icon: 'fas fa-box-open', - description: '商品详情页布局、相关推荐和底部内容设置。', - defaults: { - shop_content_layout: 'adapt', - shop_content_show_bg: false, - shop_single_tab: '', - shop_content_after: '', - shop_single_related_s: true, - shop_single_related_opt: '', - shop_main_image_default: '', - }, - groups: [ - { - key: 'layout', - title: '详情页布局', - icon: 'fas fa-columns', - fields: [ - { key: 'shop_content_layout', label: '内容布局', type: 'select', default: 'adapt', options: [{ label: '适应宽度', value: 'adapt' }, { label: '全屏', value: 'full' }, { label: '适应宽度+侧栏', value: 'sidebar' }] }, - { key: 'shop_content_show_bg', label: '详情背景盒子', type: 'switch', default: false }, - { key: 'shop_single_tab', label: '自定义 TAB 栏目', type: 'textarea', default: '', help: '每行一个 TAB:标题|内容 HTML' }, - { key: 'shop_content_after', label: '详情页底部内容', type: 'textarea', default: '', help: '追加在每个商品详情页底部的 HTML 内容' }, - ], - }, - { - key: 'related', - title: '相关推荐', - icon: 'fas fa-th', - fields: [ - { key: 'shop_single_related_s', label: '显示相关推荐', type: 'switch', default: true }, - { key: 'shop_single_related_opt', label: '推荐配置', type: 'textarea', default: '', help: '标题|关联类型(cat/discount/tag)|排序|数量|翻页|样式', dependsOn: { key: 'shop_single_related_s', value: true } }, - ], - }, - { - key: 'image', - title: '商品图片', - icon: 'fas fa-image', - fields: [ - { key: 'shop_main_image_default', label: '默认商品主图', type: 'media', default: '' }, - ], - }, - ], -} diff --git a/assets/admin/src/schema/sections/shop/timing.js b/assets/admin/src/schema/sections/shop/timing.js deleted file mode 100644 index 76c40067..00000000 --- a/assets/admin/src/schema/sections/shop/timing.js +++ /dev/null @@ -1,54 +0,0 @@ -export const timingSchema = { - key: 'shop.timing', - title: '时效与售后原因', - icon: 'fas fa-clock', - description: '确认收货、评价、退货时效和售后原因配置。', - defaults: { - shop_order_receipt_max_day: 15, - shop_comment_max_day: 7, - shop_comment_placeholder: '', - shop_comment_img_num: 3, - shop_after_sale_return_express_max_day: 7, - shop_after_sale_reason_refund: '', - shop_after_sale_reason_return: '', - shop_author_show: true, - }, - groups: [ - { - key: 'receipt', - title: '确认收货', - icon: 'fas fa-check-circle', - fields: [ - { key: 'shop_order_receipt_max_day', label: '自动确认收货(天)', type: 'number', default: 15, min: 1, max: 60, help: '发货后超过此天数自动确认收货' }, - ], - }, - { - key: 'comment', - title: '评价时效', - icon: 'fas fa-star', - fields: [ - { key: 'shop_comment_max_day', label: '评价时效(天)', type: 'number', default: 7, min: 1, max: 60, help: '确认收货后超过此天数自动好评' }, - { key: 'shop_comment_placeholder', label: '评价占位文案', type: 'input', default: '' }, - { key: 'shop_comment_img_num', label: '评价图片数', type: 'number', default: 3, min: 0, max: 9 }, - ], - }, - { - key: 'afterSale', - title: '售后退货', - icon: 'fas fa-undo-alt', - fields: [ - { key: 'shop_after_sale_return_express_max_day', label: '退货快递时效(天)', type: 'number', default: 7, min: 1, max: 30 }, - { key: 'shop_after_sale_reason_refund', label: '仅退款原因', type: 'textarea', default: '', help: '每行一个可选原因' }, - { key: 'shop_after_sale_reason_return', label: '退货退款原因', type: 'textarea', default: '', help: '每行一个可选原因' }, - ], - }, - { - key: 'author', - title: '商家信息', - icon: 'fas fa-user-tie', - fields: [ - { key: 'shop_author_show', label: '显示商家信息', type: 'switch', default: true }, - ], - }, - ], -} diff --git a/assets/css/frontend/product-archive.css b/assets/css/frontend/product-archive.css deleted file mode 100644 index 8ae89bc3..00000000 --- a/assets/css/frontend/product-archive.css +++ /dev/null @@ -1,755 +0,0 @@ -/** - * AKSRT Product Archive — 商品归档页样式 - * - * 商品分类页、标签页共用样式。 - * 卡片网格、价格徽章、空状态。 - * - * Tokens: 颜色 / 间距 / 圆角 / 阴影 全部来自 core/tokens.css - */ - -/* ================================================ - * 页面头:标题 + 计数 - * ================================================ */ -.aksrt-product-archive { - padding-top: var(--space-6); - padding-bottom: var(--space-9); -} - -.aksrt-product-archive__header { - margin-bottom: var(--space-6); - padding: var(--space-5); - background: var(--surface-1, #fff); - border-radius: var(--radius-lg, 8px); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); -} - -.aksrt-product-archive__title { - margin: 0 0 4px; - font-size: 22px; - font-weight: 700; - color: var(--color-ink); - display: flex; - align-items: center; - gap: 8px; -} - -.aksrt-product-archive__title i { - color: var(--color-primary, var(--color-primary)); -} - -.aksrt-product-archive__desc { - margin: 0 0 8px; - font-size: 14px; - color: var(--color-ink-3); - line-height: 1.6; -} - -.aksrt-product-archive__count { - margin: 0; - font-size: 13px; - color: var(--color-ink-4, #8A8F98); -} - - -.aksrt-product-archive__eyebrow { - display: inline-flex; - align-items: center; - min-height: 28px; - padding: 0 12px; - margin-bottom: 10px; - border-radius: 999px; - background: var(--surface-2, #f4f5f7); - color: var(--color-ink-3); - font-size: 12px; - font-weight: 600; -} -.aksrt-product-archive__grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); - gap: var(--space-5); - margin-bottom: var(--space-8); -} - -/* ================================================ - * 商品卡片 - * ================================================ */ -.aksrt-product-card { - display: flex; - flex-direction: column; - background: var(--surface-1, #fff); - border: 1px solid var(--color-line); - border-radius: var(--radius-lg, 8px); - overflow: hidden; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04); - transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; -} - -.aksrt-product-card:hover { - transform: translateY(-3px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); -} - -.aksrt-product-card__link { - display: flex; - flex-direction: column; - flex: 1; - text-decoration: none; - color: inherit; -} - -/* 商品图片 */ -.aksrt-product-card__image { - position: relative; - aspect-ratio: 4 / 3; - background-color: var(--surface-2, #F4F5F7); - overflow: hidden; -} - -.aksrt-product-card__image img { - width: 100%; - height: 100%; - object-fit: cover; - transition: transform 0.3s ease; -} - -.aksrt-product-card:hover .aksrt-product-card__image img { - transform: scale(1.02); -} - -.aksrt-product-card__image--placeholder { - display: flex; - align-items: center; - justify-content: center; - color: var(--color-ink-4, #8A8F98); - font-size: 32px; -} - -/* 卡片正文 */ -.aksrt-product-card__body { - display: flex; - flex-direction: column; - gap: var(--space-2); - padding: var(--space-4); - flex: 1; -} - -.aksrt-product-card__title { - margin: 0; - font-size: 15px; - font-weight: 600; - line-height: 1.4; - color: var(--color-ink); - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.aksrt-product-card__excerpt { - margin: 0; - font-size: 13px; - color: var(--color-ink-3); - line-height: 1.5; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -/* 底部:价格 + 销量 */ -.aksrt-product-card__footer { - display: flex; - align-items: baseline; - justify-content: space-between; - margin-top: auto; - padding-top: var(--space-3); - border-top: 1px solid var(--color-line-soft, #F0F1F3); -} - -.aksrt-product-card__price { - font-size: 18px; - font-weight: 700; - color: var(--color-error, #EF4444); -} - -.aksrt-product-card__sales { - font-size: 12px; - color: var(--color-ink-4, #8A8F98); -} - -/* ================================================ - * 分页 - * ================================================ */ -.aksrt-product-archive__pagination { - display: flex; - justify-content: center; - padding: var(--space-5) 0; -} - -.aksrt-product-archive__pagination .page-numbers { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 36px; - height: 36px; - padding: 0 8px; - margin: 0 4px; - border: 1px solid var(--color-line); - border-radius: var(--radius, 10px); - background: var(--surface-1, #fff); - color: var(--color-ink-2); - font-size: 14px; - text-decoration: none; - transition: all 0.15s ease; -} - -.aksrt-product-archive__pagination .page-numbers:hover { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - color: var(--color-primary, var(--color-primary)); -} - -.aksrt-product-archive__pagination .page-numbers.current { - background: var(--color-primary, var(--color-primary)); - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - color: #fff; -} - -/* ================================================ - * 空状态 - * ================================================ */ -.aksrt-product-archive__empty { - text-align: center; - padding: var(--space-9) var(--space-5); - color: var(--color-ink-3); -} - -.aksrt-product-archive__empty i { - font-size: 48px; - color: var(--color-ink-4, #8A8F98); - margin-bottom: var(--space-4); - display: block; -} - -.aksrt-product-archive__empty p { - margin: 0; - font-size: 15px; -} - -/* ================================================ - * 响应式 - * ================================================ */ -@media (max-width: 640px) { - .aksrt-product-archive__grid { - grid-template-columns: repeat(2, 1fr); - gap: var(--space-3); - } - - .aksrt-product-card__body { - padding: var(--space-3); - } - - .aksrt-product-card__title { - font-size: 14px; - } - - .aksrt-product-card__price { - font-size: 16px; - } -} - -@media (max-width: 380px) { - .aksrt-product-archive__grid { - grid-template-columns: 1fr; - } -} - -/* ================================================ - * 产品详情页样式补充 - * ================================================ */ -.aksrt-product { - padding-top: var(--space-6); - padding-bottom: var(--space-9); -} - -.aksrt-product__layout { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--space-7); - margin-bottom: var(--space-7); -} - -.aksrt-product__image { - border-radius: var(--radius-lg, 8px); - overflow: hidden; - background: var(--surface-2, #F4F5F7); -} - -.aksrt-product__img { - width: 100%; - display: block; -} - -.aksrt-product__img-placeholder { - aspect-ratio: 1; - display: flex; - align-items: center; - justify-content: center; - color: var(--color-ink-4, #8A8F98); - font-size: 64px; -} - -.aksrt-product__info { - display: flex; - flex-direction: column; - gap: var(--space-4); -} - -.aksrt-product__title { - margin: 0; - font-size: 24px; - font-weight: 700; - color: var(--color-ink); - line-height: 1.3; -} - -.aksrt-product__price-row { - display: flex; - align-items: baseline; - gap: var(--space-3); -} - -.aksrt-product__price { - font-size: 28px; - font-weight: 700; - color: var(--color-error, #EF4444); -} - -.aksrt-product__sales { - font-size: 13px; - color: var(--color-ink-4, #8A8F98); -} - -.aksrt-product__excerpt { - margin: 0; - font-size: 14px; - color: var(--color-ink-3); - line-height: 1.6; -} - -.aksrt-product__meta { - font-size: 14px; - color: var(--color-ink-2); -} - -.aksrt-product__meta i { - margin-right: 4px; -} - -.aksrt-product__actions { - margin-top: var(--space-4); -} - -.aksrt-product__buy-btn { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 12px 32px; - border: none; - border-radius: var(--radius, 10px); - background: var(--color-primary, var(--color-primary)); - color: #fff; - font-size: 16px; - font-weight: 600; - cursor: pointer; - transition: opacity 0.15s; -} - -.aksrt-product__buy-btn:hover { - opacity: 0.85; -} - -.aksrt-product__detail { - padding: var(--space-6); - background: var(--surface-1, #fff); - border-radius: var(--radius-lg, 8px); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); -} - -.aksrt-product__detail-title { - margin: 0 0 var(--space-4); - font-size: 18px; - font-weight: 600; - color: var(--color-ink); -} - -.aksrt-product__detail-content { - font-size: 15px; - line-height: 1.8; - color: var(--color-ink-2); -} - -.aksrt-product__detail-content img { - max-width: 100%; - border-radius: var(--radius, 10px); -} - -.aksrt-product__empty { - text-align: center; - padding: var(--space-10) var(--space-5); - color: var(--color-ink-3); -} - -.aksrt-product__empty i { - display: block; - margin-bottom: var(--space-4); -} - -.aksrt-product__empty p { - margin: 0; - font-size: 16px; -} - -/* 产品详情页响应式 */ -@media (max-width: 767px) { - .aksrt-product__layout { - grid-template-columns: 1fr; - gap: var(--space-5); - } - - .aksrt-product__title { - font-size: 20px; - } - - .aksrt-product__price { - font-size: 24px; - } -} - -/* ================================================ - * 购物车页样式 - * ================================================ */ -.aksrt-cart { - padding-top: var(--space-6); - padding-bottom: var(--space-9); -} - -.aksrt-cart__title { - margin: 0 0 var(--space-6); - font-size: 24px; - font-weight: 700; - color: var(--color-ink); -} - -.aksrt-cart__layout { - display: grid; - grid-template-columns: 1fr 320px; - gap: var(--space-6); -} - -.aksrt-cart__items { - min-height: 200px; -} - -.aksrt-cart__item { - display: flex; - align-items: center; - gap: var(--space-4); - padding: var(--space-4); - background: var(--surface-1, #fff); - border: 1px solid var(--color-line); - border-radius: var(--radius, 10px); - margin-bottom: var(--space-3); -} - -.aksrt-cart__item-img { - width: 80px; - height: 80px; - object-fit: cover; - border-radius: var(--radius-sm, 6px); -} - -.aksrt-cart__item-info { - flex: 1; - min-width: 0; -} - -.aksrt-cart__item-info h4 { - margin: 0 0 4px; - font-size: 15px; - font-weight: 500; - color: var(--color-ink); -} - -.aksrt-cart__item-price { - font-size: 14px; - color: var(--color-ink-3); -} - -.aksrt-cart__item-qty { - display: flex; - align-items: center; - gap: var(--space-2); -} - -.aksrt-cart__qty-btn { - width: 28px; - height: 28px; - border: 1px solid var(--color-line); - border-radius: var(--radius-sm, 6px); - background: var(--surface-1, #fff); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 14px; -} - -.aksrt-cart__qty-btn:hover { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); -} - -.aksrt-cart__item-subtotal { - font-weight: 600; - color: var(--color-error, #EF4444); - min-width: 80px; - text-align: right; -} - -.aksrt-cart__remove-btn { - width: 32px; - height: 32px; - border: none; - background: none; - cursor: pointer; - color: var(--color-ink-4, #8A8F98); - display: flex; - align-items: center; - justify-content: center; - border-radius: var(--radius-sm, 6px); -} - -.aksrt-cart__remove-btn:hover { - background: var(--surface-2, #F4F5F7); - color: var(--color-error, #EF4444); -} - -.aksrt-cart__sidebar { - position: sticky; - top: 24px; -} - -.aksrt-cart__summary { - padding: var(--space-5); - background: var(--surface-1, #fff); - border: 1px solid var(--color-line); - border-radius: var(--radius-lg, 8px); -} - -.aksrt-cart__summary-title { - margin: 0 0 var(--space-4); - font-size: 16px; - font-weight: 600; - color: var(--color-ink); -} - -.aksrt-cart__summary-row { - display: flex; - justify-content: space-between; - margin-bottom: var(--space-3); - font-size: 14px; - color: var(--color-ink-2); -} - -.aksrt-cart__summary-total { - display: flex; - justify-content: space-between; - padding-top: var(--space-3); - border-top: 1px solid var(--color-line); - margin-bottom: var(--space-5); - font-weight: 600; -} - -.aksrt-cart__total-price { - font-size: 20px; - color: var(--color-error, #EF4444); -} - -.aksrt-cart__checkout-btn { - width: 100%; - padding: 12px; - border: none; - border-radius: var(--radius, 10px); - background: var(--color-primary, var(--color-primary)); - color: #fff; - font-size: 16px; - font-weight: 600; - cursor: pointer; - transition: opacity 0.15s; -} - -.aksrt-cart__checkout-btn:hover { - opacity: 0.85; -} - -.aksrt-cart__checkout-btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.aksrt-cart__empty { - text-align: center; - padding: var(--space-9) var(--space-5); - color: var(--color-ink-3); -} - -.aksrt-cart__empty i { - display: block; - margin-bottom: var(--space-4); -} - -.aksrt-cart__empty p { - margin: 0 0 var(--space-4); - font-size: 15px; -} - -/* 购物车响应式 */ -@media (max-width: 767px) { - .aksrt-cart__layout { - grid-template-columns: 1fr; - } - - .aksrt-cart__sidebar { - position: static; - } -} - -/* ================================================ - * 商品编辑页样式 - * ================================================ */ -.aksrt-product-edit { - padding-top: var(--space-6); - padding-bottom: var(--space-9); - max-width: 720px; - margin: 0 auto; -} - -.aksrt-product-edit__title { - margin: 0 0 var(--space-6); - font-size: 24px; - font-weight: 700; - color: var(--color-ink); -} - -.aksrt-product-edit__form { - padding: var(--space-6); - background: var(--surface-1, #fff); - border-radius: var(--radius-lg, 8px); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); -} - -.aksrt-product-edit__field { - margin-bottom: var(--space-5); -} - -.aksrt-product-edit__field label { - display: block; - font-size: 14px; - font-weight: 500; - color: var(--color-ink); - margin-bottom: var(--space-2); -} - -.aksrt-product-edit__field input, -.aksrt-product-edit__field textarea { - width: 100%; - padding: 10px 14px; - font-size: 14px; - color: var(--color-ink); - background: var(--surface-1, #fff); - border: 1px solid var(--color-line); - border-radius: var(--radius, 10px); - transition: border-color 0.15s; -} - -.aksrt-product-edit__field input:focus, -.aksrt-product-edit__field textarea:focus { - outline: none; - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent); -} - -.aksrt-product-edit__field textarea { - resize: vertical; - min-height: 100px; -} - -.aksrt-product-edit__actions { - display: flex; - gap: var(--space-3); - margin-top: var(--space-6); -} - -.aksrt-product-edit__actions .btn-primary { - padding: 10px 24px; - border: none; - border-radius: var(--radius, 10px); - background: var(--color-primary, var(--color-primary)); - color: #fff; - font-size: 15px; - font-weight: 500; - cursor: pointer; - transition: opacity 0.15s; -} - -.aksrt-product-edit__actions .btn-primary:hover { - opacity: 0.85; -} - -.aksrt-product-edit__actions .btn-ghost { - padding: 10px 24px; - border: 1px solid var(--color-line); - border-radius: var(--radius, 10px); - background: var(--surface-1, #fff); - color: var(--color-ink-2); - font-size: 15px; - text-decoration: none; - transition: border-color 0.15s; -} - -.aksrt-product-edit__actions .btn-ghost:hover { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); -} - -.aksrt-product-archive__toolbar{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 16px} -.aksrt-product-archive__sort{display:inline-flex;padding:8px 14px;border-radius:999px;background:var(--surface-1,#fff);color:var(--color-ink-3,#5c6370);text-decoration:none;font-weight:600;font-size:14px;box-shadow:var(--shadow-sm)} -.aksrt-product-archive__sort.is-active,.aksrt-product-archive__sort:hover{background:var(--color-primary,#f04494);color:#fff} -.aksrt-product-archive__load-more{display:flex;justify-content:center;margin:20px 0} -.aksrt-revenue-page__layout{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;margin:20px 0} -.aksrt-revenue-page__panel{padding:18px;border-radius:16px;background:var(--surface-1,#fff);box-shadow:var(--shadow-sm)} -.aksrt-revenue-page__withdraw-form label{display:flex;flex-direction:column;gap:6px;margin-bottom:12px} -.aksrt-revenue-page__withdraw-form input,.aksrt-revenue-page__withdraw-form select,.aksrt-revenue-page__withdraw-form textarea{width:100%;padding:10px 12px;border:1px solid var(--color-line-soft,rgba(0,0,0,.08));border-radius:10px} -.aksrt-revenue-page__item{display:flex;justify-content:space-between;gap:12px;padding:12px 0;border-bottom:1px solid var(--color-line-soft,rgba(0,0,0,.06))} -.aksrt-revenue-page__item .is-in{color:#27ae60;font-weight:700} -.aksrt-revenue-page__item .is-out{color:#e74c3c;font-weight:700} -.aksrt-product-editor__hint{font-size:13px;color:var(--color-ink-4,#8a909c);margin:0 0 10px} - -/* ===== zibll-parity cat filter / skeleton ===== */ -.aksrt-shop-cat-filter.shop-cat-filter{margin-top:14px} -.shop-cat-filter-tab{display:flex;gap:10px;overflow-x:auto;list-style:none;margin:0 0 10px;padding:0 0 4px;-webkit-overflow-scrolling:touch} -.shop-cat-filter-tab li{flex:0 0 auto} -.shop-cat-filter-tab a, -.shop-cat-filter-tab span, -.shop-cat-filter-child a, -.shop-cat-filter-child span{display:inline-flex;align-items:center;padding:6px 12px;border-radius:999px;font-size:13px;font-weight:600;text-decoration:none;color:var(--color-ink-2,#5c6370);background:var(--surface-4,#f3f4f6)} -.shop-cat-filter-tab .active span, -.shop-cat-filter-child .focus-color, -.shop-cat-filter-orderby .focus-color{color:var(--color-primary,#f04494);background:color-mix(in srgb,var(--color-primary,#f04494) 12%, #fff)} -.shop-cat-filter-child{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:0 0 10px} -.shop-cat-filter-orderby{gap:8px} -.shop-cat-filter-orderby .opacity5{opacity:.55;margin-right:4px;font-size:13px} -.aksrt-product-archive__skeleton{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;padding:8px 0 16px} -.aksrt-product-card--placeholder{min-height:220px;border-radius:12px;background:linear-gradient(90deg,#f3f4f6 25%,#e5e7eb 37%,#f3f4f6 63%);background-size:400% 100%;animation:aksrtShopSk 1.2s ease infinite} -@keyframes aksrtShopSk{0%{background-position:100% 0}100%{background-position:0 0}} -@media (max-width:992px){.aksrt-product-archive__skeleton{grid-template-columns:repeat(2,minmax(0,1fr))}} -@media (max-width:560px){.aksrt-product-archive__skeleton{grid-template-columns:1fr}} diff --git a/assets/css/routes/cart.css b/assets/css/routes/cart.css deleted file mode 100644 index c7035b0e..00000000 --- a/assets/css/routes/cart.css +++ /dev/null @@ -1,400 +0,0 @@ -.aksrt-cart-page { - padding: 28px 0 56px; -} - -.aksrt-cart-page__header, -.aksrt-cart-page__panel-head, -.aksrt-cart-page__summary-total, -.aksrt-cart-page__coupon-row, -.aksrt-cart-page__item, -.aksrt-cart-page__item-qty, -.aksrt-cart-page__option-columns { - display: flex; -} - -.aksrt-cart-page__header, -.aksrt-cart-page__panel-head, -.aksrt-cart-page__summary-total, -.aksrt-cart-page__coupon-row, -.aksrt-cart-page__option-columns { - justify-content: space-between; - gap: 16px; -} - -.aksrt-cart-page__layout { - display: grid; - grid-template-columns: minmax(0, 1fr) 360px; - gap: 24px; - align-items: start; -} - -.aksrt-cart-page__title { - margin: 0 0 8px; - font-size: 30px; - line-height: 1.1; -} - -.aksrt-cart-page__subtitle, -.aksrt-cart-page__hint, -.aksrt-cart-page__option-body span { - color: var(--color-ink-3, #5d6676); -} - -.aksrt-cart-page__pending { - min-width: 280px; -} - -.aksrt-cart-page__pending-list { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 8px; -} - -.aksrt-cart-page__pending-chip { - border: 1px solid var(--color-line, #dde3eb); - border-radius: 999px; - background: var(--surface-1, #fff); - padding: 8px 12px; - font-size: 12px; -} - -.aksrt-cart-page__panel { - background: var(--surface-1, #fff); - border: none; - box-shadow: var(--shadow-sm); - border-radius: var(--radius-2xl, 16px); - padding: 20px; -} - -.aksrt-cart-page__main, -.aksrt-cart-page__grid { - display: grid; - gap: 20px; -} - -.aksrt-cart-page__grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - -.aksrt-cart-page__items { - display: grid; - gap: 12px; -} - -.aksrt-cart-page__item { - align-items: center; - gap: 14px; - padding: 16px; - border: none; - border-radius: var(--main-radius, var(--radius, 8px)); - background: var(--surface-4, #f8fafc); - box-shadow: var(--shadow-sm); -} - -.aksrt-cart-page__item-image { - width: 84px; - height: 84px; - object-fit: cover; - border-radius: 12px; -} - -.aksrt-cart-page__item-image--placeholder { - display: flex; - align-items: center; - justify-content: center; - background: var(--surface-3, #eef2f7); - color: var(--color-ink-4, #8690a0); -} - -.aksrt-cart-page__item-main { - flex: 1; -} - -.aksrt-cart-page__item-main h3, -.aksrt-cart-page__mini-title, -.aksrt-cart-page__panel h2 { - margin: 0 0 8px; -} - -.aksrt-cart-page__item-qty { - align-items: center; - gap: 10px; -} - -.aksrt-cart-page__item-qty button, -.aksrt-cart-page__item-remove { - width: 34px; - height: 34px; - border: 1px solid var(--color-line, #dce3ec); - border-radius: 10px; - background: #fff; -} - -.aksrt-cart-page__item-total { - min-width: 92px; - text-align: right; -} - -.aksrt-cart-page__option-list { - display: grid; - gap: 12px; -} - -.aksrt-cart-page__option-card { - display: flex; - gap: 10px; - padding: 14px; - border: 1px solid var(--color-line, #dde3eb); - border-radius: var(--radius-lg); - background: var(--surface-4, #fff); -} - -.aksrt-cart-page__option-body { - display: grid; - gap: 4px; -} - -.aksrt-cart-page__field { - display: grid; - gap: 8px; -} - -.aksrt-cart-page__input, -.aksrt-cart-page__textarea { - width: 100%; - border: 1px solid var(--color-line, #dce3ec); - border-radius: 12px; - padding: 11px 14px; - background: #fff; -} - -.aksrt-cart-page__summary-list { - display: grid; - gap: 12px; - margin: 0 0 20px; -} - -.aksrt-cart-page__summary-list div, -.aksrt-cart-page__summary-total { - align-items: center; -} - -.aksrt-cart-page__summary-list div { - display: flex; - justify-content: space-between; -} - -.aksrt-cart-page__summary-total { - padding-top: 16px; - margin-bottom: 18px; - border-top: 1px solid var(--color-line, #e7ebf1); -} - -.aksrt-cart-page__summary-total strong { - font-size: 28px; -} - -.aksrt-cart-page__submit { - width: 100%; -} - -.aksrt-cart-page__empty { - text-align: center; - padding: 44px 20px; -} - -.aksrt-cart-page__empty i { - display: block; - margin-bottom: 12px; - font-size: 42px; - color: var(--color-ink-4, #8690a0); -} - -@media (max-width: 980px) { - .aksrt-cart-page__layout, - .aksrt-cart-page__grid { - grid-template-columns: 1fr; - } -} - -@media (max-width: 767px) { - .aksrt-cart-page__title { - font-size: 22px; - } - - .aksrt-cart-page__option-columns { - flex-direction: column; - } - - .aksrt-cart-page__field-row { - grid-template-columns: 1fr; - } - - .aksrt-cart-page__pending { - min-width: 0; - } - - .aksrt-modal__dialog { - width: 95%; - max-height: 90vh; - } - - .aksrt-modal__body { - padding: 16px; - } -} - -@media (max-width: 480px) { - .aksrt-cart-page__header { - flex-direction: column; - align-items: flex-start; - } - - .aksrt-cart-page__summary { - position: fixed; - bottom: 0; - left: 0; - right: 0; - z-index: 100; - border-radius: 16px 16px 0 0; - box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.1); - } - - .aksrt-cart-page__submit { - width: 100%; - } -} - -/* ── Address Card Actions ── */ - -.aksrt-cart-page__address-card { - flex-direction: column; - gap: 8px; -} - -.aksrt-cart-page__address-label { - display: flex; - gap: 10px; - flex: 1; - cursor: pointer; -} - -.aksrt-cart-page__address-actions { - display: flex; - gap: 8px; - padding-left: 28px; -} - -.aksrt-cart-page__default-tag { - display: inline-block; - font-size: 11px; - padding: 1px 6px; - border-radius: 4px; - background: var(--color-primary-1, #e6f7ff); - color: var(--color-primary, #1890ff); - font-weight: 500; - vertical-align: middle; -} - -.aksrt-cart-page__address-empty { - text-align: center; - padding: 32px 16px; - color: var(--color-ink-4, #8690a0); -} - -.aksrt-cart-page__address-empty i { - font-size: 28px; - margin-bottom: 8px; - display: block; -} - -.aksrt-cart-page__field-row { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 12px; -} - -.aksrt-cart-page__checkbox { - display: flex; - align-items: center; - gap: 8px; - cursor: pointer; - font-size: 14px; -} - -/* ── Shared Modal ── */ - -.aksrt-modal { - position: fixed; - inset: 0; - z-index: 1000; - display: flex; - align-items: center; - justify-content: center; -} - -.aksrt-modal__overlay { - position: absolute; - inset: 0; - background: rgba(18, 18, 18, 0.48); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); -} - -.aksrt-modal__dialog { - position: relative; - background: var(--surface-1, #fff); - border: none; - border-radius: var(--radius-2xl, 16px); - width: 90%; - max-width: 520px; - max-height: 85vh; - overflow-y: auto; - box-shadow: var(--shadow-drawer); -} - -.aksrt-modal__header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 18px 24px; - border-bottom: 1px solid var(--color-line, #e3e8ef); -} - -.aksrt-modal__header h3 { - margin: 0; - font-size: 18px; -} - -.aksrt-modal__close { - border: none; - background: none; - font-size: 24px; - cursor: pointer; - color: var(--color-ink-3, #5d6676); - padding: 0; - line-height: 1; -} - -.aksrt-modal__body { - padding: 20px 24px; - display: grid; - gap: 16px; -} - -.aksrt-modal__footer { - display: flex; - justify-content: flex-end; - gap: 12px; - padding-top: 8px; -} - -/* Soft focus ring (#77 recipe) — avoid hard primary stroke */ -.aksrt-cart-page__input:focus, -.aksrt-cart-page__textarea:focus, -.aksrt-cart-page__qty-btn:focus-visible { - outline: none; - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent); -} diff --git a/assets/css/routes/product.css b/assets/css/routes/product.css deleted file mode 100644 index 153f5094..00000000 --- a/assets/css/routes/product.css +++ /dev/null @@ -1,491 +0,0 @@ -/** - * AKSRT Product Page — zibll-aligned - * - * 包含:商品详情页 + 前台商品编辑器 - */ - -.aksrt-product-page, -.aksrt-product-editor { - padding: 28px 0 56px; -} - -.aksrt-product-page__layout, -.aksrt-product-page__detail-grid, -.aksrt-product-editor__layout, -.aksrt-product-editor__columns, -.aksrt-product-page__details, -.aksrt-product-page__actions { - display: grid; - gap: 20px; -} - -.aksrt-product-page__layout, -.aksrt-product-editor__layout { - grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr); - align-items: start; -} - -.aksrt-product-page__detail-grid { - grid-template-columns: minmax(0, 1fr) 320px; - margin-top: 24px; -} - -/* ===== Panel surfaces ===== */ -.aksrt-product-page__panel, -.aksrt-product-editor__panel { - background: var(--surface-1); - border: none; - box-shadow: var(--shadow-sm); - border-radius: var(--radius-2xl, 16px); - padding: 20px; -} - -.aksrt-product-page__title, -.aksrt-product-editor__title { - margin: 0 0 10px; - font-size: 30px; - line-height: 1.1; - font-weight: 700; - color: var(--color-ink); -} - -.aksrt-product-page__gallery, -.aksrt-product-editor__main, -.aksrt-product-editor__sidebar, -.aksrt-product-page__summary, -.aksrt-product-page__side-panels { - display: grid; - gap: 18px; -} - -/* ===== Gallery ===== */ -.aksrt-product-page__primary-image, -.aksrt-product-page__placeholder { - border-radius: var(--radius-lg); - overflow: hidden; - background: var(--surface-2); -} - -.aksrt-product-page__primary-image img { - display: block; - width: 100%; - aspect-ratio: 1 / 1; - object-fit: cover; -} - -.aksrt-product-page__placeholder { - display: flex; - align-items: center; - justify-content: center; - min-height: 360px; - font-size: 56px; - color: var(--color-ink-4); -} - -.aksrt-product-page__thumbs { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(76px, 1fr)); - gap: 10px; -} - -.aksrt-product-page__thumb { - border: 1px solid var(--color-line); - border-radius: var(--radius); - padding: 0; - background: var(--surface-1); - overflow: hidden; - cursor: pointer; - transition: border-color var(--dur-fast) var(--ease); -} - -.aksrt-product-page__thumb:hover, -.aksrt-product-page__thumb.is-active { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); -} - -.aksrt-product-page__thumb img { - display: block; - width: 100%; - aspect-ratio: 1 / 1; - object-fit: cover; -} - - -.aksrt-product-page__service-strip { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin: 6px 0 4px; -} - -.aksrt-product-page__service-strip span { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 8px 12px; - border-radius: 999px; - background: var(--surface-4); - color: var(--color-ink-3); - font-size: 12px; - font-weight: 500; -} -.aksrt-product-page__price-row, -.aksrt-product-page__quantity, -.aksrt-product-page__term-list, -.aksrt-product-editor__actions { - display: flex; - gap: 10px; - flex-wrap: wrap; - align-items: center; -} - -.aksrt-product-page__badge, -.aksrt-product-page__term { - display: inline-flex; - align-items: center; - border-radius: var(--radius-pill); - padding: 6px 12px; - background: var(--surface-4); - color: var(--color-ink-2); - font-size: 12px; - font-weight: 500; -} - -.aksrt-product-page__badge--promo { - background: rgba(239, 68, 68, 0.12); - color: var(--color-error); -} - -.aksrt-product-page__price { - font-size: 30px; - font-weight: 800; - color: var(--color-error); - line-height: 1; -} - -.aksrt-product-page__sales { - font-size: 13px; - color: var(--color-ink-4); -} - -/* ===== Sticky buybox (zibll-aligned) ===== */ -.aksrt-product-page__summary { - position: sticky; - top: calc(var(--nav-height) + 16px); -} - -/* ===== Meta info blocks ===== */ -.aksrt-product-page__meta, -.aksrt-product-page__info-list { - display: grid; - gap: 12px; - margin: 0; -} - -.aksrt-product-page__meta div, -.aksrt-product-page__detail-block { - padding: 14px; - border: 1px solid var(--color-line); - border-radius: var(--radius-lg); - background: var(--surface-4); -} - -.aksrt-product-page__meta dt { - font-size: 12px; - color: var(--color-ink-4); - margin-bottom: 4px; -} - -.aksrt-product-page__meta dd { - margin: 0; - font-size: 14px; - color: var(--color-ink); - font-weight: 500; -} - -/* ===== Form fields ===== */ -.aksrt-product-page__field, -.aksrt-product-editor__field { - display: grid; - gap: 8px; -} - -.aksrt-product-page__input, -.aksrt-product-editor__input, -.aksrt-product-editor__textarea { - width: 100%; - border: 1px solid var(--color-line); - border-radius: var(--radius); - padding: 11px 14px; - background: var(--surface-1); - font-size: 14px; - color: var(--color-ink); - transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease); -} - -.aksrt-product-page__input:focus, -.aksrt-product-editor__input:focus, -.aksrt-product-editor__textarea:focus { - outline: none; - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent); -} - -/* ===== Quantity controls ===== */ -.aksrt-product-page__qty-btn { - width: 36px; - height: 36px; - border: 1px solid var(--color-line); - border-radius: var(--radius-sm); - background: var(--surface-1); - color: var(--color-ink-2); - cursor: pointer; - transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease); -} - -.aksrt-product-page__qty-btn:hover { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - background: var(--color-primary-softer); -} - -.aksrt-product-page__qty-input { - width: 90px; - border: 1px solid var(--color-line); - border-radius: var(--radius-sm); - padding: 8px 12px; - text-align: center; - font-size: 14px; - color: var(--color-ink); -} - -/* ===== Content ===== */ -.aksrt-product-page__content { - line-height: 1.8; - color: var(--color-ink-2); -} - -.aksrt-product-page__content img { - max-width: 100%; - border-radius: var(--radius); -} - -/* ===== Empty state ===== */ -.aksrt-product-page__empty, -.aksrt-product-editor__empty { - text-align: center; - padding: 48px 20px; -} - -.aksrt-product-page__empty i, -.aksrt-product-editor__empty i { - display: block; - font-size: 44px; - color: var(--color-ink-4); - margin-bottom: 12px; -} - -.aksrt-product-editor__checkbox { - display: flex; - gap: 10px; - align-items: center; -} - -.aksrt-product-editor__sidebar { - position: sticky; - top: 24px; -} - -/* ===== Responsive ===== */ -@media (max-width: 980px) { - .aksrt-product-page__layout, - .aksrt-product-page__detail-grid, - .aksrt-product-editor__layout, - .aksrt-product-editor__columns, - .aksrt-product-page__details { - grid-template-columns: 1fr; - } - - .aksrt-product-page__summary { - position: static; - } - - .aksrt-product-editor__sidebar { - position: static; - } -} - -@media (max-width: 767px) { - .aksrt-product-page__title, - .aksrt-product-editor__title { - font-size: 22px; - } - - .aksrt-product-page__thumbs { - gap: 6px; - } - - .aksrt-product-page__spec-values { - flex-wrap: wrap; - } - - .aksrt-product-page__actions { - grid-template-columns: 1fr; - } - - .aksrt-product-page__actions .btn-lg, - .aksrt-product-editor__actions .btn-lg { - width: 100%; - } -} - -@media (max-width: 480px) { - .aksrt-product-page, - .aksrt-product-editor { - padding: 16px 0 40px; - } - - .aksrt-product-page__price { - font-size: 24px; - } - - .aksrt-product-page__meta { - grid-template-columns: 1fr; - } -} - -/* ===== Dark mode ===== */ -html.dark .aksrt-product-page__badge--promo, -html.dark .aksrt-product-page__badge--promo { - background: rgba(239, 68, 68, 0.18); -} - -html.dark .aksrt-product-page__placeholder, -html.dark .aksrt-product-page__placeholder { - background: var(--surface-2); -} - -html.dark .aksrt-product-page__thumb, -html.dark .aksrt-product-page__thumb { - border-color: var(--color-line); -} - -html.dark .aksrt-product-page__content img, -html.dark .aksrt-product-page__content img { - opacity: 0.92; -} - -/* ============================================================ - * SKU 规格选择器 - * ============================================================ */ -.aksrt-product-page__sku-selector { - margin: 16px 0; - padding: 16px; - background: var(--surface-4); - border: 1px solid var(--color-line); - border-radius: var(--radius-lg); -} - -.aksrt-product-page__spec-group { - margin-bottom: 12px; -} - -.aksrt-product-page__spec-group:last-child { - margin-bottom: 0; -} - -.aksrt-product-page__spec-label { - display: block; - font-size: 13px; - font-weight: 600; - color: var(--color-ink-2); - margin-bottom: 8px; -} - -.aksrt-product-page__spec-values { - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.aksrt-product-page__spec-btn { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 48px; - height: 32px; - padding: 0 12px; - font-size: 13px; - font-weight: 500; - color: var(--color-ink-2); - background: var(--surface-1); - border: 1px solid var(--color-line); - border-radius: 6px; - cursor: pointer; - transition: all var(--dur-fast) var(--ease); -} - -.aksrt-product-page__spec-btn:hover { - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - color: var(--color-primary); -} - -.aksrt-product-page__spec-btn.is-active { - background: var(--color-primary-soft); - border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb)); - color: var(--color-primary); - font-weight: 600; -} - -.aksrt-product-page__spec-btn:disabled { - opacity: 0.4; - cursor: not-allowed; - text-decoration: line-through; -} - -/* SKU 信息区 */ -.aksrt-product-page__sku-info { - margin-top: 12px; - padding: 10px 12px; - background: var(--surface-1); - border-radius: 6px; - display: flex; - align-items: center; - gap: 12px; -} - -.aksrt-product-page__sku-price { - font-size: 18px; - font-weight: 700; - color: var(--color-error); -} - -.aksrt-product-page__sku-stock { - font-size: 13px; - color: var(--color-ink-3); -} - - -/* Product detail shell — align with single content stream */ -.content-shell--product .content-wrap { - padding-top: 8px; -} -.content-shell--product .aksrt-product-page { - width: 100%; -} -.aksrt-product-page__related { - margin-top: 28px; - padding-top: 8px; -} -.aksrt-product-page__related-title { - margin: 0 0 14px; - font-size: 18px; - font-weight: 700; - color: var(--color-ink, #1f2937); -} -.aksrt-product-page__related-grid.aksrt-product-archive__grid { - margin: 0; -} -.aksrt-product-page__merchant-link { - display: inline-block; - margin-top: 4px; -} diff --git a/assets/js/cart-checkout.js b/assets/js/cart-checkout.js deleted file mode 100644 index 80ede97a..00000000 --- a/assets/js/cart-checkout.js +++ /dev/null @@ -1,483 +0,0 @@ -(function () { - "use strict"; - - var page = document.querySelector("[data-aksrt-cart-page]"); - if (!page || !window.aksrtData) { - return; - } - - function restBase() { - return ((aksrtData.siteUrl || "").replace(/\/$/, "")) + "/wp-json/aksrt/v1"; - } - function rest(path, method, body) { - var opts = { - method: method || "GET", - credentials: "same-origin", - headers: { "X-WP-Nonce": aksrtData.restNonce || "" } - }; - if (body !== undefined) { - opts.headers["Content-Type"] = "application/json"; - opts.body = JSON.stringify(body); - } - return fetch(restBase() + path, opts).then(function (r) { return r.json(); }); - } - function ordersUrl() { - return aksrtData.ordersUrl || (aksrtData.routes && aksrtData.routes.orders) || "/"; - } - function shopHomeUrl() { - return (aksrtData.routes && aksrtData.routes.home) || aksrtData.siteUrl || "/"; - } - var cartItems = []; - var currencySymbol = page.getAttribute("data-currency-symbol") || "¥"; - var itemsEl = page.querySelector("[data-aksrt-cart-items]"); - var subtotalEl = page.querySelector("[data-aksrt-summary-subtotal]"); - var shippingEl = page.querySelector("[data-aksrt-summary-shipping]"); - var discountEl = page.querySelector("[data-aksrt-summary-discount]"); - var totalEl = page.querySelector("[data-aksrt-summary-total]"); - var previewMessageEl = page.querySelector("[data-aksrt-preview-message]"); - var createBtn = page.querySelector("[data-aksrt-create-order]"); - var previewBtn = page.querySelector("[data-aksrt-preview-checkout]"); - - var previewState = null; - var searchParams = new URLSearchParams(window.location.search); - - function getCart() { return cartItems; } - function setCart(items) { cartItems = Array.isArray(items) ? items : []; } - - function money(value) { - return currencySymbol + Number(value || 0).toFixed(2); - } - - function selectedValue(name) { - var checked = page.querySelector('input[name="' + name + '"]:checked'); - return checked ? checked.value : ""; - } - - function getPayload() { - var items = getCart().map(function (item) { - return { - product_id: Number(item.product_id || item.id || 0), - quantity: Number(item.qty || item.quantity || 1), - options_string: item.options_string || "" - }; - }).filter(function (item) { - return item.product_id > 0; - }); - - return { - items: items, - coupon_code: (page.querySelector("[data-aksrt-coupon-code]") || {}).value || "", - shipping_method: selectedValue("aksrt_cart_shipping"), - address_id: Number(selectedValue("aksrt_cart_address") || 0), - payment_method: selectedValue("aksrt_cart_payment"), - customer_note: (page.querySelector("[data-aksrt-customer-note]") || {}).value || "" - }; - } - - function renderCart() { - var items = getCart(); - if (!items.length) { - itemsEl.innerHTML = '' + - '
' + - '' + - "

购物车为空,去商品页添加想购买的内容。

" + - '继续浏览' + - "
"; - subtotalEl.textContent = money(0); - shippingEl.textContent = money(0); - discountEl.textContent = "-" + money(0); - totalEl.textContent = money(0); - createBtn.disabled = true; - previewState = null; - return; - } - - var total = 0; - var html = ""; - items.forEach(function (item, index) { - var qty = Number(item.qty || item.quantity || 1); - var price = Number(item.unit_price != null ? item.unit_price : (item.price != null ? item.price : (item.product && item.product.price) || 0)); - var lineTotal = Number(item.line_total != null ? item.line_total : qty * price); - total += lineTotal; - var title = item.title || (item.product && item.product.title) || "商品"; - var image = item.image || (item.product && item.product.image) || ""; - var opts = item.options_label || item.options_string || ""; - - html += '' + - '
' + - (image ? '' : '
') + - '
' + - '

' + title + "

" + - (opts ? "" + opts + "" : "") + - '

' + money(price) + "

" + - "
" + - '
' + - '' + - "" + qty + "" + - '' + - "
" + - '' + money(lineTotal) + "" + - '' + - "
"; - }); - - itemsEl.innerHTML = html; - subtotalEl.textContent = money(total); - - if (!previewState) { - shippingEl.textContent = money(0); - discountEl.textContent = "-" + money(0); - totalEl.textContent = money(total); - } - } - - function setSummary(data) { - previewState = data || null; - subtotalEl.textContent = money((data && data.subtotal) || 0); - shippingEl.textContent = money((data && data.shipping_fee) || 0); - discountEl.textContent = "-" + money((data && data.discount) || 0); - totalEl.textContent = money((data && data.total) || 0); - createBtn.disabled = !data; - } - - function request(path, body) { return rest(path, "POST", body || {}); } - - function previewCheckout() { - var payload = getPayload(); - if (!payload.items.length) { - previewMessageEl.textContent = "请先添加商品后再试算。"; - return; - } - - previewBtn.disabled = true; - previewMessageEl.textContent = "正在试算订单..."; - - request("/shop/frontend/checkout/preview", payload) - .then(function (result) { - if (!result.success) { - setSummary(null); - previewMessageEl.textContent = result.message || "试算失败。"; - return; - } - - setSummary(result.data); - previewMessageEl.textContent = "已完成试算,可直接创建订单。"; - }) - .catch(function () { - setSummary(null); - previewMessageEl.textContent = "网络错误,请稍后重试。"; - }) - .finally(function () { - previewBtn.disabled = false; - }); - } - - function createOrder() { - var payload = getPayload(); - if (!previewState) { - previewCheckout(); - return; - } - - createBtn.disabled = true; - request("/shop/frontend/orders", payload) - .then(function (result) { - if (!result.success) { - previewMessageEl.textContent = result.message || "订单创建失败。"; - createBtn.disabled = false; - return; - } - - var orderNumber = (result.data && result.data.order_number) || ""; - var nextOrdersUrl = ordersUrl() + (String(ordersUrl()).indexOf("?") >= 0 ? "&" : "?") + "order_status=pending"; - - if (!orderNumber) { - window.location.href = nextOrdersUrl; - return; - } - - // 立即发起支付:跳转型网关直接前往第三方,余额型同步完成。 - request("/shop/frontend/orders/" + orderNumber + "/pay", {}) - .then(function (pay) { - var data = (pay && pay.data) || {}; - if (pay && pay.success && data.redirect && data.payment_url && /^https?:\/\//i.test(data.payment_url)) { - window.location.href = data.payment_url; - return; - } - // 二维码 / 余额 / 失败:统一回订单中心处理(含 ?payment 提示)。 - window.location.href = nextOrdersUrl; - }) - .catch(function () { - window.location.href = nextOrdersUrl; - }); - }) - .catch(function () { - previewMessageEl.textContent = "网络错误,请稍后重试。"; - createBtn.disabled = false; - }); - } - - page.addEventListener("click", function (event) { - var actionButton = event.target.closest("[data-qty-action]"); - if (actionButton) { - var itemEl = actionButton.closest("[data-cart-index]"); - if (!itemEl) return; - var productId = Number(itemEl.getAttribute("data-product-id") || 0); - var optionsString = itemEl.getAttribute("data-options-string") || ""; - var action = actionButton.getAttribute("data-qty-action"); - var path = "/shop/frontend/cart/add"; - var body = { product_id: productId, options_string: optionsString, count: 1 }; - if (action === "dec") { - path = "/shop/frontend/cart/reduce"; - } else if (action === "remove") { - path = "/shop/frontend/cart/remove"; - body = { product_id: productId, options_string: optionsString }; - } else if (action !== "inc") { - return; - } - rest(path, "POST", body).then(function (result) { - if (!result || !result.success) { - window.alert((result && result.message) || "操作失败"); - return; - } - setSummary(null); - loadServerCart(); - }); - return; - } - - if (event.target.closest("[data-aksrt-clear-cart]")) { - rest("/shop/frontend/cart/empty", "POST", {}).then(function () { - setSummary(null); - loadServerCart(); - }); - return; - } - - if (event.target.closest("[data-aksrt-preview-checkout]")) { - previewCheckout(); - return; - } - - if (event.target.closest("[data-aksrt-create-order]")) { - createOrder(); - return; - } - - var resumeButton = event.target.closest("[data-aksrt-resume-order]"); - if (resumeButton) { - window.location.href = ordersUrl() + (String(ordersUrl()).indexOf("?") >= 0 ? "&" : "?") + "order_status=pending"; - } - }); - - function loadServerCart() { - return rest("/shop/frontend/cart", "GET").then(function (result) { - var items = (result && result.success && result.data && result.data.items) ? result.data.items : []; - items = items.map(function (it) { - it.qty = it.quantity || it.qty || 1; - it.price = it.unit_price != null ? it.unit_price : (it.product && it.product.price) || 0; - it.title = it.title || (it.product && it.product.title) || "商品"; - it.image = it.image || (it.product && it.product.image) || ""; - return it; - }); - setCart(items); - renderCart(); - }).catch(function () { - setCart([]); - renderCart(); - }); - } - - var couponInput = page.querySelector("[data-aksrt-coupon-code]"); - if (couponInput && searchParams.has("coupon")) { - couponInput.value = searchParams.get("coupon") || ""; - } - - /* ── Address Book Management ── */ - - var addressModal = page.querySelector("[data-aksrt-address-modal]"); - var addressForm = page.querySelector("[data-aksrt-address-form]"); - var addressList = page.querySelector("[data-aksrt-address-list]"); - var addressTitle = page.querySelector("[data-aksrt-address-modal-title]"); - - function openAddressModal(mode, address) { - if (!addressModal || !addressForm) return; - addressForm.reset(); - addressForm.querySelector('[name="address_id"]').value = ""; - - if (mode === "edit" && address) { - addressTitle.textContent = "编辑地址"; - addressForm.querySelector('[name="address_id"]').value = address.id || ""; - addressForm.querySelector('[name="name"]').value = address.name || ""; - addressForm.querySelector('[name="phone"]').value = address.phone || ""; - addressForm.querySelector('[name="province"]').value = address.province || ""; - addressForm.querySelector('[name="city"]').value = address.city || ""; - addressForm.querySelector('[name="district"]').value = address.district || ""; - addressForm.querySelector('[name="address"]').value = address.address || ""; - addressForm.querySelector('[name="is_default"]').checked = !!address.is_default; - } else { - addressTitle.textContent = "新增地址"; - } - - addressModal.hidden = false; - } - - function closeAddressModal() { - if (addressModal) addressModal.hidden = true; - } - - function refreshAddressList() { - fetch(restBase() + "/shop/frontend/address", { - headers: { "X-WP-Nonce": aksrtData.restNonce } - }) - .then(function (r) { return r.json(); }) - .then(function (result) { - if (!result.success || !addressList) return; - var addresses = (result.data && result.data.addresses) || []; - renderAddressList(addresses); - }) - .catch(function () {}); - } - - function renderAddressList(addresses) { - if (!addressList) return; - if (!addresses.length) { - addressList.innerHTML = - '
' + - '' + - "

暂无收货地址,请先新增。

" + - "
"; - return; - } - - var html = ""; - addresses.forEach(function (addr, index) { - html += - '
' + - '" + - '
' + - '' + - '' + - (addr.is_default ? "" : '') + - "
"; - }); - addressList.innerHTML = html; - } - - function escHtml(str) { - var div = document.createElement("div"); - div.appendChild(document.createTextNode(str || "")); - return div.innerHTML; - } - - function getAddressById(id) { - var cards = addressList ? addressList.querySelectorAll("[data-aksrt-address-card]") : []; - var found = null; - cards.forEach(function (card) { - if (String(card.getAttribute("data-aksrt-address-card")) === String(id)) { - var body = card.querySelector(".aksrt-cart-page__option-body"); - if (body) { - var spans = body.querySelectorAll("span"); - var strong = body.querySelector("strong"); - found = { - id: id, - name: strong ? strong.textContent.replace("默认", "").trim() : "", - phone: spans[0] ? spans[0].textContent : "", - full_text: spans[1] ? spans[1].textContent : "" - }; - } - } - }); - return found; - } - - // Address event delegation - page.addEventListener("click", function (event) { - if (event.target.closest("[data-aksrt-address-add]")) { - openAddressModal("add"); - return; - } - - var editBtn = event.target.closest("[data-aksrt-address-edit]"); - if (editBtn) { - var editId = editBtn.getAttribute("data-aksrt-address-edit"); - var addr = getAddressById(editId); - openAddressModal("edit", addr); - return; - } - - var deleteBtn = event.target.closest("[data-aksrt-address-delete]"); - if (deleteBtn) { - var deleteId = deleteBtn.getAttribute("data-aksrt-address-delete"); - if (!confirm("确定删除该地址吗?")) return; - rest("/shop/frontend/address/" + deleteId, "DELETE").then(function (result) { - if (!result || !result.success) { - alert((result && result.message) || "删除失败。"); - return; - } - refreshAddressList(); - }).catch(function () { alert("网络错误。"); }); - return; - } - - var defaultBtn = event.target.closest("[data-aksrt-address-set-default]"); - if (defaultBtn) { - var defaultId = defaultBtn.getAttribute("data-aksrt-address-set-default"); - request("/shop/frontend/address/" + defaultId + "/default", {}).then(function (result) { - if (!result.success) { - alert(result.message || "设置失败。"); - return; - } - refreshAddressList(); - }).catch(function () { alert("网络错误。"); }); - return; - } - - if (event.target.closest("[data-aksrt-address-modal-close]") || event.target.classList.contains("aksrt-modal__overlay")) { - closeAddressModal(); - } - }); - - if (addressForm) { - addressForm.addEventListener("submit", function (event) { - event.preventDefault(); - var formData = new FormData(addressForm); - var addressId = formData.get("address_id"); - var payload = { - name: (formData.get("name") || "").trim(), - phone: (formData.get("phone") || "").trim(), - province: (formData.get("province") || "").trim(), - city: (formData.get("city") || "").trim(), - district: (formData.get("district") || "").trim(), - address: (formData.get("address") || "").trim(), - is_default: formData.get("is_default") === "1" - }; - - if (!payload.name || !payload.phone || !payload.address) { - alert("请填写必填项。"); - return; - } - - var url = addressId - ? "/shop/frontend/address/" + addressId - : "/shop/frontend/address"; - - request(url, payload).then(function (result) { - if (!result.success) { - alert(result.message || "保存失败。"); - return; - } - closeAddressModal(); - refreshAddressList(); - }).catch(function () { alert("网络错误。"); }); - }); - } - - loadServerCart(); -})(); diff --git a/assets/js/order-center.js b/assets/js/order-center.js index 09f80630..d6e09ed3 100644 --- a/assets/js/order-center.js +++ b/assets/js/order-center.js @@ -89,10 +89,6 @@ return suffix ? base + suffix : base; } - function cartUrl() { - return aksrtData.cartUrl || (aksrtData.routes && aksrtData.routes.cart) || "/"; - } - /* ── Order review modal (zibll 1:1) ── */ var STAR_SVG = ''; @@ -434,7 +430,7 @@ badge.textContent = statusLabel || newStatus; } // Hide action buttons that are no longer relevant - var actions = card.querySelectorAll("[data-aksrt-order-pay], [data-aksrt-order-cancel], [data-aksrt-order-after-sale], [data-aksrt-order-receipt], [data-aksrt-order-rebuy]"); + var actions = card.querySelectorAll("[data-aksrt-order-pay], [data-aksrt-order-cancel]"); for (var i = 0; i < actions.length; i++) { actions[i].style.display = "none"; } @@ -640,21 +636,9 @@ // Two-step cancel inside the modal — no window.confirm. footer += ''; } - if (actions.can_receipt) { - footer += ''; - } - if (actions.can_review) { - footer += ''; - } if (actions.can_pay) { footer += ''; } - if (actions.can_after_sale) { - footer += ''; - } - if (actions.can_rebuy) { - footer += ''; - } if (actions.can_view_product && order.permalink) { footer += '查看商品'; } @@ -695,70 +679,6 @@ } } - /* ── After-Sale Modal ── */ - var afterSaleModal = page.querySelector("[data-aksrt-aftersale-modal]"); - var afterSaleForm = page.querySelector("[data-aksrt-aftersale-form]"); - var afterSaleOrderInput = afterSaleForm ? afterSaleForm.querySelector('[name="order_number"]') : null; - - function openAfterSaleModal(orderNumber) { - if (!afterSaleModal || !afterSaleForm) { - showToast("售后表单加载失败,请刷新页面后重试。"); - return; - } - afterSaleForm.reset(); - if (afterSaleOrderInput) afterSaleOrderInput.value = orderNumber; - afterSaleModal.hidden = false; - } - - function closeAfterSaleModal() { - if (afterSaleModal) afterSaleModal.hidden = true; - } - - if (afterSaleForm) { - afterSaleForm.addEventListener("submit", function (event) { - event.preventDefault(); - var formData = new FormData(afterSaleForm); - var orderNumber = formData.get("order_number"); - var reason = (formData.get("reason") || "").trim(); - if (!reason) { - showToast("请填写售后原因。"); - return; - } - - var submitBtn = afterSaleForm.querySelector('button[type="submit"]'); - if (submitBtn) { - submitBtn.disabled = true; - submitBtn.textContent = "提交中..."; - } - - request(orderPath(orderNumber, "/after-sale"), { - type: formData.get("type") || "refund", - reason: reason, - note: (formData.get("note") || "").trim() - }).then(function (result) { - if (submitBtn) { - submitBtn.disabled = false; - submitBtn.textContent = "提交售后"; - } - if (!result.success) { - showToast(result.message || "提交售后失败。"); - return; - } - closeAfterSaleModal(); - showToast("售后申请已提交。"); - // Local refresh: update card status - var card = findOrderCard(orderNumber); - updateCardStatus(card, "refunded", "已退款"); - }).catch(function () { - if (submitBtn) { - submitBtn.disabled = false; - submitBtn.textContent = "提交售后"; - } - showToast("网络错误,请稍后重试。"); - }); - }); - } - /* ── Countdown timer ── */ function updateCountdowns() { var nodes = page.querySelectorAll("[data-aksrt-order-countdown]"); @@ -808,56 +728,6 @@ return; } - // Confirm receipt - var receiptBtn = event.target.closest("[data-aksrt-order-receipt]"); - if (receiptBtn) { - var receiptOrder = receiptBtn.getAttribute("data-aksrt-order-receipt"); - if (!receiptOrder) return; - if (!window.confirm("确认已收到货物?")) return; - receiptBtn.disabled = true; - request(orderPath(receiptOrder, "/confirm-receipt"), {}) - .then(function (result) { - receiptBtn.disabled = false; - if (!result || !result.success) { - showToast((result && result.message) || "确认收货失败"); - return; - } - showToast("已确认收货"); - updateCardStatus(findOrderCard(receiptOrder), "completed", "已完成"); - if (detailOrderNumber === receiptOrder) { - closeOrderDetailModal(); - } - }) - .catch(function () { - receiptBtn.disabled = false; - showToast("网络错误"); - }); - return; - } - - // Rebuy - var rebuyBtn = event.target.closest("[data-aksrt-order-rebuy]"); - if (rebuyBtn) { - var rebuyOrder = rebuyBtn.getAttribute("data-aksrt-order-rebuy"); - if (!rebuyOrder) return; - rebuyBtn.disabled = true; - request(orderPath(rebuyOrder, "/rebuy"), {}) - .then(function (result) { - rebuyBtn.disabled = false; - if (!result || !result.success) { - showToast((result && result.message) || "再次购买失败"); - return; - } - showToast("已加入购物车"); - window.location.href = cartUrl(); - }) - .catch(function () { - rebuyBtn.disabled = false; - showToast("网络错误"); - }); - return; - } - // Review modal (zibll order_comment_modal 1:1 UX) var reviewBtn = event.target.closest("[data-aksrt-order-review]"); if (reviewBtn) { @@ -992,103 +862,8 @@ }); return; } - - // After-Sale - 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; - } - - // Return shipping - var returnShipBtn = event.target.closest("[data-aksrt-return-ship]"); - if (returnShipBtn) { - openReturnShipModal(returnShipBtn.getAttribute("data-aksrt-return-ship")); - return; - } - - // Cancel after-sale ticket - var asCancelBtn = event.target.closest("[data-aksrt-as-cancel]"); - if (asCancelBtn) { - var tid = asCancelBtn.getAttribute("data-aksrt-as-cancel"); - if (!tid || !window.confirm("确认取消该售后申请?")) return; - asCancelBtn.disabled = true; - request("/shop/frontend/after-sale/" + tid + "/cancel", { reason: "" }) - .then(function (result) { - asCancelBtn.disabled = false; - if (!result || !result.success) { - showToast((result && result.message) || "取消失败"); - return; - } - showToast(result.message || "已取消售后"); - window.location.reload(); - }) - .catch(function () { - asCancelBtn.disabled = false; - showToast("网络错误"); - }); - return; - } - - // Close after-sale modal - if (event.target.closest("[data-aksrt-aftersale-modal-close]") || (event.target.classList && event.target.classList.contains("aksrt-modal__overlay") && event.target.closest("[data-aksrt-aftersale-modal]"))) { - closeAfterSaleModal(); - return; - } - - // Close return-ship modal - if (event.target.closest("[data-aksrt-return-ship-close]") || (event.target.classList && event.target.classList.contains("aksrt-modal__overlay") && event.target.closest("[data-aksrt-return-ship-modal]"))) { - closeReturnShipModal(); - } }); - /* ── Return shipping modal ── */ - var returnShipModal = page.querySelector("[data-aksrt-return-ship-modal]"); - var returnShipForm = page.querySelector("[data-aksrt-return-ship-form]"); - function openReturnShipModal(ticketId) { - if (!returnShipModal || !returnShipForm) return; - returnShipForm.reset(); - var idInput = returnShipForm.querySelector('[name="ticket_id"]'); - if (idInput) idInput.value = ticketId || ""; - returnShipModal.hidden = false; - } - function closeReturnShipModal() { - if (returnShipModal) returnShipModal.hidden = true; - } - if (returnShipForm) { - returnShipForm.addEventListener("submit", function (event) { - event.preventDefault(); - var fd = new FormData(returnShipForm); - var tid = fd.get("ticket_id"); - var carrier = (fd.get("carrier") || "").trim(); - var tracking = (fd.get("tracking_no") || "").trim(); - if (!carrier || !tracking) { - showToast("请填写快递公司与运单号"); - return; - } - var btn = returnShipForm.querySelector('button[type="submit"]'); - if (btn) { btn.disabled = true; } - request("/shop/frontend/after-sale/" + tid + "/return-shipping", { - carrier: carrier, - tracking_no: tracking - }).then(function (result) { - if (btn) btn.disabled = false; - if (!result || !result.success) { - showToast((result && result.message) || "提交失败"); - return; - } - closeReturnShipModal(); - showToast(result.message || "退货物流已提交"); - window.location.reload(); - }).catch(function () { - if (btn) btn.disabled = false; - showToast("网络错误"); - }); - }); - } - /* ── Coupon wallet ── */ var couponRoot = page.querySelector("[data-aksrt-coupon-wallet-list]"); if (couponRoot) { diff --git a/assets/js/product-archive.js b/assets/js/product-archive.js deleted file mode 100644 index 7f387b44..00000000 --- a/assets/js/product-archive.js +++ /dev/null @@ -1,295 +0,0 @@ -/** - * Product archive — cat filter + sort + AJAX load more (zibll ajax-next UX). - * - * Uses REST /shop/frontend/products/archive and optional AksrtAjaxList events. - * - * @package AKSRT - */ -(function () { - "use strict"; - - var root = document.querySelector("[data-aksrt-product-archive]"); - if (!root) return; - - function restBase() { - var site = window.aksrtData && aksrtData.siteUrl ? String(aksrtData.siteUrl).replace(/\/$/, "") : ""; - return site + "/wp-json/aksrt/v1"; - } - - function toast(msg) { - if (typeof window.aksrtToast === "function") { - window.aksrtToast(msg); - return; - } - if (window.console) console.log(msg); - } - - var grid = root.querySelector("[data-aksrt-product-grid], #aksrt-product-grid"); - var pager = root.querySelector("[data-aksrt-ajaxpager], .ajaxpager"); - var countEl = root.querySelector("[data-aksrt-archive-count]"); - var headerHost = root.querySelector("[data-aksrt-ajax-header]"); - var loadMoreBtn = root.querySelector("[data-aksrt-shop-load-more]"); - var loader = root.querySelector("[data-aksrt-ajax-loader], .post_ajax_loader"); - - var state = { - taxonomy: root.getAttribute("data-taxonomy") || "", - termId: Number(root.getAttribute("data-term-id") || 0), - orderby: root.getAttribute("data-orderby") || "date", - order: (root.getAttribute("data-order") || "DESC").toUpperCase(), - page: Number(root.getAttribute("data-page") || 1), - maxPages: Number(root.getAttribute("data-max-pages") || 1), - paging: root.getAttribute("data-paging") || "number", - ias: root.getAttribute("data-ias") === "1", - iasMax: Number(root.getAttribute("data-ias-max") || 5), - loading: false, - autoLoads: 0, - }; - - function showLoader(on) { - if (loader) { - loader.hidden = !on; - loader.style.display = on ? "" : "none"; - loader.classList.toggle("is-loading", !!on); - } - if (pager) pager.classList.toggle("is-ajax-loading", !!on); - } - - function updateLoadMore(hasMore) { - var pag = root.querySelector(".ajax-pag"); - if (loadMoreBtn) { - loadMoreBtn.style.display = hasMore ? "" : "none"; - loadMoreBtn.disabled = false; - loadMoreBtn.textContent = hasMore ? "加载更多" : "没有更多了"; - if (!hasMore) loadMoreBtn.classList.add("ajax-nomore"); - else loadMoreBtn.classList.remove("ajax-nomore"); - } - if (pag) pag.style.display = hasMore ? "" : "none"; - } - - function syncOrderbyActive() { - root.querySelectorAll("[data-aksrt-shop-orderby]").forEach(function (a) { - var key = a.getAttribute("data-aksrt-shop-orderby") || ""; - var active = key === state.orderby; - a.classList.toggle("is-active", active); - a.classList.toggle("focus-color", active && key === "price"); - }); - } - - function fetchArchive(page, orderby, order, replace, termId) { - if (state.loading) return Promise.resolve(); - state.loading = true; - showLoader(true); - if (loadMoreBtn) { - loadMoreBtn.disabled = true; - loadMoreBtn.textContent = "加载中…"; - } - - var qs = new URLSearchParams(); - qs.set("taxonomy", state.taxonomy); - qs.set("term_id", String(typeof termId === "number" ? termId : state.termId)); - qs.set("page", String(page)); - qs.set("orderby", orderby || state.orderby); - if ((orderby || state.orderby) === "price") { - qs.set("order", order || state.order || "ASC"); - } - - var container = pager || grid || root; - try { - container.dispatchEvent( - new CustomEvent("post_ajax.before", { - bubbles: true, - detail: { page: page, orderby: orderby || state.orderby, replace: !!replace }, - }) - ); - } catch (e0) {} - - return fetch(restBase() + "/shop/frontend/products/archive?" + qs.toString(), { - method: "GET", - credentials: "same-origin", - headers: { - Accept: "application/json", - "X-WP-Nonce": (window.aksrtData && aksrtData.restNonce) || "", - "X-AKSRT-Ajax": "list", - }, - }) - .then(function (r) { - return r.json(); - }) - .then(function (result) { - state.loading = false; - showLoader(false); - if (!result || !result.success || !result.data) { - toast((result && result.message) || "加载失败"); - updateLoadMore(state.page < state.maxPages); - return; - } - var data = result.data; - state.page = Number(data.page || page); - state.maxPages = Number(data.max_pages || 1); - state.orderby = data.orderby || state.orderby; - state.order = String(data.order || state.order || "DESC").toUpperCase(); - if (typeof termId === "number" && termId > 0) { - state.termId = termId; - } - if (data.term && data.term.id) { - state.termId = Number(data.term.id); - } - root.setAttribute("data-page", String(state.page)); - root.setAttribute("data-max-pages", String(state.maxPages)); - root.setAttribute("data-orderby", state.orderby); - root.setAttribute("data-order", state.order); - root.setAttribute("data-term-id", String(state.termId)); - - if (countEl && typeof data.total !== "undefined") { - countEl.textContent = "共 " + data.total + " 件商品"; - } - var titleEl = root.querySelector("[data-aksrt-archive-title]"); - if (titleEl && data.term && data.term.name) { - titleEl.textContent = data.term.name; - } - var descEl = root.querySelector("[data-aksrt-archive-desc]"); - if (descEl && data.term) { - descEl.textContent = data.term.description || ""; - descEl.style.display = data.term.description ? "" : "none"; - } - - if (headerHost && data.header_html) { - // Keep title/count; replace filter widget if present. - var existingFilter = headerHost.querySelector("[data-aksrt-shop-cat-filter], .shop-cat-filter"); - var tmp = document.createElement("div"); - tmp.innerHTML = data.header_html; - var nextFilter = tmp.querySelector("[data-aksrt-shop-cat-filter], .shop-cat-filter") || tmp.firstElementChild; - if (existingFilter && nextFilter) { - existingFilter.replaceWith(nextFilter); - } else if (!existingFilter && nextFilter) { - headerHost.appendChild(nextFilter); - } - } - - var html = data.html || data.empty_html || ""; - if (grid) { - if (replace) { - grid.innerHTML = html; - } else if (html) { - grid.insertAdjacentHTML("beforeend", html); - } - } - - if (data.paginate_html) { - var oldPag = root.querySelector(".ajax-pag"); - var wrap = document.createElement("div"); - wrap.innerHTML = data.paginate_html; - var nextPag = wrap.firstElementChild; - if (oldPag && nextPag) oldPag.replaceWith(nextPag); - else if (!oldPag && nextPag && pager) pager.appendChild(nextPag); - loadMoreBtn = root.querySelector("[data-aksrt-shop-load-more]"); - } - - syncOrderbyActive(); - updateLoadMore(!!data.has_more || state.page < state.maxPages); - - try { - container.dispatchEvent( - new CustomEvent("post_ajax.ed", { - bubbles: true, - detail: { page: state.page, replace: !!replace, data: data }, - }) - ); - document.dispatchEvent( - new CustomEvent("aksrt:posts:rendered", { - bubbles: true, - detail: { container: grid, replace: !!replace }, - }) - ); - } catch (e1) {} - }) - .catch(function () { - state.loading = false; - showLoader(false); - updateLoadMore(state.page < state.maxPages); - toast("网络错误"); - }); - } - - function pushUrl(orderby, order, termUrl) { - try { - var u = termUrl ? new URL(termUrl, window.location.origin) : new URL(window.location.href); - if (orderby) u.searchParams.set("orderby", orderby); - else u.searchParams.delete("orderby"); - u.searchParams.delete("shop_orderby"); - if (orderby === "price" && order) u.searchParams.set("order", order); - else u.searchParams.delete("order"); - u.searchParams.delete("paged"); - window.history.replaceState({}, "", u.toString()); - } catch (err) {} - } - - root.addEventListener("click", function (e) { - // Category / sort links with ajax-next — intercept for REST (richer payload). - var termLink = e.target.closest("[data-aksrt-shop-term].ajax-next, a.ajax-next[data-aksrt-shop-term]"); - if (termLink && root.contains(termLink)) { - e.preventDefault(); - e.stopPropagation(); - var tid = Number(termLink.getAttribute("data-aksrt-shop-term") || 0); - var href = termLink.getAttribute("href") || ""; - state.autoLoads = 0; - if (tid > 0) state.termId = tid; - pushUrl(state.orderby, state.order, href); - var rt = termLink.getAttribute("route-title"); - if (rt) { - try { - document.title = rt; - } catch (e2) {} - } - fetchArchive(1, state.orderby, state.order, true, tid); - return; - } - - var sort = e.target.closest("[data-aksrt-shop-orderby]"); - if (sort && root.contains(sort) && sort.tagName === "A") { - e.preventDefault(); - e.stopPropagation(); - var orderby = sort.getAttribute("data-aksrt-shop-orderby") || "date"; - var nextOrder = sort.getAttribute("data-aksrt-shop-order") || ""; - if (orderby === "price") { - if (state.orderby === "price" && nextOrder) { - state.order = nextOrder.toUpperCase(); - } else if (state.orderby !== "price") { - state.order = "ASC"; - } else { - state.order = state.order === "ASC" ? "DESC" : "ASC"; - } - } - if (orderby === state.orderby && orderby !== "price" && state.paging === "ajax") return; - state.orderby = orderby; - state.autoLoads = 0; - pushUrl(orderby, state.order); - fetchArchive(1, orderby, state.order, true); - return; - } - - var more = e.target.closest("[data-aksrt-shop-load-more]"); - if (more && root.contains(more)) { - e.preventDefault(); - e.stopPropagation(); - if (state.page >= state.maxPages) return; - fetchArchive(state.page + 1, state.orderby, state.order, false); - } - }); - - if (state.paging === "ajax" && state.ias && "IntersectionObserver" in window && loadMoreBtn) { - var io = new IntersectionObserver( - function (entries) { - entries.forEach(function (entry) { - if (!entry.isIntersecting) return; - if (state.loading || state.page >= state.maxPages) return; - if (state.iasMax > 0 && state.autoLoads >= state.iasMax) return; - state.autoLoads += 1; - fetchArchive(state.page + 1, state.orderby, state.order, false); - }); - }, - { rootMargin: "240px" } - ); - io.observe(loadMoreBtn); - } -})(); diff --git a/assets/js/product-purchase.js b/assets/js/product-purchase.js deleted file mode 100644 index 9dea3012..00000000 --- a/assets/js/product-purchase.js +++ /dev/null @@ -1,407 +0,0 @@ -/** - * Product purchase page — options selector + server cart. - * - * @package AKSRT - */ -(function () { - "use strict"; - - function restBase() { - var root = (window.aksrtData && aksrtData.siteUrl) ? String(aksrtData.siteUrl).replace(/\/$/, "") : ""; - return root + "/wp-json/aksrt/v1"; - } - - function restNonce() { - return (window.aksrtData && aksrtData.restNonce) || ""; - } - - function toast(msg, type) { - if (typeof window.aksrtToast === "function") { - window.aksrtToast(msg, type || "info"); - return; - } - if (window.aksrt && typeof aksrt.notyf === "function") { - aksrt.notyf(msg, { type: type || "info" }); - return; - } - window.alert(msg); - } - - function rest(path, method, body) { - var opts = { - method: method || "GET", - credentials: "same-origin", - headers: { - "X-WP-Nonce": restNonce(), - }, - }; - if (body !== undefined) { - opts.headers["Content-Type"] = "application/json"; - opts.body = JSON.stringify(body); - } - return fetch(restBase() + path, opts).then(function (r) { - return r.json(); - }); - } - - function money(value, payModo) { - var n = Number(value || 0).toFixed(2); - return payModo === "points" ? n : "¥" + n; - } - - /* ── Product page ── */ - var productPage = document.querySelector("[data-aksrt-product-page]"); - if (productPage) { - var quantityInput = productPage.querySelector("[data-aksrt-quantity]"); - var productId = Number(productPage.getAttribute("data-product-id") || 0); - var titleEl = productPage.querySelector(".aksrt-product-page__title"); - var imageEl = productPage.querySelector(".aksrt-product-page__primary-image img"); - var priceEl = productPage.querySelector("[data-aksrt-dynamic-price], .aksrt-product-page__price"); - var stockEl = productPage.querySelector("[data-aksrt-stock-display]"); - var couponEl = productPage.querySelector("[data-aksrt-product-coupon]"); - var optionsRoot = productPage.querySelector("[data-aksrt-options-selector]"); - var optionResult = productPage.querySelector("[data-aksrt-option-result]"); - var selectedLabelsEl = productPage.querySelector("[data-aksrt-selected-labels]"); - var resolvedPriceEl = productPage.querySelector("[data-aksrt-resolved-price]"); - var resolvedStockEl = productPage.querySelector("[data-aksrt-resolved-stock]"); - var payModo = productPage.getAttribute("data-pay-modo") || "0"; - var basePrice = Number(productPage.getAttribute("data-base-price") || 0); - var baseStock = Number(productPage.getAttribute("data-base-stock") || 0); - var selectedByGroup = {}; - var groupCount = optionsRoot - ? optionsRoot.querySelectorAll(".aksrt-product-page__option-group").length - : 0; - - function currentOptionsKey() { - if (!groupCount) return ""; - var labels = []; - for (var i = 0; i < groupCount; i++) { - if (!selectedByGroup[i]) return null; - labels.push(selectedByGroup[i].label); - } - return labels.join("|"); - } - - function resolveSelection() { - if (!groupCount) { - return { - key: "", - labels: "", - price: basePrice, - stock: baseStock, - image: "", - }; - } - var key = currentOptionsKey(); - if (key === null) return null; - - var price = basePrice; - var image = ""; - var labels = []; - for (var i = 0; i < groupCount; i++) { - var sel = selectedByGroup[i]; - labels.push(sel.label); - price += Number(sel.priceChange || 0); - if (sel.image) image = sel.image; - } - return { - key: key, - labels: labels.join(" / "), - price: Math.max(0, price), - stock: baseStock, - image: image, - }; - } - - function paintSelection() { - var resolved = resolveSelection(); - if (!resolved) { - if (optionResult) optionResult.hidden = true; - return; - } - if (priceEl) { - if (groupCount && productPage.getAttribute("data-price-range") === "1" && currentOptionsKey() === null) { - /* keep range until complete selection */ - } else { - priceEl.textContent = money(resolved.price, payModo) + (payModo === "points" ? "" : ""); - if (payModo === "points" && priceEl.querySelector("small") === null) { - /* leave as-is */ - } - } - } - if (stockEl && resolved.stock > -1) { - stockEl.textContent = String(resolved.stock); - } - if (quantityInput && resolved.stock > 0) { - quantityInput.setAttribute("max", String(resolved.stock)); - if (Number(quantityInput.value) > resolved.stock) { - quantityInput.value = String(resolved.stock); - } - } - if (imageEl && resolved.image) { - imageEl.setAttribute("src", resolved.image); - } - if (optionResult) { - optionResult.hidden = false; - if (selectedLabelsEl) selectedLabelsEl.textContent = resolved.labels; - if (resolvedPriceEl) resolvedPriceEl.textContent = money(resolved.price, payModo); - if (resolvedStockEl) { - resolvedStockEl.textContent = - resolved.stock <= -1 ? "库存充足" : "库存 " + resolved.stock; - } - } - } - - if (optionsRoot) { - optionsRoot.addEventListener("click", function (event) { - var btn = event.target.closest("[data-option-group]"); - if (!btn || !optionsRoot.contains(btn)) return; - var group = btn.getAttribute("data-option-group"); - var label = btn.getAttribute("data-option-label") || ""; - var priceChange = Number(btn.getAttribute("data-price-change") || 0); - var image = btn.getAttribute("data-option-image") || ""; - - optionsRoot - .querySelectorAll('[data-option-group="' + group + '"]') - .forEach(function (el) { - el.classList.remove("is-active"); - }); - btn.classList.add("is-active"); - selectedByGroup[group] = { label: label, priceChange: priceChange, image: image }; - paintSelection(); - }); - } - - function ensureOptionsSelected() { - if (!groupCount) return true; - if (currentOptionsKey() === null) { - toast("请先选择商品规格", "warning"); - return false; - } - return true; - } - - function addToServerCart(replaceQty) { - if (!window.aksrtData || !aksrtData.isLoggedIn) { - window.location.href = (aksrtData && aksrtData.routes && aksrtData.routes.login) || "/"; - return; - } - if (!ensureOptionsSelected()) return; - - var qty = Number(quantityInput ? quantityInput.value : 1) || 1; - var optionsString = currentOptionsKey() || ""; - var body = { - product_id: productId, - options_string: optionsString, - count: qty, - }; - - rest("/shop/frontend/cart/add", "POST", body) - .then(function (result) { - if (!result || !result.success) { - toast((result && (result.message || result.error)) || "加入购物车失败", "error"); - return; - } - toast("已加入购物车", "success"); - if (replaceQty) { - var cartUrl = new URL( - (window.aksrtData && (aksrtData.cartUrl || (aksrtData.routes && aksrtData.routes.cart))) || "/", - window.location.origin - ); - if (couponEl && couponEl.value) { - cartUrl.searchParams.set("coupon", couponEl.value); - } - window.location.href = cartUrl.toString(); - } - }) - .catch(function () { - toast("网络错误,请稍后重试", "error"); - }); - } - - productPage.addEventListener("click", function (event) { - var qtyBtn = event.target.closest("[data-aksrt-qty-action]"); - if (qtyBtn && quantityInput) { - var action = qtyBtn.getAttribute("data-aksrt-qty-action"); - var current = Number(quantityInput.value || 1); - var max = Number(quantityInput.getAttribute("max") || 999); - quantityInput.value = action === "inc" ? Math.min(max, current + 1) : Math.max(1, current - 1); - return; - } - - if (event.target.closest("[data-aksrt-add-cart]")) { - addToServerCart(false); - return; - } - - if (event.target.closest("[data-aksrt-buy-now]")) { - addToServerCart(true); - return; - } - - var thumb = event.target.closest("[data-aksrt-product-thumb]"); - if (thumb && imageEl) { - imageEl.setAttribute("src", thumb.getAttribute("data-aksrt-product-thumb")); - return; - } - - var videoBtn = event.target.closest("[data-aksrt-product-video]"); - if (videoBtn) { - var url = videoBtn.getAttribute("data-aksrt-product-video"); - if (url) window.open(url, "_blank", "noopener"); - return; - } - - var favBtn = event.target.closest("[data-aksrt-toggle-fav]"); - if (favBtn) { - if (!window.aksrtData || !aksrtData.isLoggedIn) { - window.location.href = (aksrtData && aksrtData.routes && aksrtData.routes.login) || "/"; - return; - } - var pid = Number(favBtn.getAttribute("data-aksrt-toggle-fav") || productId); - rest("/shop/frontend/products/" + pid + "/favorite", "POST", {}) - .then(function (result) { - if (!result || !result.success) { - toast((result && result.message) || "操作失败", "error"); - return; - } - var active = !!(result.data && result.data.is_favorite); - productPage.querySelectorAll("[data-aksrt-toggle-fav]").forEach(function (btn) { - btn.classList.toggle("is-active", active); - }); - toast((result.message) || (active ? "已收藏" : "已取消收藏"), "success"); - }) - .catch(function () { - toast("网络错误", "error"); - }); - return; - } - - var tabBtn = event.target.closest("[data-aksrt-product-tab]"); - if (tabBtn) { - var tab = tabBtn.getAttribute("data-aksrt-product-tab"); - productPage.querySelectorAll("[data-aksrt-product-tab]").forEach(function (b) { - b.classList.toggle("is-active", b === tabBtn); - }); - productPage.querySelectorAll("[data-aksrt-product-panel]").forEach(function (panel) { - panel.hidden = panel.getAttribute("data-aksrt-product-panel") !== tab; - }); - return; - } - - if (event.target.closest("[data-aksrt-show-params]")) { - var paramsModal = productPage.querySelector("[data-aksrt-params-modal]"); - if (paramsModal) paramsModal.hidden = false; - return; - } - if (event.target.closest("[data-aksrt-show-services]")) { - var servicesModal = productPage.querySelector("[data-aksrt-services-modal]"); - if (servicesModal) servicesModal.hidden = false; - return; - } - if (event.target.closest("[data-aksrt-close-modal]")) { - var modal = event.target.closest("[data-aksrt-params-modal], [data-aksrt-services-modal], .aksrt-modal"); - if (modal) modal.hidden = true; - } - }); - } - - /* ── Product editor (seller) ── */ - var editor = document.querySelector("[data-aksrt-product-editor]"); - if (!editor || !window.aksrtData) { - return; - } - - var form = editor.querySelector("[data-aksrt-product-form]"); - var messageEl = editor.querySelector("[data-aksrt-product-editor-message]"); - if (!form) return; - - function selectedMultiValues(select) { - if (!select) return []; - return Array.prototype.slice - .call(select.options) - .filter(function (option) { return option.selected; }) - .map(function (option) { return Number(option.value || 0); }) - .filter(Boolean); - } - - function parseOptionsText(text) { - var groups = []; - String(text || "").split(String.fromCharCode(10)).forEach(function (line) { - line = line.replace(/\r/g, "").trim(); - if (!line) return; - var parts = line.split("|"); - if (parts.length < 2) return; - var name = parts[0].trim(); - var values = []; - parts.slice(1).join("|").split(",").forEach(function (chunk) { - chunk = chunk.trim(); - if (!chunk) return; - var segs = chunk.split(":"); - var label = (segs[0] || "").trim(); - if (!label) return; - values.push({ label: label, price_change: Number(segs[1] || 0), image: "" }); - }); - if (name && values.length) { - groups.push({ name: name, view_mode: "list", values: values }); - } - }); - return groups; - } - - form.addEventListener("submit", function (event) { - event.preventDefault(); - - var formData = new FormData(form); - var productId = Number(editor.getAttribute("data-product-id") || 0); - var gallery = String(formData.get("gallery") || "") - .split(String.fromCharCode(10)) - .map(function (line) { return line.replace(/\r/g, "").trim(); }) - .filter(Boolean); - - var payload = { - title: String(formData.get("title") || "").trim(), - price: Number(formData.get("price") || 0), - stock: Number(formData.get("stock") || 0), - excerpt: String(formData.get("excerpt") || ""), - content: String(formData.get("content") || ""), - status: String(formData.get("status") || "draft"), - categories: selectedMultiValues(form.querySelector('select[name="categories[]"]')), - tags: String(formData.get("tags") || "") - .split(",") - .map(function (tag) { return tag.trim(); }) - .filter(Boolean), - image: String(formData.get("image") || "").trim(), - gallery: gallery, - shipping_mode: String(formData.get("shipping_mode") || "express"), - shipping_fee: Number(formData.get("shipping_fee") || 0), - after_sale_policy: String(formData.get("after_sale_policy") || ""), - support_refund: formData.get("support_refund") === "1", - options: parseOptionsText(formData.get("options_text") || ""), - start_price: Number(formData.get("start_price") || 0), - pay_modo: String(formData.get("pay_modo") || "0"), - product_params: String(formData.get("product_params") || ""), - product_video: String(formData.get("product_video") || "") - .split(String.fromCharCode(10)) - .map(function (line) { return line.replace(/\r/g, "").trim(); }) - .filter(Boolean) - }; - - var path = productId > 0 ? "/shop/frontend/products/" + productId : "/shop/frontend/products"; - if (messageEl) messageEl.textContent = "正在保存商品..."; - - rest(path, "POST", payload) - .then(function (result) { - if (!result.success) { - if (messageEl) messageEl.textContent = result.message || "保存失败。"; - return; - } - var centerUrl = aksrtData.userCenterUrl || "/"; - if (messageEl) messageEl.textContent = "商品已保存,正在返回用户中心..."; - window.location.href = centerUrl; - }) - .catch(function () { - if (messageEl) messageEl.textContent = "网络错误,请稍后重试。"; - }); - }); -})(); diff --git a/functions.php b/functions.php index 09f182ca..ff6b293a 100644 --- a/functions.php +++ b/functions.php @@ -111,7 +111,7 @@ add_filter('query_vars', function ($vars) { // 主题切换离开时:清理本主题注册的 cron 任务 add_action('switch_theme', function () { - $hooks = ['aksrt_dl_token_cleanup', 'aksrt_email_verification_backfill_batch', 'aksrt_notification_cleanup', 'aksrt_reconcile_refunds', 'aksrt_after_sale_auto_cancel']; + $hooks = ['aksrt_dl_token_cleanup', 'aksrt_email_verification_backfill_batch', 'aksrt_notification_cleanup']; foreach ($hooks as $hook) { $next = wp_next_scheduled($hook); while ($next) { @@ -318,10 +318,6 @@ if (class_exists('\AKSRT\Payment\Order')) { \AKSRT\Payment\Order::get_instance()->init(); } -if (class_exists('\AKSRT\Payment\AfterSale')) { - \AKSRT\Payment\AfterSale::get_instance()->init(); -} - // Register built-in payment gateways if (class_exists('\AKSRT\Payment\Gateway')) { $gw = \AKSRT\Payment\Gateway::get_instance(); @@ -358,30 +354,6 @@ if (class_exists('\AKSRT\Core\TermMeta')) { \AKSRT\Core\TermMeta::get_instance()->init(); } -// 商城 taxonomy term meta + shop_discount 优惠活动 -if (class_exists('\AKSRT\Shop\TermMeta')) { - \AKSRT\Shop\TermMeta::get_instance()->init(); -} - -// 商品后台列表自定义列(销量/综合数据/商家/优惠活动) -if (is_admin() && class_exists('\AKSRT\Admin\ShopController')) { - \AKSRT\Admin\ShopController::get_instance()->init_product_admin_columns(); -} - -// 商品原生编辑 meta box(价格/库存/发货售后) -if (is_admin() && class_exists('\AKSRT\Shop\ProductMetaBox')) { - \AKSRT\Shop\ProductMetaBox::get_instance()->init(); -} -// 商品原生变体 meta box -if (is_admin() && class_exists('\AKSRT\Shop\ProductVariantsMetaBox')) { - \AKSRT\Shop\ProductVariantsMetaBox::get_instance()->init(); -} - -// 商品评价/评分系统 -if (class_exists('\AKSRT\Shop\ProductReview')) { - \AKSRT\Shop\ProductReview::get_instance()->init(); -} - // 文章局部配置 meta box(仅后台) if (is_admin() && class_exists('\AKSRT\Core\PostMetaBox')) { \AKSRT\Core\PostMetaBox::get_instance()->init(); @@ -434,12 +406,6 @@ if (is_admin() && class_exists('\AKSRT\Admin\Native\PayCoupons')) { if (is_admin() && class_exists('\AKSRT\Admin\Native\PayChargeCards')) { \AKSRT\Admin\Native\PayChargeCards::get_instance()->init(); } -if (is_admin() && class_exists('\AKSRT\Admin\Native\PayShipping')) { - \AKSRT\Admin\Native\PayShipping::get_instance()->init(); -} -if (is_admin() && class_exists('\AKSRT\Admin\Native\PayAfterSale')) { - \AKSRT\Admin\Native\PayAfterSale::get_instance()->init(); -} if (is_admin() && class_exists('\AKSRT\Admin\Native\PayOrders')) { \AKSRT\Admin\Native\PayOrders::get_instance()->init(); } diff --git a/inc/helpers/aksrt-commerce.php b/inc/helpers/aksrt-commerce.php deleted file mode 100644 index 228f3b1e..00000000 --- a/inc/helpers/aksrt-commerce.php +++ /dev/null @@ -1,96 +0,0 @@ - - */ - function aksrt_get_commerce_nav_items(): array { - $items = []; - - $cart = function_exists('aksrt_get_cart_page_url') ? aksrt_get_cart_page_url() : ''; - if ($cart !== '') { - $items[] = [ - 'key' => 'cart', - 'label' => __('购物车', 'aksrt'), - 'url' => $cart, - ]; - } - - $orders = function_exists('aksrt_get_orders_page_url') ? aksrt_get_orders_page_url() : ''; - if ($orders !== '') { - $items[] = [ - 'key' => 'orders', - 'label' => __('我的订单', 'aksrt'), - 'url' => $orders, - ]; - } - - $revenue = function_exists('aksrt_get_revenue_page_url') ? aksrt_get_revenue_page_url() : ''; - if ($revenue !== '') { - $items[] = [ - 'key' => 'revenue', - 'label' => __('创作收益', 'aksrt'), - 'url' => $revenue, - ]; - } - - $center = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : ''; - if ($center !== '') { - $items[] = [ - 'key' => 'center', - 'label' => __('个人中心', 'aksrt'), - 'url' => $center, - ]; - } - - return $items; - } -} - -if (!function_exists('aksrt_render_commerce_page_header')) { - /** - * Shared commerce page header: title / intro / secondary nav. - * - * @param array{title?:string,subtitle?:string,active?:string} $args - */ - function aksrt_render_commerce_page_header(array $args = []): void { - $title = (string) ($args['title'] ?? ''); - $subtitle = (string) ($args['subtitle'] ?? ''); - $active = sanitize_key((string) ($args['active'] ?? '')); - $nav = aksrt_get_commerce_nav_items(); - ?> -
- -

- - -

- - - - -
- get_product_detail_view($product_id); -} - -function aksrt_get_product_edit_page_view(int $user_id, int $product_id = 0): array { - if ($user_id <= 0 || !class_exists('\AKSRT\Shop\Shop')) { - return [ - 'allowed' => false, - 'is_edit' => false, - 'product' => [], - ]; - } - - return \AKSRT\Shop\Shop::get_instance()->get_product_edit_view($user_id, $product_id); -} - -function aksrt_get_cart_page_view(int $user_id): array { - if ($user_id <= 0 || !class_exists('\AKSRT\Shop\Shop')) { - return [ - 'payment_methods' => [], - 'shipping_methods' => [], - 'address_book' => [], - 'coupon_enabled' => false, - 'currency_symbol' => '¥', - 'pending_orders' => [], - ]; - } - - return \AKSRT\Shop\Shop::get_instance()->get_cart_page_view($user_id); -} - function aksrt_get_orders_page_view(int $user_id, int $paged = 1, int $per_page = 20, string $status = '', string $product_type = ''): array { $empty = [ 'items' => [], @@ -79,10 +44,6 @@ function aksrt_get_orders_page_view(int $user_id, int $paged = 1, int $per_page return \AKSRT\Payment\UserOrders::get_instance()->get_order_center_view($user_id, $args); } - if (class_exists('\AKSRT\Shop\Shop')) { - return \AKSRT\Shop\Shop::get_instance()->get_order_center_view($user_id, $args); - } - return $empty; } @@ -135,4 +96,4 @@ function aksrt_get_revenue_page_view(int $user_id): array { 'total_withdrawn' => $total_withdrawn, 'records' => $records, ]; -} +} \ No newline at end of file diff --git a/inc/helpers/user-center-renderers/profile-orders.php b/inc/helpers/user-center-renderers/profile-orders.php index 2e009bee..bf1d6d67 100644 --- a/inc/helpers/user-center-renderers/profile-orders.php +++ b/inc/helpers/user-center-renderers/profile-orders.php @@ -25,7 +25,7 @@ $section = sanitize_key((string) ($_GET['section'] ?? '')); if ($tab === '' && isset($_GET['tab']) && sanitize_key((string) $_GET['tab']) !== 'orders') { // When rendered as standalone legacy include, outer tab may be status. $maybe = sanitize_key((string) $_GET['tab']); - if (in_array($maybe, ['pending', 'to-ship', 'shipped', 'received', 'review', 'after'], true)) { + if (in_array($maybe, ['pending', 'after'], true)) { $tab = $maybe; } } @@ -37,9 +37,6 @@ if (isset($tab_aliases[$tab])) { $tab_status_map = [ 'pending' => \AKSRT\Payment\Order::STATUS_PENDING, - 'to-ship' => \AKSRT\Payment\Order::STATUS_PAID, - 'shipped' => \AKSRT\Payment\Order::STATUS_DELIVERED, - 'review' => \AKSRT\Payment\Order::STATUS_PENDING_REVIEW, 'after' => \AKSRT\Payment\Order::STATUS_REFUNDING, ]; if ($tab !== '' && isset($tab_status_map[$tab])) { @@ -58,15 +55,10 @@ if ($type_options === [] && class_exists('\AKSRT\Core\DomainModel')) { $type_options = array_merge(['' => __('全部类型', 'aksrt')], \AKSRT\Core\DomainModel::order_product_type_labels()); } -$shop = class_exists('\AKSRT\Shop\Shop') ? \AKSRT\Shop\Shop::get_instance() : null; - $tabs = [ '' => __('全部', 'aksrt'), 'pending' => __('待支付', 'aksrt'), - 'to-ship' => __('待发货', 'aksrt'), - 'shipped' => __('待收货', 'aksrt'), - 'review' => __('待评价', 'aksrt'), - 'after' => __('售后', 'aksrt'), + 'after' => __('退款中', 'aksrt'), ]; $base_query = ['tab' => 'orders']; @@ -115,16 +107,8 @@ $pending_count = (int) ($stats['pending'] ?? 0); -
+
- - - - - - - -
@@ -145,9 +129,7 @@ $pending_count = (int) ($stats['pending'] ?? 0);
- -
@@ -304,23 +286,11 @@ $pending_count = (int) ($stats['pending'] ?? 0); - - - - - - - - - - - -
@@ -348,7 +318,7 @@ $pending_count = (int) ($stats['pending'] ?? 0);

-

+

@@ -367,114 +337,6 @@ $pending_count = (int) ($stats['pending'] ?? 0); - - - - - - - get_user_tickets($user_id, 50); - ?> -
-

- -
- -
-
- # - - -
-
- - · -
-
- -
    - -
  1. - - -
  2. - -
- -
- - - - - - -
-
- -
- -

- -
- -

@@ -483,27 +345,4 @@ $pending_count = (int) ($stats['pending'] ?? 0);
- - get_user_favorites($user_id); - ?> -
-

- - - -

- -
- diff --git a/inc/helpers/user-center-renderers/sidebar-nav.php b/inc/helpers/user-center-renderers/sidebar-nav.php index e9520b48..9671b9e4 100644 --- a/inc/helpers/user-center-renderers/sidebar-nav.php +++ b/inc/helpers/user-center-renderers/sidebar-nav.php @@ -7,19 +7,12 @@ defined('ABSPATH') || exit; -$cart_url = function_exists('aksrt_get_cart_page_url') ? aksrt_get_cart_page_url() : ''; $level_url = isset($dashboard_tabs['level']) ? aksrt_get_user_center_tab_url('level') : ''; $auth_url = isset($dashboard_tabs['auth']) ? aksrt_get_user_center_tab_url('auth') : ''; $services = array_values(array_filter([ - [ - 'label' => __('购物车', 'aksrt'), - 'icon' => 'shopping-cart', - 'tone' => 'cart', - 'url' => $cart_url, - ], [ 'label' => __('我的等级', 'aksrt'), 'icon' => 'chart-line', diff --git a/inc/helpers/user-center-renderers/sidebar-orders.php b/inc/helpers/user-center-renderers/sidebar-orders.php index b6f898cc..e0710a57 100644 --- a/inc/helpers/user-center-renderers/sidebar-orders.php +++ b/inc/helpers/user-center-renderers/sidebar-orders.php @@ -19,12 +19,6 @@ $order_stats = class_exists('\AKSRT\Payment\Order') $order_repo = class_exists('\AKSRT\Payment\OrderRepository') ? \AKSRT\Payment\OrderRepository::get_instance() : null; -$shop_type = class_exists('\AKSRT\Core\DomainModel') - ? \AKSRT\Core\DomainModel::ORDER_PRODUCT_SHOP - : 'shop_product'; -$shop_paid = $order_repo ? $order_repo->count_user_orders($order_user_id, ['status' => \AKSRT\Payment\Order::STATUS_PAID, 'product_type' => $shop_type]) : 0; -$shop_delivered = $order_repo ? $order_repo->count_user_orders($order_user_id, ['status' => \AKSRT\Payment\Order::STATUS_DELIVERED, 'product_type' => $shop_type]) : 0; -$shop_review = $order_repo ? $order_repo->count_user_orders($order_user_id, ['status' => \AKSRT\Payment\Order::STATUS_PENDING_REVIEW, 'product_type' => $shop_type]) : 0; $order_items = [ [ 'label' => __('待支付', 'aksrt'), @@ -32,24 +26,6 @@ $order_items = [ 'tab' => 'pending', 'count' => (int) ($order_stats['pending'] ?? 0), ], - [ - 'label' => __('待发货', 'aksrt'), - 'icon' => 'gift', - 'tab' => 'to-ship', - 'count' => $shop_paid, - ], - [ - 'label' => __('待收货', 'aksrt'), - 'icon' => 'truck', - 'tab' => 'shipped', - 'count' => $shop_delivered, - ], - [ - 'label' => __('待评价', 'aksrt'), - 'icon' => 'comment-dots', - 'tab' => 'review', - 'count' => $shop_review, - ], [ 'label' => __('售后', 'aksrt'), 'icon' => 'undo-alt', diff --git a/inc/modules/user-parts/template-registry.php b/inc/modules/user-parts/template-registry.php index 90f5e4d2..0923d6f7 100644 --- a/inc/modules/user-parts/template-registry.php +++ b/inc/modules/user-parts/template-registry.php @@ -113,12 +113,6 @@ $_aksrt_special_templates = [ 'title' => '我的订单', 'fallback' => '/orders/', ], - 'cart' => [ - 'file' => 'templates/pages/page-cart.php', - 'slug' => 'cart', - 'title' => '购物车', - 'fallback' => '/cart/', - ], 'revenue' => [ 'file' => 'templates/pages/page-revenue.php', 'slug' => 'revenue', @@ -413,11 +407,6 @@ function aksrt_get_current_special_template_path(): string { return $orders_template ?: ''; } - if (get_query_var('aksrt_cart')) { - $cart_template = aksrt_locate_template_file('templates/pages/page-cart.php'); - return $cart_template ?: ''; - } - if (get_query_var('aksrt_revenue')) { $revenue_template = aksrt_locate_template_file('templates/pages/page-revenue.php'); return $revenue_template ?: ''; diff --git a/inc/modules/widgets-register.php b/inc/modules/widgets-register.php index f3ec4692..82614bc4 100644 --- a/inc/modules/widgets-register.php +++ b/inc/modules/widgets-register.php @@ -92,12 +92,7 @@ function aksrt_register_widgets() { register_widget('\AKSRT\Widgets\WidgetIconCoverCards'); register_widget('\AKSRT\Widgets\WidgetBookmarkLinks'); - // Shop product widgets (§6.9) - register_widget('\AKSRT\Widgets\WidgetProductList'); - register_widget('\AKSRT\Widgets\WidgetProductOneline'); - register_widget('\AKSRT\Widgets\WidgetProductTab'); - - aksrt_maybe_repair_sidebar_blog_widgets(); + aksrt_maybe_repair_sidebar_blog_widgets(); } add_action('widgets_init', 'aksrt_register_widgets'); diff --git a/src/AKSRT/Admin/Native/PayAfterSale.php b/src/AKSRT/Admin/Native/PayAfterSale.php deleted file mode 100644 index 5e65e751..00000000 --- a/src/AKSRT/Admin/Native/PayAfterSale.php +++ /dev/null @@ -1,357 +0,0 @@ -create([ - 'order_number' => $order_number, - 'type' => sanitize_key((string) wp_unslash($_POST['type'] ?? 'refund')), - 'reason' => sanitize_textarea_field((string) wp_unslash($_POST['reason'] ?? '')), - 'amount' => (float) ($_POST['amount'] ?? 0), - 'note' => sanitize_textarea_field((string) wp_unslash($_POST['note'] ?? '')), - ]); - $ok = !is_wp_error($id) && $id > 0; - } - - $msg = $ok ? 'after_sale_created' : 'after_sale_create_failed'; - if ($redirect_page === 'after_sale') { - wp_safe_redirect($this->page_url('aksrt_after_sale', ['aksrt_pay_msg' => $msg, 's' => $order_number])); - exit; - } - wp_safe_redirect($this->page_url('aksrt_orders', [ - 'order' => $order_number, - 'aksrt_pay_msg' => $msg, - ])); - exit; - } - - public function render_after_sale_page(): void { - $this->ensure_cap(); - $this->render_header(__('售后管理', 'aksrt')); - - if (!class_exists('\AKSRT\Payment\AfterSale')) { - echo '

' . esc_html__('售后模块未初始化。', 'aksrt') . '

'; - $this->render_footer(); - return; - } - - $status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : ''; - $type = isset($_GET['type']) ? sanitize_key((string) wp_unslash($_GET['type'])) : ''; - $search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : ''; - if ($search === '' && isset($_GET['order_number'])) { - $search = sanitize_text_field((string) wp_unslash($_GET['order_number'])); - } - $page_num = max(1, (int) ($_GET['paged'] ?? 1)); - $per_page = 20; - $svc = \AKSRT\Payment\AfterSale::get_instance(); - $stats = $svc->get_stats(); - $status_options = $this->after_sale_status_options(true); - $type_options = $this->after_sale_type_options(true); - - echo '

' . esc_html(sprintf( - /* translators: 1: total 2: pending */ - __('售后工单共 %1$d · 待处理 %2$d', 'aksrt'), - (int) ($stats['total'] ?? 0), - (int) ($stats['pending'] ?? 0) - )) . '

'; - - echo '

' . esc_html__('新建售后', 'aksrt') . '

'; - echo '
'; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_create_after_sale'); - echo '

'; - echo '

'; - echo '

'; - echo '

'; - echo '

'; - submit_button(__('创建售后', 'aksrt'), 'primary', '', false); - echo '
'; - - echo '
'; - echo ''; - echo ' '; - echo ' '; - echo ' '; - submit_button(__('筛选', 'aksrt'), 'secondary', '', false); - echo '
'; - - $list_args = [ - 'status' => $status, - 'type' => $type, - 'limit' => $per_page, - 'offset' => ($page_num - 1) * $per_page, - ]; - if ($search !== '') { - $list_args['order_number'] = $search; - } - $result = $svc->get_list($list_args); - $items = $result['items'] ?? []; - $total = (int) ($result['total'] ?? 0); - $pages = max(1, (int) ceil(max(1, $total) / $per_page)); - $row_status_options = $this->after_sale_status_options(false); - - echo ''; - echo ''; - echo ''; - if (!$items) { - echo ''; - } - foreach ($items as $row) { - $id = (int) ($row['id'] ?? 0); - $user_id = (int) ($row['user_id'] ?? 0); - $user = $user_id > 0 ? get_userdata($user_id) : null; - $st = (string) ($row['status'] ?? ''); - $order_number = (string) ($row['order_number'] ?? ''); - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - } - echo '
ID' . esc_html__('订单号', 'aksrt') . '' . esc_html__('类型', 'aksrt') . '' . esc_html__('用户', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('时间', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '
' . esc_html__('暂无售后工单', 'aksrt') . '
' . esc_html((string) $id) . '' . esc_html($order_number) . '' . esc_html((string) ($row['type_label'] ?? $type_options[$row['type'] ?? ''] ?? $row['type'] ?? '')) . '' . ($user ? esc_html($user->display_name) : esc_html((string) $user_id)) . '' . esc_html((string) ($row['status_label'] ?? $row_status_options[$st] ?? $st)) . '' . esc_html((string) ($row['created_at'] ?? '')) . ''; - if ($st === 'pending') { - echo '
'; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_after_sale_action_' . $id); - submit_button(__('同意', 'aksrt'), 'small', '', false); - echo '
'; - echo '
'; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_after_sale_action_' . $id); - echo ''; - echo '
'; - } - if ($st === 'processing') { - echo '
'; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_after_sale_action_' . $id); - submit_button(__('确认收货', 'aksrt'), 'small', '', false); - echo '
'; - } - echo '
'; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_update_after_sale_' . $id); - echo ''; - echo ''; - submit_button(__('更新', 'aksrt'), 'small', '', false); - echo '
'; - echo '
'; - $this->render_pagination($pages, $page_num); - $this->render_footer(); - } - - public function handle_update_after_sale(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('权限不足', 'aksrt')); - } - $id = isset($_POST['ticket_id']) ? (int) $_POST['ticket_id'] : 0; - $new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : ''; - $note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : ''; - check_admin_referer('aksrt_pay_update_after_sale_' . $id); - - $ok = false; - if ($id > 0 && $new_status !== '' && class_exists('\AKSRT\Payment\AfterSale')) { - $ok = \AKSRT\Payment\AfterSale::get_instance()->update_status($id, $new_status, get_current_user_id(), $note); - } - wp_safe_redirect($this->page_url('aksrt_after_sale', ['aksrt_pay_msg' => $ok ? 'after_sale_updated' : 'after_sale_failed'])); - exit; - } - - public function handle_after_sale_action(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('权限不足', 'aksrt')); - } - $id = isset($_POST['ticket_id']) ? (int) $_POST['ticket_id'] : 0; - $as_action = isset($_POST['as_action']) ? sanitize_key((string) wp_unslash($_POST['as_action'])) : ''; - check_admin_referer('aksrt_pay_after_sale_action_' . $id); - - $ok = false; - if ($id > 0 && class_exists('\AKSRT\Payment\AfterSale')) { - $svc = \AKSRT\Payment\AfterSale::get_instance(); - $operator_id = get_current_user_id(); - if ($as_action === 'approve') { - $ok = $svc->merchant_approve($id, $operator_id); - } elseif ($as_action === 'reject') { - $ok = $svc->merchant_reject($id, $operator_id, __('商家拒绝', 'aksrt')); - } elseif ($as_action === 'confirm_return') { - $ok = $svc->merchant_confirm_return($id, $operator_id); - } - } - - wp_safe_redirect($this->page_url('aksrt_after_sale', [ - 'aksrt_pay_msg' => $ok ? 'after_sale_action_ok' : 'after_sale_action_failed', - ])); - exit; - } - - - private function after_sale_status_options(bool $include_all = false): array { - $options = []; - if ($include_all) { - $options[''] = __('全部状态', 'aksrt'); - } - if (class_exists('\AKSRT\Payment\AfterSale')) { - foreach (\AKSRT\Payment\AfterSale::get_status_options() as $item) { - $value = (string) ($item['value'] ?? ''); - if ($value === '') { - continue; - } - $options[$value] = (string) ($item['label'] ?? $value); - } - } - return $options; - } - - /** - * @return array - */ - public function after_sale_type_options_public(bool $include_all = false): array { - return $this->after_sale_type_options($include_all); - } - - private function after_sale_type_options(bool $include_all = false): array { - $options = []; - if ($include_all) { - $options[''] = __('全部类型', 'aksrt'); - } - // Keep operational create options broader than public get_type_options(). - $defaults = [ - 'refund' => __('仅退款', 'aksrt'), - 'refund_return' => __('退货退款', 'aksrt'), - 'exchange' => __('换货', 'aksrt'), - 'repair' => __('维修', 'aksrt'), - ]; - if (class_exists('\AKSRT\Payment\AfterSale')) { - foreach (\AKSRT\Payment\AfterSale::get_type_options() as $item) { - $value = (string) ($item['value'] ?? ''); - if ($value === '') { - continue; - } - $defaults[$value] = (string) ($item['label'] ?? $value); - } - } - return array_merge($options, $defaults); - } - - - private function ensure_cap(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('您没有权限访问此页面。', 'aksrt')); - } - } - - private function render_header(string $title): void { - echo '
'; - echo '

' . esc_html($title) . '

'; - $this->render_flash(); - } - - private function render_footer(): void { - echo '
'; - } - - private function render_flash(): void { - if (empty($_GET['aksrt_pay_msg'])) { - return; - } - $code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg'])); - $map = [ - 'after_sale_updated' => __('售后状态已更新。', 'aksrt'), - 'after_sale_failed' => __('售后状态更新失败。', 'aksrt'), - 'after_sale_created' => __('售后工单已创建。', 'aksrt'), - 'after_sale_create_failed' => __('售后工单创建失败。', 'aksrt'), - 'after_sale_action_ok' => __('售后操作已完成。', 'aksrt'), - 'after_sale_action_failed' => __('售后操作失败。', 'aksrt'), - ]; - if (!isset($map[$code])) { - return; - } - $class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success'; - echo '

' . esc_html($map[$code]) . '

'; - } - - private function page_url(string $slug, array $args = []): string { - return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php')); - } - - private function render_pagination(int $pages, int $page_num): void { - if ($pages <= 1) { - return; - } - echo '
'; - echo paginate_links([ - 'base' => add_query_arg('paged', '%#%'), - 'format' => '', - 'prev_text' => '«', - 'next_text' => '»', - 'total' => $pages, - 'current' => $page_num, - ]); - echo '
'; - } -} diff --git a/src/AKSRT/Admin/Native/PayCoupons.php b/src/AKSRT/Admin/Native/PayCoupons.php index f9d69ad3..1fa0a4e5 100644 --- a/src/AKSRT/Admin/Native/PayCoupons.php +++ b/src/AKSRT/Admin/Native/PayCoupons.php @@ -372,7 +372,7 @@ class PayCoupons { 'ids' => $ids, ]; if ($type === 'category') { - $scope['taxonomies'] = ['aksrt_product_cat', 'category']; + $scope['taxonomies'] = ['category']; } return $scope; } diff --git a/src/AKSRT/Admin/Native/PayMenus.php b/src/AKSRT/Admin/Native/PayMenus.php index 7394a987..b5dbe027 100644 --- a/src/AKSRT/Admin/Native/PayMenus.php +++ b/src/AKSRT/Admin/Native/PayMenus.php @@ -74,28 +74,6 @@ class PayMenus { wp_die(esc_html__('下载记录模块未初始化。', 'aksrt')); }, ], - 'shipping' => [ - 'slug' => 'aksrt_shipping', - 'title' => __('发货及物流', 'aksrt'), - 'callback' => static function (): void { - if (class_exists('AKSRT\\Admin\\Native\\PayShipping')) { - PayShipping::get_instance()->render_shipping_page(); - return; - } - wp_die(esc_html__('物流模块未初始化。', 'aksrt')); - }, - ], - 'after_sale' => [ - 'slug' => 'aksrt_after_sale', - 'title' => __('售后管理', 'aksrt'), - 'callback' => static function (): void { - if (class_exists('AKSRT\\Admin\\Native\\PayAfterSale')) { - PayAfterSale::get_instance()->render_after_sale_page(); - return; - } - wp_die(esc_html__('售后模块未初始化。', 'aksrt')); - }, - ], 'coupons' => [ 'slug' => 'aksrt_coupons', 'title' => __('优惠码', 'aksrt'), @@ -203,23 +181,6 @@ class PayMenus { echo ''; } } - - if ($page !== 'aksrt_after_sale' && class_exists('\AKSRT\Payment\AfterSale')) { - $as_stats = \AKSRT\Payment\AfterSale::get_instance()->get_stats(); - $pending_as = (int) ($as_stats['pending'] ?? 0); - if ($pending_as > 0) { - $url = admin_url('admin.php?page=aksrt_after_sale&status=pending'); - echo '
'; - echo '

' . esc_html__('售后工单待处理', 'aksrt') . '

'; - echo '

' . esc_html(sprintf( - /* translators: %d: pending after-sale count */ - __('您有 %d 个售后工单待处理', 'aksrt'), - $pending_as - )) . '

'; - echo '

' . esc_html__('去处理', 'aksrt') . '

'; - echo '
'; - } - } } private function ensure_cap(): void { @@ -255,17 +216,12 @@ class PayMenus { ? \AKSRT\Payment\ChargeCard::get_instance()->get_stats() : []; - echo '

' . esc_html__('运营数据入口(对齐 zibll 商城中心)。配置项仍在 AKSRT 主题设置 SPA。', 'aksrt') . '

'; + echo '

' . esc_html__('运营数据入口。配置项仍在 AKSRT 主题设置 SPA。', 'aksrt') . '

'; echo '
'; - $pending_ship = 0; - if (class_exists('\AKSRT\Payment\OrderRepository')) { - $pending_ship = (int) (\AKSRT\Payment\OrderRepository::get_instance()->get_pending_shipment_orders(['limit' => 1])['total'] ?? 0); - } $this->stat_card(__('订单总数', 'aksrt'), (string) (int) ($order_stats['total'] ?? 0), $this->page_url('aksrt_orders')); $this->stat_card(__('待支付', 'aksrt'), (string) (int) ($order_stats['pending'] ?? 0), $this->page_url('aksrt_orders', ['status' => 'pending'])); $this->stat_card(__('已支付', 'aksrt'), (string) (int) ($order_stats['paid'] ?? 0), $this->page_url('aksrt_orders', ['status' => 'paid'])); - $this->stat_card(__('待发货', 'aksrt'), (string) $pending_ship, $this->page_url('aksrt_shipping', ['view' => 'pending'])); $this->stat_card(__('营收', 'aksrt'), number_format((float) ($order_stats['revenue'] ?? 0), 2), $this->page_url('aksrt_orders')); $this->stat_card(__('待审提现', 'aksrt'), (string) (int) ($withdraw_stats['pending'] ?? 0), $this->page_url('aksrt_withdraw', ['status' => 'pending'])); $this->stat_card(__('可用卡密', 'aksrt'), (string) (int) ($card_stats['active'] ?? 0), $this->page_url('aksrt_charge_cards', ['status' => 'active'])); diff --git a/src/AKSRT/Admin/Native/PayOrders.php b/src/AKSRT/Admin/Native/PayOrders.php index 166366a6..63a1db47 100644 --- a/src/AKSRT/Admin/Native/PayOrders.php +++ b/src/AKSRT/Admin/Native/PayOrders.php @@ -288,13 +288,6 @@ class PayOrders { $admin_note = (string) $order['note']; } - $shipping_items = class_exists('\AKSRT\Payment\Shipping') - ? \AKSRT\Payment\Shipping::get_instance()->get_by_order($order_number) - : []; - $after_sale = class_exists('\AKSRT\Payment\AfterSale') - ? (\AKSRT\Payment\AfterSale::get_instance()->get_list(['order_number' => $order_number, 'limit' => 20])['items'] ?? []) - : []; - echo '

← ' . esc_html__('返回订单列表', 'aksrt') . '

'; echo ''; @@ -344,87 +337,6 @@ class PayOrders { echo ' '; submit_button(__('保存状态', 'aksrt'), 'primary', '', false); echo ''; - - echo '

' . esc_html__('物流记录', 'aksrt') . '

'; - echo ''; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_create_shipping_' . $order_number); - echo '

'; - echo '

'; - echo '

'; - submit_button(__('创建发货', 'aksrt'), 'secondary', '', false); - echo ' ' . esc_html__('物流管理', 'aksrt') . ''; - echo ''; - if (!$shipping_items) { - echo '

' . esc_html__('暂无物流记录。', 'aksrt') . '

'; - } else { - $status_options = $this->shipping_status_options(false); - echo '
'; - foreach ($shipping_items as $item) { - $sid = (int) ($item['id'] ?? 0); - $st = (string) ($item['status'] ?? ''); - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - } - echo '
ID' . esc_html__('承运商', 'aksrt') . '' . esc_html__('运单号', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('时间', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '
' . esc_html((string) $sid) . '' . esc_html((string) ($item['carrier'] ?? '')) . '' . esc_html((string) ($item['tracking_no'] ?? '')) . '' . esc_html($status_options[$st] ?? $st) . '' . esc_html((string) ($item['created_at'] ?? '')) . '
'; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_update_shipping_' . $sid); - echo ''; - echo ''; - submit_button(__('更新', 'aksrt'), 'small', '', false); - echo '
'; - } - - echo '

' . esc_html__('售后工单', 'aksrt') . '

'; - echo '
'; - echo ''; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_create_after_sale_' . $order_number); - echo '

'; - echo '

'; - echo '

'; - echo '

'; - submit_button(__('创建售后', 'aksrt'), 'secondary', '', false); - echo ' ' . esc_html__('售后管理', 'aksrt') . ''; - echo '
'; - if (!$after_sale) { - echo '

' . esc_html__('暂无售后工单。', 'aksrt') . '

'; - } else { - echo ''; - foreach ($after_sale as $item) { - $tid = (int) ($item['id'] ?? 0); - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - } - echo '
ID' . esc_html__('类型', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('时间', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '
' . esc_html((string) $tid) . '' . esc_html((string) ($item['type_label'] ?? $item['type'] ?? '')) . '' . esc_html((string) ($item['status_label'] ?? $item['status'] ?? '')) . '' . esc_html((string) ($item['created_at'] ?? '')) . '' . esc_html__('在售后页处理', 'aksrt') . '
'; - } } public function handle_update_order_status(): void { @@ -510,30 +422,6 @@ class PayOrders { exit; } - private function shipping_status_options(bool $include_all = false): array { - if (class_exists('AKSRT\\Admin\\Native\\PayShipping')) { - return PayShipping::get_instance()->shipping_status_options_public($include_all); - } - return $include_all ? ['' => __('全部状态', 'aksrt')] : []; - } - - private function after_sale_type_options(bool $include_all = false): array { - if (class_exists('AKSRT\\Admin\\Native\\PayAfterSale')) { - return PayAfterSale::get_instance()->after_sale_type_options_public($include_all); - } - return $include_all ? ['' => __('全部类型', 'aksrt')] : [ - 'refund' => __('仅退款', 'aksrt'), - ]; - } - - private function render_carrier_field(string $selected = ''): void { - if (class_exists('AKSRT\\Admin\\Native\\PayShipping')) { - PayShipping::get_instance()->render_carrier_field_public($selected); - return; - } - echo ''; - } - private function render_order_quick_status_buttons(string $order_number, string $current_status, bool $redirect_detail = false): void { $actions = [ 'completed' => __('完成', 'aksrt'), diff --git a/src/AKSRT/Admin/Native/PayShipping.php b/src/AKSRT/Admin/Native/PayShipping.php deleted file mode 100644 index caebf180..00000000 --- a/src/AKSRT/Admin/Native/PayShipping.php +++ /dev/null @@ -1,507 +0,0 @@ -ensure_cap(); - $this->render_header(__('发货及物流', 'aksrt')); - - if (!class_exists('\AKSRT\Payment\Shipping')) { - echo '

' . esc_html__('物流模块未初始化。', 'aksrt') . '

'; - $this->render_footer(); - return; - } - - $view = isset($_GET['view']) ? sanitize_key((string) wp_unslash($_GET['view'])) : 'records'; - if (!in_array($view, ['records', 'pending'], true)) { - $view = 'records'; - } - $status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : ''; - $search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : ''; - $page_num = max(1, (int) ($_GET['paged'] ?? 1)); - $per_page = 20; - $svc = \AKSRT\Payment\Shipping::get_instance(); - $stats = $svc->get_stats(); - $status_options = $this->shipping_status_options(true); - - $pending_total = 0; - if (class_exists('\AKSRT\Payment\OrderRepository')) { - $pending_total = (int) (\AKSRT\Payment\OrderRepository::get_instance()->get_pending_shipment_orders(['limit' => 1, 'search' => ''])['total'] ?? 0); - } - - echo '

' . esc_html(sprintf( - /* translators: 1: shipping records 2: pending shipment orders */ - __('物流记录 %1$d 条 · 待发货订单 %2$d', 'aksrt'), - (int) ($stats['total'] ?? 0), - $pending_total - )) . '

'; - - echo ''; - - if ($view === 'pending') { - $this->render_pending_shipment_board($search, $page_num, $per_page); - $this->render_footer(); - return; - } - - echo '

' . esc_html__('新建发货', 'aksrt') . '

'; - echo '
'; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_create_shipping'); - echo '

'; - echo '

'; - echo '

'; - echo '

'; - submit_button(__('创建发货', 'aksrt'), 'primary', '', false); - echo '
'; - - echo '
'; - echo ''; - echo ''; - echo ' '; - echo ' '; - submit_button(__('筛选', 'aksrt'), 'secondary', '', false); - echo '
'; - - $args = [ - 'status' => $status, - 'limit' => $per_page, - 'offset' => ($page_num - 1) * $per_page, - ]; - if ($search !== '') { - if (preg_match('/^[A-Za-z0-9_-]+$/', $search)) { - $args['order_number'] = $search; - } else { - $args['tracking_no'] = $search; - } - } - $result = $svc->get_list($args); - $items = $result['items'] ?? []; - $total = (int) ($result['total'] ?? 0); - $pages = max(1, (int) ceil(max(1, $total) / $per_page)); - $row_status_options = $this->shipping_status_options(false); - $show_express_id = isset($_GET['express_id']) ? (int) $_GET['express_id'] : 0; - - echo ''; - echo ''; - echo ''; - if (!$items) { - echo ''; - } - foreach ($items as $row) { - $id = (int) ($row['id'] ?? 0); - $st = (string) ($row['status'] ?? ''); - $order_number = (string) ($row['order_number'] ?? ''); - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - if ($show_express_id === $id) { - echo ''; - } - } - echo '
ID' . esc_html__('订单号', 'aksrt') . '' . esc_html__('承运商', 'aksrt') . '' . esc_html__('运单号', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('时间', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '
' . esc_html__('暂无物流记录', 'aksrt') . '
' . esc_html((string) $id) . '' . esc_html($order_number) . '' . esc_html((string) ($row['carrier'] ?? '')) . '' . esc_html((string) ($row['tracking_no'] ?? '')) . '' . esc_html($row_status_options[$st] ?? $st) . '' . esc_html((string) ($row['created_at'] ?? '')) . ''; - echo '
'; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_update_shipping_' . $id); - echo ''; - echo ''; - submit_button(__('更新', 'aksrt'), 'small', '', false); - echo '
'; - echo '
'; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_query_express_' . $id); - submit_button(__('查物流', 'aksrt'), 'small', '', false); - echo '
'; - echo '' . esc_html__('轨迹', 'aksrt') . ''; - echo '
'; - $this->render_express_timeline_panel($id); - echo '
'; - $this->render_pagination($pages, $page_num); - $this->render_footer(); - } - - private function render_pending_shipment_board(string $search, int $page_num, int $per_page): void { - if (!class_exists('\AKSRT\Payment\OrderRepository')) { - echo '

' . esc_html__('订单服务不可用。', 'aksrt') . '

'; - return; - } - - echo '
'; - echo ''; - echo ''; - echo ' '; - submit_button(__('筛选', 'aksrt'), 'secondary', '', false); - echo '
'; - - $result = \AKSRT\Payment\OrderRepository::get_instance()->get_pending_shipment_orders([ - 'search' => $search, - 'limit' => $per_page, - 'offset' => ($page_num - 1) * $per_page, - ]); - $items = $result['items'] ?? []; - $total = (int) ($result['total'] ?? 0); - $pages = max(1, (int) ($result['max_pages'] ?? 1)); - $labels = class_exists('\AKSRT\Core\DomainModel') - ? \AKSRT\Core\DomainModel::order_status_labels() - : []; - - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - - if (!$items) { - echo ''; - } - - foreach ($items as $row) { - $order_number = (string) ($row['order_number'] ?? ''); - $user_id = (int) ($row['user_id'] ?? 0); - $user = $user_id > 0 ? get_userdata($user_id) : null; - $status_key = (string) ($row['status'] ?? ''); - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - } - echo '
' . esc_html__('订单号', 'aksrt') . '' . esc_html__('用户', 'aksrt') . '' . esc_html__('类型', 'aksrt') . '' . esc_html__('金额', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('支付时间', 'aksrt') . '' . esc_html__('立即发货', 'aksrt') . '
' . esc_html__('暂无待发货订单', 'aksrt') . '
' . esc_html($order_number) . '' . ($user ? esc_html($user->display_name . ' (#' . $user_id . ')') : esc_html((string) $user_id)) . '' . esc_html((string) ($row['product_type'] ?? '')) . '' . esc_html(number_format((float) ($row['amount'] ?? 0), 2)) . ' ' . esc_html((string) ($row['currency'] ?? '')) . '' . esc_html($labels[$status_key] ?? $status_key) . '' . esc_html((string) ($row['paid_at'] ?? $row['created_at'] ?? '')) . ''; - echo '
'; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_create_shipping_' . $order_number); - $this->render_carrier_field(''); - echo ''; - echo ''; - submit_button(__('发货', 'aksrt'), 'primary small', '', false); - echo '
'; - echo '
'; - $this->render_pagination($pages, $page_num); - } - - public function handle_update_shipping(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('权限不足', 'aksrt')); - } - $id = isset($_POST['shipping_id']) ? (int) $_POST['shipping_id'] : 0; - $new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : ''; - $note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : ''; - check_admin_referer('aksrt_pay_update_shipping_' . $id); - - $ok = false; - if ($id > 0 && $new_status !== '' && class_exists('\AKSRT\Payment\Shipping')) { - $ok = \AKSRT\Payment\Shipping::get_instance()->update_status($id, $new_status, $note, get_current_user_id()); - } - - $redirect_order = isset($_POST['redirect_order']) ? sanitize_text_field((string) wp_unslash($_POST['redirect_order'])) : ''; - if ($redirect_order !== '') { - wp_safe_redirect($this->page_url('aksrt_orders', [ - 'order' => $redirect_order, - 'aksrt_pay_msg' => $ok ? 'shipping_updated' : 'shipping_failed', - ])); - exit; - } - - wp_safe_redirect($this->page_url('aksrt_shipping', [ - 'view' => 'records', - 'aksrt_pay_msg' => $ok ? 'shipping_updated' : 'shipping_failed', - ])); - exit; - } - - public function handle_query_express(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('权限不足', 'aksrt')); - } - $id = isset($_POST['shipping_id']) ? (int) $_POST['shipping_id'] : 0; - $force = !empty($_POST['force']); - check_admin_referer('aksrt_pay_query_express_' . $id); - - $ok = false; - $order_number = ''; - if ($id > 0 && class_exists('\AKSRT\Payment\Shipping')) { - $svc = \AKSRT\Payment\Shipping::get_instance(); - $record = $svc->get($id); - $order_number = is_array($record) ? (string) ($record['order_number'] ?? '') : ''; - $result = $svc->query_express($id, $force); - $ok = !empty($result['success']); - } - - wp_safe_redirect($this->page_url('aksrt_shipping', [ - 'view' => 'records', - 'express_id' => $id, - 's' => $order_number, - 'aksrt_pay_msg' => $ok ? 'express_ok' : 'express_failed', - ])); - exit; - } - - public function handle_create_shipping(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('权限不足', 'aksrt')); - } - $order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : ''; - $redirect_page = isset($_POST['redirect_page']) ? sanitize_key((string) wp_unslash($_POST['redirect_page'])) : 'order'; - $nonce_action = $redirect_page === 'shipping' ? 'aksrt_pay_create_shipping' : ('aksrt_pay_create_shipping_' . $order_number); - check_admin_referer($nonce_action); - - $ok = false; - if ($order_number !== '' && class_exists('\AKSRT\Payment\Shipping')) { - if (class_exists('\AKSRT\Payment\OrderRepository')) { - $order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number); - if (!$order) { - $this->redirect_shipping_create_result(false, $order_number, $redirect_page); - } - } - $id = \AKSRT\Payment\Shipping::get_instance()->create([ - 'order_number' => $order_number, - 'carrier' => sanitize_text_field((string) wp_unslash($_POST['carrier'] ?? '')), - 'tracking_no' => sanitize_text_field((string) wp_unslash($_POST['tracking_no'] ?? '')), - 'note' => sanitize_textarea_field((string) wp_unslash($_POST['note'] ?? '')), - 'operator_id' => get_current_user_id(), - ]); - $ok = $id > 0; - } - - $this->redirect_shipping_create_result($ok, $order_number, $redirect_page); - } - - private function redirect_shipping_create_result(bool $ok, string $order_number, string $redirect_page): void { - $msg = $ok ? 'shipping_created' : 'shipping_create_failed'; - if ($redirect_page === 'shipping') { - wp_safe_redirect($this->page_url('aksrt_shipping', [ - 'view' => 'records', - 'aksrt_pay_msg' => $msg, - 's' => $order_number, - ])); - exit; - } - if ($redirect_page === 'pending') { - wp_safe_redirect($this->page_url('aksrt_shipping', [ - 'view' => 'pending', - 'aksrt_pay_msg' => $msg, - ])); - exit; - } - wp_safe_redirect($this->page_url('aksrt_orders', [ - 'order' => $order_number, - 'aksrt_pay_msg' => $msg, - ])); - exit; - } - - - public function shipping_status_options_public(bool $include_all = false): array { - return $this->shipping_status_options($include_all); - } - - private function shipping_status_options(bool $include_all = false): array { - $options = []; - if ($include_all) { - $options[''] = __('全部状态', 'aksrt'); - } - if (class_exists('\AKSRT\Payment\Shipping')) { - foreach (\AKSRT\Payment\Shipping::get_status_options() as $item) { - $value = (string) ($item['value'] ?? ''); - if ($value === '') { - continue; - } - $options[$value] = (string) ($item['label'] ?? $value); - } - } - if (count($options) === (int) $include_all) { - $options = array_merge($options, [ - 'shipped' => __('已发货', 'aksrt'), - 'in_transit' => __('运输中', 'aksrt'), - 'delivered' => __('已签收', 'aksrt'), - 'returned' => __('已退回', 'aksrt'), - ]); - } - return $options; - } - public function render_carrier_field_public(string $selected = ''): void { - $this->render_carrier_field($selected); - } - - private function render_carrier_field(string $selected = ''): void { - $options = []; - if (class_exists('\AKSRT\Payment\Shipping')) { - foreach (\AKSRT\Payment\Shipping::get_carrier_options() as $item) { - $value = (string) ($item['value'] ?? ''); - $label = (string) ($item['label'] ?? $value); - if ($value !== '') { - $options[$value] = $label; - } - } - } - if ($options) { - echo ''; - return; - } - echo ''; - } - private function render_express_timeline_panel(int $shipping_id): void { - if ($shipping_id <= 0 || !class_exists('\AKSRT\Payment\Shipping')) { - echo '

' . esc_html__('无法加载物流轨迹。', 'aksrt') . '

'; - return; - } - $svc = \AKSRT\Payment\Shipping::get_instance(); - // Prefer cached data; do not force remote query on pure view. - $timeline = $svc->build_express_timeline($shipping_id); - $record = $svc->get($shipping_id); - echo '
'; - echo '

' . esc_html__('物流轨迹', 'aksrt') . ' · '; - echo esc_html((string) ($record['carrier'] ?? '')) . ' / ' . esc_html((string) ($record['tracking_no'] ?? '')); - echo '

'; - if (!$timeline) { - echo '

' . esc_html__('暂无轨迹,可点击「查物流」拉取快递 API。', 'aksrt') . '

'; - echo '
'; - return; - } - echo '
    '; - // newest first for operators - $timeline = array_reverse($timeline); - foreach ($timeline as $item) { - $time = (string) ($item['time'] ?? ''); - $label = (string) ($item['label'] ?? ''); - echo '
  1. ' . esc_html($time) . ' ' . esc_html($label) . '
  2. '; - } - echo '
'; - } - - - private function ensure_cap(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('您没有权限访问此页面。', 'aksrt')); - } - } - - private function render_header(string $title): void { - echo '
'; - echo '

' . esc_html($title) . '

'; - $this->render_flash(); - } - - private function render_footer(): void { - echo '
'; - } - - private function render_flash(): void { - if (empty($_GET['aksrt_pay_msg'])) { - return; - } - $code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg'])); - $map = [ - 'shipping_updated' => __('物流状态已更新。', 'aksrt'), - 'shipping_failed' => __('物流状态更新失败。', 'aksrt'), - 'shipping_created' => __('发货记录已创建。', 'aksrt'), - 'shipping_create_failed' => __('发货记录创建失败。', 'aksrt'), - 'express_ok' => __('物流轨迹已更新。', 'aksrt'), - 'express_failed' => __('物流轨迹查询失败。', 'aksrt'), - ]; - if (!isset($map[$code])) { - return; - } - $class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success'; - echo '

' . esc_html($map[$code]) . '

'; - } - - private function page_url(string $slug, array $args = []): string { - return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php')); - } - - private function render_pagination(int $pages, int $page_num): void { - if ($pages <= 1) { - return; - } - echo '
'; - echo paginate_links([ - 'base' => add_query_arg('paged', '%#%'), - 'format' => '', - 'prev_text' => '«', - 'next_text' => '»', - 'total' => $pages, - 'current' => $page_num, - ]); - echo '
'; - } -} diff --git a/src/AKSRT/Admin/Schema/SchemaShop.php b/src/AKSRT/Admin/Schema/SchemaShop.php deleted file mode 100644 index 838d1577..00000000 --- a/src/AKSRT/Admin/Schema/SchemaShop.php +++ /dev/null @@ -1,28 +0,0 @@ - 'shop', - 'title' => '商城&商品', - 'version' => '1.0.0', - 'sections' => [ - 'basic' => SchemaShopBasic::build(), - 'list' => SchemaShopList::build(), - 'contact' => SchemaShopContact::build(), - 'single' => SchemaShopSingle::build(), - 'product-defaults' => SchemaShopProductDefaults::build(), - 'shipping-config' => SchemaShopShippingConfig::build(), - 'timing' => SchemaShopTiming::build(), - ], - ]; - } -} diff --git a/src/AKSRT/Admin/Schema/SchemaShopBasic.php b/src/AKSRT/Admin/Schema/SchemaShopBasic.php deleted file mode 100644 index 91f2d1ed..00000000 --- a/src/AKSRT/Admin/Schema/SchemaShopBasic.php +++ /dev/null @@ -1,37 +0,0 @@ - 'basic', - 'title' => '商城基础', - 'version' => '1.0.0', - 'option_key' => 'aksrt_settings', - 'groups' => [ - ['key' => 'general', 'title' => '商城开关', 'fields' => [ - ['key' => 'shop_s', 'label' => '启用商城', 'type' => 'switch', 'default' => true], - ['key' => 'shop_home_url', 'label' => '商城首页链接', 'type' => 'input', 'default' => ''], - ]], - ['key' => 'breadcrumbs', 'title' => '面包屑', 'fields' => [ - ['key' => 'shop_breadcrumbs_s', 'label' => '显示面包屑', 'type' => 'switch', 'default' => true], - ['key' => 'shop_breadcrumbs_home', 'label' => '显示首页链接', 'type' => 'switch', 'default' => true], - ['key' => 'shop_breadcrumbs_shop_home', 'label' => '显示商城首页', 'type' => 'switch', 'default' => false], - ['key' => 'shop_breadcrumbs_shop_home_name', 'label' => '商城首页名称', 'type' => 'input', 'default' => '商城首页'], - ]], - ['key' => 'rewrite', 'title' => 'URL 别名', 'fields' => [ - ['key' => 'shop_product_rewrite_slug', 'label' => '商品 URL 别名', 'type' => 'input', 'default' => 'product'], - ['key' => 'shop_cart_rewrite_slug', 'label' => '购物车 URL 别名', 'type' => 'input', 'default' => 'cart'], - ['key' => 'shop_rewrite_suffix_html_s', 'label' => '.html 伪静态后缀', 'type' => 'switch', 'default' => true], - ]], - ], - ]; - } -} diff --git a/src/AKSRT/Admin/Schema/SchemaShopContact.php b/src/AKSRT/Admin/Schema/SchemaShopContact.php deleted file mode 100644 index e6e19563..00000000 --- a/src/AKSRT/Admin/Schema/SchemaShopContact.php +++ /dev/null @@ -1,30 +0,0 @@ - 'contact', - 'title' => '客服配置', - 'version' => '1.0.0', - 'option_key' => 'aksrt_settings', - 'groups' => [ - ['key' => 'button', 'title' => '客服按钮', 'fields' => [ - ['key' => 'shop_contact_msg_s', 'label' => '显示客服按钮', 'type' => 'switch', 'default' => true], - ['key' => 'shop_contact_name', 'label' => '按钮文案', 'type' => 'input', 'default' => '联系客服'], - ['key' => 'shop_contact_note', 'label' => '客服备注', 'type' => 'textarea', 'default' => ''], - ]], - ['key' => 'channels', 'title' => '联系方式', 'fields' => [ - ['key' => 'shop_contact_more', 'label' => '多客服联系方式', 'type' => 'textarea', 'default' => '', 'help' => '每行一个:名称|图标|链接|二维码图片 URL'], - ]], - ], - ]; - } -} diff --git a/src/AKSRT/Admin/Schema/SchemaShopList.php b/src/AKSRT/Admin/Schema/SchemaShopList.php deleted file mode 100644 index da6c4c58..00000000 --- a/src/AKSRT/Admin/Schema/SchemaShopList.php +++ /dev/null @@ -1,51 +0,0 @@ - 'list', - 'title' => '商品列表', - 'version' => '1.0.0', - 'option_key' => 'aksrt_settings', - 'groups' => [ - ['key' => 'pagination', 'title' => '列表分页', 'fields' => [ - ['key' => 'shop_list_orderby', 'label' => '默认排序', 'type' => 'select', 'default' => 'date', 'options' => [ - ['label' => '最新发布', 'value' => 'date'], - ['label' => '销量最高', 'value' => 'sales'], - ['label' => '价格最低', 'value' => 'price_asc'], - ['label' => '价格最高', 'value' => 'price_desc'], - ['label' => '评价最多', 'value' => 'comments'], - ]], - ['key' => 'shop_list_count', 'label' => '每页数量', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 100], - ['key' => 'shop_list_paging', 'label' => '翻页模式', 'type' => 'radio', 'default' => 'number', 'options' => [ - ['label' => '数字翻页', 'value' => 'number'], - ['label' => 'AJAX 加载', 'value' => 'ajax'], - ]], - ['key' => 'shop_list_ias_s', 'label' => '滚动自动加载', 'type' => 'switch', 'default' => false], - ['key' => 'shop_list_ias_max', 'label' => '自动加载页数', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 50], - ]], - ['key' => 'style', 'title' => '列表样式', 'fields' => [ - ['key' => 'shop_list_style', 'label' => '卡片样式', 'type' => 'select', 'default' => 'default', 'options' => [ - ['label' => '默认', 'value' => 'default'], - ['label' => '竖向大卡片', 'value' => 'card-lg'], - ['label' => '横向小卡片', 'value' => 'card-sm'], - ]], - ['key' => 'shop_sales_show', 'label' => '销量显示', 'type' => 'select', 'default' => 'show', 'options' => [ - ['label' => '显示', 'value' => 'show'], - ['label' => '不显示', 'value' => 'hide'], - ['label' => '超量显示', 'value' => 'threshold'], - ]], - ['key' => 'shop_sales_show_min', 'label' => '起显阈值', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 99999], - ]], - ], - ]; - } -} diff --git a/src/AKSRT/Admin/Schema/SchemaShopProductDefaults.php b/src/AKSRT/Admin/Schema/SchemaShopProductDefaults.php deleted file mode 100644 index d3700878..00000000 --- a/src/AKSRT/Admin/Schema/SchemaShopProductDefaults.php +++ /dev/null @@ -1,46 +0,0 @@ - 'product-defaults', - 'title' => '商品默认参数', - 'version' => '1.0.0', - 'option_key' => 'aksrt_settings', - 'groups' => [ - ['key' => 'policy', 'title' => '购买策略', 'fields' => [ - ['key' => 'shop_limit_buy', 'label' => '默认限购', 'type' => 'number', 'default' => -1, 'min' => -1, 'max' => 9999, 'help' => '-1 不限,0 禁购,正数为限购数量'], - ['key' => 'shop_guest_buy', 'label' => '免登录购买', 'type' => 'switch', 'default' => false], - ['key' => 'shop_email_fill', 'label' => '邮箱必留', 'type' => 'switch', 'default' => false], - ['key' => 'shop_rebate', 'label' => '允许返佣', 'type' => 'switch', 'default' => true], - ]], - ['key' => 'shipping', 'title' => '运费默认', 'fields' => [ - ['key' => 'shop_shipping_fee', 'label' => '默认运费类型', 'type' => 'select', 'default' => 'free', 'options' => [ - ['label' => '包邮', 'value' => 'free'], - ['label' => '固定运费', 'value' => 'fixed'], - ['label' => '按金额计算', 'value' => 'amount'], - ]], - ]], - ['key' => 'afterSale', 'title' => '售后与服务', 'fields' => [ - ['key' => 'shop_after_sale', 'label' => '默认售后政策', 'type' => 'select', 'default' => 'refund_return', 'options' => [ - ['label' => '仅退款', 'value' => 'refund'], - ['label' => '退货退款', 'value' => 'refund_return'], - ['label' => '不支持售后', 'value' => 'none'], - ]], - ['key' => 'shop_service', 'label' => '服务保障', 'type' => 'textarea', 'default' => ''], - ]], - ['key' => 'params', 'title' => '默认参数模板', 'fields' => [ - ['key' => 'shop_product_params_default', 'label' => '商品参数模板', 'type' => 'textarea', 'default' => ''], - ]], - ], - ]; - } -} diff --git a/src/AKSRT/Admin/Schema/SchemaShopShippingConfig.php b/src/AKSRT/Admin/Schema/SchemaShopShippingConfig.php deleted file mode 100644 index 7df3b1b9..00000000 --- a/src/AKSRT/Admin/Schema/SchemaShopShippingConfig.php +++ /dev/null @@ -1,41 +0,0 @@ - 'shipping-config', - 'title' => '物流配置', - 'version' => '1.0.0', - 'option_key' => 'aksrt_settings', - 'groups' => [ - ['key' => 'address', 'title' => '收货地址', 'fields' => [ - ['key' => 'shop_manual_address', 'label' => '手动填写收货地址', 'type' => 'switch', 'default' => false], - ]], - ['key' => 'express', 'title' => '快递查询', 'fields' => [ - ['key' => 'shop_express_api_sdk', 'label' => '快递查询接口', 'type' => 'select', 'default' => 'kuaidi100', 'options' => [ - ['label' => '快递100', 'value' => 'kuaidi100'], - ['label' => '阿里云', 'value' => 'aliyun'], - ['label' => '快递鸟', 'value' => 'kdniao'], - ]], - ['key' => 'shop_express_query_interval', 'label' => '查询刷新间隔(分钟)', 'type' => 'number', 'default' => 60, 'min' => 30, 'max' => 240], - ]], - ['key' => 'credentials', 'title' => '接口凭据', 'fields' => [ - ['key' => 'shop_express_kdniao_opt', 'label' => '快递鸟凭据', 'type' => 'textarea', 'default' => ''], - ['key' => 'shop_express_kuaidi100_opt', 'label' => '快递100凭据', 'type' => 'textarea', 'default' => ''], - ['key' => 'shop_express_aliyun_opt', 'label' => '阿里云凭据', 'type' => 'textarea', 'default' => ''], - ]], - ['key' => 'companies', 'title' => '物流公司', 'fields' => [ - ['key' => 'shop_shipping_company', 'label' => '物流公司列表', 'type' => 'textarea', 'default' => ''], - ]], - ], - ]; - } -} diff --git a/src/AKSRT/Admin/Schema/SchemaShopSingle.php b/src/AKSRT/Admin/Schema/SchemaShopSingle.php deleted file mode 100644 index b7b92837..00000000 --- a/src/AKSRT/Admin/Schema/SchemaShopSingle.php +++ /dev/null @@ -1,39 +0,0 @@ - 'single', - 'title' => '商品详情', - 'version' => '1.0.0', - 'option_key' => 'aksrt_settings', - 'groups' => [ - ['key' => 'layout', 'title' => '详情页布局', 'fields' => [ - ['key' => 'shop_content_layout', 'label' => '内容布局', 'type' => 'select', 'default' => 'adapt', 'options' => [ - ['label' => '适应宽度', 'value' => 'adapt'], - ['label' => '全屏', 'value' => 'full'], - ['label' => '适应宽度+侧栏', 'value' => 'sidebar'], - ]], - ['key' => 'shop_content_show_bg', 'label' => '详情背景盒子', 'type' => 'switch', 'default' => false], - ['key' => 'shop_single_tab', 'label' => '自定义 TAB 栏目', 'type' => 'textarea', 'default' => ''], - ['key' => 'shop_content_after', 'label' => '详情页底部内容', 'type' => 'textarea', 'default' => ''], - ]], - ['key' => 'related', 'title' => '相关推荐', 'fields' => [ - ['key' => 'shop_single_related_s', 'label' => '显示相关推荐', 'type' => 'switch', 'default' => true], - ['key' => 'shop_single_related_opt', 'label' => '推荐配置', 'type' => 'textarea', 'default' => ''], - ]], - ['key' => 'image', 'title' => '商品图片', 'fields' => [ - ['key' => 'shop_main_image_default', 'label' => '默认商品主图', 'type' => 'media', 'default' => ''], - ]], - ], - ]; - } -} diff --git a/src/AKSRT/Admin/Schema/SchemaShopTiming.php b/src/AKSRT/Admin/Schema/SchemaShopTiming.php deleted file mode 100644 index e6f5e10d..00000000 --- a/src/AKSRT/Admin/Schema/SchemaShopTiming.php +++ /dev/null @@ -1,38 +0,0 @@ - 'timing', - 'title' => '时效与售后原因', - 'version' => '1.0.0', - 'option_key' => 'aksrt_settings', - 'groups' => [ - ['key' => 'receipt', 'title' => '确认收货', 'fields' => [ - ['key' => 'shop_order_receipt_max_day', 'label' => '自动确认收货(天)', 'type' => 'number', 'default' => 15, 'min' => 1, 'max' => 60], - ]], - ['key' => 'comment', 'title' => '评价时效', 'fields' => [ - ['key' => 'shop_comment_max_day', 'label' => '评价时效(天)', 'type' => 'number', 'default' => 7, 'min' => 1, 'max' => 60], - ['key' => 'shop_comment_placeholder', 'label' => '评价占位文案', 'type' => 'input', 'default' => ''], - ['key' => 'shop_comment_img_num', 'label' => '评价图片数', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 9], - ]], - ['key' => 'afterSale', 'title' => '售后退货', 'fields' => [ - ['key' => 'shop_after_sale_return_express_max_day', 'label' => '退货快递时效(天)', 'type' => 'number', 'default' => 7, 'min' => 1, 'max' => 30], - ['key' => 'shop_after_sale_reason_refund', 'label' => '仅退款原因', 'type' => 'textarea', 'default' => ''], - ['key' => 'shop_after_sale_reason_return', 'label' => '退货退款原因', 'type' => 'textarea', 'default' => ''], - ]], - ['key' => 'author', 'title' => '商家信息', 'fields' => [ - ['key' => 'shop_author_show', 'label' => '显示商家信息', 'type' => 'switch', 'default' => true], - ]], - ], - ]; - } -} diff --git a/src/AKSRT/Admin/ShopController.php b/src/AKSRT/Admin/ShopController.php deleted file mode 100644 index 4d9ca529..00000000 --- a/src/AKSRT/Admin/ShopController.php +++ /dev/null @@ -1,453 +0,0 @@ -admin = $admin; - } - - public function register_routes(): void { - register_rest_route(self::API_NAMESPACE, '/shop/frontend/checkout/preview', [ - 'methods' => 'POST', - 'callback' => [$this, 'preview_checkout'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders', [ - 'methods' => 'POST', - 'callback' => [$this, 'create_frontend_order'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_frontend_orders'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P[A-Za-z0-9_-]+)/pay', [ - 'methods' => 'POST', - 'callback' => [$this, 'pay_frontend_order'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P[A-Za-z0-9_-]+)/cancel', [ - 'methods' => 'POST', - 'callback' => [$this, 'cancel_frontend_order'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P[A-Za-z0-9_-]+)', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_frontend_order'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P[A-Za-z0-9_-]+)/after-sale', [ - 'methods' => 'POST', - 'callback' => [$this, 'create_frontend_after_sale'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/after-sale', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_frontend_after_sales'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/after-sale/(?P\d+)/return-shipping', [ - 'methods' => 'POST', - 'callback' => [$this, 'submit_frontend_return_shipping'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/after-sale/(?P\d+)/cancel', [ - 'methods' => 'POST', - 'callback' => [$this, 'cancel_frontend_after_sale'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/coupons', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_frontend_coupons'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - // ── Address book CRUD (frontend) ── - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/address', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_frontend_addresses'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/address', [ - 'methods' => 'POST', - 'callback' => [$this, 'create_frontend_address'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/address/(?P\d+)', [ - 'methods' => 'POST', - 'callback' => [$this, 'update_frontend_address'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/address/(?P\d+)', [ - 'methods' => 'DELETE', - 'callback' => [$this, 'delete_frontend_address'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/address/(?P\d+)/default', [ - 'methods' => 'POST', - 'callback' => [$this, 'set_default_frontend_address'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/products', [ - 'methods' => 'POST', - 'callback' => [$this, 'create_frontend_product'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/products/(?P\d+)', [ - 'methods' => 'POST', - 'callback' => [$this, 'update_frontend_product'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - // Cart endpoints (§6.4) - register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_cart'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/add', [ - 'methods' => 'POST', - 'callback' => [$this, 'handle_add_to_cart'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/reduce', [ - 'methods' => 'POST', - 'callback' => [$this, 'handle_reduce_from_cart'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/remove', [ - 'methods' => 'POST', - 'callback' => [$this, 'handle_remove_from_cart'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/remove-multi', [ - 'methods' => 'POST', - 'callback' => [$this, 'handle_remove_multi_from_cart'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/empty', [ - 'methods' => 'POST', - 'callback' => [$this, 'handle_empty_cart'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/count', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_cart_count'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - // Shop overview / dashboard - register_rest_route(self::API_NAMESPACE, '/shop/overview', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_overview'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // Product taxonomy options used by the product editor. - register_rest_route(self::API_NAMESPACE, '/shop/products/taxonomy', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_product_taxonomy'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // Create product category or tag term for the product editor. - register_rest_route(self::API_NAMESPACE, '/shop/products/taxonomy', [ - 'methods' => 'POST', - 'callback' => [$this, 'create_product_taxonomy_term'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // Product list - register_rest_route(self::API_NAMESPACE, '/shop/products', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_products'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // Product create - register_rest_route(self::API_NAMESPACE, '/shop/products', [ - 'methods' => 'POST', - 'callback' => [$this, 'create_product'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // Product update - register_rest_route(self::API_NAMESPACE, '/shop/products/(?P\d+)', [ - 'methods' => 'POST', - 'callback' => [$this, 'update_product'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // Product single read - register_rest_route(self::API_NAMESPACE, '/shop/products/(?P\d+)', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_product'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // Order list - register_rest_route(self::API_NAMESPACE, '/shop/orders', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_orders'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // Order single read - register_rest_route(self::API_NAMESPACE, '/shop/orders/(?P[A-Za-z0-9_-]+)', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_order'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // Order status update — canonical field is order_number - register_rest_route(self::API_NAMESPACE, '/shop/orders/update-status', [ - 'methods' => 'POST', - 'callback' => [$this, 'update_order_status'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // Gateway status - register_rest_route(self::API_NAMESPACE, '/shop/gateways', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_gateways'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // Coupon list (admin) - register_rest_route(self::API_NAMESPACE, '/shop/coupons', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_coupons'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // Coupon create (admin) - register_rest_route(self::API_NAMESPACE, '/shop/coupons', [ - 'methods' => 'POST', - 'callback' => [$this, 'create_coupon'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // Coupon disable (admin) - register_rest_route(self::API_NAMESPACE, '/shop/coupons/(?P\d+)/disable', [ - 'methods' => 'POST', - 'callback' => [$this, 'disable_coupon'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // ── After Sale ── - - // After-sale list (admin) - register_rest_route(self::API_NAMESPACE, '/shop/after-sale', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_after_sale_list'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // After-sale create (admin) - register_rest_route(self::API_NAMESPACE, '/shop/after-sale', [ - 'methods' => 'POST', - 'callback' => [$this, 'create_after_sale'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // After-sale status update (admin) - register_rest_route(self::API_NAMESPACE, '/shop/after-sale/(?P\d+)/update-status', [ - 'methods' => 'POST', - 'callback' => [$this, 'update_after_sale_status'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/after-sale/(?P\d+)', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_after_sale_detail'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // ── Shipping ── - - // Shipping list (admin) - register_rest_route(self::API_NAMESPACE, '/shop/shipping', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_shipping_list'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // Shipping create (admin) - register_rest_route(self::API_NAMESPACE, '/shop/shipping', [ - 'methods' => 'POST', - 'callback' => [$this, 'create_shipping'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // Shipping status update (admin) - register_rest_route(self::API_NAMESPACE, '/shop/shipping/(?P\d+)/update-status', [ - 'methods' => 'POST', - 'callback' => [$this, 'update_shipping_status'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/shipping/(?P\d+)', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_shipping_detail'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/shipping/batch-update-status', [ - 'methods' => 'POST', - 'callback' => [$this, 'batch_update_shipping_status'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - - // ── R68: Order export + confirm receipt + after-sale reasons ── - - register_rest_route(self::API_NAMESPACE, '/shop/orders/export', [ - 'methods' => 'GET', - 'callback' => [$this, 'export_orders'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // ── R69: Frontend after-sale reasons (§6.8) ── - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/after-sale/reasons', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_frontend_after_sale_reasons'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P[A-Za-z0-9_-]+)/confirm-receipt', [ - 'methods' => 'POST', - 'callback' => [$this, 'confirm_receipt'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P[A-Za-z0-9_-]+)/rebuy', [ - 'methods' => 'POST', - 'callback' => [$this, 'rebuy_frontend_order'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/products/archive', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_frontend_product_archive'], - 'permission_callback' => '__return_true', - ]); - - // Product list widget AJAX (zibll ajax_widget_ui product lists). - register_rest_route(self::API_NAMESPACE, '/shop/frontend/widgets/products', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_frontend_widget_products'], - 'permission_callback' => '__return_true', - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/revenue', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_frontend_revenue'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/revenue/withdraw', [ - 'methods' => 'POST', - 'callback' => [$this, 'create_frontend_withdraw'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P[A-Za-z0-9_-]+)/review', [ - 'methods' => 'POST', - 'callback' => [$this, 'submit_frontend_product_review'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P[A-Za-z0-9_-]+)/review-modal', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_frontend_product_review_modal'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - -register_rest_route(self::API_NAMESPACE, '/shop/frontend/products/(?P\d+)/favorite', [ - 'methods' => 'POST', - 'callback' => [$this, 'toggle_frontend_product_favorite'], - 'permission_callback' => [$this, 'check_logged_in_permission'], - ]); - - register_rest_route(self::API_NAMESPACE, '/shop/after-sale/reasons', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_after_sale_reasons'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // ── Product Variants / SKU ── - - // List variants for a product - register_rest_route(self::API_NAMESPACE, '/shop/products/(?P\d+)/variants', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_product_variants'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // Save variants for a product (replace all) - register_rest_route(self::API_NAMESPACE, '/shop/products/(?P\d+)/variants', [ - 'methods' => 'POST', - 'callback' => [$this, 'save_product_variants'], - 'permission_callback' => [$this, 'check_permission'], - ]); - - // Delete a variant - register_rest_route(self::API_NAMESPACE, '/shop/variants/(?P\d+)', [ - 'methods' => 'DELETE', - 'callback' => [$this, 'delete_variant'], - 'permission_callback' => [$this, 'check_permission'], - ]); - } - - public function check_logged_in_permission(): bool { - return is_user_logged_in(); - } - -} diff --git a/src/AKSRT/Admin/ShopControllerCouponGatewayOperations.php b/src/AKSRT/Admin/ShopControllerCouponGatewayOperations.php deleted file mode 100644 index 50ca7b57..00000000 --- a/src/AKSRT/Admin/ShopControllerCouponGatewayOperations.php +++ /dev/null @@ -1,97 +0,0 @@ -success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'total_pages' => 1]]); - } - - $page = max(1, (int) $request->get_param('page')); - $per_page = max(1, min(100, (int) $request->get_param('per_page'))) ?: 20; - $status = sanitize_key((string) $request->get_param('status')); - $search = sanitize_text_field((string) $request->get_param('search')); - - $result = \AKSRT\Payment\Coupon::get_instance()->get_list([ - 'status' => $status, - 'search' => $search, - 'limit' => $per_page, - 'offset' => ($page - 1) * $per_page, - ]); - - return $this->success(['data' => $result]); - } - - /** - * POST /shop/coupons — 创建优惠券 - */ - public function create_coupon(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Payment\Coupon')) { - return $this->error('优惠券系统未启用', 500); - } - - $params = $this->get_json_params($request); - $code = sanitize_text_field((string) ($params['code'] ?? '')); - - if ($code === '') { - return $this->error('优惠码不能为空'); - } - - $id = \AKSRT\Payment\Coupon::get_instance()->create($params); - - if ($id > 0) { - return $this->success([ - 'message' => __('优惠券已创建', 'aksrt'), - 'data' => ['id' => $id], - ], 201); - } - - return $this->error(__('创建失败(可能优惠码已存在)', 'aksrt')); - } - - /** - * POST /shop/coupons/{id}/disable — 禁用优惠券 - */ - public function disable_coupon(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Payment\Coupon')) { - return $this->error('优惠券系统未启用', 500); - } - - $id = (int) $request->get_param('id'); - if ($id <= 0) { - return $this->error('参数无效'); - } - - $success = \AKSRT\Payment\Coupon::get_instance()->disable($id); - - if ($success) { - return $this->success(['message' => __('优惠券已禁用', 'aksrt')]); - } - return $this->error(__('操作失败', 'aksrt')); - } - - /** - * GET /shop/gateways — 支付网关状态列表 - */ - public function get_gateways(): \WP_REST_Response { - if (!class_exists('\AKSRT\Payment\Gateway')) { - return $this->success(['data' => ['gateways' => []]]); - } - - return $this->success([ - 'data' => [ - 'gateways' => \AKSRT\Payment\Gateway::get_instance()->get_status_summary(), - ], - ]); - } -} diff --git a/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php b/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php deleted file mode 100644 index 4e50cac9..00000000 --- a/src/AKSRT/Admin/ShopControllerFrontendFlowOperations.php +++ /dev/null @@ -1,717 +0,0 @@ -error('商城服务不可用', 500); - } - - $result = \AKSRT\Shop\Shop::get_instance()->preview_checkout(get_current_user_id(), $this->get_json_params($request)); - if (empty($result['success'])) { - return $this->error($result['message'] ?? '结算预览失败'); - } - - return $this->success(['data' => $result['data']]); - } - - public function create_frontend_order(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - - $result = \AKSRT\Shop\Shop::get_instance()->create_checkout_order(get_current_user_id(), $this->get_json_params($request)); - if (empty($result['success'])) { - return $this->error($result['message'] ?? '订单创建失败'); - } - - return $this->success([ - 'message' => __('订单已创建', 'aksrt'), - 'data' => $result['data'], - ], 201); - } - - public function get_frontend_orders(\WP_REST_Request $request): \WP_REST_Response { - $page = max(1, (int) $request->get_param('page')); - $per_page = max(1, min(50, (int) $request->get_param('per_page'))) ?: 20; - $status = sanitize_key((string) $request->get_param('status')); - $product_type = sanitize_key((string) $request->get_param('product_type')); - - $args = [ - 'page' => $page, - 'per_page' => $per_page, - 'status' => $status, - 'product_type' => $product_type, - ]; - - if (class_exists('\AKSRT\Payment\UserOrders')) { - $view = \AKSRT\Payment\UserOrders::get_instance()->get_order_center_view(get_current_user_id(), $args); - } elseif (class_exists('\AKSRT\Shop\Shop')) { - $view = \AKSRT\Shop\Shop::get_instance()->get_order_center_view(get_current_user_id(), $args); - } else { - $view = ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1]; - } - - return $this->success(['data' => $view]); - } - - public function get_frontend_order(\WP_REST_Request $request): \WP_REST_Response { - $order_number = sanitize_text_field((string) $request->get_param('order_number')); - $order = null; - if (class_exists('\AKSRT\Payment\UserOrders')) { - $order = \AKSRT\Payment\UserOrders::get_instance()->get_order_detail_view(get_current_user_id(), $order_number); - } elseif (class_exists('\AKSRT\Shop\Shop')) { - $order = \AKSRT\Shop\Shop::get_instance()->get_order_detail_view(get_current_user_id(), $order_number); - } - if (!$order) { - return $this->error('订单不存在', 404); - } - - return $this->success(['data' => $order]); - } - - public function pay_frontend_order(\WP_REST_Request $request): \WP_REST_Response { - $params = $this->get_json_params($request); - $payment_method = sanitize_key((string) ($params['payment_method'] ?? '')); - $order_number = sanitize_text_field((string) $request->get_param('order_number')); - - if (class_exists('\AKSRT\Payment\UserOrders')) { - $result = \AKSRT\Payment\UserOrders::get_instance()->pay_order(get_current_user_id(), $order_number, $payment_method); - } elseif (class_exists('\AKSRT\Shop\Shop')) { - $result = \AKSRT\Shop\Shop::get_instance()->pay_order(get_current_user_id(), $order_number, $payment_method ?: 'balance'); - } else { - return $this->error('订单服务不可用', 500); - } - if (empty($result['success'])) { - return $this->error($result['message'] ?? '支付失败'); - } - - return $this->success(['data' => $result['data']]); - } - - public function cancel_frontend_order(\WP_REST_Request $request): \WP_REST_Response { - $order_number = sanitize_text_field((string) $request->get_param('order_number')); - if (class_exists('\AKSRT\Payment\UserOrders')) { - $result = \AKSRT\Payment\UserOrders::get_instance()->cancel_order(get_current_user_id(), $order_number); - } elseif (class_exists('\AKSRT\Shop\Shop')) { - $result = \AKSRT\Shop\Shop::get_instance()->cancel_order(get_current_user_id(), $order_number); - } else { - return $this->error('订单服务不可用', 500); - } - if (empty($result['success'])) { - return $this->error($result['message'] ?? '取消订单失败'); - } - - return $this->success(['data' => $result['data']]); - } - - public function create_frontend_after_sale(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - - $params = $this->get_json_params($request); - $params['order_number'] = sanitize_text_field((string) $request->get_param('order_number')); - $result = \AKSRT\Shop\Shop::get_instance()->create_after_sale_request(get_current_user_id(), $params); - if (empty($result['success'])) { - return $this->error($result['message'] ?? '提交售后失败'); - } - - return $this->success([ - 'message' => __('售后申请已提交', 'aksrt'), - 'data' => $result['data'], - ], 201); - } - - public function get_frontend_after_sales(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - $data = \AKSRT\Shop\Shop::get_instance()->get_user_after_sales(get_current_user_id()); - return $this->success(['data' => $data]); - } - - public function submit_frontend_return_shipping(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - $id = (int) $request->get_param('id'); - $params = $this->get_json_params($request); - $result = \AKSRT\Shop\Shop::get_instance()->submit_after_sale_return(get_current_user_id(), $id, $params); - if (empty($result['success'])) { - return $this->error($result['message'] ?? '提交退货物流失败'); - } - return $this->success([ - 'message' => $result['message'] ?? __('退货物流已提交', 'aksrt'), - 'data' => $result['data'] ?? [], - ]); - } - - public function cancel_frontend_after_sale(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - $id = (int) $request->get_param('id'); - $params = $this->get_json_params($request); - $reason = sanitize_text_field((string) ($params['reason'] ?? '')); - $result = \AKSRT\Shop\Shop::get_instance()->cancel_after_sale(get_current_user_id(), $id, $reason); - if (empty($result['success'])) { - return $this->error($result['message'] ?? '取消售后失败'); - } - return $this->success(['message' => $result['message'] ?? __('已取消', 'aksrt')]); - } - - /** - * Active public coupons for wallet UI (not personal inventory — site-wide active codes). - */ - public function get_frontend_coupons(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Payment\Coupon')) { - return $this->error('优惠券不可用', 500); - } - $list = \AKSRT\Payment\Coupon::get_instance()->get_list([ - 'status' => 'active', - 'limit' => 50, - 'offset' => 0, - ]); - $user_id = get_current_user_id(); - $items = []; - foreach ((array) ($list['items'] ?? []) as $c) { - $items[] = [ - 'id' => (int) ($c['id'] ?? 0), - 'code' => (string) ($c['code'] ?? ''), - 'type' => (string) ($c['type'] ?? ''), - 'value' => (float) ($c['value'] ?? 0), - 'min_amount' => (float) ($c['min_amount'] ?? 0), - 'max_discount' => (float) ($c['max_discount'] ?? 0), - 'expires_at' => (string) ($c['expires_at'] ?? ''), - 'user_used' => $user_id > 0 - ? (int) \AKSRT\Payment\Coupon::get_instance()->get_user_usage_count((int) ($c['id'] ?? 0), $user_id) - : 0, - 'per_user_limit' => (int) ($c['per_user_limit'] ?? 0), - ]; - } - return $this->success(['data' => ['items' => $items]]); - } - - public function get_cart(\WP_REST_Request $request): \WP_REST_Response { - $user_id = get_current_user_id(); - $shop = \AKSRT\Shop\Shop::get_instance(); - $cart = $shop->get_cart($user_id); - - $items = []; - foreach ($cart as $pid => $opts_map) { - $product_id = (int) $pid; - $product = $shop->build_product_card_view($product_id); - if (empty($product) || !is_array($opts_map)) { - continue; - } - foreach ($opts_map as $opt_str => $qty) { - $options_string = $opt_str === '_default' ? '' : (string) $opt_str; - $unit_price = method_exists($shop, 'resolve_option_price') - ? (float) $shop->resolve_option_price($product_id, $options_string !== '' ? $options_string : '_default') - : (float) ($product['price'] ?? 0); - $options_label = method_exists($shop, 'resolve_option_labels') - ? (string) $shop->resolve_option_labels($product_id, $options_string !== '' ? $options_string : '_default') - : ''; - $option_image = method_exists($shop, 'get_option_image') - ? (string) $shop->get_option_image($product_id, $options_string !== '' ? $options_string : '_default') - : ''; - $stock = method_exists($shop, 'resolve_option_stock') - ? (int) $shop->resolve_option_stock($product_id, $options_string !== '' ? $options_string : '_default') - : (int) ($product['stock'] ?? 0); - - $items[] = [ - 'product_id' => $product_id, - 'options_string' => $options_string, - 'options_label' => $options_label, - 'quantity' => (int) $qty, - 'unit_price' => $unit_price, - 'line_total' => round($unit_price * (int) $qty, 2), - 'stock' => $stock, - 'image' => $option_image !== '' ? $option_image : (string) ($product['image'] ?? ''), - 'title' => (string) ($product['title'] ?? ''), - 'product' => $product, - ]; - } - } - - return $this->success([ - 'data' => [ - 'items' => $items, - 'count' => $shop->get_cart_count($user_id), - ], - ]); - } - - public function get_cart_count(\WP_REST_Request $request): \WP_REST_Response { - $user_id = get_current_user_id(); - return $this->success(['data' => ['count' => \AKSRT\Shop\Shop::get_instance()->get_cart_count($user_id)]]); - } - - public function handle_add_to_cart(\WP_REST_Request $request): \WP_REST_Response { - $params = $this->get_json_params($request); - $product_id = (int) ($params['product_id'] ?? 0); - $options_string = sanitize_text_field((string) ($params['options_string'] ?? '')); - $count = max(1, (int) ($params['count'] ?? 1)); - - $result = \AKSRT\Shop\Shop::get_instance()->add_to_cart(get_current_user_id(), $product_id, $options_string, $count); - - if (isset($result['error'])) { - return $this->error($result['error']); - } - return $this->success(['data' => $result]); - } - - public function handle_reduce_from_cart(\WP_REST_Request $request): \WP_REST_Response { - $params = $this->get_json_params($request); - $product_id = (int) ($params['product_id'] ?? 0); - $options_string = sanitize_text_field((string) ($params['options_string'] ?? '')); - $count = max(1, (int) ($params['count'] ?? 1)); - - $result = \AKSRT\Shop\Shop::get_instance()->reduce_from_cart(get_current_user_id(), $product_id, $options_string, $count); - - if (isset($result['error'])) { - return $this->error($result['error']); - } - return $this->success(['data' => $result]); - } - - public function handle_remove_from_cart(\WP_REST_Request $request): \WP_REST_Response { - $params = $this->get_json_params($request); - $product_id = (int) ($params['product_id'] ?? 0); - $options_string = sanitize_text_field((string) ($params['options_string'] ?? '')); - - $result = \AKSRT\Shop\Shop::get_instance()->remove_from_cart(get_current_user_id(), $product_id, $options_string); - - if (isset($result['error'])) { - return $this->error($result['error']); - } - return $this->success(['data' => $result]); - } - - public function handle_remove_multi_from_cart(\WP_REST_Request $request): \WP_REST_Response { - $params = $this->get_json_params($request); - $items = is_array($params['items'] ?? null) ? $params['items'] : []; - - $result = \AKSRT\Shop\Shop::get_instance()->remove_multi_from_cart(get_current_user_id(), $items); - - if (isset($result['error'])) { - return $this->error($result['error']); - } - return $this->success(['data' => $result]); - } - - public function handle_empty_cart(\WP_REST_Request $request): \WP_REST_Response { - $result = \AKSRT\Shop\Shop::get_instance()->empty_cart(get_current_user_id()); - - if (isset($result['error'])) { - return $this->error($result['error']); - } - return $this->success(['data' => $result]); - } - - public function toggle_frontend_product_favorite(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - - $product_id = (int) $request->get_param('id'); - $result = \AKSRT\Shop\Shop::get_instance()->toggle_favorite(get_current_user_id(), $product_id); - if (empty($result['success'])) { - return $this->error(__('收藏操作失败', 'aksrt')); - } - - return $this->success([ - 'message' => !empty($result['is_favorite']) ? __('已收藏', 'aksrt') : __('已取消收藏', 'aksrt'), - 'data' => $result, - ]); - } - - public function rebuy_frontend_order(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - - $order_number = sanitize_text_field((string) $request->get_param('order_number')); - $result = \AKSRT\Shop\Shop::get_instance()->re_purchase(get_current_user_id(), $order_number); - if (empty($result['success'])) { - return $this->error($result['message'] ?? __('再次购买失败', 'aksrt')); - } - - return $this->success([ - 'message' => __('商品已加入购物车', 'aksrt'), - 'data' => $result, - ]); - } - - public function get_frontend_addresses(): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->success(['data' => ['addresses' => []]]); - } - - $addresses = \AKSRT\Shop\Shop::get_instance()->get_address_book(get_current_user_id()); - return $this->success(['data' => ['addresses' => $addresses]]); - } - - public function create_frontend_address(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - - $params = $this->get_json_params($request); - $user_id = get_current_user_id(); - $result = \AKSRT\Shop\Shop::get_instance()->add_address($user_id, $params); - - if (empty($result['success'])) { - return $this->error($result['error'] ?? '添加地址失败'); - } - - return $this->success([ - 'message' => __('地址已添加', 'aksrt'), - 'data' => ['address' => $result['address']], - ], 201); - } - - public function update_frontend_address(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - - $address_id = (int) $request->get_param('id'); - $params = $this->get_json_params($request); - $user_id = get_current_user_id(); - $result = \AKSRT\Shop\Shop::get_instance()->update_address($user_id, $address_id, $params); - - if (empty($result['success'])) { - return $this->error($result['error'] ?? '更新地址失败'); - } - - return $this->success([ - 'message' => __('地址已更新', 'aksrt'), - 'data' => ['address' => $result['address']], - ]); - } - - public function delete_frontend_address(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - - $address_id = (int) $request->get_param('id'); - $user_id = get_current_user_id(); - $result = \AKSRT\Shop\Shop::get_instance()->delete_address($user_id, $address_id); - - if (empty($result['success'])) { - return $this->error($result['error'] ?? '删除地址失败'); - } - - return $this->success(['message' => __('地址已删除', 'aksrt')]); - } - - public function set_default_frontend_address(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - - $address_id = (int) $request->get_param('id'); - $user_id = get_current_user_id(); - $result = \AKSRT\Shop\Shop::get_instance()->set_default_address($user_id, $address_id); - - if (empty($result['success'])) { - return $this->error($result['error'] ?? '设置默认地址失败'); - } - - return $this->success(['message' => __('已设为默认地址', 'aksrt')]); - } - - public function get_frontend_product_archive(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - - $taxonomy = sanitize_key((string) ($request->get_param('taxonomy') ?? '')); - $term_id = (int) ($request->get_param('term_id') ?? 0); - $page = max(1, (int) ($request->get_param('page') ?? 1)); - $orderby = sanitize_key((string) ($request->get_param('orderby') ?? '')); - $order = sanitize_key((string) ($request->get_param('order') ?? '')); - - $view = \AKSRT\Shop\Shop::get_instance()->get_product_archive_view([ - 'taxonomy' => $taxonomy, - 'term_id' => $term_id, - 'paged' => $page, - 'orderby' => $orderby, - 'order' => $order, - ]); - - $sales_cfg = is_array($view['sales'] ?? null) ? $view['sales'] : []; - $html = ''; - if (!empty($view['products']) && is_array($view['products'])) { - ob_start(); - foreach ($view['products'] as $product) { - if (function_exists('get_template_part')) { - get_template_part('templates/parts/shop/product', 'card', [ - 'product' => $product, - 'sales_cfg' => $sales_cfg, - ]); - } - } - $html = (string) ob_get_clean(); - } - - $empty_html = ''; - if ($html === '' && function_exists('aksrt_get_ajax_null')) { - $empty_html = aksrt_get_ajax_null(__('内容空空如也', 'aksrt')); - } elseif ($html === '') { - $empty_html = '

' . esc_html__('内容空空如也', 'aksrt') . '

'; - } - - // Header HTML for cat filter replace (when filter present). - $header_html = ''; - if (!empty($view['filter']) && is_array($view['filter'])) { - ob_start(); - $this->render_product_cat_filter_html($view); - $header_html = (string) ob_get_clean(); - } - - $paginate_html = ''; - $page_n = (int) ($view['page'] ?? 1); - $max_pages = (int) ($view['max_pages'] ?? 1); - $has_more = $page_n < $max_pages; - if ($has_more && (string) ($view['paging'] ?? '') === 'ajax') { - $next_url = add_query_arg([ - 'taxonomy' => $taxonomy, - 'term_id' => $term_id, - 'page' => $page_n + 1, - 'orderby' => (string) ($view['orderby'] ?? 'date'), - 'order' => (string) ($view['order'] ?? ''), - ], rest_url('aksrt/v1/shop/frontend/products/archive')); - if (function_exists('aksrt_get_ajax_next_paginate')) { - $paginate_html = aksrt_get_ajax_next_paginate( - (int) ($view['total'] ?? 0), - $page_n, - max(1, (int) ($view['per_page'] ?? 12)), - $next_url - ); - } - } - - return $this->success([ - 'data' => array_merge($view, [ - 'html' => $html !== '' ? $html : $empty_html, - 'empty_html' => $empty_html, - 'header_html' => $header_html, - 'paginate_html' => $paginate_html, - 'has_more' => $has_more, - 'title' => !empty($view['term']['name']) - ? (string) $view['term']['name'] . ' - ' . wp_strip_all_tags(get_bloginfo('name')) - : '', - ]), - ]); - } - - /** - * GET /shop/frontend/widgets/products — product list widget AJAX pager. - */ - public function get_frontend_widget_products(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Widgets\WidgetProductList')) { - return $this->error('商品组件不可用', 500); - } - - $instance = [ - 'per_page' => max(1, min(50, (int) ($request->get_param('per_page') ?? 8))), - 'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'date')), - 'order' => sanitize_key((string) ($request->get_param('order') ?? 'DESC')), - 'cat_ids' => sanitize_text_field((string) ($request->get_param('cat_ids') ?? '')), - 'tag_ids' => sanitize_text_field((string) ($request->get_param('tag_ids') ?? '')), - 'discount_ids' => sanitize_text_field((string) ($request->get_param('discount_ids') ?? '')), - 'exclude_cat' => !empty($request->get_param('exclude_cat')) ? 1 : 0, - 'style' => sanitize_key((string) ($request->get_param('style') ?? 'default')), - 'paginate' => sanitize_key((string) ($request->get_param('paginate') ?? 'ajax')), - ]; - $page = max(1, (int) ($request->get_param('page') ?? $request->get_param('paged') ?? 1)); - $payload = \AKSRT\Widgets\WidgetProductList::query_payload($instance, $page); - - return $this->success([ - 'html' => (string) ($payload['html'] ?? ''), - 'paginate_html' => (string) ($payload['paginate_html'] ?? ''), - 'has_more' => !empty($payload['has_more']), - 'page' => (int) ($payload['page'] ?? $page), - 'max_pages' => (int) ($payload['max_pages'] ?? 1), - 'total' => (int) ($payload['total'] ?? 0), - ]); - } - - /** - * Render cat filter bar HTML for AJAX header replace. - */ - private function render_product_cat_filter_html(array $view): void { - $filter = is_array($view['filter'] ?? null) ? $view['filter'] : []; - $orderby = (string) ($view['orderby'] ?? ($filter['orderby'] ?? 'date')); - $order = strtoupper((string) ($view['order'] ?? ($filter['order'] ?? 'DESC'))); - $term = is_array($view['term'] ?? null) ? $view['term'] : []; - $current_id = (int) ($filter['current_id'] ?? ($term['id'] ?? 0)); - include get_template_directory() . '/templates/parts/shop/product-cat-filter.php'; - } - - public function get_frontend_revenue(\WP_REST_Request $request): \WP_REST_Response { - if (!function_exists('aksrt_get_revenue_page_view')) { - return $this->error('收益服务不可用', 500); - } - $view = aksrt_get_revenue_page_view(get_current_user_id()); - $withdrawals = []; - if (class_exists('\AKSRT\Payment\Withdrawal')) { - $rows = \AKSRT\Payment\Withdrawal::get_instance()->get_user_withdrawals(get_current_user_id(), ['limit' => 20]); - foreach ($rows as $row) { - $withdrawals[] = [ - 'id' => (int) ($row['id'] ?? 0), - 'amount' => (float) ($row['amount'] ?? 0), - 'status' => (string) ($row['status'] ?? ''), - 'method' => (string) ($row['method'] ?? ''), - 'created_at' => (string) ($row['created_at'] ?? ''), - 'note' => (string) ($row['note'] ?? ''), - ]; - } - } - $settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_withdrawal_settings() : []; - return $this->success([ - 'data' => array_merge($view, [ - 'withdrawals' => $withdrawals, - 'settings' => [ - 'min_amount' => (float) ($settings['min_amount'] ?? 50), - 'service_charge_rate' => (float) ($settings['service_charge_rate'] ?? 0), - 'text' => (string) ($settings['text'] ?? ''), - ], - ]), - ]); - } - - public function create_frontend_withdraw(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Payment\Withdrawal') || !class_exists('\AKSRT\Credits\Credits')) { - return $this->error('提现服务不可用', 500); - } - - $params = $this->get_json_params($request); - $amount = (float) ($params['amount'] ?? 0); - $method = sanitize_key((string) ($params['method'] ?? 'alipay')); - $account = sanitize_textarea_field((string) ($params['account_info'] ?? $params['account'] ?? '')); - - $settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_withdrawal_settings() : []; - $min = (float) ($settings['min_amount'] ?? 50); - if ($amount < $min) { - return $this->error(sprintf(__('最低提现金额为 %s', 'aksrt'), $min)); - } - if ($account === '') { - return $this->error(__('请填写收款账户信息', 'aksrt')); - } - - $user_id = get_current_user_id(); - $balance = (float) \AKSRT\Credits\Credits::balance($user_id); - if ($amount > $balance) { - return $this->error(__('可提现余额不足', 'aksrt')); - } - - $cost = (int) round($amount); - if ($cost <= 0) { - return $this->error(__('提现金额无效', 'aksrt')); - } - $deducted = \AKSRT\Credits\Credits::deduct($user_id, $cost, 'withdraw', ['type' => 'withdraw'], __('用户提现申请', 'aksrt')); - if (is_wp_error($deducted)) { - return $this->error($deducted->get_error_message()); - } - - $id = \AKSRT\Payment\Withdrawal::get_instance()->create($user_id, $amount, $method, $account); - if ($id <= 0) { - \AKSRT\Credits\Credits::add($user_id, $cost, 'withdraw_refund', ['type' => 'withdraw'], __('提现失败退回', 'aksrt')); - return $this->error(__('提现申请创建失败', 'aksrt'), 500); - } - - return $this->success([ - 'message' => __('提现申请已提交', 'aksrt'), - 'data' => [ - 'id' => $id, - 'amount' => $amount, - 'balance' => (float) \AKSRT\Credits\Credits::balance($user_id), - ], - ], 201); - } - - public function submit_frontend_product_review(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\ProductReview')) { - return $this->error('评价服务不可用', 500); - } - $params = $this->get_json_params($request); - $order_number = sanitize_text_field((string) $request->get_param('order_number')); - $product_id = (int) ($params['product_id'] ?? 0); - $result = \AKSRT\Shop\ProductReview::get_instance()->submit_review( - get_current_user_id(), - $order_number, - $product_id, - $params - ); - if (empty($result['success'])) { - return $this->error($result['message'] ?? __('评价失败', 'aksrt')); - } - return $this->success([ - 'message' => __('评价成功', 'aksrt'), - 'data' => $result, - ], 201); - } - - /** - * GET review modal HTML for an order (zibll order_comment_modal parity). - */ - public function get_frontend_product_review_modal(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\ProductReview')) { - return $this->error(__('评价服务不可用', 'aksrt'), 500); - } - $order_number = sanitize_text_field((string) $request->get_param('order_number')); - $result = \AKSRT\Shop\ProductReview::get_instance()->get_review_modal_html( - get_current_user_id(), - $order_number - ); - if (empty($result['success'])) { - return $this->error((string) ($result['message'] ?? __('无法打开评价', 'aksrt'))); - } - return $this->success([ - 'html' => (string) ($result['html'] ?? ''), - 'product_id' => (int) ($result['product_id'] ?? 0), - 'need_shipping' => !empty($result['need_shipping']), - 'img_max' => (int) ($result['img_max'] ?? 0), - 'remain_seconds' => (int) ($result['remain_seconds'] ?? 0), - ]); - } - - public function create_frontend_product(\WP_REST_Request $request): \WP_REST_Response { - $params = $this->get_json_params($request); - $params['author_id'] = get_current_user_id(); - $frontend_request = new \WP_REST_Request('POST', '/'); - $frontend_request->set_body(wp_json_encode($params)); - - return $this->create_product($frontend_request); - } - - public function update_frontend_product(\WP_REST_Request $request): \WP_REST_Response { - $id = (int) $request->get_param('id'); - $post = get_post($id); - if (!$post || $post->post_type !== 'aksrt_product') { - return $this->error('商品不存在', 404); - } - - if ((int) $post->post_author !== get_current_user_id() && !current_user_can('manage_options')) { - return $this->error('没有权限编辑该商品', 403); - } - - return $this->update_product($request); - } -} diff --git a/src/AKSRT/Admin/ShopControllerFulfillmentOperations.php b/src/AKSRT/Admin/ShopControllerFulfillmentOperations.php deleted file mode 100644 index 6cdb1c20..00000000 --- a/src/AKSRT/Admin/ShopControllerFulfillmentOperations.php +++ /dev/null @@ -1,353 +0,0 @@ -success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'max_pages' => 1]]); - } - - $page = max(1, (int) $request->get_param('page')); - $per_page = max(1, min(100, (int) $request->get_param('per_page'))) ?: 20; - $status = sanitize_key((string) $request->get_param('status')); - $stage = sanitize_key((string) $request->get_param('stage')); - $order = sanitize_text_field((string) $request->get_param('order_number')); - $type = sanitize_key((string) $request->get_param('type')); - $date_from = sanitize_text_field((string) $request->get_param('date_from')); - $date_to = sanitize_text_field((string) $request->get_param('date_to')); - - $result = \AKSRT\Payment\AfterSale::get_instance()->get_list([ - 'limit' => $per_page, - 'offset' => ($page - 1) * $per_page, - 'status' => $status, - 'stage' => $stage, - 'order_number' => $order, - 'type' => $type, - 'date_from' => $date_from, - 'date_to' => $date_to, - 'include_order_context' => true, - ]); - - $stats = \AKSRT\Payment\AfterSale::get_instance()->get_stats(); - - return $this->success(['data' => array_merge($result, [ - 'stats' => $stats, - 'filters' => [ - 'status' => $status, - 'stage' => $stage, - 'order_number' => $order, - 'type' => $type, - 'date_from' => $date_from, - 'date_to' => $date_to, - ], - 'options' => [ - 'status' => \AKSRT\Payment\AfterSale::get_status_options(), - 'stage' => \AKSRT\Payment\AfterSale::get_stage_options(), - 'type' => \AKSRT\Payment\AfterSale::get_type_options(), - ], - ])]); - } - - /** - * POST /shop/after-sale — 创建售后工单 - */ - public function create_after_sale(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Payment\AfterSale')) { - return $this->error('售后系统未启用', 500); - } - - $params = $this->get_json_params($request); - $order_number = sanitize_text_field((string) ($params['order_number'] ?? '')); - $reason = sanitize_textarea_field((string) ($params['reason'] ?? '')); - - if ($order_number === '' || $reason === '') { - return $this->error('订单号和原因不能为空'); - } - - $order = class_exists('\AKSRT\Payment\OrderRepository') - ? \AKSRT\Payment\OrderRepository::get_instance()->get($order_number) - : null; - - if (!$order) { - return $this->error(__('订单不存在', 'aksrt'), 404); - } - - unset($params['user_id'], $params['applicant_user_id']); - $id = \AKSRT\Payment\AfterSale::get_instance()->create($params); - - if (is_wp_error($id)) { - return $this->error($id->get_error_message(), 422); - } - if ($id > 0) { - return $this->success([ - 'message' => __('售后工单已创建', 'aksrt'), - 'data' => ['id' => $id], - ], 201); - } - - return $this->error(__('创建失败', 'aksrt')); - } - - /** - * POST /shop/after-sale/{id}/update-status — 更新售后工单状态 - */ - public function update_after_sale_status(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Payment\AfterSale')) { - return $this->error('售后系统未启用', 500); - } - - $id = (int) $request->get_param('id'); - $params = $this->get_json_params($request); - $new_status = sanitize_key((string) ($params['status'] ?? '')); - $note = sanitize_textarea_field((string) ($params['operator_note'] ?? '')); - - if ($id <= 0 || $new_status === '') { - return $this->error('参数无效'); - } - - $success = \AKSRT\Payment\AfterSale::get_instance()->update_status( - $id, - $new_status, - get_current_user_id(), - $note - ); - - if ($success) { - return $this->success(['message' => __('状态已更新', 'aksrt')]); - } - return $this->error(__('更新失败', 'aksrt')); - } - - /** - * GET /shop/shipping — 发货记录列表 - */ - public function get_shipping_list(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Payment\Shipping')) { - return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'max_pages' => 1]]); - } - - $page = max(1, (int) $request->get_param('page')); - $per_page = max(1, min(100, (int) $request->get_param('per_page'))) ?: 20; - $status = sanitize_key((string) $request->get_param('status')); - $order = sanitize_text_field((string) $request->get_param('order_number')); - $date_from = sanitize_text_field((string) $request->get_param('date_from')); - $date_to = sanitize_text_field((string) $request->get_param('date_to')); - $tracking_no = sanitize_text_field((string) $request->get_param('tracking_no')); - - $result = \AKSRT\Payment\Shipping::get_instance()->get_list([ - 'limit' => $per_page, - 'offset' => ($page - 1) * $per_page, - 'status' => $status, - 'order_number' => $order, - 'date_from' => $date_from, - 'date_to' => $date_to, - 'tracking_no' => $tracking_no, - 'include_order_context' => true, - ]); - - $stats = \AKSRT\Payment\Shipping::get_instance()->get_stats(); - - return $this->success(['data' => array_merge($result, [ - 'stats' => $stats, - 'filters' => [ - 'status' => $status, - 'order_number' => $order, - 'date_from' => $date_from, - 'date_to' => $date_to, - 'tracking_no' => $tracking_no, - ], - 'options' => [ - 'status' => \AKSRT\Payment\Shipping::get_status_options(), - 'carrier' => \AKSRT\Payment\Shipping::get_carrier_options(), - ], - ])]); - } - - /** - * POST /shop/shipping — 创建发货记录 - */ - public function create_shipping(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Payment\Shipping')) { - return $this->error('物流系统未启用', 500); - } - - $params = $this->get_json_params($request); - $order_number = sanitize_text_field((string) ($params['order_number'] ?? '')); - $carrier = sanitize_text_field((string) ($params['carrier'] ?? '')); - $tracking_no = sanitize_text_field((string) ($params['tracking_no'] ?? '')); - - if ($order_number === '' || $carrier === '' || $tracking_no === '') { - return $this->error('订单号、快递公司和物流单号不能为空'); - } - - $order = class_exists('\AKSRT\Payment\OrderRepository') - ? \AKSRT\Payment\OrderRepository::get_instance()->get($order_number) - : null; - - if (!$order) { - return $this->error(__('订单不存在', 'aksrt'), 404); - } - - $params['operator_id'] = get_current_user_id(); - - $id = \AKSRT\Payment\Shipping::get_instance()->create($params); - - if ($id > 0) { - return $this->success([ - 'message' => __('发货记录已创建', 'aksrt'), - 'data' => ['id' => $id], - ], 201); - } - - return $this->error(__('创建失败', 'aksrt')); - } - - /** - * POST /shop/shipping/{id}/update-status — 更新发货状态 - */ - public function update_shipping_status(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Payment\Shipping')) { - return $this->error('物流系统未启用', 500); - } - - $id = (int) $request->get_param('id'); - $params = $this->get_json_params($request); - $new_status = sanitize_key((string) ($params['status'] ?? '')); - $note = sanitize_textarea_field((string) ($params['note'] ?? '')); - - if ($id <= 0 || $new_status === '') { - return $this->error('参数无效'); - } - - $success = \AKSRT\Payment\Shipping::get_instance()->update_status($id, $new_status, $note, get_current_user_id()); - - if ($success) { - return $this->success(['message' => __('状态已更新', 'aksrt')]); - } - return $this->error(__('更新失败', 'aksrt')); - } - - public function get_after_sale_detail(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Payment\AfterSale')) { - return $this->error('售后系统未启用', 500); - } - - $id = (int) $request->get_param('id'); - if ($id <= 0) { - return $this->error('参数无效'); - } - - $record = \AKSRT\Payment\AfterSale::get_instance()->get_with_order_context($id); - if (!$record) { - return $this->error(__('售后工单不存在', 'aksrt'), 404); - } - - return $this->success([ - 'data' => [ - 'record' => $record, - 'timeline' => $this->build_after_sale_timeline($record), - ], - ]); - } - - public function get_shipping_detail(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Payment\Shipping')) { - return $this->error('物流系统未启用', 500); - } - - $id = (int) $request->get_param('id'); - if ($id <= 0) { - return $this->error('参数无效'); - } - - $record = \AKSRT\Payment\Shipping::get_instance()->get_with_order_context($id); - if (!$record) { - return $this->error(__('发货记录不存在', 'aksrt'), 404); - } - - return $this->success([ - 'data' => [ - 'record' => $record, - 'timeline' => $this->build_shipping_timeline($record), - ], - ]); - } - - public function batch_update_shipping_status(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Payment\Shipping')) { - return $this->error('物流系统未启用', 500); - } - - $params = $this->get_json_params($request); - $ids = is_array($params['ids'] ?? null) ? $params['ids'] : []; - $status = sanitize_key((string) ($params['status'] ?? '')); - $note = sanitize_textarea_field((string) ($params['note'] ?? '')); - - if (empty($ids) || $status === '') { - return $this->error('参数无效'); - } - - $result = \AKSRT\Payment\Shipping::get_instance()->batch_update_status($ids, $status, $note, get_current_user_id()); - - return $this->success([ - 'message' => __('批量状态更新已执行', 'aksrt'), - 'data' => $result, - ]); - } - - private function build_shipping_timeline(array $record): array { - $timeline = []; - - foreach ([ - 'created_at' => __('创建发货记录', 'aksrt'), - 'updated_at' => __('更新物流状态', 'aksrt'), - 'delivered_at' => __('确认签收', 'aksrt'), - ] as $field => $label) { - if (!empty($record[$field])) { - $timeline[] = [ - 'time' => (string) $record[$field], - 'label' => $label, - ]; - } - } - - usort($timeline, static function ($a, $b) { - return strcmp((string) ($a['time'] ?? ''), (string) ($b['time'] ?? '')); - }); - - return $timeline; - } - - private function build_after_sale_timeline(array $record): array { - $timeline = []; - - foreach ([ - 'created_at' => __('提交售后申请', 'aksrt'), - 'updated_at' => __('更新售后状态', 'aksrt'), - 'resolved_at' => __('售后处理结束', 'aksrt'), - ] as $field => $label) { - if (!empty($record[$field])) { - $timeline[] = [ - 'time' => (string) $record[$field], - 'label' => $label, - ]; - } - } - - usort($timeline, static function ($a, $b) { - return strcmp((string) ($a['time'] ?? ''), (string) ($b['time'] ?? '')); - }); - - return $timeline; - } -} diff --git a/src/AKSRT/Admin/ShopControllerOrderOperations.php b/src/AKSRT/Admin/ShopControllerOrderOperations.php deleted file mode 100644 index 18647018..00000000 --- a/src/AKSRT/Admin/ShopControllerOrderOperations.php +++ /dev/null @@ -1,136 +0,0 @@ -get_param('page')); - $per_page = max(1, min(50, (int) $request->get_param('per_page'))) ?: 20; - $search = sanitize_text_field((string) $request->get_param('search')); - $status = sanitize_key((string) $request->get_param('status')); - - if (class_exists('\AKSRT\Payment\OrderRepository')) { - $repo = \AKSRT\Payment\OrderRepository::get_instance(); - - $args = [ - 'limit' => $per_page, - 'offset' => ($page - 1) * $per_page, - ]; - if ($status !== '' && $status !== 'all') { - $args['status'] = $status; - } - - $orders = $repo->get_orders_admin($args, $search); - $total = $repo->get_orders_count($args, $search); - - return $this->success([ - 'data' => [ - 'items' => $orders, - 'total' => $total, - 'page' => $page, - 'total_pages' => max(1, (int) ceil($total / $per_page)), - ], - ]); - } - - return $this->error('订单仓库不可用', 500); - } - - /** - * GET /shop/orders/{order_number} — 单个订单详情(含上下文) - */ - public function get_order(\WP_REST_Request $request): \WP_REST_Response { - $order_number = sanitize_text_field((string) $request->get_param('order_number')); - - if (!class_exists('\AKSRT\Payment\OrderRepository')) { - return $this->error('订单服务不可用', 500); - } - - $repo = \AKSRT\Payment\OrderRepository::get_instance(); - $order = $repo->get($order_number); - if (!$order) { - return $this->error('订单不存在', 404); - } - - $context = $repo->get_admin_order_context($order_number); - $timeline = []; - if (class_exists('\AKSRT\Shop\Shop')) { - $shipping_records = class_exists('\AKSRT\Payment\Shipping') ? \AKSRT\Payment\Shipping::get_instance()->get_by_order($order_number) : []; - $after_sale_result = class_exists('\AKSRT\Payment\AfterSale') ? \AKSRT\Payment\AfterSale::get_instance()->get_list(['order_number' => $order_number, 'limit' => 20]) : ['items' => []]; - $after_sale_items = $after_sale_result['items'] ?? []; - - $timeline = []; - $date_fields = [ - 'created_at' => '订单创建', - 'paid_at' => '完成支付', - ]; - foreach ($date_fields as $field => $label) { - if (!empty($order[$field])) { - $timeline[] = ['time' => $order[$field], 'title' => $label, 'type' => 'order']; - } - } - foreach ($shipping_records as $record) { - $timeline[] = [ - 'time' => $record['created_at'] ?? '', - 'title' => '物流更新:' . ($record['status_label'] ?? $record['status'] ?? ''), - 'detail' => ($record['carrier_label'] ?? '') . ' / ' . ($record['tracking_no'] ?? ''), - 'type' => 'shipping', - ]; - } - foreach ($after_sale_items as $record) { - $timeline[] = [ - 'time' => $record['created_at'] ?? '', - 'title' => '售后:' . ($record['type_label'] ?? '') . ' — ' . ($record['status_label'] ?? ''), - 'detail' => $record['reason'] ?? '', - 'type' => 'after_sale', - ]; - } - usort($timeline, static function ($a, $b) { - return strcmp((string) ($b['time'] ?? ''), (string) ($a['time'] ?? '')); - }); - } - - return $this->success([ - 'data' => $order, - 'context' => $context, - 'timeline' => $timeline, - ]); - } - - /** - * POST /shop/orders/update-status — 更新订单状态 - * Canonical field: order_number (前端必须发送 order_number) - */ - public function update_order_status(\WP_REST_Request $request): \WP_REST_Response { - $params = $this->get_json_params($request); - $order_number = sanitize_text_field((string) ($params['order_number'] ?? '')); - $new_status = sanitize_key((string) ($params['status'] ?? '')); - $note = sanitize_textarea_field((string) ($params['note'] ?? '')); - - if ($order_number === '' || $new_status === '') { - return $this->error('参数无效'); - } - - $success = false; - if (class_exists('\AKSRT\Payment\OrderRepository')) { - $success = \AKSRT\Payment\OrderRepository::get_instance()->update_status($order_number, $new_status, [ - 'note' => $note, - 'force' => true, - ]); - } - - if ($success) { - return $this->success(['message' => __('订单状态已更新', 'aksrt')]); - } - return $this->error(__('更新失败', 'aksrt')); - } -} diff --git a/src/AKSRT/Admin/ShopControllerProductAdminColumns.php b/src/AKSRT/Admin/ShopControllerProductAdminColumns.php deleted file mode 100644 index 1472a11a..00000000 --- a/src/AKSRT/Admin/ShopControllerProductAdminColumns.php +++ /dev/null @@ -1,187 +0,0 @@ - $label) { - $new[$key] = $label; - if ($key === 'title') { - $new['product_price'] = __('价格', 'aksrt'); - $new['product_stock'] = __('库存', 'aksrt'); - $new['product_sales'] = __('销量', 'aksrt'); - $new['product_stats'] = __('综合数据', 'aksrt'); - $new['product_author_name'] = __('商家', 'aksrt'); - $new['product_discount'] = __('优惠活动', 'aksrt'); - } - } - return $new; - } - - public function render_product_column(string $column, int $post_id): void { - switch ($column) { - case 'product_price': - $price = get_post_meta($post_id, '_aksrt_price', true); - echo esc_html(number_format_i18n((float) $price, 2)); - break; - case 'product_stock': - $stock = get_post_meta($post_id, '_aksrt_stock', true); - $stock_type = get_post_meta($post_id, '_aksrt_stock_type', true); - if ($stock_type === 'opts') { - echo esc_html__('按选项', 'aksrt'); - } elseif ((int) $stock === -1) { - echo esc_html__('无限', 'aksrt'); - } else { - echo esc_html((string) (int) $stock); - } - break; - case 'product_sales': - echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_sales', true)); - break; - case 'product_stats': - $views = (int) get_post_meta($post_id, '_aksrt_views', true); - $fav = (int) get_post_meta($post_id, '_aksrt_favorite_count', true); - printf( - esc_html__('阅读 %1$d / 收藏 %2$d', 'aksrt'), - $views, - $fav - ); - break; - case 'product_author_name': - $post = get_post($post_id); - if ($post) { - $author = get_userdata($post->post_author); - echo esc_html($author ? $author->display_name : '—'); - } - break; - case 'product_discount': - $terms = wp_get_object_terms($post_id, 'shop_discount', ['fields' => 'all']); - if (!is_wp_error($terms) && !empty($terms)) { - $term_ids = wp_list_pluck($terms, 'term_id'); - $names = wp_list_pluck($terms, 'name'); - echo ''; - echo esc_html(implode(', ', $names)); - } else { - echo '—'; - } - break; - } - } - - public function sortable_product_columns(array $columns): array { - $columns['product_sales'] = 'product_sales'; - return $columns; - } - - public function render_product_quick_edit(string $column_name, string $post_type): void { - if ($column_name !== 'product_discount' || $post_type !== 'aksrt_product') { - return; - } - wp_nonce_field('aksrt_product_quick_edit', 'aksrt_product_qe_nonce'); - $discounts = get_terms([ - 'taxonomy' => 'shop_discount', - 'hide_empty' => false, - ]); - ?> -
-
-

- - -
-
- 0 && term_exists($term_id, 'shop_discount')) { - wp_set_object_terms($post_id, [$term_id], 'shop_discount'); - } - } - } - - public function product_quick_edit_script(): void { - $screen = get_current_screen(); - if (!$screen || $screen->post_type !== 'aksrt_product') { - return; - } - ?> - - publish ?? 0); - } - - $order_stats = ['total' => 0, 'pending' => 0, 'paid' => 0, 'revenue' => 0]; - if (class_exists('\AKSRT\Payment\OrderRepository')) { - $order_stats = \AKSRT\Payment\OrderRepository::get_instance()->get_stats(); - } - - $revenue_stats = ['total' => 0, 'pending' => 0, 'settled' => 0]; - if (class_exists('\AKSRT\Payment\Revenue')) { - $revenue_stats = \AKSRT\Payment\Revenue::get_instance()->get_stats(); - } - - $gateway_summary = []; - if (class_exists('\AKSRT\Payment\Gateway')) { - $gateway_summary = \AKSRT\Payment\Gateway::get_instance()->get_status_summary(); - } - - return $this->success([ - 'data' => [ - 'enabled' => true, - 'product_count' => $product_count, - 'order_count' => $order_stats['total'], - 'order_pending' => $order_stats['pending'] ?? 0, - 'revenue_total' => $order_stats['revenue'] ?? 0, - 'revenue_pending' => $revenue_stats['pending'] ?? 0, - 'gateways' => $gateway_summary, - ], - ]); - } - - public function get_product_taxonomy(): \WP_REST_Response { - return $this->success([ - 'data' => [ - 'categories' => $this->format_product_terms('aksrt_product_cat'), - 'tags' => $this->format_product_terms('aksrt_product_tag'), - ], - ]); - } - - /** - * POST /shop/products/taxonomy — create category or tag term. - * - * Body: { taxonomy: 'category'|'tag'|'aksrt_product_cat'|'aksrt_product_tag', name: string, parent?: int } - */ - public function create_product_taxonomy_term(\WP_REST_Request $request): \WP_REST_Response { - $params = $this->get_json_params($request); - $raw_tax = sanitize_key((string) ($params['taxonomy'] ?? 'category')); - $name = sanitize_text_field((string) ($params['name'] ?? '')); - $parent = max(0, (int) ($params['parent'] ?? 0)); - - $taxonomy_map = [ - 'category' => 'aksrt_product_cat', - 'tag' => 'aksrt_product_tag', - 'aksrt_product_cat' => 'aksrt_product_cat', - 'aksrt_product_tag' => 'aksrt_product_tag', - ]; - $taxonomy = $taxonomy_map[$raw_tax] ?? ''; - - if ($taxonomy === '' || !taxonomy_exists($taxonomy)) { - return $this->error(__('无效的分类法', 'aksrt')); - } - if ($name === '') { - return $this->error(__('名称不能为空', 'aksrt')); - } - - $existing = term_exists($name, $taxonomy, $taxonomy === 'aksrt_product_cat' ? $parent : 0); - if (is_array($existing) && !empty($existing['term_id'])) { - $term = get_term((int) $existing['term_id'], $taxonomy); - if ($term && !is_wp_error($term)) { - return $this->success([ - 'message' => __('术语已存在', 'aksrt'), - 'data' => [ - 'id' => (int) $term->term_id, - 'name' => (string) $term->name, - 'slug' => (string) $term->slug, - 'parent' => (int) $term->parent, - 'taxonomy' => $taxonomy === 'aksrt_product_cat' ? 'category' : 'tag', - 'existing' => true, - ], - ]); - } - } - - $args = []; - if ($taxonomy === 'aksrt_product_cat' && $parent > 0) { - $args['parent'] = $parent; - } - - $result = wp_insert_term($name, $taxonomy, $args); - if (is_wp_error($result)) { - return $this->error($result->get_error_message()); - } - - $term_id = (int) ($result['term_id'] ?? 0); - $term = get_term($term_id, $taxonomy); - if (!$term || is_wp_error($term)) { - return $this->error(__('创建失败', 'aksrt')); - } - - return $this->success([ - 'message' => __('术语已创建', 'aksrt'), - 'data' => [ - 'id' => (int) $term->term_id, - 'name' => (string) $term->name, - 'slug' => (string) $term->slug, - 'parent' => (int) $term->parent, - 'taxonomy' => $taxonomy === 'aksrt_product_cat' ? 'category' : 'tag', - 'existing' => false, - ], - ], 201); - } - - /** - * @return array - */ - private function format_product_terms(string $taxonomy): array { - if (!taxonomy_exists($taxonomy)) { - return []; - } - - $terms = get_terms([ - 'taxonomy' => $taxonomy, - 'hide_empty' => false, - ]); - if (is_wp_error($terms) || !is_array($terms)) { - return []; - } - - return array_map(static function (\WP_Term $term): array { - return [ - 'id' => (int) $term->term_id, - 'name' => (string) $term->name, - 'slug' => (string) $term->slug, - 'parent' => (int) $term->parent, - 'count' => (int) $term->count, - ]; - }, $terms); - } - - /** - * GET /shop/products — 商品列表,支持 search、status、page、per_page - */ - public function get_products(\WP_REST_Request $request): \WP_REST_Response { - $page = max(1, (int) $request->get_param('page')); - $per_page = max(1, min(50, (int) $request->get_param('per_page'))) ?: 20; - $search = sanitize_text_field((string) $request->get_param('search')); - $status = sanitize_key((string) $request->get_param('status')); - - $query_args = [ - 'post_type' => 'aksrt_product', - 'posts_per_page' => $per_page, - 'paged' => $page, - 'orderby' => 'date', - 'order' => 'DESC', - ]; - - if ($status !== '') { - $query_args['post_status'] = $status; - } else { - $query_args['post_status'] = ['publish', 'draft', 'pending']; - } - - if ($search !== '') { - $query_args['s'] = $search; - } - - $query = new \WP_Query($query_args); - - $products = []; - while ($query->have_posts()) { - $query->the_post(); - $id = get_the_ID(); - $products[] = [ - 'id' => $id, - 'title' => get_the_title(), - 'price' => (float) get_post_meta($id, '_aksrt_price', true), - 'image' => get_the_post_thumbnail_url($id, 'medium') ?: '', - 'stock' => (int) get_post_meta($id, '_aksrt_stock', true), - 'sales' => (int) get_post_meta($id, '_aksrt_sales', true), - 'status' => get_post_status(), - 'created' => get_the_date('Y-m-d H:i'), - ]; - } - wp_reset_postdata(); - - return $this->success([ - 'data' => [ - 'items' => $products, - 'total' => (int) $query->found_posts, - 'total_pages' => (int) $query->max_num_pages, - 'page' => $page, - ], - ]); - } - - /** - * GET /shop/products/{id} — 单个商品详情 - */ - public function get_product(\WP_REST_Request $request): \WP_REST_Response { - $id = (int) $request->get_param('id'); - $post = get_post($id); - - if (!$post || $post->post_type !== 'aksrt_product') { - return $this->error('商品不存在', 404); - } - - return $this->success([ - 'data' => [ - 'id' => $post->ID, - 'title' => $post->post_title, - 'content' => $post->post_content, - 'excerpt' => $post->post_excerpt, - 'price' => (float) get_post_meta($post->ID, '_aksrt_price', true), - 'image' => (string) (get_post_meta($post->ID, '_aksrt_product_image', true) ?: get_the_post_thumbnail_url($post->ID, 'medium') ?: ''), - 'stock' => (int) get_post_meta($post->ID, '_aksrt_stock', true), - 'sales' => (int) get_post_meta($post->ID, '_aksrt_sales', true), - 'status' => $post->post_status, - 'created' => get_the_date('Y-m-d H:i', $post), - 'modified' => get_the_modified_date('Y-m-d H:i', $post), - 'categories' => wp_get_post_terms($post->ID, 'aksrt_product_cat', ['fields' => 'names']) ?: [], - 'tags' => wp_get_post_terms($post->ID, 'aksrt_product_tag', ['fields' => 'names']) ?: [], - 'gallery' => get_post_meta($post->ID, '_aksrt_product_gallery', true) ?: [], - 'shipping_mode' => (string) get_post_meta($post->ID, '_aksrt_shipping_mode', true), - 'shipping_fee' => (float) get_post_meta($post->ID, '_aksrt_shipping_fee', true), - 'after_sale_policy' => (string) get_post_meta($post->ID, '_aksrt_after_sale_policy', true), - 'support_refund' => get_post_meta($post->ID, '_aksrt_support_refund', true) !== '0', - ], - ]); - } - - /** - * POST /shop/products — 创建商品 - */ - public function create_product(\WP_REST_Request $request): \WP_REST_Response { - $params = $this->get_json_params($request); - - $title = sanitize_text_field((string) ($params['title'] ?? '')); - $content = wp_kses_post((string) ($params['content'] ?? '')); - $excerpt = sanitize_textarea_field((string) ($params['excerpt'] ?? '')); - $price = (float) ($params['price'] ?? 0); - $stock = (int) ($params['stock'] ?? 0); - $status = sanitize_key((string) ($params['status'] ?? 'publish')); - $image = esc_url_raw((string) ($params['image'] ?? '')); - $gallery = is_array($params['gallery'] ?? null) ? array_values(array_filter(array_map('esc_url_raw', $params['gallery']))) : []; - $shipping_mode = sanitize_key((string) ($params['shipping_mode'] ?? 'express')); - $shipping_fee = max(0, (float) ($params['shipping_fee'] ?? 0)); - $after_sale_policy = sanitize_textarea_field((string) ($params['after_sale_policy'] ?? '')); - $support_refund = !isset($params['support_refund']) || !empty($params['support_refund']); - $author_id = max(0, (int) ($params['author_id'] ?? 0)); - - if ($title === '') { - return $this->error('商品名称不能为空'); - } - - $valid_statuses = ['publish', 'draft', 'pending']; - if (!in_array($status, $valid_statuses, true)) { - $status = 'draft'; - } - - $post_id = wp_insert_post([ - 'post_type' => 'aksrt_product', - 'post_title' => $title, - 'post_content' => $content, - 'post_excerpt' => $excerpt, - 'post_status' => $status, - 'post_author' => $author_id > 0 ? $author_id : get_current_user_id(), - 'meta_input' => [ - '_aksrt_price' => max(0, $price), - '_aksrt_stock' => max(0, $stock), - '_aksrt_sales' => 0, - '_aksrt_score' => 0, - '_aksrt_views' => 0, - '_aksrt_favorite_count' => 0, - '_aksrt_product_gallery' => $gallery, - '_aksrt_shipping_mode' => $shipping_mode, - '_aksrt_shipping_fee' => $shipping_fee, - '_aksrt_after_sale_policy' => $after_sale_policy, - '_aksrt_support_refund' => $support_refund ? '1' : '0', - '_aksrt_product_video' => is_array($params['product_video'] ?? null) ? array_values(array_filter(array_map('esc_url_raw', $params['product_video']))) : [], - '_aksrt_product_params' => sanitize_textarea_field((string) ($params['product_params'] ?? '')), - '_aksrt_pay_modo' => sanitize_key((string) ($params['pay_modo'] ?? '0')), - '_aksrt_start_price' => max(0, (float) ($params['start_price'] ?? 0)), - '_aksrt_product_rebate' => sanitize_text_field((string) ($params['product_rebate'] ?? '')), - '_aksrt_limit_buy' => max(-1, min(9999, (int) ($params['limit_buy'] ?? -1))), - '_aksrt_limit_buy_note' => sanitize_text_field((string) ($params['limit_buy_note'] ?? '')), - '_aksrt_stock_type' => sanitize_key((string) ($params['stock_type'] ?? 'all')), - '_aksrt_user_required' => sanitize_textarea_field((string) ($params['user_required'] ?? '')), - '_aksrt_shipping_type' => sanitize_key((string) ($params['shipping_type'] ?? $shipping_mode)), - '_aksrt_shipping_delivery_desc' => sanitize_text_field((string) ($params['shipping_delivery_desc'] ?? '')), - '_aksrt_shipping_fee_type' => sanitize_key((string) ($params['shipping_fee_type'] ?? 'free')), - '_aksrt_shipping_fee_free_amount'=> max(0, (float) ($params['shipping_fee_free_amount'] ?? 0)), - '_aksrt_after_sale_matrix' => sanitize_textarea_field((string) ($params['after_sale_matrix'] ?? '')), - '_aksrt_service' => sanitize_textarea_field((string) ($params['service'] ?? '')), - '_aksrt_single_tab' => sanitize_textarea_field((string) ($params['single_tab'] ?? '')), - '_aksrt_content_layout' => sanitize_key((string) ($params['content_layout'] ?? '')), - '_aksrt_content_show_bg' => sanitize_text_field((string) ($params['content_show_bg'] ?? '')), - '_aksrt_delivery_note' => sanitize_text_field((string) ($params['delivery_note'] ?? '')), - '_aksrt_marketing_headline' => sanitize_text_field((string) ($params['marketing_headline'] ?? '')), - '_aksrt_shipping_tip' => sanitize_text_field((string) ($params['shipping_tip'] ?? '')), - '_aksrt_service_tip' => sanitize_text_field((string) ($params['service_tip'] ?? '')), - ], - ], true); - - if (is_wp_error($post_id)) { - return $this->error($post_id->get_error_message()); - } - - if (!empty($params['categories']) && is_array($params['categories'])) { - wp_set_object_terms($post_id, $params['categories'], 'aksrt_product_cat'); - } - if (!empty($params['tags']) && is_array($params['tags'])) { - wp_set_object_terms($post_id, $params['tags'], 'aksrt_product_tag'); - } - if ($image !== '') { - update_post_meta($post_id, '_aksrt_product_image', $image); - } - - if (isset($params['options']) && is_array($params['options']) && class_exists('\AKSRT\Shop\Shop')) { - \AKSRT\Shop\Shop::get_instance()->save_product_options((int) $post_id, $params['options']); - } - - return $this->success([ - 'message' => __('商品已创建', 'aksrt'), - 'data' => ['id' => $post_id], - ], 201); - } - - /** - * POST /shop/products/{id} — 更新商品(含上下架) - */ - public function update_product(\WP_REST_Request $request): \WP_REST_Response { - $id = (int) $request->get_param('id'); - $post = get_post($id); - - if (!$post || $post->post_type !== 'aksrt_product') { - return $this->error('商品不存在', 404); - } - - $params = $this->get_json_params($request); - $update = ['ID' => $id]; - - if (isset($params['title'])) { - $update['post_title'] = sanitize_text_field($params['title']); - } - if (isset($params['content'])) { - $update['post_content'] = wp_kses_post($params['content']); - } - if (isset($params['excerpt'])) { - $update['post_excerpt'] = sanitize_textarea_field($params['excerpt']); - } - if (isset($params['status'])) { - $new_status = sanitize_key($params['status']); - $valid_statuses = ['publish', 'draft', 'pending', 'trash']; - if (in_array($new_status, $valid_statuses, true)) { - $update['post_status'] = $new_status; - } - } - - $meta = []; - if (isset($params['price'])) { - $meta['_aksrt_price'] = max(0, (float) $params['price']); - } - if (isset($params['stock'])) { - $meta['_aksrt_stock'] = max(0, (int) $params['stock']); - } - if (isset($params['gallery']) && is_array($params['gallery'])) { - $meta['_aksrt_product_gallery'] = array_values(array_filter(array_map('esc_url_raw', $params['gallery']))); - } - if (isset($params['shipping_mode'])) { - $meta['_aksrt_shipping_mode'] = sanitize_key((string) $params['shipping_mode']); - } - if (isset($params['shipping_fee'])) { - $meta['_aksrt_shipping_fee'] = max(0, (float) $params['shipping_fee']); - } - if (isset($params['after_sale_policy'])) { - $meta['_aksrt_after_sale_policy'] = sanitize_textarea_field((string) $params['after_sale_policy']); - } - if (isset($params['support_refund'])) { - $meta['_aksrt_support_refund'] = !empty($params['support_refund']) ? '1' : '0'; - } - if (isset($params['product_video']) && is_array($params['product_video'])) { - $meta['_aksrt_product_video'] = array_values(array_filter(array_map('esc_url_raw', $params['product_video']))); - } - if (isset($params['product_params'])) { - $meta['_aksrt_product_params'] = sanitize_textarea_field((string) $params['product_params']); - } - if (isset($params['pay_modo'])) { - $meta['_aksrt_pay_modo'] = sanitize_key((string) $params['pay_modo']); - } - if (isset($params['start_price'])) { - $meta['_aksrt_start_price'] = max(0, (float) $params['start_price']); - } - if (isset($params['product_rebate'])) { - $meta['_aksrt_product_rebate'] = sanitize_text_field((string) $params['product_rebate']); - } - if (isset($params['limit_buy'])) { - $meta['_aksrt_limit_buy'] = max(-1, min(9999, (int) $params['limit_buy'])); - } - if (isset($params['limit_buy_note'])) { - $meta['_aksrt_limit_buy_note'] = sanitize_text_field((string) $params['limit_buy_note']); - } - if (isset($params['stock_type'])) { - $meta['_aksrt_stock_type'] = sanitize_key((string) $params['stock_type']); - } - if (isset($params['stock_opts']) && is_array($params['stock_opts'])) { - $meta['_aksrt_stock_opts'] = wp_json_encode($params['stock_opts']); - } - if (isset($params['user_required'])) { - $meta['_aksrt_user_required'] = sanitize_textarea_field((string) $params['user_required']); - } - if (isset($params['shipping_type'])) { - $meta['_aksrt_shipping_type'] = sanitize_key((string) $params['shipping_type']); - } - if (isset($params['shipping_delivery_desc'])) { - $meta['_aksrt_shipping_delivery_desc'] = sanitize_text_field((string) $params['shipping_delivery_desc']); - } - if (isset($params['auto_delivery']) && is_array($params['auto_delivery'])) { - $meta['_aksrt_auto_delivery'] = wp_json_encode($params['auto_delivery']); - } - if (isset($params['shipping_fee_type'])) { - $meta['_aksrt_shipping_fee_type'] = sanitize_key((string) $params['shipping_fee_type']); - } - if (isset($params['shipping_fee_free_amount'])) { - $meta['_aksrt_shipping_fee_free_amount'] = max(0, (float) $params['shipping_fee_free_amount']); - } - if (isset($params['after_sale_matrix'])) { - $meta['_aksrt_after_sale_matrix'] = sanitize_textarea_field((string) $params['after_sale_matrix']); - } - if (isset($params['service'])) { - $meta['_aksrt_service'] = sanitize_textarea_field((string) $params['service']); - } - if (isset($params['single_tab'])) { - $meta['_aksrt_single_tab'] = sanitize_textarea_field((string) $params['single_tab']); - } - if (isset($params['content_layout'])) { - $meta['_aksrt_content_layout'] = sanitize_key((string) $params['content_layout']); - } - if (isset($params['content_show_bg'])) { - $meta['_aksrt_content_show_bg'] = sanitize_text_field((string) $params['content_show_bg']); - } - if (isset($params['delivery_note'])) { - $meta['_aksrt_delivery_note'] = sanitize_text_field((string) $params['delivery_note']); - } - if (isset($params['marketing_headline'])) { - $meta['_aksrt_marketing_headline'] = sanitize_text_field((string) $params['marketing_headline']); - } - if (isset($params['shipping_tip'])) { - $meta['_aksrt_shipping_tip'] = sanitize_text_field((string) $params['shipping_tip']); - } - if (isset($params['service_tip'])) { - $meta['_aksrt_service_tip'] = sanitize_text_field((string) $params['service_tip']); - } - if (!empty($meta)) { - $update['meta_input'] = $meta; - } - - $result = wp_update_post($update, true); - - if (is_wp_error($result)) { - return $this->error($result->get_error_message()); - } - - if (isset($params['categories']) && is_array($params['categories'])) { - wp_set_object_terms($id, $params['categories'], 'aksrt_product_cat'); - } - if (isset($params['tags']) && is_array($params['tags'])) { - wp_set_object_terms($id, $params['tags'], 'aksrt_product_tag'); - } - if (isset($params['image'])) { - update_post_meta($id, '_aksrt_product_image', esc_url_raw((string) $params['image'])); - } - - if (isset($params['options']) && is_array($params['options']) && class_exists('\AKSRT\Shop\Shop')) { - \AKSRT\Shop\Shop::get_instance()->save_product_options($id, $params['options']); - } - - return $this->success([ - 'message' => __('商品已更新', 'aksrt'), - ]); - } -} diff --git a/src/AKSRT/Admin/ShopControllerVariantExportOperations.php b/src/AKSRT/Admin/ShopControllerVariantExportOperations.php deleted file mode 100644 index 62c2d5f6..00000000 --- a/src/AKSRT/Admin/ShopControllerVariantExportOperations.php +++ /dev/null @@ -1,113 +0,0 @@ -get_param('id'); - $product = get_post($product_id); - if (!$product || $product->post_type !== 'aksrt_product') { - return $this->error('商品不存在', 404); - } - - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->success(['data' => ['variants' => []]]); - } - - $variants = \AKSRT\Shop\Shop::get_instance()->get_variants($product_id); - return $this->success(['data' => ['variants' => $variants]]); - } - - public function save_product_variants(\WP_REST_Request $request): \WP_REST_Response { - $product_id = (int) $request->get_param('id'); - $product = get_post($product_id); - if (!$product || $product->post_type !== 'aksrt_product') { - return $this->error('商品不存在', 404); - } - - $params = method_exists($this, 'get_json_params') - ? $this->get_json_params($request) - : ($request->get_json_params() ?? []); - $variants = $params['variants'] ?? []; - if (!is_array($variants)) { - return $this->error('变体数据格式错误'); - } - - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城系统未启用', 501); - } - - $saved = \AKSRT\Shop\Shop::get_instance()->save_variants($product_id, $variants); - return $this->success(['message' => '变体已保存', 'data' => ['variants' => $saved]]); - } - - public function delete_variant(\WP_REST_Request $request): \WP_REST_Response { - $variant_id = (int) $request->get_param('id'); - - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城系统未启用', 501); - } - - $result = \AKSRT\Shop\Shop::get_instance()->delete_variant($variant_id); - if (!$result) { - return $this->error('删除失败', 500); - } - - return $this->success(['message' => '变体已删除']); - } - - public function export_orders(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - - $args = [ - 'status' => sanitize_key((string) $request->get_param('status')), - 'date_from' => sanitize_text_field((string) $request->get_param('date_from')), - 'date_to' => sanitize_text_field((string) $request->get_param('date_to')), - 'limit' => max(1, min(5000, (int) ($request->get_param('limit') ?: 1000))), - ]; - - $result = \AKSRT\Shop\Shop::get_instance()->export_orders_csv($args); - - return new \WP_REST_Response($result['csv'], 200, [ - 'Content-Type' => 'text/csv; charset=utf-8', - 'Content-Disposition' => 'attachment; filename="orders-' . date('Ymd-His') . '.csv"', - 'X-Order-Count' => $result['count'], - ]); - } - - public function confirm_receipt(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Shop\Shop')) { - return $this->error('商城服务不可用', 500); - } - - $order_number = sanitize_text_field((string) $request->get_param('order_number')); - $result = \AKSRT\Shop\Shop::get_instance()->confirm_receipt(get_current_user_id(), $order_number); - if (empty($result['success'])) { - return $this->error($result['message'] ?? '确认收货失败'); - } - - return $this->success(['data' => $result['data']]); - } - - public function get_after_sale_reasons(\WP_REST_Request $request): \WP_REST_Response { - if (class_exists('\AKSRT\Payment\AfterSale')) { - return $this->success(['data' => \AKSRT\Payment\AfterSale::get_reason_options()]); - } - return $this->success(['data' => ['refund' => [], 'return' => []]]); - } - - public function get_frontend_after_sale_reasons(\WP_REST_Request $request): \WP_REST_Response { - if (class_exists('\AKSRT\Payment\AfterSale')) { - return $this->success(['data' => \AKSRT\Payment\AfterSale::get_reason_options()]); - } - return $this->success(['data' => ['refund' => [], 'return' => []]]); - } -} diff --git a/src/AKSRT/Core/DBMigratorPart2.php b/src/AKSRT/Core/DBMigratorPart2.php index 3e2227b9..7e00f6db 100644 --- a/src/AKSRT/Core/DBMigratorPart2.php +++ b/src/AKSRT/Core/DBMigratorPart2.php @@ -261,24 +261,6 @@ trait DBMigratorPart2 { ) {$charset_collate};"; dbDelta($sql_restriction); - // ---------- 13. 商品变体/SKU ---------- - $sql_variant = "CREATE TABLE {$prefix}aksrt_product_variant ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - product_id BIGINT UNSIGNED NOT NULL, - sku VARCHAR(64) NOT NULL DEFAULT '', - attributes TEXT DEFAULT NULL, - price DECIMAL(10,2) NOT NULL DEFAULT 0, - stock INT NOT NULL DEFAULT 0, - image VARCHAR(512) NOT NULL DEFAULT '', - sort_order INT NOT NULL DEFAULT 0, - created_at DATETIME NOT NULL, - PRIMARY KEY (id), - KEY idx_product (product_id), - KEY idx_sku (sku), - KEY idx_sort (sort_order) - ) {$charset_collate};"; - dbDelta($sql_variant); - // ── follow ── $sql_follow = "CREATE TABLE {$prefix}aksrt_follow ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, @@ -329,64 +311,6 @@ trait DBMigratorPart2 { ) {$charset_collate};"; dbDelta($sql_coupon_usage); - // ── after_sale ── - $sql_after_sale = "CREATE TABLE {$prefix}aksrt_after_sale ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - order_number VARCHAR(32) NOT NULL DEFAULT '', - user_id BIGINT UNSIGNED NOT NULL DEFAULT 0, - type VARCHAR(20) NOT NULL DEFAULT 'refund', - reason TEXT NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'pending', - amount DECIMAL(10,2) NOT NULL DEFAULT 0, - note TEXT DEFAULT NULL, - operator_id BIGINT UNSIGNED DEFAULT NULL, - operator_note TEXT DEFAULT NULL, - refund_request_id VARCHAR(64) DEFAULT NULL, - provider VARCHAR(32) NOT NULL DEFAULT '', - provider_refund_id VARCHAR(128) NOT NULL DEFAULT '', - provider_status VARCHAR(24) NOT NULL DEFAULT 'not_started', - refund_effect_status VARCHAR(24) NOT NULL DEFAULT 'not_started', - refund_error TEXT DEFAULT NULL, - refund_attempts INT UNSIGNED NOT NULL DEFAULT 0, - refund_requested_at DATETIME DEFAULT NULL, - provider_updated_at DATETIME DEFAULT NULL, - refund_succeeded_at DATETIME DEFAULT NULL, - refund_effected_at DATETIME DEFAULT NULL, - order_status_before VARCHAR(20) NOT NULL DEFAULT '', - created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', - updated_at DATETIME DEFAULT NULL, - resolved_at DATETIME DEFAULT NULL, - PRIMARY KEY (id), - UNIQUE KEY uniq_refund_request (refund_request_id), - KEY idx_order (order_number), - KEY idx_user (user_id), - KEY idx_status (status), - KEY idx_type (type), - KEY idx_provider_status (provider_status), - KEY idx_created (created_at) - ) {$charset_collate};"; - dbDelta($sql_after_sale); - - // ── order_shipping ── - $sql_shipping = "CREATE TABLE {$prefix}aksrt_order_shipping ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - order_number VARCHAR(32) NOT NULL DEFAULT '', - carrier VARCHAR(60) NOT NULL DEFAULT '', - tracking_no VARCHAR(128) NOT NULL DEFAULT '', - status VARCHAR(20) NOT NULL DEFAULT 'shipped', - note TEXT DEFAULT NULL, - operator_id BIGINT UNSIGNED DEFAULT NULL, - created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', - updated_at DATETIME DEFAULT NULL, - delivered_at DATETIME DEFAULT NULL, - PRIMARY KEY (id), - KEY idx_order (order_number), - KEY idx_status (status), - KEY idx_tracking (tracking_no), - KEY idx_created (created_at) - ) {$charset_collate};"; - dbDelta($sql_shipping); - // ── charge_card (zibll-style typed card_pass) ── $sql_charge_card = "CREATE TABLE {$prefix}aksrt_charge_card ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, @@ -461,25 +385,6 @@ trait DBMigratorPart2 { ) {$charset_collate};"; dbDelta($sql_order_items); - // ── card_key 卡密表 (§6.2 自动发货) ── - $sql_card_key = "CREATE TABLE {$prefix}aksrt_card_key ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - product_id BIGINT UNSIGNED NOT NULL DEFAULT 0, - options_key VARCHAR(255) NOT NULL DEFAULT '', - card_code TEXT NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'available', - order_number VARCHAR(32) NOT NULL DEFAULT '', - used_by BIGINT UNSIGNED DEFAULT NULL, - used_at DATETIME DEFAULT NULL, - created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', - PRIMARY KEY (id), - KEY idx_product (product_id), - KEY idx_options (options_key(50)), - KEY idx_status (status), - KEY idx_order (order_number) - ) {$charset_collate};"; - dbDelta($sql_card_key); - self::migrate_identity_tables($prefix, $charset_collate); } } diff --git a/src/AKSRT/Core/DBMigratorPart3.php b/src/AKSRT/Core/DBMigratorPart3.php index d3718b6c..45efddfc 100644 --- a/src/AKSRT/Core/DBMigratorPart3.php +++ b/src/AKSRT/Core/DBMigratorPart3.php @@ -31,16 +31,12 @@ trait DBMigratorPart3 { 'report' => 'aksrt_report', 'follow' => 'aksrt_follow', 'coupon' => 'aksrt_coupon', - 'after_sale' => 'aksrt_after_sale', - 'order_shipping' => 'aksrt_order_shipping', 'charge_card' => 'aksrt_charge_card', 'rebate_log' => 'aksrt_rebate_log', - 'coupon_usage' => 'aksrt_coupon_usage', - 'card_key' => 'aksrt_card_key', - 'user_restriction' => 'aksrt_user_restriction', - 'product_variant' => 'aksrt_product_variant', - 'identity_application' => 'aksrt_identity_application', - 'identity_event' => 'aksrt_identity_event', + 'coupon_usage' => 'aksrt_coupon_usage', + 'user_restriction' => 'aksrt_user_restriction', + 'identity_application' => 'aksrt_identity_application', + 'identity_event' => 'aksrt_identity_event', ]; if (!isset($allowed[$name])) { return ''; diff --git a/src/AKSRT/Core/DomainModel.php b/src/AKSRT/Core/DomainModel.php index 8640f164..55391b4b 100644 --- a/src/AKSRT/Core/DomainModel.php +++ b/src/AKSRT/Core/DomainModel.php @@ -30,7 +30,6 @@ class DomainModel { public const ORDER_PRODUCT_RECHARGE = 'recharge'; /** Cash purchase of points (zibll order_type=9). */ public const ORDER_PRODUCT_POINTS_BUY = 'points_buy'; - public const ORDER_PRODUCT_SHOP = 'shop_product'; /** * Human labels for aksrt_order.product_type (front-end order center). @@ -39,7 +38,6 @@ class DomainModel { */ public static function order_product_type_labels(): array { return [ - self::ORDER_PRODUCT_SHOP => __('商城商品', 'aksrt'), self::ORDER_PRODUCT_RESOURCE => __('付费内容', 'aksrt'), self::ORDER_PRODUCT_VIP => __('VIP 会员', 'aksrt'), self::ORDER_PRODUCT_RECHARGE => __('余额充值', 'aksrt'), diff --git a/src/AKSRT/Core/ScriptsPart1b.php b/src/AKSRT/Core/ScriptsPart1b.php index fbe88914..c5518ac4 100644 --- a/src/AKSRT/Core/ScriptsPart1b.php +++ b/src/AKSRT/Core/ScriptsPart1b.php @@ -635,13 +635,11 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js'; } } - // 交易中心共享壳层(购物车 / 订单 / 收益) - // Virtual rewrite routes (aksrt_orders / aksrt_cart / aksrt_revenue) also count. + // 交易中心共享壳层(订单 / 收益) + // Virtual rewrite routes (aksrt_orders / aksrt_revenue) also count. $is_commerce_center = is_page_template('templates/pages/page-orders.php') - || is_page_template('templates/pages/page-cart.php') || is_page_template('templates/pages/page-revenue.php') || (bool) get_query_var('aksrt_orders') - || (bool) get_query_var('aksrt_cart') || (bool) get_query_var('aksrt_revenue'); if ($is_commerce_center) { $commerce_css = get_template_directory() . '/assets/css/routes/commerce.css'; @@ -735,93 +733,6 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js'; } } - // Product archive pages (category / tag) - if (is_tax('aksrt_product_cat') || is_tax('aksrt_product_tag')) { - wp_enqueue_style( - 'aksrt-product-archive', - $base_uri . '/assets/css/frontend/product-archive.css', - ['aksrt-tokens'], - $version - ); - } - - // Product detail page - if (is_page_template('templates/pages/page-product.php')) { - wp_enqueue_style( - 'aksrt-route-product', - $base_uri . '/assets/css/routes/product.css', - ['aksrt-tokens'], - file_exists(get_template_directory() . '/assets/css/routes/product.css') - ? (string) filemtime(get_template_directory() . '/assets/css/routes/product.css') - : $version - ); - wp_enqueue_script( - 'aksrt-product-purchase', - $base_uri . '/assets/js/product-purchase.js', - ['aksrt-script'], - file_exists(get_template_directory() . '/assets/js/product-purchase.js') - ? (string) filemtime(get_template_directory() . '/assets/js/product-purchase.js') - : $version, - true - ); - } - - // Cart page - if (is_page_template('templates/pages/page-cart.php') || (bool) get_query_var('aksrt_cart')) { - wp_enqueue_style( - 'aksrt-route-cart', - $base_uri . '/assets/css/routes/cart.css', - ['aksrt-tokens'], - file_exists(get_template_directory() . '/assets/css/routes/cart.css') - ? (string) filemtime(get_template_directory() . '/assets/css/routes/cart.css') - : $version - ); - wp_enqueue_script( - 'aksrt-cart-checkout', - $base_uri . '/assets/js/cart-checkout.js', - ['aksrt-script'], - file_exists(get_template_directory() . '/assets/js/cart-checkout.js') - ? (string) filemtime(get_template_directory() . '/assets/js/cart-checkout.js') - : $version, - true - ); - } - - // Product edit page - if (is_page_template('templates/pages/page-product-edit.php')) { - wp_enqueue_style( - 'aksrt-route-product', - $base_uri . '/assets/css/routes/product.css', - ['aksrt-tokens'], - file_exists(get_template_directory() . '/assets/css/routes/product.css') - ? (string) filemtime(get_template_directory() . '/assets/css/routes/product.css') - : $version - ); - wp_enqueue_script( - 'aksrt-product-purchase', - $base_uri . '/assets/js/product-purchase.js', - ['aksrt-script'], - file_exists(get_template_directory() . '/assets/js/product-purchase.js') - ? (string) filemtime(get_template_directory() . '/assets/js/product-purchase.js') - : $version, - true - ); - } - - // 商品编辑页共用 page-edit.js - if (is_page_template('templates/pages/page-product-edit.php')) { - $page_edit_js = get_template_directory() . '/assets/js/page-edit.js'; - if (file_exists($page_edit_js)) { - wp_enqueue_script( - 'aksrt-page-edit', - $base_uri . '/assets/js/page-edit.js', - ['aksrt-script'], - (string) filemtime($page_edit_js), - true - ); - } - } - // Unified share-data builder — single payload assembler for every share // surface (dropdown, copy, WeChat, poster). Loaded on any singular view. if (is_singular()) { diff --git a/src/AKSRT/Core/ScriptsPart2.php b/src/AKSRT/Core/ScriptsPart2.php index d88803e1..73bb8a03 100644 --- a/src/AKSRT/Core/ScriptsPart2.php +++ b/src/AKSRT/Core/ScriptsPart2.php @@ -264,7 +264,6 @@ trait ScriptsPart2 { */ private function resolve_frontend_routes(): array { return [ - 'cart' => $this->resolve_template_page_url('templates/pages/page-cart.php', '/cart/'), 'orders' => $this->resolve_template_page_url('templates/pages/page-orders.php', '/orders/'), 'resources' => home_url('/resources/'), ]; diff --git a/src/AKSRT/Core/Setup.php b/src/AKSRT/Core/Setup.php index 8e87c415..729d9542 100644 --- a/src/AKSRT/Core/Setup.php +++ b/src/AKSRT/Core/Setup.php @@ -31,20 +31,6 @@ class Setup { add_action('init', [$this, 'register_nav_menus'], 0); add_action('init', [$this, 'register_post_types'], 1); add_action('init', [$this, 'register_meta_fields'], 5); - add_action('init', [$this, 'register_shop_rewrite_rules'], 20); - add_filter('query_vars', [$this, 'register_shop_query_vars']); - add_action('template_redirect', [$this, 'shop_template_redirect']); - // Shop fulfillment hook (§6.5 R68) - if (class_exists('\AKSRT\Shop\Shop')) { - add_action('aksrt_order_paid', [\AKSRT\Shop\Shop::get_instance(), 'handle_order_paid'], 10, 2); - } - // Auto-confirm receipt cron (§6.5 R68) - if (!wp_next_scheduled('aksrt_auto_confirm_receipt')) { - wp_schedule_event(time(), 'hourly', 'aksrt_auto_confirm_receipt'); - } - if (class_exists('\AKSRT\Shop\Shop')) { - add_action('aksrt_auto_confirm_receipt', [\AKSRT\Shop\Shop::get_instance(), 'run_auto_confirm_cron']); - } // 文章浏览量统计(单次访问) add_action('wp', 'aksrt_set_post_views'); // 隐藏默认WordPress管理菜单 @@ -110,97 +96,6 @@ class Setup { * 通过 _aksrt_is_resource meta 标记是否为资源。 */ public function register_post_types(): void { - // Shop rewrite settings (safe fallback if settings not yet loaded) - $shop_rewrite = []; - if (class_exists('\AKSRT\Settings\Settings')) { - $shop = \AKSRT\Settings\Settings::get_shop_settings(); - $shop_rewrite = $shop['rewrite'] ?? []; - } - $product_slug = sanitize_key((string) ($shop_rewrite['product_slug'] ?? 'product')); - $suffix_html = !empty($shop_rewrite['suffix_html']); - - // Shop admin visibility: align with zibll native sidebar when shop is enabled. - $shop_enabled = true; - if (class_exists('\AKSRT\Settings\Settings')) { - $shop_settings = \AKSRT\Settings\Settings::get_shop_settings(); - $shop_enabled = !empty($shop_settings['enabled']); - } - // Always register UI in admin so settings/tools remain reachable; hide menu when disabled. - $shop_show_ui = true; - $shop_show_menu = is_admin() ? $shop_enabled : false; - - // 商品 CPT — native WP sidebar (zibll: shop_product @ pos 35) - register_post_type('aksrt_product', [ - 'labels' => [ - 'name' => __('商城', 'aksrt'), - 'singular_name' => __('商品', 'aksrt'), - 'add_new' => __('新增商品', 'aksrt'), - 'add_new_item' => __('新增商品', 'aksrt'), - 'edit_item' => __('编辑商品', 'aksrt'), - 'new_item' => __('新商品', 'aksrt'), - 'view_item' => __('查看商品', 'aksrt'), - 'search_items' => __('搜索商品', 'aksrt'), - 'not_found' => __('未找到商品', 'aksrt'), - 'not_found_in_trash' => __('回收站中无商品', 'aksrt'), - 'all_items' => __('所有商品', 'aksrt'), - 'menu_name' => __('商城', 'aksrt'), - ], - 'public' => true, - 'publicly_queryable' => true, - 'show_ui' => $shop_show_ui, - 'show_in_menu' => $shop_show_menu, - 'show_in_rest' => true, - 'has_archive' => true, - 'hierarchical' => false, - 'menu_position' => 35, - 'menu_icon' => 'dashicons-store', - 'supports' => ['title', 'editor', 'excerpt', 'thumbnail', 'author', 'revisions'], - 'rewrite' => ['slug' => $product_slug, 'with_front' => false], - 'capability_type' => 'post', - 'map_meta_cap' => true, - ]); - - // 商品分类 taxonomy(独立于文章 category) - register_taxonomy('aksrt_product_cat', 'aksrt_product', [ - 'labels' => [ - 'name' => __('商品分类', 'aksrt'), - 'singular_name' => __('商品分类', 'aksrt'), - 'search_items' => __('搜索商品分类', 'aksrt'), - 'all_items' => __('所有商品分类', 'aksrt'), - 'parent_item' => __('父级分类', 'aksrt'), - 'edit_item' => __('编辑分类', 'aksrt'), - 'add_new_item' => __('新增分类', 'aksrt'), - 'menu_name' => __('商品分类', 'aksrt'), - ], - 'public' => true, - 'hierarchical' => true, - 'show_ui' => $shop_show_ui, - 'show_in_menu' => $shop_show_menu, - 'show_in_rest' => true, - 'show_admin_column' => true, - 'rewrite' => ['slug' => 'product-cat', 'with_front' => false], - ]); - - // 商品标签 taxonomy(独立于文章 post_tag) - register_taxonomy('aksrt_product_tag', 'aksrt_product', [ - 'labels' => [ - 'name' => __('商品标签', 'aksrt'), - 'singular_name' => __('商品标签', 'aksrt'), - 'search_items' => __('搜索商品标签', 'aksrt'), - 'all_items' => __('所有商品标签', 'aksrt'), - 'edit_item' => __('编辑标签', 'aksrt'), - 'add_new_item' => __('新增标签', 'aksrt'), - 'menu_name' => __('特色标签', 'aksrt'), - ], - 'public' => true, - 'hierarchical' => false, - 'show_ui' => $shop_show_ui, - 'show_in_menu' => $shop_show_menu, - 'show_in_rest' => true, - 'show_admin_column' => true, - 'rewrite' => ['slug' => 'product-tag', 'with_front' => false], - ]); - // 项目展示 register_post_type('project', [ 'labels' => [ diff --git a/src/AKSRT/Core/SetupPart1.php b/src/AKSRT/Core/SetupPart1.php index 85d30190..350d8f83 100644 --- a/src/AKSRT/Core/SetupPart1.php +++ b/src/AKSRT/Core/SetupPart1.php @@ -382,30 +382,6 @@ trait SetupPart1 { 'after_title' => $title_after, ]); - // ── Shop widget areas (§6.9) ── - $shop_widgets = [ - 'shop_home_top_fluid' => ['商城首页 · 顶部通栏', '商城首页顶部全宽区域。'], - 'shop_home_sidebar' => ['商城首页 · 侧栏', '商城首页侧栏区域。'], - 'shop_product_top_fluid' => ['商品详情页 · 顶部通栏', '商品详情页顶部全宽区域。'], - 'shop_product_sidebar' => ['商品详情页 · 侧栏', '商品详情页侧栏区域(PC 端)。'], - 'shop_product_bottom_fluid' => ['商品详情页 · 底部通栏', '商品详情页底部全宽区域。'], - 'shop_cart_top_fluid' => ['购物车页 · 顶部通栏', '购物车页顶部全宽区域。'], - 'shop_cart_bottom_fluid' => ['购物车页 · 底部通栏', '购物车页底部全宽区域。'], - 'shop_order_top_fluid' => ['订单页 · 顶部通栏', '订单中心顶部全宽。'], - 'shop_order_bottom_fluid' => ['订单页 · 底部通栏', '订单中心底部全宽。'], - ]; - foreach ($shop_widgets as $sid => $meta) { - register_sidebar([ - 'id' => $sid, - 'name' => $meta[0], - 'description' => $meta[1], - 'before_widget' => $card_wrapper_before, - 'after_widget' => '', - 'before_title' => $title_before, - 'after_title' => $title_after, - ]); - } - // ── Per-page dynamic sidebars (zibll zib_page_register_sidebar parity) ── // Pages that opt in via meta `widgets_register=1` + `widgets_register_container` // (a serialized list of area keys) get page_{key}_{ID} sidebars registered, and diff --git a/src/AKSRT/Core/SetupPart2.php b/src/AKSRT/Core/SetupPart2.php index a92a175f..f53d6d4c 100644 --- a/src/AKSRT/Core/SetupPart2.php +++ b/src/AKSRT/Core/SetupPart2.php @@ -23,97 +23,5 @@ trait SetupPart2 { // 移除仪表盘中的不必要组件 remove_meta_box('dashboard_primary', 'dashboard', 'normal'); remove_meta_box('dashboard_quick_press', 'dashboard', 'side'); - - // Separators around shop (35) and forum (40) — match zibll admin_menu_separator. - global $menu; - if (!is_array($menu)) { - return; - } - if (!isset($menu[34])) { - $menu[34] = ['', 'read', 'separator-aksrt-shop', '', 'wp-menu-separator']; - } - } - - /** - * Register shop rewrite rules for: - * - /shop/{ID}.html — product detail (ID-based, .html suffix) - * - /shop/{ID}.html/comment-{page} — product comment pagination - * - /{cart_slug}/ — cart page (fixed alias) - * - * Runs at init priority 20 (after CPT registration at priority 1). - */ - public function register_shop_rewrite_rules(): void { - if (!class_exists('\AKSRT\Settings\Settings')) { - return; - } - $shop = \AKSRT\Settings\Settings::get_shop_settings(); - if (empty($shop['enabled'])) { - return; - } - - $rw = $shop['rewrite'] ?? []; - $product_slug = sanitize_key((string) ($rw['product_slug'] ?? 'product')); - $cart_slug = sanitize_key((string) ($rw['cart_slug'] ?? 'cart')); - $suffix_html = !empty($rw['suffix_html']); - - // Product ID-based .html routes: /shop/{ID}.html - if ($suffix_html) { - add_rewrite_rule( - '^shop/(\d+)\.html/comment-page-([0-9]{1,})/?$', - 'index.php?post_type=aksrt_product&p=$matches[1]&cpage=$matches[2]', - 'top' - ); - add_rewrite_rule( - '^shop/(\d+)\.html/?$', - 'index.php?post_type=aksrt_product&p=$matches[1]', - 'top' - ); - // Slug-based .html: /{product_slug}/{slug}.html - add_rewrite_rule( - '^' . preg_quote($product_slug) . '/([^/]+)\.html/comment-page-([0-9]{1,})/?$', - 'index.php?post_type=aksrt_product&aksrt_product=$matches[1]&cpage=$matches[3]', - 'top' - ); - add_rewrite_rule( - '^' . preg_quote($product_slug) . '/([^/]+)\.html/?$', - 'index.php?post_type=aksrt_product&aksrt_product=$matches[1]', - 'top' - ); - } - - // Cart page: /{cart_slug}/ - add_rewrite_rule( - '^' . preg_quote($cart_slug) . '/?$', - 'index.php?pagename=' . $cart_slug, - 'top' - ); - } - - /** - * Register shop-specific query vars. - */ - public function register_shop_query_vars(array $vars): array { - $vars[] = 'product_id'; - $vars[] = 'shop_action'; - return $vars; - } - - /** - * Template redirect for shop pages. - * - * - Single aksrt_product: serve page-product.php template - * - Cart page: serve page-cart.php template - */ - public function shop_template_redirect(): void { - // Single product: if the main query resolves to aksrt_product, use our template - if (is_singular('aksrt_product')) { - $tpl = AKSRT_DIR . '/templates/pages/page-product.php'; - if (file_exists($tpl)) { - // Set product_id query var for template consumption - set_query_var('product_id', get_queried_object_id()); - include $tpl; - exit; - } - } } } diff --git a/src/AKSRT/Payment/AfterSale.php b/src/AKSRT/Payment/AfterSale.php deleted file mode 100644 index 73bc764c..00000000 --- a/src/AKSRT/Payment/AfterSale.php +++ /dev/null @@ -1,1385 +0,0 @@ -get($order_number); - if (!$order) { - return new \WP_Error('aksrt_after_sale_order', __('订单不存在。', 'aksrt')); - } - - $user_id = (int) ($order['user_id'] ?? 0); - if ($user_id <= 0 || ($applicant_id > 0 && $applicant_id !== $user_id)) { - return new \WP_Error('aksrt_after_sale_owner', __('无权为该订单申请售后。', 'aksrt')); - } - if (!$this->can_apply_after_sale($order, $type)) { - return new \WP_Error('aksrt_after_sale_state', __('当前订单状态不支持该售后类型。', 'aksrt')); - } - if (!$this->within_application_window($order, $type)) { - return new \WP_Error('aksrt_after_sale_expired', __('该订单已超过售后申请期限。', 'aksrt')); - } - - $refund_type = $this->is_refund_type($type); - if ($refund_type && $amount <= 0) { - return new \WP_Error('aksrt_after_sale_amount', __('退款金额必须大于零。', 'aksrt')); - } - if (!$refund_type) { - $amount = 0.0; - } - - $creation_lock = 'aksrt_as_' . substr(hash('sha256', $order_number), 0, 48); - $lock_acquired = (int) $wpdb->get_var($wpdb->prepare('SELECT GET_LOCK(%s, 5)', $creation_lock)); - if ($lock_acquired !== 1) { - return new \WP_Error('aksrt_after_sale_busy', __('该订单正在处理售后申请,请稍后重试。', 'aksrt')); - } - - $active_statuses = [self::STATUS_PENDING, self::STATUS_PROCESSING, self::STATUS_APPROVED]; - $placeholders = implode(', ', array_fill(0, count($active_statuses), '%s')); - $active_sql = "SELECT COUNT(*) FROM {$this->table()} WHERE order_number = %s AND status IN ({$placeholders})"; - $active = (int) $wpdb->get_var($wpdb->prepare($active_sql, array_merge([$order_number], $active_statuses))); - if ($active > 0) { - $wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $creation_lock)); - return new \WP_Error('aksrt_after_sale_active', __('该订单已有处理中的售后申请。', 'aksrt')); - } - - if ($refund_type) { - $reserved_sql = "SELECT COALESCE(SUM(amount), 0) FROM {$this->table()} - WHERE order_number = %s - AND type IN (%s, %s, %s) - AND status IN (%s, %s, %s, %s)"; - $reserved = (float) $wpdb->get_var($wpdb->prepare( - $reserved_sql, - $order_number, - self::TYPE_REFUND, - self::TYPE_REFUND_RETURN, - self::TYPE_INSURED_PRICE, - self::STATUS_PENDING, - self::STATUS_PROCESSING, - self::STATUS_APPROVED, - self::STATUS_COMPLETED - )); - $remaining = max(0.0, round((float) ($order['amount'] ?? 0) - $reserved, 2)); - if ($amount > $remaining) { - $wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $creation_lock)); - return new \WP_Error('aksrt_after_sale_excess', __('退款金额超过订单剩余可退金额。', 'aksrt')); - } - } - - $result = $wpdb->insert($this->table(), [ - 'order_number' => $order_number, - 'user_id' => $user_id, - 'type' => $type, - 'reason' => $reason, - 'status' => self::STATUS_PENDING, - 'amount' => $amount, - 'note' => $note, - 'order_status_before' => sanitize_key((string) ($order['status'] ?? '')), - 'created_at' => current_time('mysql'), - ]); - - $wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $creation_lock)); - if (!$result) { - return new \WP_Error('aksrt_after_sale_db', __('售后申请创建失败。', 'aksrt')); - } - $ticket_id = (int) $wpdb->insert_id; - $this->append_event($ticket_id, 'created', __('用户提交售后申请', 'aksrt'), $user_id); - return $ticket_id; - } - - /** - * 获取售后工单列表 - */ - public function get_list(array $args = []): array { - global $wpdb; - - $limit = max(1, min(100, (int) ($args['limit'] ?? 20))); - $offset = max(0, (int) ($args['offset'] ?? 0)); - $include_order_context = !empty($args['include_order_context']); - $status = sanitize_key((string) ($args['status'] ?? '')); - $stage = sanitize_key((string) ($args['stage'] ?? '')); - $order_number = sanitize_text_field((string) ($args['order_number'] ?? '')); - $type = sanitize_key((string) ($args['type'] ?? '')); - $date_from = sanitize_text_field((string) ($args['date_from'] ?? '')); - $date_to = sanitize_text_field((string) ($args['date_to'] ?? '')); - - $where = []; - $params = []; - - if ($status !== '') { - $where[] = 'status = %s'; - $params[] = $status; - } - if ($stage !== '' && $status === '') { - $stage_statuses = $this->map_stage_to_statuses($stage); - if (!empty($stage_statuses)) { - $where[] = 'status IN (' . implode(', ', array_fill(0, count($stage_statuses), '%s')) . ')'; - $params = array_merge($params, $stage_statuses); - } - } - if ($order_number !== '') { - $where[] = 'order_number = %s'; - $params[] = $order_number; - } - if ($type !== '') { - $where[] = 'type = %s'; - $params[] = $type; - } - if ($date_from !== '') { - $where[] = 'created_at >= %s'; - $params[] = $date_from . ' 00:00:00'; - } - if ($date_to !== '') { - $where[] = 'created_at <= %s'; - $params[] = $date_to . ' 23:59:59'; - } - - $where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; - - $count_sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}"; - $total = $params ? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params)) : (int) $wpdb->get_var($count_sql); - - $sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d"; - $params[] = $limit; - $params[] = $offset; - - $items = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A); - $items = array_map([$this, 'decorate_record'], (array) $items); - - if ($include_order_context && !empty($items) && class_exists('AKSRT\\Payment\\OrderRepository')) { - $contexts = OrderRepository::get_instance()->get_admin_order_context_map(array_column($items, 'order_number')); - foreach ($items as &$item) { - $item['order_context'] = $contexts[(string) ($item['order_number'] ?? '')] ?? []; - } - unset($item); - } - - return [ - 'items' => $items ?: [], - 'total' => $total, - 'page' => floor($offset / $limit) + 1, - 'max_pages' => ceil($total / $limit), - ]; - } - - /** - * 获取单个售后工单 - */ - public function get(int $id): ?array { - global $wpdb; - $row = $wpdb->get_row($wpdb->prepare( - "SELECT * FROM {$this->table()} WHERE id = %d", - $id - ), ARRAY_A); - return $row ? $this->decorate_record($row) : null; - } - - /** - * 获取单个售后单并附带订单上下文。 - */ - public function get_with_order_context(int $id): ?array { - $record = $this->get($id); - if (!$record) { - return null; - } - - $record['order_context'] = class_exists('AKSRT\\Payment\\OrderRepository') - ? OrderRepository::get_instance()->get_admin_order_context((string) ($record['order_number'] ?? '')) - : []; - - return $record; - } - - public static function get_type_options(): array { - return [ - ['value' => self::TYPE_REFUND, 'label' => __('退款', 'aksrt')], - ['value' => self::TYPE_EXCHANGE, 'label' => __('换货', 'aksrt')], - ['value' => self::TYPE_REPAIR, 'label' => __('维修', 'aksrt')], - ]; - } - - public static function get_status_options(): array { - return [ - ['value' => self::STATUS_PENDING, 'label' => __('待处理', 'aksrt')], - ['value' => self::STATUS_PROCESSING, 'label' => __('处理中', 'aksrt')], - ['value' => self::STATUS_APPROVED, 'label' => __('已通过', 'aksrt')], - ['value' => self::STATUS_REJECTED, 'label' => __('已拒绝', 'aksrt')], - ['value' => self::STATUS_COMPLETED, 'label' => __('已完成', 'aksrt')], - ['value' => self::STATUS_CANCELLED, 'label' => __('已取消', 'aksrt')], - ]; - } - - public static function get_stage_options(): array { - return [ - ['value' => 'review', 'label' => __('审核处理中', 'aksrt')], - ['value' => 'approved', 'label' => __('已同意,待完成', 'aksrt')], - ['value' => 'completed', 'label' => __('售后完成', 'aksrt')], - ['value' => 'rejected', 'label' => __('售后拒绝', 'aksrt')], - ['value' => 'cancelled', 'label' => __('已取消', 'aksrt')], - ]; - } - - /** - * R68: Get after-sale reason options from shop settings (§6.8). - * - * @return array{refund: array, return: array} - */ - public static function get_reason_options(): array { - if (!class_exists('\AKSRT\Settings\Settings')) { - return ['refund' => [], 'return' => []]; - } - $shop = \AKSRT\Settings\Settings::get_shop_settings(); - $refund_text = (string) ($shop['timing']['after_sale_reason_refund'] ?? ''); - $return_text = (string) ($shop['timing']['after_sale_reason_return'] ?? ''); - - return [ - 'refund' => self::parse_reason_lines($refund_text), - 'return' => self::parse_reason_lines($return_text), - ]; - } - - private static function parse_reason_lines(string $text): array { - if ($text === '') { - return []; - } - $lines = array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $text))); - $result = []; - foreach (array_values($lines) as $i => $line) { - $result[] = [ - 'id' => $i + 1, - 'label' => $line, - ]; - } - return $result; - } - - /** - * @param array $order_numbers - * @return array> - */ - public function get_summary_map(array $order_numbers): array { - global $wpdb; - - $order_numbers = array_values(array_unique(array_filter(array_map( - static function ($order_number): string { - return sanitize_text_field((string) $order_number); - }, - $order_numbers - )))); - - if (empty($order_numbers)) { - return []; - } - - $map = []; - foreach ($order_numbers as $order_number) { - $map[$order_number] = [ - 'count' => 0, - 'latest' => [], - ]; - } - - $placeholders = implode(', ', array_fill(0, count($order_numbers), '%s')); - $sql = "SELECT * FROM {$this->table()} WHERE order_number IN ({$placeholders}) ORDER BY created_at DESC, id DESC"; - $rows = $wpdb->get_results($wpdb->prepare($sql, $order_numbers), ARRAY_A); - - foreach ((array) $rows as $row) { - $record = $this->decorate_record($row); - $order_number = (string) ($record['order_number'] ?? ''); - - if (!isset($map[$order_number])) { - continue; - } - - $map[$order_number]['count']++; - if (empty($map[$order_number]['latest'])) { - $map[$order_number]['latest'] = $this->build_summary_item($record); - } - } - - return $map; - } - - /** - * 更新售后工单状态 - */ - public function update_status(int $id, string $new_status, int $operator_id = 0, string $operator_note = ''): bool { - global $wpdb; - - $record = $this->get($id); - if (!$record) { - return false; - } - - $valid = [self::STATUS_PENDING, self::STATUS_PROCESSING, self::STATUS_APPROVED, self::STATUS_REJECTED, self::STATUS_COMPLETED, self::STATUS_CANCELLED]; - if (!in_array($new_status, $valid, true)) return false; - if (!$this->can_transition_status((string) ($record['status'] ?? ''), $new_status)) { - return false; - } - - $update = [ - 'status' => $new_status, - 'updated_at' => current_time('mysql'), - ]; - - if ($operator_id > 0) { - $update['operator_id'] = $operator_id; - } - if ($operator_note !== '') { - $update['operator_note'] = sanitize_textarea_field($operator_note); - } - if (in_array($new_status, [self::STATUS_COMPLETED, self::STATUS_REJECTED], true)) { - $update['resolved_at'] = current_time('mysql'); - } - - if ($new_status === self::STATUS_COMPLETED && !$this->complete_refund($record)) { - return false; - } - - $updated = (bool) $wpdb->update($this->table(), $update, ['id' => $id]); - if ($updated) { - $note = $operator_note !== '' ? $operator_note : sprintf(__('售后状态更新为:%s', 'aksrt'), $this->get_status_label($new_status)); - $this->sync_order_status($record, $new_status, $note); - } - - return $updated; - } - - /** - * 获取统计 - */ - public function get_stats(): array { - global $wpdb; - $table = $this->table(); - - $counts = $wpdb->get_results( - "SELECT status, COUNT(*) as cnt FROM {$table} GROUP BY status", - ARRAY_A - ); - $counts = array_column($counts, 'cnt', 'status'); - - $stats = [ - 'total' => 0, - 'pending' => 0, - 'processing' => 0, - 'approved' => 0, - 'rejected' => 0, - 'completed' => 0, - 'cancelled' => 0, - ]; - - foreach ($counts as $status => $cnt) { - if (isset($stats[$status])) { - $stats[$status] = (int) $cnt; - } - $stats['total'] += (int) $cnt; - } - - return $stats; - } - private function decorate_record(array $record): array { - $record['id'] = (int) ($record['id'] ?? 0); - $record['user_id'] = (int) ($record['user_id'] ?? 0); - $record['operator_id'] = isset($record['operator_id']) ? (int) $record['operator_id'] : 0; - $record['amount'] = (float) ($record['amount'] ?? 0); - $record['status_label'] = $this->get_status_label((string) ($record['status'] ?? '')); - $record['type_label'] = $this->get_type_label((string) ($record['type'] ?? '')); - $record['operator'] = $this->get_user_summary((int) ($record['operator_id'] ?? 0)); - $record['applicant'] = $this->get_user_summary((int) ($record['user_id'] ?? 0)); - $record['stage'] = $this->build_stage_payload($record); - return $record; - } - - private function build_stage_payload(array $record): array { - $status = (string) ($record['status'] ?? ''); - - if (in_array($status, [self::STATUS_PENDING, self::STATUS_PROCESSING], true)) { - return [ - 'key' => 'review', - 'label' => __('审核处理中', 'aksrt'), - ]; - } - - if ($status === self::STATUS_APPROVED) { - return [ - 'key' => 'approved', - 'label' => __('已同意,待完成', 'aksrt'), - ]; - } - - if ($status === self::STATUS_COMPLETED) { - return [ - 'key' => 'completed', - 'label' => __('售后完成', 'aksrt'), - ]; - } - - if ($status === self::STATUS_REJECTED) { - return [ - 'key' => 'rejected', - 'label' => __('售后拒绝', 'aksrt'), - ]; - } - - return [ - 'key' => 'cancelled', - 'label' => __('已取消', 'aksrt'), - ]; - } - - private function get_status_label(string $status): string { - foreach (self::get_status_options() as $item) { - if (($item['value'] ?? '') === $status) { - return (string) ($item['label'] ?? $status); - } - } - - return $status; - } - - private function get_type_label(string $type): string { - foreach (self::get_type_options() as $item) { - if (($item['value'] ?? '') === $type) { - return (string) ($item['label'] ?? $type); - } - } - - return $type; - } - - private function get_user_summary(int $user_id): array { - if ($user_id <= 0) { - return []; - } - - $user = get_userdata($user_id); - if (!$user) { - return []; - } - - return [ - 'id' => $user_id, - 'display_name' => (string) $user->display_name, - 'user_login' => (string) $user->user_login, - 'user_email' => (string) $user->user_email, - ]; - } - - /** - * @return array - */ - private function map_stage_to_statuses(string $stage): array { - $map = [ - 'review' => [self::STATUS_PENDING, self::STATUS_PROCESSING], - 'approved' => [self::STATUS_APPROVED], - 'completed' => [self::STATUS_COMPLETED], - 'rejected' => [self::STATUS_REJECTED], - 'cancelled' => [self::STATUS_CANCELLED], - ]; - - return $map[$stage] ?? []; - } - - private function can_transition_status(string $current_status, string $new_status): bool { - $map = [ - self::STATUS_PENDING => [self::STATUS_PROCESSING, self::STATUS_APPROVED, self::STATUS_REJECTED, self::STATUS_CANCELLED], - self::STATUS_PROCESSING => [self::STATUS_APPROVED, self::STATUS_REJECTED, self::STATUS_CANCELLED], - self::STATUS_APPROVED => [self::STATUS_COMPLETED, self::STATUS_CANCELLED], - self::STATUS_REJECTED => [], - self::STATUS_COMPLETED => [], - self::STATUS_CANCELLED => [], - ]; - - return in_array($new_status, $map[$current_status] ?? [], true); - } - - private function build_summary_item(array $record): array { - return [ - 'id' => (int) ($record['id'] ?? 0), - 'type' => (string) ($record['type'] ?? ''), - 'type_label' => (string) ($record['type_label'] ?? ''), - 'status' => (string) ($record['status'] ?? ''), - 'status_label' => (string) ($record['status_label'] ?? ''), - 'stage' => is_array($record['stage'] ?? null) ? $record['stage'] : [], - 'amount' => (float) ($record['amount'] ?? 0), - 'reason' => (string) ($record['reason'] ?? ''), - 'created_at' => (string) ($record['created_at'] ?? ''), - 'updated_at' => (string) ($record['updated_at'] ?? ''), - 'resolved_at' => (string) ($record['resolved_at'] ?? ''), - ]; - } - - /** - * R69: Execute local refund effects and sales rollback once. - * - * @return true|\WP_Error - */ - private function execute_refund(?array $order, float $refund_amount) { - if (!$order || $refund_amount <= 0) { - return new \WP_Error('aksrt_refund_order', __('退款订单或金额无效。', 'aksrt')); - } - - $user_id = (int) ($order['user_id'] ?? 0); - if ($user_id <= 0) { - return new \WP_Error('aksrt_refund_user', __('退款用户无效。', 'aksrt')); - } - - $payment_method = (string) ($order['payment_method'] ?? 'balance'); - $order_number = (string) ($order['order_number'] ?? ''); - - if ($payment_method === 'balance') { - if (!class_exists('\AKSRT\Credits\Balance') || !\AKSRT\Credits\Balance::enabled()) { - return new \WP_Error('aksrt_refund_balance', __('余额退款服务不可用。', 'aksrt')); - } - $credited = \AKSRT\Credits\Balance::add( - $user_id, - $refund_amount, - 'order_refund', - ['type' => 'order', 'order' => $order_number], - sprintf(__('售后退款:订单 %s', 'aksrt'), $order_number) - ); - if (is_wp_error($credited)) { - return $credited; - } - } elseif ($payment_method === 'points') { - if (!class_exists('\AKSRT\Credits\Credits') || floor($refund_amount) !== $refund_amount) { - return new \WP_Error('aksrt_refund_points', __('积分退款金额无效或积分服务不可用。', 'aksrt')); - } - $credited = \AKSRT\Credits\Credits::add( - $user_id, - (int) $refund_amount, - 'order_refund', - ['order_number' => $order_number, 'type' => 'refund'], - sprintf(__('售后退回积分:订单 %s', 'aksrt'), $order_number) - ); - if (is_wp_error($credited)) { - return $credited; - } - } elseif (!in_array($payment_method, ['wechatpay', 'alipay', 'stripe'], true)) { - return new \WP_Error('aksrt_refund_gateway', __('当前支付方式不支持自动退款。', 'aksrt')); - } - - // Full-order refunds roll back sales once after the monetary refund succeeds. - if (abs((float) ($order['amount'] ?? 0) - $refund_amount) <= 0.01) { - $meta = json_decode((string) ($order['note'] ?? ''), true); - $items = is_array($meta) ? ($meta['items'] ?? []) : []; - if (empty($items) && !empty($order['product_id'])) { - $items = [['product_id' => (int) $order['product_id'], 'quantity' => 1]]; - } - foreach ($items as $item) { - $pid = (int) ($item['product_id'] ?? 0); - $qty = max(1, (int) ($item['quantity'] ?? 1)); - if ($pid <= 0) { - continue; - } - $sales = (int) get_post_meta($pid, '_aksrt_sales_volume', true); - update_post_meta($pid, '_aksrt_sales_volume', max(0, $sales - $qty)); - } - } - - return true; - } - - private function publish_refund_event(array $order, float $refund_amount): void { - $order_number = (string) ($order['order_number'] ?? ''); - $user_id = (int) ($order['user_id'] ?? 0); - $payment_method = (string) ($order['payment_method'] ?? ''); - do_action('aksrt_shop_refund_executed', $order_number, $user_id, $refund_amount, $payment_method); - } - - /** - * Execute and persist a refund before completing the after-sale ticket. - */ - private function complete_refund(array $record): bool { - global $wpdb; - - $type = (string) ($record['type'] ?? ''); - if (!$this->is_refund_type($type)) { - return true; - } - if (!class_exists('AKSRT\\Payment\\OrderRepository')) { - return false; - } - - $ticket_id = (int) ($record['id'] ?? 0); - $order_number = sanitize_text_field((string) ($record['order_number'] ?? '')); - $order = OrderRepository::get_instance()->get($order_number); - $refund_amount = (float) ($record['amount'] ?? 0); - if ($ticket_id <= 0 || !$order || $refund_amount <= 0) { - return false; - } - - $latest = $this->get_by_id($ticket_id) ?: $record; - if (($latest['refund_effect_status'] ?? '') === 'succeeded') { - return true; - } - if (($latest['provider_status'] ?? '') === 'succeeded' - && ($latest['refund_effect_status'] ?? '') === 'processing') { - return false; - } - - $payment_method = sanitize_key((string) ($order['payment_method'] ?? 'balance')); - $request_id = (string) ($latest['refund_request_id'] ?? ''); - if ($request_id === '') { - $request_id = sprintf('as_%d_%s', $ticket_id, substr(hash('sha256', $order_number), 0, 16)); - } - - $provider_status = (string) ($latest['provider_status'] ?? 'not_started'); - if ($provider_status !== 'succeeded') { - $claimed = $wpdb->query($wpdb->prepare( - "UPDATE {$this->table()} - SET refund_request_id = %s, provider = %s, provider_status = 'submitting', - refund_attempts = refund_attempts + 1, refund_requested_at = COALESCE(refund_requested_at, %s), - provider_updated_at = %s, refund_error = NULL, updated_at = %s - WHERE id = %d AND provider_status IN ('not_started', 'failed', 'pending', 'unknown')", - $request_id, - $payment_method, - current_time('mysql'), - current_time('mysql'), - current_time('mysql'), - $ticket_id - )); - if ($claimed !== 1) { - return false; - } - - if (in_array($payment_method, ['wechatpay', 'alipay', 'stripe'], true)) { - $result = $this->refund_via_gateway($payment_method, $order_number, $refund_amount, $request_id); - $status = sanitize_key((string) ($result['status'] ?? (!empty($result['success']) ? 'unknown' : 'failed'))); - if (!in_array($status, ['pending', 'succeeded', 'failed', 'unknown'], true)) { - $status = 'unknown'; - } - $wpdb->update($this->table(), [ - 'refund_request_id' => sanitize_text_field((string) ($result['request_id'] ?? $request_id)), - 'provider_refund_id' => sanitize_text_field((string) ($result['refund_id'] ?? '')), - 'provider_status' => $status, - 'refund_error' => empty($result['success']) ? sanitize_textarea_field((string) ($result['message'] ?? '')) : null, - 'provider_updated_at' => current_time('mysql'), - 'refund_succeeded_at' => $status === 'succeeded' ? current_time('mysql') : null, - 'updated_at' => current_time('mysql'), - ], ['id' => $ticket_id]); - if ($status !== 'succeeded') { - return false; - } - } elseif (in_array($payment_method, ['balance', 'points'], true)) { - $wpdb->update($this->table(), [ - 'provider_status' => 'succeeded', - 'provider_updated_at' => current_time('mysql'), - 'refund_succeeded_at' => current_time('mysql'), - 'updated_at' => current_time('mysql'), - ], ['id' => $ticket_id]); - } else { - $wpdb->update($this->table(), [ - 'provider_status' => 'failed', - 'refund_error' => __('当前支付方式不支持自动退款。', 'aksrt'), - 'provider_updated_at' => current_time('mysql'), - 'updated_at' => current_time('mysql'), - ], ['id' => $ticket_id]); - return false; - } - } - - $wpdb->query('START TRANSACTION'); - $effect_claimed = $wpdb->query($wpdb->prepare( - "UPDATE {$this->table()} - SET refund_effect_status = 'processing', updated_at = %s - WHERE id = %d AND refund_effect_status IN ('not_started', 'failed')", - current_time('mysql'), - $ticket_id - )); - if ($effect_claimed !== 1) { - $wpdb->query('ROLLBACK'); - return false; - } - - $effect = $this->execute_refund($order, $refund_amount); - if (is_wp_error($effect)) { - $wpdb->query('ROLLBACK'); - $wpdb->update($this->table(), [ - 'refund_effect_status' => 'failed', - 'refund_error' => $effect->get_error_message(), - 'updated_at' => current_time('mysql'), - ], ['id' => $ticket_id]); - $this->log_refund_error($order_number, $payment_method, $effect->get_error_message()); - return false; - } - - $effect_saved = $wpdb->update($this->table(), [ - 'refund_effect_status' => 'succeeded', - 'refund_effected_at' => current_time('mysql'), - 'refund_error' => null, - 'updated_at' => current_time('mysql'), - ], ['id' => $ticket_id]); - if ($effect_saved === false) { - $wpdb->query('ROLLBACK'); - return false; - } - $committed = $wpdb->query('COMMIT'); - if ($committed === false) { - return false; - } - $this->publish_refund_event($order, $refund_amount); - return true; - } - - /** - * @return array{success:bool,status:string,refund_id?:string,message?:string} - */ - private function refund_via_gateway(string $payment_method, string $order_number, float $refund_amount, string $request_id): array { - $gateway = Gateway::get_instance()->get_gateway($payment_method); - if (!$gateway) { - $this->log_refund_error($order_number, $payment_method, 'gateway unavailable'); - return ['success' => false, 'status' => 'failed', 'message' => 'gateway unavailable']; - } - - try { - $result = $gateway->refund($order_number, $refund_amount, $request_id); - } catch (\Throwable $e) { - $this->log_refund_error($order_number, $payment_method, $e->getMessage()); - return ['success' => false, 'status' => 'unknown', 'message' => $e->getMessage()]; - } - - if (empty($result['success'])) { - $this->log_refund_error($order_number, $payment_method, (string) ($result['message'] ?? 'unknown error')); - $result['status'] = $result['status'] ?? 'unknown'; - } - - return $result; - } - - private function log_refund_error(string $order_number, string $payment_method, string $message): void { - error_log(sprintf( - '[AKSRT AfterSale] gateway refund failed order=%s gateway=%s: %s', - $order_number, - $payment_method, - $message - )); - } - - private function sync_order_status(array $record, string $new_status, string $note = ''): void { - if (!class_exists('AKSRT\\Payment\\OrderRepository') || !class_exists('AKSRT\\Payment\\Order')) { - return; - } - - $type = (string) ($record['type'] ?? ''); - $is_refund_type = in_array($type, [self::TYPE_REFUND, self::TYPE_REFUND_RETURN, self::TYPE_INSURED_PRICE], true); - if (!$is_refund_type) { - return; - } - - $order_number = sanitize_text_field((string) ($record['order_number'] ?? '')); - if ($order_number === '') { - return; - } - - $repository = OrderRepository::get_instance(); - - if (in_array($new_status, [self::STATUS_PROCESSING, self::STATUS_APPROVED], true)) { - $order = $repository->get($order_number); - $current_status = sanitize_key((string) ($order['status'] ?? '')); - if ($current_status !== '' && $current_status !== Order::STATUS_REFUNDING) { - global $wpdb; - $wpdb->update($this->table(), [ - 'order_status_before' => $current_status, - 'updated_at' => current_time('mysql'), - ], ['id' => (int) ($record['id'] ?? 0)]); - } - $repository->update_status($order_number, Order::STATUS_REFUNDING, [ - 'note' => $note, - ]); - return; - } - - if ($new_status === self::STATUS_COMPLETED) { - $order = $repository->get($order_number); - if (!$order) { - return; - } - if (($order['status'] ?? '') !== Order::STATUS_REFUNDING) { - $repository->update_status($order_number, Order::STATUS_REFUNDING, [ - 'note' => $note, - ]); - $order = $repository->get($order_number) ?: $order; - } - - $refund_amount = (float) ($record['amount'] ?? 0); - global $wpdb; - $completed_other = (float) $wpdb->get_var($wpdb->prepare( - "SELECT COALESCE(SUM(amount), 0) FROM {$this->table()} - WHERE order_number = %s AND id <> %d - AND type IN (%s, %s, %s) AND status = %s", - $order_number, - (int) ($record['id'] ?? 0), - self::TYPE_REFUND, - self::TYPE_REFUND_RETURN, - self::TYPE_INSURED_PRICE, - self::STATUS_COMPLETED - )); - $total_refunded = round($completed_other + $refund_amount, 2); - if ($total_refunded + 0.01 >= (float) ($order['amount'] ?? 0)) { - $repository->update_status($order_number, Order::STATUS_REFUNDED, [ - 'note' => $note, - ]); - } else { - $previous_status = sanitize_key((string) ($record['order_status_before'] ?? '')); - if (!in_array($previous_status, [ - Order::STATUS_PAID, - Order::STATUS_DELIVERED, - Order::STATUS_COMPLETED, - Order::STATUS_PENDING_REVIEW, - ], true)) { - $previous_status = Order::STATUS_PAID; - } - $repository->update_status($order_number, $previous_status, [ - 'force' => true, - 'note' => __('部分退款已完成。', 'aksrt'), - ]); - } - } - } - - /* ============================================================ - * Merchant Actions (§6.8) - * ============================================================ */ - - public function merchant_approve(int $ticket_id, int $operator_id, string $note = ''): bool { - global $wpdb; - $record = $this->get_by_id($ticket_id); - if (!$record || $record['status'] !== self::STATUS_PENDING) { - return false; - } - - $type = (string) $record['type']; - $needs_return = in_array($type, [self::TYPE_REFUND_RETURN, self::TYPE_EXCHANGE, self::TYPE_REPLACEMENT], true); - $new_status = $needs_return ? self::STATUS_PROCESSING : self::STATUS_APPROVED; - $progress = $needs_return ? self::PROGRESS_WAITING_USER_SHIP : self::PROGRESS_WAITING_MERCHANT; - - $wpdb->update($this->table(), [ - 'status' => $new_status, - 'operator_id' => $operator_id, - 'operator_note' => sanitize_textarea_field($note), - 'updated_at' => current_time('mysql'), - ], ['id' => $ticket_id]); - - update_post_meta($ticket_id, '_aksrt_as_progress', $progress); - $this->sync_order_status($record, $new_status, $note ?: __('商家已同意售后', 'aksrt')); - return true; - } - - public function merchant_reject(int $ticket_id, int $operator_id, string $note = ''): bool { - global $wpdb; - $record = $this->get_by_id($ticket_id); - if (!$record || $record['status'] !== self::STATUS_PENDING) { - return false; - } - - $wpdb->update($this->table(), [ - 'status' => self::STATUS_REJECTED, - 'operator_id' => $operator_id, - 'operator_note' => sanitize_textarea_field($note), - 'updated_at' => current_time('mysql'), - ], ['id' => $ticket_id]); - - update_post_meta($ticket_id, '_aksrt_as_progress', 0); - return true; - } - - public function user_submit_return_shipping(int $ticket_id, int $user_id, string $carrier, string $tracking_no): bool { - global $wpdb; - $record = $this->get_by_id($ticket_id); - if (!$record || (int) ($record['user_id'] ?? 0) !== $user_id) { - return false; - } - if ($record['status'] !== self::STATUS_PROCESSING) { - return false; - } - - $return_data = [ - 'carrier' => sanitize_text_field($carrier), - 'tracking_no' => sanitize_text_field($tracking_no), - 'submitted_at' => current_time('mysql'), - ]; - - // Keep human-readable note and structured shipping meta. - $note = sprintf( - /* translators: 1: carrier 2: tracking */ - __('用户已寄回:%1$s %2$s', 'aksrt'), - $return_data['carrier'], - $return_data['tracking_no'] - ); - - $wpdb->update($this->table(), [ - 'note' => $note, - 'updated_at' => current_time('mysql'), - ], ['id' => $ticket_id]); - - update_post_meta($ticket_id, '_aksrt_as_progress', self::PROGRESS_WAITING_MERCHANT); - update_post_meta($ticket_id, '_aksrt_as_return_shipping', $return_data); - $this->append_event($ticket_id, 'return_shipped', $note, $user_id); - return true; - } - - /** - * User cancels a pending after-sale ticket. - */ - public function user_cancel(int $ticket_id, int $user_id, string $reason = ''): bool { - global $wpdb; - $record = $this->get_by_id($ticket_id); - if (!$record || (int) ($record['user_id'] ?? 0) !== $user_id) { - return false; - } - $status = (string) ($record['status'] ?? ''); - if (!in_array($status, [self::STATUS_PENDING, self::STATUS_PROCESSING], true)) { - return false; - } - // Only allow cancel before merchant confirmed return completion. - $progress = (int) get_post_meta($ticket_id, '_aksrt_as_progress', true); - if ($status === self::STATUS_PROCESSING && $progress === self::PROGRESS_DONE) { - return false; - } - - $wpdb->update($this->table(), [ - 'status' => self::STATUS_CANCELLED, - 'updated_at' => current_time('mysql'), - ], ['id' => $ticket_id]); - $this->restore_order_after_cancel($record); - - $msg = $reason !== '' - ? sprintf(__('用户取消售后:%s', 'aksrt'), sanitize_text_field($reason)) - : __('用户取消售后', 'aksrt'); - $this->append_event($ticket_id, 'user_cancelled', $msg, $user_id); - return true; - } - - /** - * @return list - */ - public function get_events(int $ticket_id): array { - $events = get_post_meta($ticket_id, '_aksrt_as_events', true); - return is_array($events) ? array_values($events) : []; - } - - public function append_event(int $ticket_id, string $event, string $label, int $user_id = 0): void { - $events = $this->get_events($ticket_id); - $events[] = [ - 'time' => current_time('mysql'), - 'event' => sanitize_key($event), - 'label' => sanitize_text_field($label), - 'user_id' => $user_id, - ]; - // Keep last 50 events. - if (count($events) > 50) { - $events = array_slice($events, -50); - } - update_post_meta($ticket_id, '_aksrt_as_events', $events); - } - - /** - * List tickets for a frontend user. - */ - public function get_user_tickets(int $user_id, int $limit = 50): array { - if ($user_id <= 0) { - return []; - } - global $wpdb; - $rows = $wpdb->get_results($wpdb->prepare( - "SELECT * FROM {$this->table()} WHERE user_id = %d ORDER BY created_at DESC LIMIT %d", - $user_id, - max(1, min(100, $limit)) - ), ARRAY_A); - $items = []; - foreach ((array) $rows as $row) { - $items[] = $this->decorate_user_ticket($row); - } - return $items; - } - - /** - * @param array $row - * @return array - */ - public function decorate_user_ticket(array $row): array { - $id = (int) ($row['id'] ?? 0); - $decorated = method_exists($this, 'decorate_record') ? $this->decorate_record($row) : $row; - $progress = (int) get_post_meta($id, '_aksrt_as_progress', true); - $labels = self::get_progress_labels(); - $return = get_post_meta($id, '_aksrt_as_return_shipping', true); - $status = (string) ($row['status'] ?? ''); - $type = (string) ($row['type'] ?? ''); - $needs_ship = in_array($type, [self::TYPE_REFUND_RETURN, self::TYPE_EXCHANGE, self::TYPE_REPLACEMENT], true) - && $status === self::STATUS_PROCESSING - && $progress === self::PROGRESS_WAITING_USER_SHIP; - - $decorated['id'] = $id; - $decorated['progress'] = $progress; - $decorated['progress_label'] = $labels[$progress] ?? ''; - $decorated['return_shipping'] = is_array($return) ? $return : null; - $decorated['events'] = $this->get_events($id); - $decorated['actions'] = [ - 'can_cancel' => in_array($status, [self::STATUS_PENDING, self::STATUS_PROCESSING], true), - 'can_return_ship' => $needs_ship, - ]; - return $decorated; - } - - public function merchant_confirm_return(int $ticket_id, int $operator_id, string $note = ''): bool { - global $wpdb; - $record = $this->get_by_id($ticket_id); - if (!$record || $record['status'] !== self::STATUS_PROCESSING) { - return false; - } - - $type = (string) $record['type']; - - if (in_array($type, [self::TYPE_EXCHANGE, self::TYPE_REPLACEMENT], true)) { - update_post_meta($ticket_id, '_aksrt_as_progress', self::PROGRESS_WAITING_USER_RECEIVE); - $wpdb->update($this->table(), [ - 'operator_note' => sanitize_textarea_field($note), - 'updated_at' => current_time('mysql'), - ], ['id' => $ticket_id]); - return true; - } - - if (!$this->complete_refund($record)) { - return false; - } - - $updated = (bool) $wpdb->update($this->table(), [ - 'status' => self::STATUS_COMPLETED, - 'operator_id' => $operator_id, - 'operator_note' => sanitize_textarea_field($note), - 'updated_at' => current_time('mysql'), - 'resolved_at' => current_time('mysql'), - ], ['id' => $ticket_id]); - if (!$updated) { - return false; - } - - update_post_meta($ticket_id, '_aksrt_as_progress', self::PROGRESS_DONE); - $this->sync_order_status($record, self::STATUS_COMPLETED, $note ?: __('售后已完成', 'aksrt')); - return true; - } - - /* ============================================================ - * Shipping status window + type constraints (§6.8) - * ============================================================ */ - - private function is_refund_type(string $type): bool { - return in_array($type, [ - self::TYPE_REFUND, - self::TYPE_REFUND_RETURN, - self::TYPE_INSURED_PRICE, - ], true); - } - - private function within_application_window(array $order, string $type): bool { - $days = (int) ($this->get_type_timeouts()[$type] ?? 0); - if ($days <= 0) { - return true; - } - - $base = (string) ($order['completed_at'] - ?? $order['delivered_at'] - ?? $order['paid_at'] - ?? $order['created_at'] - ?? ''); - $timestamp = $base !== '' ? strtotime($base) : false; - if ($timestamp === false) { - return true; - } - - return $timestamp >= current_time('timestamp') - ($days * DAY_IN_SECONDS); - } - - public function can_apply_after_sale(array $order, string $type): bool { - $status = (string) ($order['status'] ?? ''); - if (!in_array($status, [ - Order::STATUS_PAID, - Order::STATUS_DELIVERED, - Order::STATUS_COMPLETED, - Order::STATUS_PENDING_REVIEW, - ], true)) { - return false; - } - - $shipping_status = (int) ($order['shipping_status'] ?? 0); - - if ($type === self::TYPE_REFUND) { - return true; - } - - if (in_array($type, [self::TYPE_REFUND_RETURN, self::TYPE_EXCHANGE, self::TYPE_REPLACEMENT], true)) { - return $shipping_status >= \AKSRT\Core\DomainModel::SHIPPING_STATUS_SHIPPED; - } - - if ($type === self::TYPE_WARRANTY) { - return $shipping_status >= \AKSRT\Core\DomainModel::SHIPPING_STATUS_COMPLETED; - } - - return true; - } - - public function get_type_timeouts(): array { - $shop = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_shop_settings() : []; - $timing = $shop['timing'] ?? []; - return [ - self::TYPE_REFUND => (int) ($timing['refund_max_day'] ?? 7), - self::TYPE_REFUND_RETURN => (int) ($timing['refund_return_max_day'] ?? 7), - self::TYPE_EXCHANGE => (int) ($timing['exchange_max_day'] ?? 7), - self::TYPE_REPLACEMENT => (int) ($timing['replacement_max_day'] ?? 7), - self::TYPE_WARRANTY => (int) ($timing['warranty_max_day'] ?? 30), - self::TYPE_INSURED_PRICE => (int) ($timing['insured_price_max_day'] ?? 15), - ]; - } - - private function restore_order_after_cancel(array $record): void { - if (!$this->is_refund_type((string) ($record['type'] ?? '')) - || !class_exists('AKSRT\\Payment\\OrderRepository')) { - return; - } - - $order_number = sanitize_text_field((string) ($record['order_number'] ?? '')); - $order = OrderRepository::get_instance()->get($order_number); - if (!$order || ($order['status'] ?? '') !== Order::STATUS_REFUNDING) { - return; - } - - $previous = sanitize_key((string) ($record['order_status_before'] ?? '')); - if (!in_array($previous, [ - Order::STATUS_PAID, - Order::STATUS_DELIVERED, - Order::STATUS_COMPLETED, - Order::STATUS_PENDING_REVIEW, - ], true)) { - $previous = Order::STATUS_PAID; - } - OrderRepository::get_instance()->update_status($order_number, $previous, [ - 'force' => true, - 'note' => __('售后申请已取消,恢复订单状态。', 'aksrt'), - ]); - } - - public function reconcile_pending_refunds(): void { - global $wpdb; - - $submitting_cutoff = date('Y-m-d H:i:s', current_time('timestamp') - (15 * MINUTE_IN_SECONDS)); - $rows = $wpdb->get_results( - $wpdb->prepare( - "SELECT * FROM {$this->table()} - WHERE ( - provider_status IN ('pending', 'unknown') - OR (provider_status = 'submitting' AND provider_updated_at < %s) - OR (provider_status = 'succeeded' AND refund_effect_status = 'processing' AND updated_at < %s) - ) - AND status IN ('processing', 'approved') - ORDER BY provider_updated_at ASC - LIMIT 50", - $submitting_cutoff, - $submitting_cutoff - ), - ARRAY_A - ); - foreach ((array) $rows as $record) { - $provider_status = (string) ($record['provider_status'] ?? ''); - $effect_status = (string) ($record['refund_effect_status'] ?? ''); - if ($provider_status === 'succeeded' && $effect_status === 'processing') { - $wpdb->update($this->table(), [ - 'refund_effect_status' => 'failed', - 'refund_error' => __('本地退款效果状态不确定,请人工核对余额或积分流水后重试。', 'aksrt'), - 'updated_at' => current_time('mysql'), - ], ['id' => (int) $record['id']]); - continue; - } - - $provider = sanitize_key((string) ($record['provider'] ?? '')); - $request_id = (string) ($record['refund_request_id'] ?? ''); - $gateway = Gateway::get_instance()->get_gateway($provider); - if (!$gateway || $request_id === '' || !method_exists($gateway, 'query_refund')) { - continue; - } - - try { - $result = $gateway->query_refund( - $request_id, - (string) ($record['order_number'] ?? '') - ); - } catch (\Throwable $e) { - continue; - } - $status = sanitize_key((string) ($result['status'] ?? 'unknown')); - if (!in_array($status, ['pending', 'succeeded', 'failed', 'unknown'], true)) { - $status = 'unknown'; - } - $wpdb->update($this->table(), [ - 'provider_status' => $status, - 'provider_refund_id' => sanitize_text_field((string) ($result['refund_id'] ?? $record['provider_refund_id'] ?? '')), - 'refund_error' => $status === 'failed' ? sanitize_textarea_field((string) ($result['message'] ?? '')) : null, - 'provider_updated_at' => current_time('mysql'), - 'refund_succeeded_at' => $status === 'succeeded' ? current_time('mysql') : null, - 'updated_at' => current_time('mysql'), - ], ['id' => (int) $record['id']]); - - if ($status === 'unknown' - && (int) ($record['refund_attempts'] ?? 0) < 3 - && in_array($provider_status, ['submitting', 'unknown'], true)) { - $wpdb->update($this->table(), [ - 'provider_status' => 'failed', - 'provider_updated_at' => current_time('mysql'), - 'refund_error' => __('未查询到退款记录,将使用原请求号幂等重试。', 'aksrt'), - 'updated_at' => current_time('mysql'), - ], ['id' => (int) $record['id']]); - $fresh = $this->get_by_id((int) $record['id']); - if ($fresh) { - $this->complete_refund($fresh); - } - continue; - } - - if ($status === 'succeeded') { - $fresh = $this->get_by_id((int) $record['id']); - if ($fresh && $this->complete_refund($fresh)) { - $this->update_status( - (int) $record['id'], - self::STATUS_COMPLETED, - 0, - __('系统对账确认退款成功。', 'aksrt') - ); - } - } - } - } - - /* ============================================================ - * Auto-cancel Cron (§6.8) - * ============================================================ */ - - public function run_auto_cancel_cron(): void { - global $wpdb; - $table = $this->table(); - - $timeouts = $this->get_type_timeouts(); - - foreach ($timeouts as $type => $max_days) { - if ($max_days <= 0) continue; - - $cutoff = date('Y-m-d H:i:s', strtotime("-{$max_days} days")); - - $stale = $wpdb->get_results($wpdb->prepare( - "SELECT id, order_number, type, status FROM {$table} WHERE type = %s AND status IN (%s, %s) AND created_at < %s LIMIT 200", - $type, self::STATUS_PENDING, self::STATUS_PROCESSING, $cutoff - ), ARRAY_A); - - foreach ($stale as $record) { - $wpdb->update($table, [ - 'status' => self::STATUS_CANCELLED, - 'updated_at' => current_time('mysql'), - 'operator_note' => sprintf(__('系统自动取消(超过 %d 天未处理)', 'aksrt'), $max_days), - ], ['id' => (int) $record['id']]); - } - } - } - - private function get_by_id(int $id): ?array { - global $wpdb; - $row = $wpdb->get_row($wpdb->prepare( - "SELECT * FROM {$this->table()} WHERE id = %d", - $id - ), ARRAY_A); - return $row ?: null; - } - - public static function get_valid_types(): array { - return [ - self::TYPE_REFUND => __('仅退款', 'aksrt'), - self::TYPE_REFUND_RETURN => __('退货退款', 'aksrt'), - self::TYPE_EXCHANGE => __('换货', 'aksrt'), - self::TYPE_REPAIR => __('维修', 'aksrt'), - self::TYPE_REPLACEMENT => __('补发', 'aksrt'), - self::TYPE_WARRANTY => __('保修', 'aksrt'), - self::TYPE_INSURED_PRICE => __('保价', 'aksrt'), - ]; - } - - public static function get_progress_labels(): array { - return [ - self::PROGRESS_WAITING_USER_SHIP => __('等待用户发货', 'aksrt'), - self::PROGRESS_WAITING_MERCHANT => __('等待商家处理', 'aksrt'), - self::PROGRESS_WAITING_USER_RECEIVE => __('等待用户收货', 'aksrt'), - self::PROGRESS_DONE => __('已完成', 'aksrt'), - ]; - } -} diff --git a/src/AKSRT/Payment/Coupon.php b/src/AKSRT/Payment/Coupon.php index be1e85c0..91fbc20b 100644 --- a/src/AKSRT/Payment/Coupon.php +++ b/src/AKSRT/Payment/Coupon.php @@ -159,7 +159,7 @@ class Coupon { } // Product / content scope check. - // Supports shop product cats and content post cats. + // Supports product and content post cats. if (!empty($coupon['product_scope']) && !empty($args['product_ids'])) { $scope = is_array($coupon['product_scope']) ? $coupon['product_scope'] : json_decode($coupon['product_scope'], true); if (is_array($scope) && !empty($scope)) { @@ -168,11 +168,7 @@ class Coupon { $product_ids = array_map('intval', (array) $args['product_ids']); $taxonomies = array_values(array_filter(array_map('sanitize_key', (array) ($scope['taxonomies'] ?? [])))); if ($taxonomies === []) { - // content context prefers category; shop admin may pass aksrt_product_cat. - $context = sanitize_key((string) ($args['context'] ?? '')); - $taxonomies = ($context === 'content') - ? ['category'] - : ['aksrt_product_cat', 'category']; + $taxonomies = ['aksrt_product_cat', 'category']; } if ($scope_type === 'include' && empty(array_intersect($product_ids, $scope_ids))) { diff --git a/src/AKSRT/Payment/Discount.php b/src/AKSRT/Payment/Discount.php deleted file mode 100644 index e0d8c55b..00000000 --- a/src/AKSRT/Payment/Discount.php +++ /dev/null @@ -1,167 +0,0 @@ - Sorted by priority desc. - */ - public function get_active_discounts(int $product_id): array { - $terms = wp_get_object_terms($product_id, 'shop_discount', ['fields' => 'all']); - if (is_wp_error($terms) || empty($terms)) { - return []; - } - - $now = current_time('timestamp'); - $active = []; - - foreach ($terms as $term) { - $meta = \AKSRT\Shop\TermMeta::get_discount_meta($term->term_id); - - // Time limit check - if (!empty($meta['start']) && strtotime($meta['start']) > $now) { - continue; - } - if (!empty($meta['end']) && strtotime($meta['end']) < $now) { - continue; - } - - $meta['term_id'] = $term->term_id; - $meta['name'] = $term->name; - $active[] = $meta; - } - - // Sort by priority desc - usort($active, function ($a, $b) { - return ((int) $b['priority']) - ((int) $a['priority']); - }); - - return $active; - } - - /** - * Calculate discount for a single line item. - * - * @param float $unit_price Unit price - * @param int $quantity Quantity - * @param int $product_id Product ID - * @param int $user_id User ID (for user_limit checks) - * @return array{ - * original: float, - * discount_amount: float, - * final_price: float, - * gifts: array, - * applied: array - * } - */ - public function calculate(float $unit_price, int $quantity, int $product_id, int $user_id = 0): array { - $original = $unit_price * $quantity; - $discount_amount = 0.0; - $gifts = []; - $applied = []; - - $discounts = $this->get_active_discounts($product_id); - - foreach ($discounts as $d) { - // User limit check - if (!empty($d['user_limit']) && $user_id > 0) { - if (!$this->check_user_limit($d['user_limit'], $user_id)) { - continue; - } - } - - // Price limit check - if ((float) $d['price_limit'] > 0 && $original < (float) $d['price_limit']) { - continue; - } - - $type = $d['type'] ?? 'reduction'; - $value = (float) ($d['value'] ?? 0); - - switch ($type) { - case 'reduction': - // 立减 - $discount_amount += min($value * $quantity, $original - $discount_amount); - break; - - case 'discount': - // 打折 (value = 折扣系数 e.g. 8.5 = 85折) - if ($value > 0 && $value < 10) { - $rate = $value / 10.0; - $save = ($original - $discount_amount) * (1 - $rate); - $discount_amount += $save; - } - break; - - case 'gift': - // 买赠 — collect gift config, no price reduction - if (!empty($d['gift_config'])) { - $gift_data = json_decode($d['gift_config'], true); - if (is_array($gift_data)) { - $gifts[] = $gift_data; - } - } - break; - } - - $applied[] = [ - 'term_id' => $d['term_id'], - 'name' => $d['name'] ?? '', - 'type' => $type, - 'value' => $value, - ]; - } - - $discount_amount = min($discount_amount, $original); - - return [ - 'original' => $original, - 'discount_amount' => round($discount_amount, 2), - 'final_price' => round($original - $discount_amount, 2), - 'gifts' => $gifts, - 'applied' => $applied, - ]; - } - - /** - * Check if a user meets the user_limit requirement. - * - * @param string $limit 'vip' / 'vip_2' / 'auth' - * @param int $user_id - * @return bool - */ - private function check_user_limit(string $limit, int $user_id): bool { - if (empty($limit)) { - return true; - } - - switch ($limit) { - case 'vip': - return function_exists('aksrt_is_vip') ? aksrt_is_vip($user_id) : false; - case 'vip_2': - return function_exists('aksrt_is_vip2') ? aksrt_is_vip2($user_id) : false; - case 'auth': - return function_exists('aksrt_is_verified') ? aksrt_is_verified($user_id) : false; - default: - return true; - } - } -} diff --git a/src/AKSRT/Payment/OrderCenterRest.php b/src/AKSRT/Payment/OrderCenterRest.php index 1fbac6a1..2fb736e6 100644 --- a/src/AKSRT/Payment/OrderCenterRest.php +++ b/src/AKSRT/Payment/OrderCenterRest.php @@ -81,17 +81,10 @@ class OrderCenterRest { } $payment_method = sanitize_key((string) ($params['payment_method'] ?? '')); - if (class_exists(UserOrders::class)) { - $result = UserOrders::get_instance()->pay_order(get_current_user_id(), $order_number, $payment_method); - } elseif (class_exists('\AKSRT\Shop\Shop')) { - $result = \AKSRT\Shop\Shop::get_instance()->pay_order( - get_current_user_id(), - $order_number, - $payment_method - ); - } else { + if (!class_exists(UserOrders::class)) { return $this->error(__('订单服务不可用', 'aksrt'), 500); - } + } + $result = UserOrders::get_instance()->pay_order(get_current_user_id(), $order_number, $payment_method); if (empty($result['success'])) { return $this->error((string) ($result['message'] ?? __('支付失败', 'aksrt')), 400); @@ -103,13 +96,10 @@ class OrderCenterRest { public function cancel(\WP_REST_Request $request): \WP_REST_Response { $order_number = sanitize_text_field((string) $request->get_param('order_number')); - if (class_exists(UserOrders::class)) { - $result = UserOrders::get_instance()->cancel_order(get_current_user_id(), $order_number); - } elseif (class_exists('\AKSRT\Shop\Shop')) { - $result = \AKSRT\Shop\Shop::get_instance()->cancel_order(get_current_user_id(), $order_number); - } else { + if (!class_exists(UserOrders::class)) { return $this->error(__('订单服务不可用', 'aksrt'), 500); - } + } + $result = UserOrders::get_instance()->cancel_order(get_current_user_id(), $order_number); if (empty($result['success'])) { return $this->error((string) ($result['message'] ?? __('取消订单失败', 'aksrt')), 400); @@ -122,11 +112,10 @@ class OrderCenterRest { $order_number = sanitize_text_field((string) $request->get_param('order_number')); $order = null; - if (class_exists(UserOrders::class)) { - $order = UserOrders::get_instance()->get_order_detail_view(get_current_user_id(), $order_number); - } elseif (class_exists('\AKSRT\Shop\Shop')) { - $order = \AKSRT\Shop\Shop::get_instance()->get_order_detail_view(get_current_user_id(), $order_number); - } + if (!class_exists(UserOrders::class)) { + return $this->error(__('订单服务不可用', 'aksrt'), 500); + } + $order = UserOrders::get_instance()->get_order_detail_view(get_current_user_id(), $order_number); if (!$order) { return $this->error(__('订单不存在', 'aksrt'), 404); diff --git a/src/AKSRT/Payment/OrderRepositoryAdmin.php b/src/AKSRT/Payment/OrderRepositoryAdmin.php index ef590752..eda12da1 100644 --- a/src/AKSRT/Payment/OrderRepositoryAdmin.php +++ b/src/AKSRT/Payment/OrderRepositoryAdmin.php @@ -63,18 +63,9 @@ trait OrderRepositoryAdmin { } $resolved_order_numbers = array_keys($orders); - $shipping_map = class_exists('AKSRT\\Payment\\Shipping') - ? Shipping::get_instance()->get_summary_map($resolved_order_numbers) - : []; - $after_sale_map = class_exists('AKSRT\\Payment\\AfterSale') - ? AfterSale::get_instance()->get_summary_map($resolved_order_numbers) - : []; foreach ($orders as $order_number => $order) { - $contexts[$order_number] = $this->build_admin_order_context($order, [ - 'shipping_summary' => $shipping_map[$order_number] ?? [], - 'after_sale_summary' => $after_sale_map[$order_number] ?? [], - ]); + $contexts[$order_number] = $this->build_admin_order_context($order, []); } return $contexts; @@ -130,64 +121,6 @@ trait OrderRepositoryAdmin { /** * 管理后台订单列表 — 支持 search(order_number / product_type 模糊匹配) */ - /** - * Paid orders that still need shipping (no shipping row yet). - * - * @return array{items: array>, total: int, page: int, max_pages: int} - */ - public function get_pending_shipment_orders(array $args = []): array { - global $wpdb; - - $limit = max(1, min(100, (int) ($args['limit'] ?? 20))); - $offset = max(0, (int) ($args['offset'] ?? 0)); - $search = sanitize_text_field((string) ($args['search'] ?? $args['s'] ?? '')); - - $order_table = $this->table(); - // Must match Shipping::table() key. - $shipping_table = class_exists('\AKSRT\Core\DBMigrator') - ? \AKSRT\Core\DBMigrator::table('order_shipping') - : $wpdb->prefix . 'aksrt_order_shipping'; - - $where = ["o.status = %s"]; - $params = ['paid']; - - if ($search !== '') { - $where[] = '(o.order_number LIKE %s OR o.product_type LIKE %s)'; - $like = '%' . $wpdb->esc_like($search) . '%'; - $params[] = $like; - $params[] = $like; - } - - // NOT EXISTS avoids ONLY_FULL_GROUP_BY issues from LEFT JOIN + GROUP BY. - $where[] = "NOT EXISTS ( - SELECT 1 FROM {$shipping_table} s - WHERE s.order_number = o.order_number - )"; - $where_sql = 'WHERE ' . implode(' AND ', $where); - - $count_sql = "SELECT COUNT(*) FROM {$order_table} o {$where_sql}"; - $total = (int) $wpdb->get_var($wpdb->prepare($count_sql, $params)); - - $sql = "SELECT o.* - FROM {$order_table} o - {$where_sql} - ORDER BY COALESCE(o.paid_at, o.created_at) DESC, o.id DESC - LIMIT %d OFFSET %d"; - $query_params = $params; - $query_params[] = $limit; - $query_params[] = $offset; - $rows = $wpdb->get_results($wpdb->prepare($sql, $query_params), ARRAY_A); - $items = array_map([$this, 'normalize_row'], (array) $rows); - - return [ - 'items' => $items, - 'total' => $total, - 'page' => (int) floor($offset / $limit) + 1, - 'max_pages' => (int) ceil(max(1, $total) / $limit), - ]; - } - - public function get_orders_admin(array $args = [], string $search = ''): array { global $wpdb; @@ -499,8 +432,6 @@ trait OrderRepositoryAdmin { $discount = (float) ($meta['discount'] ?? 0); $amount = (float) ($order['amount'] ?? 0); $subtotal = $this->calculate_order_subtotal($items, max(0, $amount + $discount - $shipping_fee)); - $shipping_summary = is_array($related['shipping_summary'] ?? null) ? $related['shipping_summary'] : []; - $after_sale_summary = is_array($related['after_sale_summary'] ?? null) ? $related['after_sale_summary'] : []; $address['full_text'] = $this->build_address_text($address); @@ -534,12 +465,12 @@ trait OrderRepositoryAdmin { 'fee' => $shipping_fee, ], 'shipping' => [ - 'count' => (int) ($shipping_summary['count'] ?? 0), - 'latest' => is_array($shipping_summary['latest'] ?? null) ? $shipping_summary['latest'] : [], + 'count' => 0, + 'latest' => [], ], 'after_sale' => [ - 'count' => (int) ($after_sale_summary['count'] ?? 0), - 'latest' => is_array($after_sale_summary['latest'] ?? null) ? $after_sale_summary['latest'] : [], + 'count' => 0, + 'latest' => [], ], 'coupon' => $coupon, 'pricing' => [ diff --git a/src/AKSRT/Payment/Shipping.php b/src/AKSRT/Payment/Shipping.php deleted file mode 100644 index f35665c3..00000000 --- a/src/AKSRT/Payment/Shipping.php +++ /dev/null @@ -1,371 +0,0 @@ -insert($this->table(), [ - 'order_number' => $order_number, - 'carrier' => $carrier, - 'tracking_no' => $tracking_no, - 'status' => self::STATUS_SHIPPED, - 'note' => $note, - 'operator_id' => $operator_id ?: null, - 'created_at' => current_time('mysql'), - ]); - - $insert_id = $result ? (int) $wpdb->insert_id : 0; - - if ($insert_id > 0) { - $this->sync_order_status($order_number, self::STATUS_SHIPPED, sprintf( - __('发货记录已创建:%1$s %2$s', 'aksrt'), - $this->get_carrier_label($carrier), - $tracking_no - )); - - // R68: Email notification on shipping creation (§6.6) - $this->send_shipping_notification($order_number, $carrier, $tracking_no, $note); - } - - return $insert_id; - } - - /** - * 获取发货记录列表 - */ - public function get_list(array $args = []): array { - global $wpdb; - - $limit = max(1, min(100, (int) ($args['limit'] ?? 20))); - $offset = max(0, (int) ($args['offset'] ?? 0)); - $include_order_context = !empty($args['include_order_context']); - $status = sanitize_key((string) ($args['status'] ?? '')); - $order_number = sanitize_text_field((string) ($args['order_number'] ?? '')); - $date_from = sanitize_text_field((string) ($args['date_from'] ?? '')); - $date_to = sanitize_text_field((string) ($args['date_to'] ?? '')); - $tracking_no = sanitize_text_field((string) ($args['tracking_no'] ?? '')); - - $where = []; - $params = []; - - if ($status !== '') { - $where[] = 'status = %s'; - $params[] = $status; - } - if ($order_number !== '') { - $where[] = 'order_number = %s'; - $params[] = $order_number; - } - if ($tracking_no !== '') { - $where[] = 'tracking_no LIKE %s'; - $params[] = '%' . $wpdb->esc_like($tracking_no) . '%'; - } - if ($date_from !== '') { - $where[] = 'created_at >= %s'; - $params[] = $date_from . ' 00:00:00'; - } - if ($date_to !== '') { - $where[] = 'created_at <= %s'; - $params[] = $date_to . ' 23:59:59'; - } - - $where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; - - $count_sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}"; - $total = $params ? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params)) : (int) $wpdb->get_var($count_sql); - - $sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d"; - $params[] = $limit; - $params[] = $offset; - - $items = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A); - $items = array_map([$this, 'decorate_record'], (array) $items); - - if ($include_order_context && !empty($items) && class_exists('AKSRT\\Payment\\OrderRepository')) { - $contexts = OrderRepository::get_instance()->get_admin_order_context_map(array_column($items, 'order_number')); - foreach ($items as &$item) { - $item['order_context'] = $contexts[(string) ($item['order_number'] ?? '')] ?? []; - } - unset($item); - } - - return [ - 'items' => $items ?: [], - 'total' => $total, - 'page' => floor($offset / $limit) + 1, - 'max_pages' => ceil($total / $limit), - ]; - } - - /** - * 获取单个发货记录 - */ - public function get(int $id): ?array { - global $wpdb; - $row = $wpdb->get_row($wpdb->prepare( - "SELECT * FROM {$this->table()} WHERE id = %d", - $id - ), ARRAY_A); - return $row ? $this->decorate_record($row) : null; - } - - /** - * 获取订单的发货记录 - */ - public function get_by_order(string $order_number): array { - global $wpdb; - $items = $wpdb->get_results($wpdb->prepare( - "SELECT * FROM {$this->table()} WHERE order_number = %s ORDER BY created_at DESC", - $order_number - ), ARRAY_A); - return array_map([$this, 'decorate_record'], (array) $items); - } - - /** - * 获取单个发货记录并附带订单上下文。 - */ - public function get_with_order_context(int $id): ?array { - $record = $this->get($id); - if (!$record) { - return null; - } - - $record['order_context'] = class_exists('AKSRT\\Payment\\OrderRepository') - ? OrderRepository::get_instance()->get_admin_order_context((string) ($record['order_number'] ?? '')) - : []; - - return $record; - } - - /** - * @param array $order_numbers - * @return array> - */ - public function get_summary_map(array $order_numbers): array { - global $wpdb; - - $order_numbers = array_values(array_unique(array_filter(array_map( - static function ($order_number): string { - return sanitize_text_field((string) $order_number); - }, - $order_numbers - )))); - - if (empty($order_numbers)) { - return []; - } - - $map = []; - foreach ($order_numbers as $order_number) { - $map[$order_number] = [ - 'count' => 0, - 'latest' => [], - ]; - } - - $placeholders = implode(', ', array_fill(0, count($order_numbers), '%s')); - $sql = "SELECT * FROM {$this->table()} WHERE order_number IN ({$placeholders}) ORDER BY created_at DESC, id DESC"; - $rows = $wpdb->get_results($wpdb->prepare($sql, $order_numbers), ARRAY_A); - - foreach ((array) $rows as $row) { - $record = $this->decorate_record($row); - $order_number = (string) ($record['order_number'] ?? ''); - - if (!isset($map[$order_number])) { - continue; - } - - $map[$order_number]['count']++; - if (empty($map[$order_number]['latest'])) { - $map[$order_number]['latest'] = $this->build_summary_item($record); - } - } - - return $map; - } - - public function batch_update_status(array $ids, string $new_status, string $note = '', int $operator_id = 0): array { - $ids = array_values(array_unique(array_filter(array_map('intval', $ids)))); - $result = [ - 'updated_ids' => [], - 'failed' => [], - ]; - - foreach ($ids as $id) { - $record = $this->get($id); - if (!$record) { - $result['failed'][] = [ - 'id' => $id, - 'reason' => __('发货记录不存在', 'aksrt'), - ]; - continue; - } - - if (!$this->can_transition_status((string) ($record['status'] ?? ''), $new_status)) { - $result['failed'][] = [ - 'id' => $id, - 'reason' => __('当前状态不允许批量更新到目标状态', 'aksrt'), - ]; - continue; - } - - if ($this->update_status($id, $new_status, $note, $operator_id)) { - $result['updated_ids'][] = $id; - } else { - $result['failed'][] = [ - 'id' => $id, - 'reason' => __('状态更新失败', 'aksrt'), - ]; - } - } - - return $result; - } - - /** - * 更新发货状态 - */ - public function update_status(int $id, string $new_status, string $note = '', int $operator_id = 0): bool { - global $wpdb; - - $record = $this->get($id); - if (!$record) { - return false; - } - - $valid = [self::STATUS_SHIPPED, self::STATUS_IN_TRANSIT, self::STATUS_DELIVERED, self::STATUS_RETURNED]; - if (!in_array($new_status, $valid, true)) return false; - if (!$this->can_transition_status((string) ($record['status'] ?? ''), $new_status)) { - return false; - } - - $update = [ - 'status' => $new_status, - 'updated_at' => current_time('mysql'), - ]; - - if ($note !== '') { - $update['note'] = sanitize_textarea_field($note); - } - if ($operator_id > 0) { - $update['operator_id'] = $operator_id; - } - if ($new_status === self::STATUS_DELIVERED) { - $update['delivered_at'] = current_time('mysql'); - } - - $updated = (bool) $wpdb->update($this->table(), $update, ['id' => $id]); - if ($updated) { - $this->sync_order_status( - (string) ($record['order_number'] ?? ''), - $new_status, - $note !== '' ? $note : sprintf(__('物流状态更新为:%s', 'aksrt'), $this->get_status_label($new_status)) - ); - } - - return $updated; - } - - /** - * 获取统计 - */ - public function get_stats(): array { - global $wpdb; - $table = $this->table(); - - $counts = $wpdb->get_results( - "SELECT status, COUNT(*) as cnt FROM {$table} GROUP BY status", - ARRAY_A - ); - $counts = array_column($counts, 'cnt', 'status'); - - $stats = [ - 'total' => 0, - 'shipped' => 0, - 'in_transit' => 0, - 'delivered' => 0, - 'returned' => 0, - ]; - - foreach ($counts as $status => $cnt) { - if (isset($stats[$status])) { - $stats[$status] = (int) $cnt; - } - $stats['total'] += (int) $cnt; - } - - return $stats; - } - - /** - * 获取常用快递公司列表 - */ - public static function get_carrier_options(): array { - return [ - ['value' => 'sf', 'label' => '顺丰速运'], - ['value' => 'yd', 'label' => '韵达快递'], - ['value' => 'zt', 'label' => '中通快递'], - ['value' => 'yt', 'label' => '圆通速递'], - ['value' => 'sto', 'label' => '申通快递'], - ['value' => 'jd', 'label' => '京东物流'], - ['value' => 'ems', 'label' => 'EMS'], - ['value' => 'db', 'label' => '德邦快递'], - ['value' => 'yunda', 'label' => '韵达快递'], - ['value' => 'other', 'label' => '其他'], - ]; - } - public static function get_status_options(): array { - return [ - ['value' => self::STATUS_SHIPPED, 'label' => __('已发货', 'aksrt')], - ['value' => self::STATUS_IN_TRANSIT, 'label' => __('运输中', 'aksrt')], - ['value' => self::STATUS_DELIVERED, 'label' => __('已签收', 'aksrt')], - ['value' => self::STATUS_RETURNED, 'label' => __('已退回', 'aksrt')], - ]; - } - - /** - * Send email notification to order customer when shipping record is created. - */ -} diff --git a/src/AKSRT/Payment/ShippingPart1.php b/src/AKSRT/Payment/ShippingPart1.php deleted file mode 100644 index 30903f27..00000000 --- a/src/AKSRT/Payment/ShippingPart1.php +++ /dev/null @@ -1,371 +0,0 @@ -get($order_number); - if (!$order) { - return; - } - - $user_id = (int) ($order['user_id'] ?? 0); - if ($user_id <= 0) { - return; - } - - $user = get_userdata($user_id); - if (!$user || empty($user->user_email)) { - return; - } - - $carrier_label = $this->get_carrier_label($carrier); - $site_name = get_bloginfo('name'); - $subject = sprintf(__('[%s] 您的订单已发货', 'aksrt'), $site_name); - $body = sprintf( - __('您好 %1$s,\n\n您的订单 %2$s 已发货。\n\n物流公司:%3$s\n运单号:%4$s\n%5$s\n\n如有疑问请联系客服。\n\n%s', 'aksrt'), - $user->display_name, - $order_number, - $carrier_label, - $tracking_no, - $note !== '' ? sprintf(__('备注:%s', 'aksrt'), $note) : '', - $site_name - ); - - wp_mail($user->user_email, $subject, $body); - } - - private function decorate_record(array $record): array { - $record['id'] = (int) ($record['id'] ?? 0); - $record['operator_id'] = isset($record['operator_id']) ? (int) $record['operator_id'] : 0; - $record['status_label'] = $this->get_status_label((string) ($record['status'] ?? '')); - $record['carrier_label'] = $this->get_carrier_label((string) ($record['carrier'] ?? '')); - $record['operator'] = $this->get_user_summary((int) ($record['operator_id'] ?? 0)); - return $record; - } - - private function get_carrier_label(string $carrier): string { - foreach (self::get_carrier_options() as $item) { - if (($item['value'] ?? '') === $carrier) { - return (string) ($item['label'] ?? $carrier); - } - } - - return $carrier; - } - - private function get_status_label(string $status): string { - foreach (self::get_status_options() as $item) { - if (($item['value'] ?? '') === $status) { - return (string) ($item['label'] ?? $status); - } - } - - return $status; - } - - private function get_user_summary(int $user_id): array { - if ($user_id <= 0) { - return []; - } - - $user = get_userdata($user_id); - if (!$user) { - return []; - } - - return [ - 'id' => $user_id, - 'display_name' => (string) $user->display_name, - 'user_login' => (string) $user->user_login, - ]; - } - - private function build_summary_item(array $record): array { - return [ - 'id' => (int) ($record['id'] ?? 0), - 'status' => (string) ($record['status'] ?? ''), - 'status_label' => (string) ($record['status_label'] ?? ''), - 'carrier' => (string) ($record['carrier'] ?? ''), - 'carrier_label' => (string) ($record['carrier_label'] ?? ''), - 'tracking_no' => (string) ($record['tracking_no'] ?? ''), - 'created_at' => (string) ($record['created_at'] ?? ''), - 'updated_at' => (string) ($record['updated_at'] ?? ''), - 'delivered_at' => (string) ($record['delivered_at'] ?? ''), - ]; - } - - private function can_transition_status(string $current_status, string $new_status): bool { - $map = [ - self::STATUS_SHIPPED => [self::STATUS_IN_TRANSIT, self::STATUS_DELIVERED, self::STATUS_RETURNED], - self::STATUS_IN_TRANSIT => [self::STATUS_DELIVERED, self::STATUS_RETURNED], - self::STATUS_DELIVERED => [], - self::STATUS_RETURNED => [], - ]; - - return in_array($new_status, $map[$current_status] ?? [], true); - } - - private function sync_order_status(string $order_number, string $shipping_status, string $note = ''): void { - if ($order_number === '' || !class_exists('AKSRT\\Payment\\OrderRepository') || !class_exists('AKSRT\\Payment\\Order')) { - return; - } - - if (!in_array($shipping_status, [self::STATUS_SHIPPED, self::STATUS_IN_TRANSIT, self::STATUS_DELIVERED], true)) { - return; - } - - OrderRepository::get_instance()->update_status($order_number, Order::STATUS_DELIVERED, [ - 'note' => $note, - ]); - } - - /* ============================================================ - * Express Query API (§6.6 快递查询) - * - * Supports kuaidi100, aliyun, kdniao. - * Traces cached in order_shipping.express_data JSON. - * Refresh interval from shop settings. - * ============================================================ */ - - public function query_express(int $shipping_id, bool $force = false): array { - global $wpdb; - $record = $this->get($shipping_id); - if (!$record) { - return ['success' => false, 'message' => __('发货记录不存在', 'aksrt')]; - } - - $tracking_no = (string) ($record['tracking_no'] ?? ''); - $carrier = (string) ($record['carrier'] ?? ''); - if ($tracking_no === '' || $carrier === '') { - return ['success' => false, 'message' => __('缺少运单号或物流公司', 'aksrt')]; - } - - $cached = $this->get_express_cache($shipping_id); - if (!$force && !empty($cached) && !$this->should_refresh_cache($cached)) { - return ['success' => true, 'data' => $cached]; - } - - if (!empty($cached['signed'])) { - return ['success' => true, 'data' => $cached]; - } - - $shop_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_shop_settings() : []; - $api_sdk = sanitize_key((string) ($shop_settings['express']['api_sdk'] ?? 'kuaidi100')); - - $result = $this->call_express_api($api_sdk, $carrier, $tracking_no, $shop_settings); - - if ($result['success']) { - $express_data = [ - 'query_time' => current_time('mysql'), - 'traces' => $result['traces'] ?? [], - 'state' => $result['state'] ?? 'unknown', - 'signed' => !empty($result['signed']), - 'api' => $api_sdk, - ]; - - $wpdb->update( - $this->table(), - ['note' => wp_json_encode($express_data, JSON_UNESCAPED_UNICODE)], - ['id' => $shipping_id], - ['%s'], - ['%d'] - ); - - if (!empty($result['signed']) && ($record['status'] ?? '') !== self::STATUS_DELIVERED) { - $this->update_status($shipping_id, self::STATUS_DELIVERED, __('快递已签收(API检测)', 'aksrt')); - } - - return ['success' => true, 'data' => $express_data]; - } - - return $result; - } - - private function get_express_cache(int $shipping_id): array { - global $wpdb; - $note = $wpdb->get_var($wpdb->prepare( - "SELECT note FROM {$this->table()} WHERE id = %d", - $shipping_id - )); - if (!$note) return []; - $data = json_decode($note, true); - return is_array($data) && isset($data['traces']) ? $data : []; - } - - private function should_refresh_cache(array $cached): bool { - if (empty($cached['query_time'])) return true; - - $shop = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_shop_settings() : []; - $interval = max(30, min(240, (int) ($shop['express']['query_interval'] ?? 60))); - - $last = strtotime($cached['query_time']); - return (time() - $last) > ($interval * 60); - } - - private function call_express_api(string $sdk, string $carrier, string $tracking_no, array $settings): array { - $carrier_code = $this->map_carrier_code($sdk, $carrier); - - if ($sdk === 'kuaidi100') { - return $this->query_kuaidi100($carrier_code, $tracking_no, $settings); - } - if ($sdk === 'kdniao') { - return $this->query_kdniao($carrier_code, $tracking_no, $settings); - } - if ($sdk === 'aliyun') { - return $this->query_aliyun($carrier_code, $tracking_no, $settings); - } - - return ['success' => false, 'message' => __('未配置快递查询接口', 'aksrt')]; - } - - private function query_kuaidi100(string $com, string $num, array $settings): array { - $key = sanitize_text_field((string) ($settings['express']['kuaidi100_key'] ?? '')); - $customer = sanitize_text_field((string) ($settings['express']['kuaidi100_customer'] ?? '')); - if ($key === '' || $customer === '') { - return ['success' => false, 'message' => __('快递100 API 未配置', 'aksrt')]; - } - - $param = wp_json_encode(['com' => $com, 'num' => $num]); - $sign = strtoupper(md5($param . $key . $customer)); - - $response = wp_remote_post('https://poll.kuaidi100.com/poll/query.do', [ - 'body' => ['customer' => $customer, 'sign' => $sign, 'param' => $param], - 'timeout' => 10, - ]); - - if (is_wp_error($response)) { - return ['success' => false, 'message' => $response->get_error_message()]; - } - - $body = json_decode(wp_remote_retrieve_body($response), true); - if (!is_array($body) || (int) ($body['status'] ?? 0) !== 200) { - return ['success' => false, 'message' => (string) ($body['message'] ?? __('查询失败', 'aksrt'))]; - } - - $state = (string) ($body['state'] ?? '0'); - $signed = $state === '3'; - - $traces = []; - foreach ((array) ($body['data'] ?? []) as $item) { - $traces[] = [ - 'time' => (string) ($item['ftime'] ?? $item['time'] ?? ''), - 'context' => (string) ($item['context'] ?? ''), - ]; - } - - return ['success' => true, 'traces' => $traces, 'state' => $state, 'signed' => $signed]; - } - - private function query_kdniao(string $code, string $num, array $settings): array { - $app_id = sanitize_text_field((string) ($settings['express']['kdniao_app_id'] ?? '')); - $app_key = sanitize_text_field((string) ($settings['express']['kdniao_app_key'] ?? '')); - if ($app_id === '' || $app_key === '') { - return ['success' => false, 'message' => __('快递鸟 API 未配置', 'aksrt')]; - } - - $request_data = wp_json_encode(['ShipperCode' => $code, 'LogisticCode' => $num]); - $data_sign = urlencode(base64_encode(md5($request_data . $app_key))); - - $response = wp_remote_post('https://api.kdniao.com/Ebusiness/EbusinessOrderHandle.aspx', [ - 'body' => [ - 'RequestData' => urlencode($request_data), - 'EBusinessID' => $app_id, - 'RequestType' => '1002', - 'DataSign' => $data_sign, - 'DataType' => '2', - ], - 'timeout' => 10, - ]); - - if (is_wp_error($response)) { - return ['success' => false, 'message' => $response->get_error_message()]; - } - - $body = json_decode(wp_remote_retrieve_body($response), true); - if (!is_array($body) || !($body['Success'] ?? false)) { - return ['success' => false, 'message' => (string) ($body['Reason'] ?? __('查询失败', 'aksrt'))]; - } - - $state = (string) ($body['State'] ?? '0'); - $signed = $state === '3'; - - $traces = []; - foreach ((array) ($body['Traces'] ?? []) as $item) { - $traces[] = [ - 'time' => (string) ($item['AcceptTime'] ?? ''), - 'context' => (string) ($item['AcceptStation'] ?? ''), - ]; - } - - return ['success' => true, 'traces' => $traces, 'state' => $state, 'signed' => $signed]; - } - - private function query_aliyun(string $code, string $num, array $settings): array { - $app_code = sanitize_text_field((string) ($settings['express']['aliyun_app_code'] ?? '')); - if ($app_code === '') { - return ['success' => false, 'message' => __('阿里云快递 API 未配置', 'aksrt')]; - } - - $url = add_query_arg(['no' => $num, 'type' => $code], 'https://wuliu.market.alicloudapi.com/kdi'); - - $response = wp_remote_get($url, [ - 'headers' => ['Authorization' => 'APPCODE ' . $app_code], - 'timeout' => 10, - ]); - - if (is_wp_error($response)) { - return ['success' => false, 'message' => $response->get_error_message()]; - } - - $body = json_decode(wp_remote_retrieve_body($response), true); - if (!is_array($body) || (int) ($body['status'] ?? -1) !== 0) { - return ['success' => false, 'message' => (string) ($body['msg'] ?? __('查询失败', 'aksrt'))]; - } - - $result_data = $body['result'] ?? []; - $state = (string) ($result_data['deliverystatus'] ?? '0'); - $signed = $state === '3'; - - $traces = []; - foreach ((array) ($result_data['list'] ?? []) as $item) { - $traces[] = [ - 'time' => (string) ($item['time'] ?? ''), - 'context' => (string) ($item['status'] ?? ''), - ]; - } - - return ['success' => true, 'traces' => $traces, 'state' => $state, 'signed' => $signed]; - } - - private function map_carrier_code(string $sdk, string $carrier): string { - $kuaidi100_map = [ - 'sf' => 'shunfeng', 'yd' => 'yunda', 'zt' => 'zhongtong', 'yt' => 'yuantong', - 'sto' => 'shentong', 'jd' => 'jd', 'ems' => 'ems', 'db' => 'debangkuaidi', - 'yunda' => 'yunda', - ]; - $kdniao_map = [ - 'sf' => 'SF', 'yd' => 'YD', 'zt' => 'ZTO', 'yt' => 'YTO', - 'sto' => 'STO', 'jd' => 'JD', 'ems' => 'EMS', 'db' => 'DBL', - 'yunda' => 'YD', - ]; - - if ($sdk === 'kuaidi100') { - return $kuaidi100_map[$carrier] ?? $carrier; - } - if ($sdk === 'kdniao') { - return $kdniao_map[$carrier] ?? strtoupper($carrier); - } - return $carrier; - } -} diff --git a/src/AKSRT/Payment/ShippingPart2.php b/src/AKSRT/Payment/ShippingPart2.php deleted file mode 100644 index 102eb94a..00000000 --- a/src/AKSRT/Payment/ShippingPart2.php +++ /dev/null @@ -1,152 +0,0 @@ -get($shipping_id); - if (!$record) return []; - - $cached = $this->get_express_cache($shipping_id); - $timeline = []; - - $timeline[] = [ - 'time' => (string) ($record['created_at'] ?? ''), - 'label' => sprintf(__('商家已发货:%s %s', 'aksrt'), $record['carrier_label'] ?? '', $record['tracking_no'] ?? ''), - 'type' => 'shipped', - ]; - - if (!empty($cached['traces'])) { - foreach ($cached['traces'] as $trace) { - $timeline[] = [ - 'time' => (string) ($trace['time'] ?? ''), - 'label' => (string) ($trace['context'] ?? ''), - 'type' => 'trace', - ]; - } - } - - if (!empty($cached['signed'])) { - $timeline[] = [ - 'time' => (string) ($record['delivered_at'] ?? ($cached['query_time'] ?? '')), - 'label' => __('快递已签收', 'aksrt'), - 'type' => 'signed', - ]; - } - - usort($timeline, static fn($a, $b) => strcmp($a['time'] ?? '', $b['time'] ?? '')); - return $timeline; - } - - /* ============================================================ - * Shipping Fee Resolution (§6.6 运费模板) - * - * 3 types: free / fixed / amount (满额包邮) - * Inheritance: product → category → theme defaults - * ============================================================ */ - - public function resolve_shipping_fee(int $product_id, float $order_amount = 0, array $address = []): array { - $config = $this->get_product_shipping_config($product_id); - $fee_type = (string) ($config['fee_type'] ?? 'fixed'); - $fee_amount = (float) ($config['fee_amount'] ?? 0); - $free_threshold = (float) ($config['free_threshold'] ?? 0); - - if ($fee_type === 'free') { - return ['fee' => 0.0, 'type' => 'free', 'label' => __('包邮', 'aksrt')]; - } - - if ($fee_type === 'amount' && $free_threshold > 0 && $order_amount >= $free_threshold) { - return ['fee' => 0.0, 'type' => 'free_threshold', 'label' => sprintf(__('满%.0f包邮', 'aksrt'), $free_threshold)]; - } - - $region_fee = $this->resolve_region_fee($config, $address); - if ($region_fee !== null) { - return ['fee' => $region_fee, 'type' => 'region', 'label' => __('地区运费', 'aksrt')]; - } - - return ['fee' => $fee_amount, 'type' => 'fixed', 'label' => sprintf(__('运费 ¥%.2f', 'aksrt'), $fee_amount)]; - } - - private function get_product_shipping_config(int $product_id): array { - $meta = get_post_meta($product_id, '_aksrt_shipping_config', true); - if (is_array($meta) && !empty($meta['fee_type'])) { - return $meta; - } - - if (class_exists('\AKSRT\Shop\TermMeta')) { - $cat_config = \AKSRT\Shop\TermMeta::resolve_inheritance($product_id, 'shipping_fee'); - if (is_array($cat_config) && !empty($cat_config['fee_type'])) { - return $cat_config; - } - } - - if (class_exists('\AKSRT\Settings\Settings')) { - $shop = \AKSRT\Settings\Settings::get_shop_settings(); - return [ - 'fee_type' => sanitize_key((string) ($shop['defaults']['shipping_fee_type'] ?? 'fixed')), - 'fee_amount' => (float) ($shop['defaults']['shipping_fee_amount'] ?? 12), - 'free_threshold' => (float) ($shop['defaults']['shipping_free_threshold'] ?? 0), - 'region_fees' => (array) ($shop['defaults']['region_fees'] ?? []), - ]; - } - - return ['fee_type' => 'fixed', 'fee_amount' => 12, 'free_threshold' => 0, 'region_fees' => []]; - } - - private function resolve_region_fee(array $config, array $address): ?float { - $region_fees = (array) ($config['region_fees'] ?? []); - if (empty($region_fees) || empty($address['province'])) { - return null; - } - - $province = (string) $address['province']; - foreach ($region_fees as $region) { - $provinces = (array) ($region['provinces'] ?? []); - if (in_array($province, $provinces, true)) { - return (float) ($region['fee'] ?? 0); - } - } - - return null; - } - - public static function get_china_regions(): array { - $file = get_template_directory() . '/assets/data/china-regions.json'; - if (!file_exists($file)) { - return self::get_builtin_provinces(); - } - $data = json_decode((string) file_get_contents($file), true); - return is_array($data) ? $data : self::get_builtin_provinces(); - } - - private static function get_builtin_provinces(): array { - return [ - ['code' => '11', 'name' => '北京市'], ['code' => '12', 'name' => '天津市'], - ['code' => '13', 'name' => '河北省'], ['code' => '14', 'name' => '山西省'], - ['code' => '15', 'name' => '内蒙古自治区'], ['code' => '21', 'name' => '辽宁省'], - ['code' => '22', 'name' => '吉林省'], ['code' => '23', 'name' => '黑龙江省'], - ['code' => '31', 'name' => '上海市'], ['code' => '32', 'name' => '江苏省'], - ['code' => '33', 'name' => '浙江省'], ['code' => '34', 'name' => '安徽省'], - ['code' => '35', 'name' => '福建省'], ['code' => '36', 'name' => '江西省'], - ['code' => '37', 'name' => '山东省'], ['code' => '41', 'name' => '河南省'], - ['code' => '42', 'name' => '湖北省'], ['code' => '43', 'name' => '湖南省'], - ['code' => '44', 'name' => '广东省'], ['code' => '45', 'name' => '广西壮族自治区'], - ['code' => '46', 'name' => '海南省'], ['code' => '50', 'name' => '重庆市'], - ['code' => '51', 'name' => '四川省'], ['code' => '52', 'name' => '贵州省'], - ['code' => '53', 'name' => '云南省'], ['code' => '54', 'name' => '西藏自治区'], - ['code' => '61', 'name' => '陕西省'], ['code' => '62', 'name' => '甘肃省'], - ['code' => '63', 'name' => '青海省'], ['code' => '64', 'name' => '宁夏回族自治区'], - ['code' => '65', 'name' => '新疆维吾尔自治区'], - ['code' => '71', 'name' => '台湾省'], - ['code' => '81', 'name' => '香港特别行政区'], - ['code' => '82', 'name' => '澳门特别行政区'], - ]; - } -} diff --git a/src/AKSRT/Payment/UserOrders.php b/src/AKSRT/Payment/UserOrders.php index 231e0925..246bb5c5 100644 --- a/src/AKSRT/Payment/UserOrders.php +++ b/src/AKSRT/Payment/UserOrders.php @@ -52,9 +52,7 @@ class UserOrders { if ($status !== '') { $repo_args['status'] = $status; } - if (in_array($status, [Order::STATUS_PAID, Order::STATUS_DELIVERED, Order::STATUS_PENDING_REVIEW], true)) { - $repo_args['product_type'] = \AKSRT\Core\DomainModel::ORDER_PRODUCT_SHOP; - } elseif ($product_type !== '' && $product_type !== 'all') { + if ($product_type !== '' && $product_type !== 'all') { $repo_args['product_type'] = $product_type; } @@ -124,8 +122,6 @@ class UserOrders { $product_type = sanitize_key((string) ($order['product_type'] ?? '')); $status = (string) ($order['status'] ?? Order::STATUS_PENDING); $product_id = (int) ($order['product_id'] ?? 0); - $is_shop = $product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_SHOP - || $product_type === 'shop'; $currency = sanitize_key((string) ($order['currency'] ?? 'CNY')); $is_points = $currency === 'points' || sanitize_key((string) ($order['payment_method'] ?? '')) === 'points' @@ -139,17 +135,11 @@ class UserOrders { $permalink = $this->resolve_order_permalink($product_type, $product_id, $meta); $order_items = []; - if ($is_shop && class_exists('\AKSRT\Shop\Shop')) { - $order_items = \AKSRT\Shop\Shop::get_instance()->get_order_items($order_number); - } $has_structured_items = !empty($order_items); $product = []; - if ($has_structured_items && class_exists('\AKSRT\Shop\Shop')) { - $product = \AKSRT\Shop\Shop::get_instance()->build_product_card_view((int) ($order_items[0]['product_id'] ?? 0)); - } elseif ($is_shop && $product_id > 0 && class_exists('\AKSRT\Shop\Shop')) { - $product = \AKSRT\Shop\Shop::get_instance()->build_product_card_view($product_id); - } + // Shop product cards were removed with the shop feature; content/VIP + // orders fall through to the shared meta-based fields below. if (!empty($product['title'])) { $title = (string) $product['title']; @@ -163,20 +153,6 @@ class UserOrders { $shipping_records = []; $after_sale_items = []; - if ($is_shop) { - if (class_exists(Shipping::class)) { - $shipping_records = Shipping::get_instance()->get_by_order($order_number); - } - if (class_exists(AfterSale::class)) { - $as = AfterSale::get_instance()->get_list([ - 'limit' => 20, - 'offset' => 0, - 'order_number' => $order_number, - ]); - $after_sale_items = $as['items'] ?? []; - } - } - $subtotal = (float) ($meta['items'][0]['line_total'] ?? $order['amount'] ?? 0); $shipping_fee = (float) ($meta['shipping_fee'] ?? 0); $discount = (float) ($meta['discount'] ?? 0); @@ -186,10 +162,10 @@ class UserOrders { $can_pay = in_array($status, [Order::STATUS_PENDING, Order::STATUS_EXPIRED, Order::STATUS_CREATED], true) && $payment_value !== 'manual_review'; $can_cancel = in_array($status, [Order::STATUS_PENDING, Order::STATUS_CREATED, Order::STATUS_EXPIRED], true); - $can_after_sale = $is_shop && in_array($status, [Order::STATUS_PAID, Order::STATUS_DELIVERED, Order::STATUS_COMPLETED], true); - $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_after_sale = false; + $can_receipt = false; + $can_review = false; + $can_rebuy = false; // Order detail modal is always available; product permalink is optional. $can_view = true; $can_view_product = $permalink !== '' && in_array($status, [ @@ -306,11 +282,7 @@ class UserOrders { return ['success' => false, 'message' => __('订单不存在。', 'aksrt')]; } - // Prefer shop checkout when available (handles points + shop meta). - if (class_exists('\AKSRT\Shop\Shop')) { - return \AKSRT\Shop\Shop::get_instance()->pay_order($user_id, $order_number, $payment_method); - } - + // Resume via shared gateway stack (any product_type; shop removed). $repo = OrderRepository::get_instance(); $order = $repo->get_user_order($user_id, $order_number); if (!$order) { diff --git a/src/AKSRT/Settings/DomainGroups.php b/src/AKSRT/Settings/DomainGroups.php index d78197a5..7f8a2a16 100644 --- a/src/AKSRT/Settings/DomainGroups.php +++ b/src/AKSRT/Settings/DomainGroups.php @@ -27,15 +27,6 @@ trait DomainGroups { Integration::update_wechat_settings($settings); } - /** - * 获取商城设置 - * - * @return array - */ - public static function get_shop_settings(): array { - return ForumShop::get_shop_settings(); - } - /** * 获取文档模式设置 */ diff --git a/src/AKSRT/Settings/ForumShop.php b/src/AKSRT/Settings/ForumShop.php deleted file mode 100644 index e6506dc3..00000000 --- a/src/AKSRT/Settings/ForumShop.php +++ /dev/null @@ -1,185 +0,0 @@ - $enabled, - 'home_url' => $home_url, - 'breadcrumbs' => [ - 'enabled' => $breadcrumbs_s, - 'home' => $breadcrumbs_home, - 'shop_home' => $breadcrumbs_shop_home, - 'shop_home_name' => $breadcrumbs_shop_home_name, - ], - 'rewrite' => [ - 'product_slug' => $product_rewrite_slug, - 'cart_slug' => $cart_rewrite_slug, - 'suffix_html' => $rewrite_suffix_html, - ], - 'list' => [ - 'orderby' => $list_orderby, - 'count' => $list_count, - 'paging' => $list_paging, - 'ias_s' => $list_ias_s, - 'ias_max' => $list_ias_max, - 'style' => $list_style, - ], - 'sales' => [ - 'show' => $sales_show, - 'show_min' => $sales_show_min, - ], - 'contact' => [ - 'msg_s' => $contact_msg_s, - 'name' => $contact_name, - 'note' => $contact_note, - 'more' => $contact_more, - ], - 'single' => [ - 'content_layout' => $content_layout, - 'content_show_bg' => $content_show_bg, - 'tab' => $single_tab, - 'content_after' => $content_after, - 'related_s' => $related_s, - 'related_opt' => $related_opt, - 'main_image_default' => $main_image_default, - ], - 'product_defaults' => [ - 'limit_buy' => $limit_buy, - 'guest_buy' => $guest_buy, - 'email_fill' => $email_fill, - 'rebate' => $rebate, - 'shipping_fee' => $shipping_fee, - 'after_sale' => $after_sale, - 'service' => $service, - 'params_default' => $params_default, - ], - 'shipping' => [ - 'manual_address' => $manual_address, - 'express_api_sdk' => $express_api_sdk, - 'express_query_interval' => $express_query_interval, - 'express_kdniao_opt' => $express_kdniao_opt, - 'express_kuaidi100_opt' => $express_kuaidi100_opt, - 'express_aliyun_opt' => $express_aliyun_opt, - 'shipping_company' => $shipping_company, - ], - 'timing' => [ - 'receipt_max_day' => $receipt_max_day, - 'comment_max_day' => $comment_max_day, - 'comment_placeholder' => $comment_placeholder, - 'comment_img_num' => $comment_img_num, - 'after_sale_return_max_day' => $after_sale_return_max_day, - 'after_sale_reason_refund' => $after_sale_reason_refund, - 'after_sale_reason_return' => $after_sale_reason_return, - ], - 'author_show' => $author_show, - ]; - } -} \ No newline at end of file diff --git a/src/AKSRT/Shop/AddressService.php b/src/AKSRT/Shop/AddressService.php deleted file mode 100644 index 082b5610..00000000 --- a/src/AKSRT/Shop/AddressService.php +++ /dev/null @@ -1,204 +0,0 @@ -shop = $shop; - } - - /** - * 获取用户地址簿(公开接口) - */ - public function get_address_book(int $user_id): array { - return $this->get_user_address_book($user_id); - } - - public function get_user_address_book(int $user_id): array { - $saved = get_user_meta($user_id, 'aksrt_shop_addresses', true); - if (is_array($saved) && !empty($saved)) { - return array_values(array_filter(array_map([$this, 'sanitize_address'], $saved))); - } - - return []; - } - - private function sanitize_address($address): array { - if (!is_array($address)) { - return []; - } - - $name = sanitize_text_field((string) ($address['name'] ?? '')); - $phone = sanitize_text_field((string) ($address['phone'] ?? '')); - $province = sanitize_text_field((string) ($address['province'] ?? '')); - $city = sanitize_text_field((string) ($address['city'] ?? '')); - $district = sanitize_text_field((string) ($address['district'] ?? '')); - $street = sanitize_text_field((string) ($address['address'] ?? '')); - - return [ - 'id' => (int) ($address['id'] ?? 0), - 'name' => $name, - 'phone' => $phone, - 'province' => $province, - 'city' => $city, - 'district' => $district, - 'address' => $street, - 'is_default' => !empty($address['is_default']), - 'full_text' => trim($name . ' / ' . $province . $city . $district . $street), - ]; - } - - /** - * 新增地址 - * - * @param int $user_id - * @param array $data 地址数据(name, phone, province, city, district, address, is_default) - * @return array{success:bool, address:array|null, error:string|null} - */ - public function add_address(int $user_id, array $data): array { - $address = $this->sanitize_address($data); - if ($address['name'] === '' || $address['phone'] === '' || $address['address'] === '') { - return ['success' => false, 'address' => null, 'error' => __('姓名、手机号和详细地址为必填项。', 'aksrt')]; - } - - $book = $this->get_user_address_book($user_id); - $address['id'] = $this->next_address_id($book); - - if (!empty($data['is_default']) || empty($book)) { - foreach ($book as &$item) { - $item['is_default'] = false; - } - $address['is_default'] = true; - } - - $book[] = $address; - update_user_meta($user_id, 'aksrt_shop_addresses', $book); - - return ['success' => true, 'address' => $address, 'error' => null]; - } - - /** - * 更新地址 - * - * @param int $user_id - * @param int $address_id - * @param array $data - * @return array{success:bool, address:array|null, error:string|null} - */ - public function update_address(int $user_id, int $address_id, array $data): array { - $book = $this->get_user_address_book($user_id); - $found = false; - - foreach ($book as &$item) { - if ((int) ($item['id'] ?? 0) !== $address_id) { - continue; - } - $found = true; - $merged = array_merge($item, $this->sanitize_address(array_merge($item, $data))); - $merged['id'] = $address_id; - - if ($merged['name'] === '' || $merged['phone'] === '' || $merged['address'] === '') { - return ['success' => false, 'address' => null, 'error' => __('姓名、手机号和详细地址为必填项。', 'aksrt')]; - } - - if (!empty($data['is_default'])) { - foreach ($book as &$clear) { - $clear['is_default'] = false; - } - $merged['is_default'] = true; - } - - $item = $merged; - break; - } - - if (!$found) { - return ['success' => false, 'address' => null, 'error' => __('地址不存在。', 'aksrt')]; - } - - update_user_meta($user_id, 'aksrt_shop_addresses', $book); - - return ['success' => true, 'address' => $merged, 'error' => null]; - } - - /** - * 删除地址 - * - * @param int $user_id - * @param int $address_id - * @return array{success:bool, error:string|null} - */ - public function delete_address(int $user_id, int $address_id): array { - $book = $this->get_user_address_book($user_id); - $found = false; - $was_default = false; - $filtered = []; - - foreach ($book as $item) { - if ((int) ($item['id'] ?? 0) === $address_id) { - $found = true; - $was_default = !empty($item['is_default']); - continue; - } - $filtered[] = $item; - } - - if (!$found) { - return ['success' => false, 'error' => __('地址不存在。', 'aksrt')]; - } - - if ($was_default && !empty($filtered)) { - $filtered[0]['is_default'] = true; - } - - update_user_meta($user_id, 'aksrt_shop_addresses', array_values($filtered)); - - return ['success' => true, 'error' => null]; - } - - /** - * 设置默认地址 - * - * @param int $user_id - * @param int $address_id - * @return array{success:bool, error:string|null} - */ - public function set_default_address(int $user_id, int $address_id): array { - $book = $this->get_user_address_book($user_id); - $found = false; - - foreach ($book as &$item) { - $item['is_default'] = ((int) ($item['id'] ?? 0) === $address_id); - if ($item['is_default']) { - $found = true; - } - } - - if (!$found) { - return ['success' => false, 'error' => __('地址不存在。', 'aksrt')]; - } - - update_user_meta($user_id, 'aksrt_shop_addresses', $book); - - return ['success' => true, 'error' => null]; - } - - /** - * 生成下一个地址 ID - */ - private function next_address_id(array $book): int { - $max = 0; - foreach ($book as $item) { - $max = max($max, (int) ($item['id'] ?? 0)); - } - return $max + 1; - } -} diff --git a/src/AKSRT/Shop/CartService.php b/src/AKSRT/Shop/CartService.php deleted file mode 100644 index 92877445..00000000 --- a/src/AKSRT/Shop/CartService.php +++ /dev/null @@ -1,173 +0,0 @@ -shop = $shop; - } - - public function get_cart(int $user_id): array { - if ($user_id <= 0) { - return []; - } - $cart = get_user_meta($user_id, 'shop_cart', true); - return is_array($cart) ? $cart : []; - } - - public function get_cart_count(int $user_id): int { - $cart = $this->get_cart($user_id); - $count = 0; - foreach ($cart as $opts) { - if (is_array($opts)) { - foreach ($opts as $qty) { - $count += (int) $qty; - } - } - } - return $count; - } - - public function add_to_cart(int $user_id, int $product_id, string $options_string = '', int $count = 1): array { - if ($user_id <= 0 || $product_id <= 0 || $count <= 0) { - return ['error' => 'invalid_params']; - } - $validation = $this->validate_cart_item($product_id, $options_string); - if ($validation !== true) { - return ['error' => $validation]; - } - - $cart = $this->get_cart($user_id); - $pid_key = (string) $product_id; - $opt_key = $options_string !== '' ? $options_string : '_default'; - - if (!isset($cart[$pid_key]) || !is_array($cart[$pid_key])) { - $cart[$pid_key] = []; - } - $cart[$pid_key][$opt_key] = ((int) ($cart[$pid_key][$opt_key] ?? 0)) + $count; - - // Enforce limit_buy if set - $limit = (int) get_post_meta($product_id, '_aksrt_limit_buy', true); - if ($limit > 0 && $cart[$pid_key][$opt_key] > $limit) { - $cart[$pid_key][$opt_key] = $limit; - } - - update_user_meta($user_id, 'shop_cart', $cart); - return ['success' => true, 'cart_count' => $this->get_cart_count($user_id)]; - } - - public function reduce_from_cart(int $user_id, int $product_id, string $options_string = '', int $count = 1): array { - if ($user_id <= 0 || $product_id <= 0 || $count <= 0) { - return ['error' => 'invalid_params']; - } - $cart = $this->get_cart($user_id); - $pid_key = (string) $product_id; - $opt_key = $options_string !== '' ? $options_string : '_default'; - - if (isset($cart[$pid_key][$opt_key])) { - $cart[$pid_key][$opt_key] -= $count; - if ($cart[$pid_key][$opt_key] <= 0) { - unset($cart[$pid_key][$opt_key]); - } - } - if (empty($cart[$pid_key])) { - unset($cart[$pid_key]); - } - - update_user_meta($user_id, 'shop_cart', $cart); - return ['success' => true, 'cart_count' => $this->get_cart_count($user_id)]; - } - - public function remove_from_cart(int $user_id, int $product_id, string $options_string = ''): array { - if ($user_id <= 0) { - return ['error' => 'invalid_params']; - } - $cart = $this->get_cart($user_id); - $pid_key = (string) $product_id; - $opt_key = $options_string !== '' ? $options_string : '_default'; - - if ($options_string === '' && isset($cart[$pid_key])) { - // Remove all options for this product - unset($cart[$pid_key]); - } elseif (isset($cart[$pid_key][$opt_key])) { - unset($cart[$pid_key][$opt_key]); - if (empty($cart[$pid_key])) { - unset($cart[$pid_key]); - } - } - - update_user_meta($user_id, 'shop_cart', $cart); - return ['success' => true, 'cart_count' => $this->get_cart_count($user_id)]; - } - - public function remove_multi_from_cart(int $user_id, array $items): array { - if ($user_id <= 0) { - return ['error' => 'invalid_params']; - } - $cart = $this->get_cart($user_id); - foreach ($items as $item) { - $pid_key = (string) ($item['product_id'] ?? 0); - $opt_key = !empty($item['options_string']) ? $item['options_string'] : '_default'; - if (isset($cart[$pid_key][$opt_key])) { - unset($cart[$pid_key][$opt_key]); - if (empty($cart[$pid_key])) { - unset($cart[$pid_key]); - } - } - } - update_user_meta($user_id, 'shop_cart', $cart); - return ['success' => true, 'cart_count' => $this->get_cart_count($user_id)]; - } - - public function empty_cart(int $user_id): array { - if ($user_id <= 0) { - return ['error' => 'invalid_params']; - } - delete_user_meta($user_id, 'shop_cart'); - return ['success' => true, 'cart_count' => 0]; - } - - /** - * Validate a product for cart addition. - * Returns true if valid, or a string error code. - */ - public function validate_cart_item(int $product_id, string $options_string = '') { - $post = get_post($product_id); - if (!$post || $post->post_type !== 'aksrt_product') { - return 'invalid_product'; - } - if ($post->post_status !== 'publish') { - return 'product_not_published'; - } - - // Check stock (unless unlimited) - $stock = (int) get_post_meta($product_id, '_aksrt_stock', true); - if ($stock > -1 && $stock <= 0) { - return 'out_of_stock'; - } - - // Check guest_buy — if disabled and user not logged in - if (!is_user_logged_in()) { - $guest = get_post_meta($product_id, '_aksrt_guest_buy', true); - if ($guest === '0' || $guest === '') { - // Fall back to inheritance chain via TermMeta - if (class_exists('AKSRT\Shop\TermMeta')) { - $resolved = TermMeta::resolve_inheritance($product_id, 'guest_buy'); - if (empty($resolved)) { - return 'login_required'; - } - } - } - } - - return true; - } -} diff --git a/src/AKSRT/Shop/CheckoutService.php b/src/AKSRT/Shop/CheckoutService.php deleted file mode 100644 index ced3fe94..00000000 --- a/src/AKSRT/Shop/CheckoutService.php +++ /dev/null @@ -1,414 +0,0 @@ -shop = $shop; - } - - public function preview_checkout(int $user_id, array $payload): array { - if ($user_id <= 0) { - return ['success' => false, 'message' => __('请先登录后再结算。', 'aksrt')]; - } - - $items = $this->normalize_checkout_items($payload['items'] ?? []); - if (empty($items)) { - return ['success' => false, 'message' => __('请选择至少一个商品。', 'aksrt')]; - } - - $product_views = []; - $subtotal = 0.0; - foreach ($items as $item) { - $product = $this->shop->get_product_detail_view((int) $item['product_id']); - if (empty($product) || empty($product['purchase_enabled'])) { - return ['success' => false, 'message' => __('存在不可购买商品,请刷新后重试。', 'aksrt')]; - } - - if ((int) $product['stock'] < (int) $item['quantity']) { - return ['success' => false, 'message' => sprintf(__('商品 %s 库存不足。', 'aksrt'), $product['title'])]; - } - - $line_total = round(((float) $product['price']) * (int) $item['quantity'], 2); - - // R69: Apply discount engine (shop_discount taxonomy campaigns) - $discount_result = ['discount_amount' => 0, 'gifts' => [], 'applied' => []]; - if (class_exists('\AKSRT\Payment\Discount')) { - $discount_result = \AKSRT\Payment\Discount::get_instance()->calculate( - (float) $product['price'], - (int) $item['quantity'], - (int) $product['id'], - $user_id - ); - $line_total = $discount_result['final_price']; - } - - $subtotal += $line_total; - $product_views[] = [ - 'product_id' => (int) $product['id'], - 'title' => $product['title'], - 'price' => (float) $product['price'], - 'quantity' => (int) $item['quantity'], - 'line_total' => $line_total, - 'image' => $product['image'], - 'shipping' => $product['shipping'], - 'discount_amount' => (float) ($discount_result['discount_amount'] ?? 0), - 'gifts' => (array) ($discount_result['gifts'] ?? []), - 'applied_discounts' => (array) ($discount_result['applied'] ?? []), - ]; - } - - $shipping_method = sanitize_key((string) ($payload['shipping_method'] ?? 'express')); - $shipping_profile = $this->resolve_shipping_quote($product_views, $shipping_method); - - $coupon_code = sanitize_text_field((string) ($payload['coupon_code'] ?? '')); - $product_ids = array_values(array_unique(array_map(static function ($row) { - return (int) ($row['product_id'] ?? $row['id'] ?? 0); - }, $product_views))); - $coupon_result = $this->resolve_coupon_discount($coupon_code, $subtotal + $shipping_profile['fee'], $product_ids); - if (!$coupon_result['success']) { - return $coupon_result; - } - - $total = max(0, round($subtotal + $shipping_profile['fee'] - $coupon_result['discount'], 2)); - $address = $this->resolve_checkout_address($user_id, (int) ($payload['address_id'] ?? 0)); - $payment_method = $this->resolve_payment_method((string) ($payload['payment_method'] ?? 'balance')); - - return [ - 'success' => true, - 'data' => [ - 'items' => $product_views, - 'subtotal' => round($subtotal, 2), - 'shipping_fee' => $shipping_profile['fee'], - 'shipping_method' => $shipping_profile['method'], - 'discount' => $coupon_result['discount'], - 'coupon' => $coupon_result['coupon'], - 'address' => $address, - 'payment_method' => $payment_method, - 'total' => $total, - ], - ]; - } - - public function create_checkout_order(int $user_id, array $payload): array { - $preview = $this->preview_checkout($user_id, $payload); - if (empty($preview['success'])) { - return $preview; - } - - $data = $preview['data']; - $primary_item = $data['items'][0] ?? null; - if (!$primary_item || !class_exists('\AKSRT\Payment\OrderRepository')) { - return ['success' => false, 'message' => __('订单创建失败。', 'aksrt')]; - } - - $payment_method = sanitize_key((string) ($data['payment_method']['value'] ?? 'balance')); - $meta_payload = [ - 'customer_note' => sanitize_textarea_field((string) ($payload['customer_note'] ?? '')), - 'items' => $data['items'], - 'coupon' => $data['coupon'], - 'shipping_fee' => $data['shipping_fee'], - 'shipping_method' => $data['shipping_method'], - 'address' => $data['address'], - 'payment_method' => $data['payment_method'], - 'discount' => $data['discount'], - ]; - - $expiry_minutes = 30; - if (class_exists('\AKSRT\Settings\Settings')) { - $order_settings = \AKSRT\Settings\Settings::get_order_settings(); - $expiry_minutes = max(5, min(1440, (int) ($order_settings['expiry'] ?? 30))); - } - - $order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([ - 'user_id' => $user_id, - 'product_type' => 'shop_product', - 'product_id' => (int) $primary_item['product_id'], - 'amount' => (float) $data['total'], - 'currency' => 'CNY', - 'status' => \AKSRT\Payment\Order::STATUS_PENDING, - 'payment_method' => $payment_method, - 'expires_at' => date('Y-m-d H:i:s', strtotime('+' . $expiry_minutes . ' minutes')), - 'note' => wp_json_encode($meta_payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), - ]); - - if ($order_number === '') { - return ['success' => false, 'message' => __('订单创建失败。', 'aksrt')]; - } - - $this->shop->save_order_items($order_number, $data['items']); - - if (!empty($data['coupon']['code']) && class_exists('\AKSRT\Payment\Coupon')) { - $coupon_instance = \AKSRT\Payment\Coupon::get_instance(); - $coupon_instance->use_coupon($data['coupon']['code']); - // R69: Log coupon usage for per-user tracking - $coupon_row = $coupon_instance->validate($data['coupon']['code']); - $coupon_id = (int) ($coupon_row['coupon']['id'] ?? 0); - if ($coupon_id > 0) { - $coupon_instance->log_usage( - $coupon_id, - $user_id, - $order_number, - (float) ($data['discount'] ?? 0) - ); - } - } - - $detail = $this->shop->get_order_detail_view($user_id, $order_number); - - return [ - 'success' => true, - 'data' => [ - 'order_number' => $order_number, - 'order' => $detail, - ], - ]; - } - - public function pay_order(int $user_id, string $order_number, string $payment_method): array { - if ($user_id <= 0 || $order_number === '' || !class_exists('\AKSRT\Payment\OrderRepository')) { - return ['success' => false, 'message' => __('订单不存在。', 'aksrt')]; - } - - $repo = \AKSRT\Payment\OrderRepository::get_instance(); - $order = $repo->get_user_order($user_id, $order_number); - if (!$order) { - return ['success' => false, 'message' => __('订单不存在。', 'aksrt')]; - } - - $status = (string) ($order['status'] ?? ''); - if (!in_array($status, [ - \AKSRT\Payment\Order::STATUS_CREATED, - \AKSRT\Payment\Order::STATUS_PENDING, - ], true)) { - return ['success' => false, 'message' => __('当前订单状态不可支付。', 'aksrt')]; - } - - // Payment attempts are bound to the method selected when the order was created. - $stored_method = sanitize_key((string) ($order['payment_method'] ?? '')); - $payment_method = sanitize_key($payment_method ?: ($stored_method ?: 'balance')); - if ($stored_method !== '' && $payment_method !== $stored_method) { - return ['success' => false, 'message' => __('支付方式与订单不一致,请重新下单。', 'aksrt')]; - } - - $gateway_manager = class_exists('\AKSRT\Payment\Gateway') ? \AKSRT\Payment\Gateway::get_instance() : null; - $gateway = $gateway_manager ? $gateway_manager->get_gateway($payment_method) : null; - - if (!$gateway) { - return ['success' => false, 'message' => __('当前支付方式不可用。', 'aksrt')]; - } - - if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) { - return ['success' => false, 'message' => __('该支付方式尚未配置,请联系站长。', 'aksrt')]; - } - - // Points payment is a purely internal settlement (no provider attempt), - // but still needs the attempt lock against double-click double deduction. - if ($payment_method === 'points') { - $lock = class_exists('\AKSRT\Payment\PaymentAttemptLock') - ? \AKSRT\Payment\PaymentAttemptLock::acquire($order_number) - : false; - if ($lock === false) { - return ['success' => false, 'message' => __('该订单正在发起支付,请稍后重试。', 'aksrt')]; - } - try { - return $this->pay_with_points($user_id, $order_number, $order); - } finally { - \AKSRT\Payment\PaymentAttemptLock::release($order_number, $lock); - } - } - - // 携带订单号与摘要,供网关生成支付单。 - // PaymentOrchestrator 内部持有 attempt 锁并分配 provider out_trade_no。 - $order['subject'] = $order['subject'] ?? sprintf(__('订单 %s', 'aksrt'), $order_number); - $order['payment_method'] = $payment_method; - - $payment_result = \AKSRT\Payment\PaymentOrchestrator::pay($gateway, $order, [ - 'paid_note' => __('用户在前台完成支付。', 'aksrt'), - ]); - if (empty($payment_result['success'])) { - return ['success' => false, 'message' => $payment_result['message']]; - } - - // 跳转/扫码型网关(支付宝/微信/PayPal/Stripe):返回支付凭据,订单保持待支付, - // 由 /payment/{gateway}/notify|return 回调确认到账;此处绝不预先标记已支付。 - if (!empty($payment_result['redirect'])) { - return [ - 'success' => true, - 'data' => [ - 'redirect' => true, - 'payment_url' => (string) ($payment_result['payment_url'] ?? ''), - 'qr_content' => (string) ($payment_result['qr_content'] ?? ''), - 'order_number' => $order_number, - 'message' => $payment_result['message'], - ], - ]; - } - - return [ - 'success' => true, - 'data' => [ - 'order' => $this->shop->get_order_detail_view($user_id, $order_number), - 'message' => $payment_result['message'], - ], - ]; - } - - private function pay_with_points(int $user_id, string $order_number, array $order): array { - if (!class_exists('\AKSRT\Credits\Credits')) { - return ['success' => false, 'message' => __('积分系统不可用。', 'aksrt')]; - } - - $amount = (int) round((float) ($order['amount'] ?? 0)); - if ($amount <= 0) { - return ['success' => false, 'message' => __('订单金额无效。', 'aksrt')]; - } - - $balance = \AKSRT\Credits\Credits::balance($user_id); - if ($balance < $amount) { - return ['success' => false, 'message' => sprintf(__('积分不足,需要 %d 积分,当前余额 %d。', 'aksrt'), $amount, $balance)]; - } - - $deducted = \AKSRT\Credits\Credits::deduct( - $user_id, - $amount, - 'shop_points_pay', - ['order' => $order_number], - sprintf(__('积分购买订单 %s', 'aksrt'), $order_number) - ); - if (is_wp_error($deducted)) { - return ['success' => false, 'message' => $deducted->get_error_message() ?: __('积分扣除失败。', 'aksrt')]; - } - - $repo = \AKSRT\Payment\OrderRepository::get_instance(); - $paid = $repo->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [ - 'payment_method' => 'points', - 'payment_transaction_id' => 'points_' . $order_number, - 'note' => sprintf(__('积分支付 %d 积分。', 'aksrt'), $amount), - 'require_transition' => true, - ]); - if (!$paid) { - $compensation = \AKSRT\Credits\Credits::add( - $user_id, - $amount, - 'shop_points_pay_rollback', - ['order' => $order_number], - sprintf(__('订单 %s 支付失败退回积分', 'aksrt'), $order_number) - ); - if (is_wp_error($compensation)) { - error_log(sprintf( - '[AKSRT Payment] points rollback failed order=%s: %s', - $order_number, - $compensation->get_error_message() - )); - return ['success' => false, 'message' => __('订单状态已变化,积分补偿失败,请联系管理员处理。', 'aksrt')]; - } - return ['success' => false, 'message' => __('订单状态已变化,积分已退回。', 'aksrt')]; - } - - return [ - 'success' => true, - 'data' => [ - 'order' => $this->shop->get_order_detail_view($user_id, $order_number), - 'message' => __('积分支付成功。', 'aksrt'), - ], - ]; - } - - private function normalize_checkout_items($items): array { - if (!is_array($items)) { - return []; - } - - $normalized = []; - foreach ($items as $item) { - $product_id = (int) ($item['product_id'] ?? 0); - $quantity = max(1, (int) ($item['quantity'] ?? 1)); - if ($product_id <= 0) { - continue; - } - - $normalized[] = [ - 'product_id' => $product_id, - 'quantity' => $quantity, - ]; - } - - return $normalized; - } - - private function resolve_coupon_discount(string $coupon_code, float $order_amount, array $product_ids = []): array { - if ($coupon_code === '') { - return ['success' => true, 'discount' => 0.0, 'coupon' => []]; - } - - if (!class_exists('\AKSRT\Payment\Coupon')) { - return ['success' => false, 'message' => __('优惠券系统不可用。', 'aksrt')]; - } - - $result = \AKSRT\Payment\Coupon::get_instance()->validate($coupon_code, $order_amount, [ - 'user_id' => get_current_user_id(), - 'product_ids' => array_values(array_filter(array_map('intval', $product_ids))), - 'context' => 'shop', - ]); - if (empty($result['valid'])) { - return ['success' => false, 'message' => $result['message'] ?? __('优惠券不可用。', 'aksrt')]; - } - - return [ - 'success' => true, - 'discount' => (float) ($result['discount'] ?? 0), - 'coupon' => [ - 'code' => (string) ($result['coupon']['code'] ?? $coupon_code), - 'type' => (string) ($result['coupon']['type'] ?? ''), - 'value' => (float) ($result['coupon']['value'] ?? 0), - ], - ]; - } - - private function resolve_shipping_quote(array $items, string $shipping_method): array { - $methods = $this->shop->get_shipping_method_options(); - $method = $methods[$shipping_method] ?? $methods['express']; - - $base_fee = (float) ($method['fee'] ?? 0); - foreach ($items as $item) { - $base_fee += (float) ($item['shipping']['fee'] ?? 0); - } - - return [ - 'method' => $method, - 'fee' => round($base_fee, 2), - ]; - } - - private function resolve_checkout_address(int $user_id, int $address_id): array { - $addresses = $this->shop->get_address_book($user_id); - foreach ($addresses as $address) { - if ((int) ($address['id'] ?? 0) === $address_id) { - return $address; - } - } - - return $addresses[0] ?? []; - } - - private function resolve_payment_method(string $value): array { - $methods = $this->shop->get_payment_method_options(); - return $methods[$value] ?? reset($methods); - } - - public function get_checkout_hint(array $product): array { - return [ - 'delivery' => $product['shipping']['delivery_note'] ?: __('付款后自动进入订单中心,可继续查看物流和售后进度。', 'aksrt'), - 'service' => $product['after_sale']['policy'] ?: __('支持在订单中心发起售后申请。', 'aksrt'), - ]; - } -} diff --git a/src/AKSRT/Shop/Part1.php b/src/AKSRT/Shop/Part1.php deleted file mode 100644 index 53087750..00000000 --- a/src/AKSRT/Shop/Part1.php +++ /dev/null @@ -1,677 +0,0 @@ - false, 'message' => __('售后系统不可用。', 'aksrt')]; - } - - $order_number = sanitize_text_field((string) ($payload['order_number'] ?? '')); - $reason = sanitize_textarea_field((string) ($payload['reason'] ?? '')); - $type = sanitize_key((string) ($payload['type'] ?? \AKSRT\Payment\AfterSale::TYPE_REFUND)); - - if ($order_number === '' || $reason === '') { - return ['success' => false, 'message' => __('请填写售后原因。', 'aksrt')]; - } - - $order = $this->get_order_detail_view($user_id, $order_number); - if (!$order) { - return ['success' => false, 'message' => __('订单不存在。', 'aksrt')]; - } - - $ticket_id = \AKSRT\Payment\AfterSale::get_instance()->create([ - 'order_number' => $order_number, - 'applicant_user_id' => $user_id, - 'type' => $type, - 'reason' => $reason, - 'amount' => (float) ($payload['amount'] ?? $order['amount'] ?? 0), - 'note' => sanitize_textarea_field((string) ($payload['note'] ?? '')), - ]); - - if (is_wp_error($ticket_id)) { - return ['success' => false, 'message' => $ticket_id->get_error_message()]; - } - if ($ticket_id <= 0) { - return ['success' => false, 'message' => __('售后申请提交失败。', 'aksrt')]; - } - - return [ - 'success' => true, - 'data' => [ - 'ticket_id' => $ticket_id, - 'order' => $this->get_order_detail_view($user_id, $order_number), - ], - ]; - } - - /** - * List after-sale tickets for current user. - */ - public function get_user_after_sales(int $user_id): array { - if ($user_id <= 0 || !class_exists('\AKSRT\Payment\AfterSale')) { - return ['items' => []]; - } - $items = \AKSRT\Payment\AfterSale::get_instance()->get_user_tickets($user_id, 50); - return ['items' => $items]; - } - - /** - * User submits return shipping info. - */ - public function submit_after_sale_return(int $user_id, int $ticket_id, array $payload): array { - if ($user_id <= 0 || $ticket_id <= 0 || !class_exists('\AKSRT\Payment\AfterSale')) { - return ['success' => false, 'message' => __('售后系统不可用。', 'aksrt')]; - } - $carrier = sanitize_text_field((string) ($payload['carrier'] ?? '')); - $tracking = sanitize_text_field((string) ($payload['tracking_no'] ?? '')); - if ($carrier === '' || $tracking === '') { - return ['success' => false, 'message' => __('请填写快递公司与运单号。', 'aksrt')]; - } - $ok = \AKSRT\Payment\AfterSale::get_instance()->user_submit_return_shipping($ticket_id, $user_id, $carrier, $tracking); - if (!$ok) { - return ['success' => false, 'message' => __('提交退货物流失败,请确认工单状态。', 'aksrt')]; - } - $ticket = \AKSRT\Payment\AfterSale::get_instance()->get($ticket_id); - return [ - 'success' => true, - 'message' => __('退货物流已提交', 'aksrt'), - 'data' => $ticket ? \AKSRT\Payment\AfterSale::get_instance()->decorate_user_ticket($ticket) : ['id' => $ticket_id], - ]; - } - - /** - * User cancels after-sale ticket. - */ - public function cancel_after_sale(int $user_id, int $ticket_id, string $reason = ''): array { - if ($user_id <= 0 || $ticket_id <= 0 || !class_exists('\AKSRT\Payment\AfterSale')) { - return ['success' => false, 'message' => __('售后系统不可用。', 'aksrt')]; - } - $ok = \AKSRT\Payment\AfterSale::get_instance()->user_cancel($ticket_id, $user_id, $reason); - if (!$ok) { - return ['success' => false, 'message' => __('无法取消该售后工单。', 'aksrt')]; - } - return ['success' => true, 'message' => __('已取消售后申请', 'aksrt')]; - } - - public function get_product_archive_view(array $args = []): array { - $taxonomy = sanitize_key((string) ($args['taxonomy'] ?? '')); - $term_id = (int) ($args['term_id'] ?? 0); - $paged = max(1, (int) ($args['paged'] ?? $args['page'] ?? 1)); - - $shop_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_shop_settings() : []; - $list = is_array($shop_settings['list'] ?? null) ? $shop_settings['list'] : []; - $per_page = max(4, min(100, (int) ($args['per_page'] ?? ($list['count'] ?? 12)))); - - // Normalize orderby aliases (zibll + legacy AKSRT keys). - $orderby_raw = sanitize_key((string) ($args['orderby'] ?? ($list['orderby'] ?? 'date'))); - $order = strtoupper(sanitize_key((string) ($args['order'] ?? ''))); - if (!in_array($order, ['ASC', 'DESC'], true)) { - $order = ''; - } - // Legacy split price keys → price + order. - if ($orderby_raw === 'price_asc') { - $orderby_raw = 'price'; - $order = 'ASC'; - } elseif ($orderby_raw === 'price_desc') { - $orderby_raw = 'price'; - $order = 'DESC'; - } elseif ($orderby_raw === 'sales_volume') { - $orderby_raw = 'sales'; - } elseif ($orderby_raw === 'zibpay_price') { - $orderby_raw = 'price'; - } elseif ($orderby_raw === 'comments') { - $orderby_raw = 'score'; - } - $allowed_orderby = ['date', 'views', 'favorite_count', 'score', 'sales', 'price', 'modified']; - if (!in_array($orderby_raw, $allowed_orderby, true)) { - $orderby_raw = 'date'; - } - if ($orderby_raw === 'price' && $order === '') { - $order = 'ASC'; - } - if ($order === '' && $orderby_raw !== 'price') { - $order = 'DESC'; - } - $orderby = $orderby_raw; - - $paging = sanitize_key((string) ($args['paging'] ?? ($list['paging'] ?? 'number'))); - if (!in_array($paging, ['number', 'ajax'], true)) { - $paging = 'number'; - } - $ias_s = array_key_exists('ias_s', $args) ? !empty($args['ias_s']) : !empty($list['ias_s']); - $ias_max = max(1, min(50, (int) ($args['ias_max'] ?? ($list['ias_max'] ?? 5)))); - $style = sanitize_key((string) ($list['style'] ?? 'default')); - - $empty = [ - 'products' => [], - 'term' => null, - 'total' => 0, - 'max_pages' => 0, - 'page' => $paged, - 'orderby' => $orderby, - 'order' => $order, - 'paging' => $paging, - 'ias_s' => $ias_s, - 'ias_max' => $ias_max, - 'per_page' => $per_page, - 'style' => $style, - 'sales' => is_array($shop_settings['sales'] ?? null) ? $shop_settings['sales'] : [], - 'filter' => null, - ]; - - if (!in_array($taxonomy, ['aksrt_product_cat', 'aksrt_product_tag'], true) && empty($args['allow_all'])) { - return $empty; - } - - $query_args = [ - 'post_type' => 'aksrt_product', - 'posts_per_page' => $per_page, - 'paged' => $paged, - 'post_status' => 'publish', - 'order' => $order ?: 'DESC', - 'orderby' => 'date', - ]; - - $term = null; - if ($taxonomy !== '' && $term_id > 0) { - $term = get_term($term_id, $taxonomy); - if (is_wp_error($term) || !$term) { - return $empty; - } - $query_args['tax_query'] = [[ - 'taxonomy' => $taxonomy, - 'field' => 'term_id', - 'terms' => $term_id, - ]]; - } - - if ($orderby === 'sales') { - $query_args['meta_key'] = '_aksrt_sales'; - $query_args['orderby'] = 'meta_value_num'; - $query_args['order'] = 'DESC'; - } elseif ($orderby === 'price') { - $query_args['meta_key'] = '_aksrt_price'; - $query_args['orderby'] = 'meta_value_num'; - $query_args['order'] = $order === 'ASC' ? 'ASC' : 'DESC'; - } elseif ($orderby === 'favorite_count') { - $query_args['meta_key'] = '_aksrt_favorite_count'; - $query_args['orderby'] = 'meta_value_num'; - $query_args['order'] = 'DESC'; - } elseif ($orderby === 'views') { - $query_args['meta_key'] = '_aksrt_views'; - $query_args['orderby'] = 'meta_value_num'; - $query_args['order'] = 'DESC'; - } elseif ($orderby === 'score') { - $query_args['meta_key'] = '_aksrt_score_average'; - $query_args['orderby'] = 'meta_value_num'; - $query_args['order'] = 'DESC'; - } elseif ($orderby === 'modified') { - $query_args['orderby'] = 'modified'; - $query_args['order'] = 'DESC'; - } - - $query = new \WP_Query($query_args); - $products = []; - while ($query->have_posts()) { - $query->the_post(); - $products[] = $this->build_product_card_view(get_the_ID()); - } - wp_reset_postdata(); - - $filter = null; - if ($taxonomy === 'aksrt_product_cat' && $term) { - $filter = $this->build_product_cat_filter((int) $term->term_id, $orderby, $order); - } - - return [ - 'products' => $products, - 'term' => $term ? [ - 'id' => (int) $term->term_id, - 'name' => (string) $term->name, - 'description' => (string) $term->description, - 'count' => (int) $term->count, - 'taxonomy' => $taxonomy, - 'parent' => (int) $term->parent, - 'url' => get_term_link($term), - ] : null, - 'total' => (int) $query->found_posts, - 'max_pages' => (int) $query->max_num_pages, - 'page' => $paged, - 'orderby' => $orderby, - 'order' => $order, - 'paging' => $paging, - 'ias_s' => $ias_s, - 'ias_max' => $ias_max, - 'per_page' => $per_page, - 'style' => $style, - 'sales' => is_array($shop_settings['sales'] ?? null) ? $shop_settings['sales'] : [], - 'filter' => $filter, - ]; - } - - /** - * Hierarchical cat filter data (zibll zib_shop_get_cat_filter). - * - * @return array{siblings:array,children:array,parent_id:int,mode:string} - */ - public function build_product_cat_filter(int $cat_id, string $orderby = 'date', string $order = 'DESC'): array { - $taxonomy = 'aksrt_product_cat'; - $cat = get_term($cat_id, $taxonomy); - if (!$cat || is_wp_error($cat)) { - return ['siblings' => [], 'children' => [], 'parent_id' => 0, 'mode' => 'flat', 'orderby' => $orderby, 'order' => $order]; - } - - $map_term = static function ($t) use ($orderby, $order): array { - $url = get_term_link($t); - if (!is_wp_error($url) && $orderby) { - $url = add_query_arg(array_filter([ - 'orderby' => $orderby, - 'order' => $orderby === 'price' ? $order : null, - ]), $url); - } - return [ - 'id' => (int) $t->term_id, - 'name' => (string) $t->name, - 'url' => is_wp_error($url) ? '' : (string) $url, - 'count' => (int) $t->count, - ]; - }; - - $parent_id = (int) $cat->parent; - $children = get_terms([ - 'taxonomy' => $taxonomy, - 'parent' => $cat_id, - 'hide_empty' => false, - ]); - if (is_wp_error($children)) { - $children = []; - } - - if ($parent_id > 0) { - $siblings = get_terms([ - 'taxonomy' => $taxonomy, - 'parent' => $parent_id, - 'hide_empty' => false, - ]); - } else { - $siblings = get_terms([ - 'taxonomy' => $taxonomy, - 'parent' => 0, - 'hide_empty' => false, - ]); - } - if (is_wp_error($siblings)) { - $siblings = []; - } - - $mode = 'siblings'; - $parent_terms = []; - if (empty($children) && $parent_id > 0) { - $parent_term = get_term($parent_id, $taxonomy); - if ($parent_term && !is_wp_error($parent_term) && (int) $parent_term->parent > 0) { - $parent_terms = get_terms([ - 'taxonomy' => $taxonomy, - 'parent' => (int) $parent_term->parent, - 'hide_empty' => false, - ]); - if (is_wp_error($parent_terms)) { - $parent_terms = []; - } - $mode = 'parent_siblings'; - } elseif ($parent_id > 0) { - $parent_terms = get_terms([ - 'taxonomy' => $taxonomy, - 'parent' => 0, - 'hide_empty' => false, - ]); - if (is_wp_error($parent_terms)) { - $parent_terms = []; - } - $mode = 'parent_siblings'; - } - } elseif (!empty($children)) { - $mode = 'with_children'; - } - - $parent_url = ''; - if ($parent_id > 0) { - $plink = get_term_link($parent_id, $taxonomy); - if (!is_wp_error($plink)) { - $parent_url = add_query_arg(array_filter([ - 'orderby' => $orderby, - 'order' => $orderby === 'price' ? $order : null, - ]), $plink); - } - } - - return [ - 'mode' => $mode, - 'current_id' => $cat_id, - 'parent_id' => $parent_id, - 'parent_url' => $parent_url, - 'siblings' => array_map($map_term, empty($parent_terms) ? $siblings : $parent_terms), - 'children' => array_map($map_term, $mode === 'with_children' ? $children : ($mode === 'parent_siblings' ? $siblings : [])), - 'child_tabs' => array_map($map_term, $children), - 'orderby' => $orderby, - 'order' => $order, - ]; - } - - public function get_product_categories(): array { - $terms = get_terms([ - 'taxonomy' => 'aksrt_product_cat', - 'hide_empty' => true, - 'orderby' => 'count', - 'order' => 'DESC', - ]); - - if (is_wp_error($terms)) { - return []; - } - - return array_map(static function ($t) { - return [ - 'id' => $t->term_id, - 'name' => $t->name, - 'count' => $t->count, - 'url' => get_term_link($t), - ]; - }, $terms); - } - - /* ============================================================ - * Merchant / Store Mode (§6.10) - * ============================================================ */ - - public function get_store_profile(int $author_id): array { - if ($author_id <= 0) return []; - - $user = get_userdata($author_id); - if (!$user) return []; - - $product_count = $this->count_author_products($author_id); - $shop_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_shop_settings() : []; - $author_show = !isset($shop_settings['general']['shop_author_show']) || !empty($shop_settings['general']['shop_author_show']); - - if (!$author_show) return []; - - $contact = $this->get_store_contact($author_id); - - return [ - 'author_id' => $author_id, - 'name' => (string) $user->display_name, - 'avatar' => function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($author_id) : get_avatar_url($author_id), - 'description' => (string) get_the_author_meta('description', $author_id), - 'url' => get_author_posts_url($author_id), - 'product_count' => $product_count, - 'contact' => $contact, - 'addresses' => $this->get_address_book($author_id), - ]; - } - - public function get_store_products(int $author_id, int $page = 1, int $per_page = 12): array { - $query = new \WP_Query([ - 'post_type' => 'aksrt_product', - 'author' => $author_id, - 'posts_per_page' => $per_page, - 'paged' => $page, - 'post_status' => 'publish', - 'orderby' => 'date', - 'order' => 'DESC', - ]); - - $products = []; - while ($query->have_posts()) { - $query->the_post(); - $products[] = $this->build_product_card_view(get_the_ID()); - } - wp_reset_postdata(); - - return [ - 'items' => $products, - 'total' => (int) $query->found_posts, - 'max_pages' => (int) $query->max_num_pages, - ]; - } - - public function count_author_products(int $author_id): int { - global $wpdb; - return (int) $wpdb->get_var($wpdb->prepare( - "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'aksrt_product' AND post_status = 'publish' AND post_author = %d", - $author_id - )); - } - - private function get_store_contact(int $author_id): array { - $raw = get_user_meta($author_id, 'aksrt_shop_contact', true); - if (!is_array($raw)) return []; - - $contacts = []; - foreach ($raw as $c) { - $name = sanitize_text_field((string) ($c['name'] ?? '')); - if ($name === '') continue; - $contacts[] = [ - 'name' => $name, - 'icon' => sanitize_text_field((string) ($c['icon'] ?? '')), - 'link' => esc_url_raw((string) ($c['link'] ?? '')), - 'qr' => esc_url_raw((string) ($c['qr'] ?? '')), - ]; - } - return $contacts; - } - - public function get_stats(): array { - $product_count = 0; - $post_type_obj = get_post_type_object('aksrt_product'); - if ($post_type_obj) { - $counts = wp_count_posts('aksrt_product'); - $product_count = (int) ($counts->publish ?? 0); - } - - return [ - 'product_count' => $product_count, - 'order_count' => class_exists('\AKSRT\Payment\OrderRepository') ? \AKSRT\Payment\OrderRepository::get_instance()->get_stats()['total'] : 0, - 'revenue_total' => class_exists('\AKSRT\Payment\OrderRepository') ? \AKSRT\Payment\OrderRepository::get_instance()->get_stats()['revenue'] : 0, - ]; - } - - public function resolve_main_image(int $product_id): string { - return $this->product_service()->resolve_main_image($product_id); - } - - public function build_product_card_view(int $product_id): array { - return $this->product_service()->build_product_card_view($product_id); - } - - private function decorate_order_view(array $order, bool $with_detail = false): array { - $meta = $order['meta_payload'] ?? []; - $order_number = (string) ($order['order_number'] ?? ''); - - $order_items = $this->get_order_items($order_number); - $has_structured_items = !empty($order_items); - - $product = $has_structured_items - ? $this->build_product_card_view((int) ($order_items[0]['product_id'] ?? 0)) - : $this->build_product_card_view((int) ($order['product_id'] ?? 0)); - - $shipping_records = class_exists('\AKSRT\Payment\Shipping') ? \AKSRT\Payment\Shipping::get_instance()->get_by_order($order_number) : []; - $after_sale_records = class_exists('\AKSRT\Payment\AfterSale') ? \AKSRT\Payment\AfterSale::get_instance()->get_list([ - 'limit' => 20, - 'offset' => 0, - 'order_number' => $order_number, - ]) : ['items' => []]; - - $status = (string) ($order['status'] ?? \AKSRT\Payment\Order::STATUS_PENDING); - $subtotal = (float) ($meta['items'][0]['line_total'] ?? $order['amount']); - $shipping_fee = (float) ($meta['shipping_fee'] ?? 0); - $discount = (float) ($meta['discount'] ?? 0); - $expires_at = (string) ($order['expires_at'] ?? ''); - $payment_value = (string) ($meta['payment_method']['value'] ?? $order['payment_method'] ?? 'balance'); - - $view = [ - 'id' => (int) ($order['id'] ?? 0), - 'order_number' => (string) ($order['order_number'] ?? ''), - 'amount' => (float) ($order['amount'] ?? 0), - 'status' => $status, - 'status_label' => \AKSRT\Payment\Order::get_valid_statuses()[$status] ?? $status, - 'product_name' => $product['title'] ?? __('商品订单', 'aksrt'), - 'product' => $product, - 'created_at' => (string) ($order['created_at'] ?? ''), - 'paid_at' => (string) ($order['paid_at'] ?? ''), - 'expires_at' => $expires_at, - 'currency_symbol' => '¥', - 'subtotal' => $subtotal, - 'shipping_fee' => $shipping_fee, - 'discount' => $discount, - 'address' => $meta['address'] ?? [], - 'payment_method' => $meta['payment_method'] ?? ['value' => $order['payment_method'] ?? '', 'label' => __('站内余额', 'aksrt')], - 'coupon' => $meta['coupon'] ?? [], - 'items' => $has_structured_items ? $order_items : ($meta['items'] ?? []), - 'shipping_method' => $meta['shipping_method'] ?? [], - 'customer_note' => $meta['customer_note'] ?? '', - 'pay_url' => '#', - 'actions' => [ - 'can_pay' => in_array($status, [\AKSRT\Payment\Order::STATUS_PENDING, \AKSRT\Payment\Order::STATUS_EXPIRED], true) && $payment_value !== 'manual_review', - 'can_cancel' => in_array($status, [\AKSRT\Payment\Order::STATUS_PENDING, \AKSRT\Payment\Order::STATUS_CREATED, \AKSRT\Payment\Order::STATUS_EXPIRED], true), - 'can_after_sale' => in_array($status, [\AKSRT\Payment\Order::STATUS_PAID, \AKSRT\Payment\Order::STATUS_DELIVERED, \AKSRT\Payment\Order::STATUS_COMPLETED], true), - 'can_receipt' => $status === \AKSRT\Payment\Order::STATUS_DELIVERED, - ], - 'time_left' => $this->get_order_countdown($expires_at, $status), - ]; - - if ($with_detail) { - $view['shipping_records'] = $shipping_records; - $view['after_sale_records'] = $after_sale_records['items'] ?? []; - $view['timeline'] = $this->build_order_timeline($order, $shipping_records, $after_sale_records['items'] ?? []); - } - - return $view; - } - - /** - * Shop-side timeline; keep behaviour aligned with UserOrders. - * - * @param array $order - * @param array $shipping_records - * @param array $after_sale_records - * @return array - */ - private function build_order_timeline(array $order, array $shipping_records, array $after_sale_records): array { - // Prefer the unified order-center builder when available. - if (class_exists('\AKSRT\Payment\UserOrders')) { - $view = \AKSRT\Payment\UserOrders::get_instance()->decorate_order_view($order, true); - if (is_array($view['timeline'] ?? null)) { - return $view['timeline']; - } - } - - $timeline = []; - $created = (string) ($order['created_at'] ?? ''); - if ($created !== '') { - $timeline[] = ['key' => 'created', 'time' => $created, 'label' => __('订单创建', 'aksrt')]; - } - $status = (string) ($order['status'] ?? ''); - $paid_like = [ - \AKSRT\Payment\Order::STATUS_PAID, - \AKSRT\Payment\Order::STATUS_DELIVERED, - \AKSRT\Payment\Order::STATUS_COMPLETED, - \AKSRT\Payment\Order::STATUS_PENDING_REVIEW, - \AKSRT\Payment\Order::STATUS_REFUNDED, - \AKSRT\Payment\Order::STATUS_REFUNDING, - ]; - if (!empty($order['paid_at']) || in_array($status, $paid_like, true)) { - $timeline[] = [ - 'key' => 'paid', - 'time' => (string) ($order['paid_at'] ?? $created), - 'label' => __('完成支付', 'aksrt'), - ]; - } - foreach ($shipping_records as $record) { - $timeline[] = [ - 'key' => 'shipping', - 'time' => (string) ($record['created_at'] ?? ''), - 'label' => sprintf(__('物流更新:%s', 'aksrt'), $record['tracking_no'] ?? ''), - ]; - } - foreach ($after_sale_records as $record) { - $timeline[] = [ - 'key' => 'after_sale', - 'time' => (string) ($record['created_at'] ?? ''), - 'label' => sprintf(__('售后申请:%s', 'aksrt'), $record['reason'] ?? ''), - ]; - } - usort($timeline, static function ($a, $b) { - return strcmp((string) ($a['time'] ?? ''), (string) ($b['time'] ?? '')); - }); - return $timeline; - } - - private function get_order_countdown(string $expires_at, string $status): int { - if ($expires_at === '' || !in_array($status, [\AKSRT\Payment\Order::STATUS_PENDING, \AKSRT\Payment\Order::STATUS_EXPIRED], true)) { - return 0; - } - - $left = strtotime($expires_at) - time(); - return max(0, $left); - } - - public function get_limit_buy_config(int $product_id): array { - return $this->product_service()->get_limit_buy_config($product_id); - } - - public function get_product_services(int $product_id): array { - return $this->product_service()->get_product_services($product_id); - } - - public function get_product_params(int $product_id): array { - return $this->product_service()->get_product_params($product_id); - } - - public function get_address_book(int $user_id): array { - return $this->address_service()->get_address_book($user_id); - } - - public function add_address(int $user_id, array $data): array { - return $this->address_service()->add_address($user_id, $data); - } - - public function update_address(int $user_id, int $address_id, array $data): array { - return $this->address_service()->update_address($user_id, $address_id, $data); - } - - public function delete_address(int $user_id, int $address_id): array { - return $this->address_service()->delete_address($user_id, $address_id); - } - - public function set_default_address(int $user_id, int $address_id): array { - return $this->address_service()->set_default_address($user_id, $address_id); - } - - public function get_payment_method_options(): array { - return $this->product_service()->get_payment_method_options(); - } - - public function get_shipping_method_options(): array { - return $this->product_service()->get_shipping_method_options(); - } - - /* ============================================================ - * Product Variants / SKU - * ============================================================ */ - - public function get_variants(int $product_id): array { - return $this->product_service()->get_variants($product_id); - } - - public function get_variant(int $variant_id): ?array { - return $this->product_service()->get_variant($variant_id); - } -} diff --git a/src/AKSRT/Shop/Part2.php b/src/AKSRT/Shop/Part2.php deleted file mode 100644 index 1e14f9e0..00000000 --- a/src/AKSRT/Shop/Part2.php +++ /dev/null @@ -1,436 +0,0 @@ -product_service()->save_variants($product_id, $variants); - } - - public function delete_variant(int $variant_id): bool { - return $this->product_service()->delete_variant($variant_id); - } - - public function has_variants(int $product_id): bool { - return $this->product_service()->has_variants($product_id); - } - - /** - * Get the price range for a product (considering variants). - */ - public function get_price_range(int $product_id): array { - $base_price = (float) get_post_meta($product_id, '_aksrt_price', true); - $variants = $this->get_variants($product_id); - - if (empty($variants)) { - return ['min' => $base_price, 'max' => $base_price, 'has_variants' => false]; - } - - $prices = array_column($variants, 'price'); - $prices[] = $base_price; - $prices = array_filter($prices, fn($p) => $p > 0); - - if (empty($prices)) { - return ['min' => 0, 'max' => 0, 'has_variants' => true]; - } - - return [ - 'min' => min($prices), - 'max' => max($prices), - 'has_variants' => true, - ]; - } - - // ── R68: Fulfillment hook (§6.5) ── - - /** - * aksrt_order_paid listener: deduct stock + increment sales. - * Registered via \AKSRT\Core\Setup::init(). - */ - public function handle_order_paid(string $order_number, array $order): void { - if ($this->order_item_table() !== '') { - $items = $this->get_order_items($order_number); - if (!empty($items)) { - $this->handle_order_paid_enhanced($order_number, $order); - return; - } - } - - $note_raw = (string) ($order['note'] ?? ''); - $meta = json_decode($note_raw, true); - if (!is_array($meta)) { - $meta = []; - } - - $items = $meta['items'] ?? []; - if (empty($items) && !empty($order['product_id'])) { - $items = [ - [ - 'product_id' => (int) $order['product_id'], - 'quantity' => 1, - ], - ]; - } - - foreach ($items as $item) { - $pid = (int) ($item['product_id'] ?? 0); - $qty = max(1, (int) ($item['quantity'] ?? 1)); - if ($pid <= 0) { - continue; - } - - // Deduct stock (skip if unlimited, i.e. stock <= -1) - $stock = (int) get_post_meta($pid, '_aksrt_stock', true); - if ($stock > 0) { - $new_stock = max(0, $stock - $qty); - update_post_meta($pid, '_aksrt_stock', $new_stock); - } - - // Increment sales_volume - $sales = (int) get_post_meta($pid, '_aksrt_sales_volume', true); - update_post_meta($pid, '_aksrt_sales_volume', $sales + $qty); - } - - /** - * Fires after shop fulfillment (stock + sales). - * - * @param string $order_number - * @param array $order - * @param array $items Normalized item list - */ - do_action('aksrt_shop_fulfillment_done', $order_number, $order, $items); - } - - // ── R68: Confirm receipt (§6.5) ── - - /** - * User confirms receipt: delivered -> completed. - */ - public function confirm_receipt(int $user_id, string $order_number): array { - if ($user_id <= 0 || $order_number === '' || !class_exists('\AKSRT\Payment\OrderRepository')) { - return ['success' => false, 'message' => __('订单不存在。', 'aksrt')]; - } - - $repo = \AKSRT\Payment\OrderRepository::get_instance(); - $order = $repo->get_user_order($user_id, $order_number); - if (!$order) { - return ['success' => false, 'message' => __('订单不存在。', 'aksrt')]; - } - - $status = (string) ($order['status'] ?? ''); - if ($status !== \AKSRT\Payment\Order::STATUS_DELIVERED) { - return ['success' => false, 'message' => __('当前订单状态不可确认收货。', 'aksrt')]; - } - - $repo->update_status($order_number, \AKSRT\Payment\Order::STATUS_COMPLETED, [ - 'note' => __('用户确认收货。', 'aksrt'), - ]); - - return [ - 'success' => true, - 'data' => [ - 'order' => $this->get_order_detail_view($user_id, $order_number), - ], - ]; - } - - /** - * Auto-confirm cron: complete orders delivered > receipt_max_day ago. - */ - public function run_auto_confirm_cron(): void { - if (!class_exists('\AKSRT\Payment\OrderRepository') || !class_exists('\AKSRT\Settings\Settings')) { - return; - } - - $shop = \AKSRT\Settings\Settings::get_shop_settings(); - $max_day = (int) ($shop['timing']['receipt_max_day'] ?? 15); - if ($max_day <= 0) { - $max_day = 15; - } - - $cutoff = date('Y-m-d H:i:s', strtotime("-{$max_day} days")); - $repo = \AKSRT\Payment\OrderRepository::get_instance(); - $orders = $repo->get_orders_admin(['status' => \AKSRT\Payment\Order::STATUS_DELIVERED, 'limit' => 500]); - - foreach ($orders as $order) { - $delivered_at = (string) ($order['delivered_at'] ?? ''); - if ($delivered_at === '' || $delivered_at > $cutoff) { - continue; - } - $repo->update_status((string) $order['order_number'], \AKSRT\Payment\Order::STATUS_COMPLETED, [ - 'note' => sprintf(__('系统自动确认收货(超过 %d 天)。', 'aksrt'), $max_day), - ]); - } - } - - // ── R68: Order CSV export (§6.5) ── - - /** - * Export orders as CSV rows. Admin-only. - */ - public function export_orders_csv(array $args = []): array { - if (!class_exists('\AKSRT\Payment\OrderRepository')) { - return ['csv' => '', 'count' => 0]; - } - - $status = sanitize_key((string) ($args['status'] ?? '')); - $date_from = sanitize_text_field((string) ($args['date_from'] ?? '')); - $date_to = sanitize_text_field((string) ($args['date_to'] ?? '')); - $limit = max(1, min(5000, (int) ($args['limit'] ?? 1000))); - - $repo = \AKSRT\Payment\OrderRepository::get_instance(); - $orders = $repo->get_orders_admin(['status' => $status, 'limit' => $limit]); - - if ($date_from !== '') { - $orders = array_filter($orders, function ($o) use ($date_from) { - return ($o['created_at'] ?? '') >= $date_from . ' 00:00:00'; - }); - } - if ($date_to !== '') { - $orders = array_filter($orders, function ($o) use ($date_to) { - return ($o['created_at'] ?? '') <= $date_to . ' 23:59:59'; - }); - } - - $header = ['order_number', 'user_id', 'product_type', 'product_id', 'amount', 'currency', 'status', 'payment_method', 'created_at', 'paid_at']; - $rows = [implode(',', $header)]; - - foreach ($orders as $o) { - $rows[] = implode(',', [ - (string) ($o['order_number'] ?? ''), - (int) ($o['user_id'] ?? 0), - (string) ($o['product_type'] ?? ''), - (int) ($o['product_id'] ?? 0), - (float) ($o['amount'] ?? 0), - (string) ($o['currency'] ?? 'CNY'), - (string) ($o['status'] ?? ''), - (string) ($o['payment_method'] ?? ''), - (string) ($o['created_at'] ?? ''), - (string) ($o['paid_at'] ?? ''), - ]); - } - - $bom = "\xEF\xBB\xBF"; - $csv = $bom . implode("\r\n", $rows); - - return ['csv' => $csv, 'count' => count($orders)]; - } - - // ── R68: Configurable shipping/payment (§6.11) ── - - /** - * Get payment method options from settings, falling back to hardcoded defaults. - */ - public function get_payment_method_options_configurable(): array { - if (!class_exists('\AKSRT\Settings\Settings')) { - return $this->get_payment_method_options(); - } - $shop = \AKSRT\Settings\Settings::get_shop_settings(); - $enabled = (string) ($shop['product_defaults']['payment_methods'] ?? ''); - if ($enabled === '') { - return $this->get_payment_method_options(); - } - - $all = $this->get_payment_method_options(); - $keys = array_map('trim', explode(',', $enabled)); - $result = []; - foreach ($keys as $k) { - $k = sanitize_key($k); - if (isset($all[$k])) { - $result[$k] = $all[$k]; - } - } - - return $result ?: $this->get_payment_method_options(); - } - - /** - * Get shipping method options from settings, falling back to hardcoded defaults. - */ - public function get_shipping_method_options_configurable(): array { - if (!class_exists('\AKSRT\Settings\Settings')) { - return $this->get_shipping_method_options(); - } - $shop = \AKSRT\Settings\Settings::get_shop_settings(); - $enabled = (string) ($shop['product_defaults']['shipping_methods'] ?? ''); - $fee_override = (float) ($shop['product_defaults']['shipping_fee_fixed'] ?? 0); - if ($enabled === '' && $fee_override <= 0) { - return $this->get_shipping_method_options(); - } - - $all = $this->get_shipping_method_options(); - if ($enabled !== '') { - $keys = array_map('trim', explode(',', $enabled)); - $filtered = []; - foreach ($keys as $k) { - $k = sanitize_key($k); - if (isset($all[$k])) { - $filtered[$k] = $all[$k]; - } - } - $all = $filtered ?: $all; - } - - if ($fee_override > 0) { - foreach ($all as $k => &$v) { - if ($k !== 'download') { - $v['fee'] = $fee_override; - } - } - unset($v); - } - - return $all; - } - - /** - * Get after-sale reason options from settings. - */ - public function get_after_sale_reason_options(): array { - if (!class_exists('\AKSRT\Settings\Settings')) { - return ['refund' => [], 'return' => []]; - } - $shop = \AKSRT\Settings\Settings::get_shop_settings(); - $refund_raw = (string) ($shop['timing']['after_sale_reason_refund'] ?? ''); - $return_raw = (string) ($shop['timing']['after_sale_reason_return'] ?? ''); - - return [ - 'refund' => $this->parse_reason_lines($refund_raw), - 'return' => $this->parse_reason_lines($return_raw), - ]; - } - - /** - * Parse newline-separated reason text into indexed array. - */ - private function parse_reason_lines(string $text): array { - if ($text === '') { - return []; - } - $lines = array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $text))); - $result = []; - foreach (array_values($lines) as $i => $line) { - $result[] = [ - 'id' => $i + 1, - 'label' => $line, - ]; - } - return $result; - } - - /* ============================================================ - * Product Options (§6.2 选项组机制) - * - * Storage: post_meta '_aksrt_product_options' = [ - * [ 'name' => '颜色', 'view_mode' => 'list', 'values' => [ - * [ 'label' => '红色', 'price_change' => 0, 'image' => '' ], - * [ 'label' => '蓝色', 'price_change' => 10, 'image' => 'url' ], - * ]], - * ] - * - * options_key = "红色|M" (pipe-delimited selected labels per group) - * ============================================================ */ - - public function get_product_options(int $product_id): array { - $raw = get_post_meta($product_id, '_aksrt_product_options', true); - if (!is_array($raw) || empty($raw)) { - return []; - } - return array_values(array_filter(array_map([$this, 'sanitize_option_group'], $raw))); - } - - public function save_product_options(int $product_id, array $groups): void { - $sanitized = array_values(array_filter(array_map([$this, 'sanitize_option_group'], $groups))); - update_post_meta($product_id, '_aksrt_product_options', $sanitized); - update_post_meta($product_id, '_aksrt_has_options', count($sanitized) > 0 ? '1' : '0'); - } - - public function has_options(int $product_id): bool { - return get_post_meta($product_id, '_aksrt_has_options', true) === '1'; - } - - private function sanitize_option_group($group): array { - if (!is_array($group)) return []; - $name = sanitize_text_field((string) ($group['name'] ?? '')); - if ($name === '') return []; - - $view_mode_raw = $group['view_mode'] ?? ''; - $view_mode = in_array($view_mode_raw, ['list', 'img'], true) ? $view_mode_raw : 'list'; - $values = []; - foreach ((array) ($group['values'] ?? []) as $v) { - $label = sanitize_text_field((string) ($v['label'] ?? '')); - if ($label === '') continue; - $values[] = [ - 'label' => $label, - 'price_change' => round((float) ($v['price_change'] ?? 0), 2), - 'image' => esc_url_raw((string) ($v['image'] ?? '')), - ]; - } - if (empty($values)) return []; - - return ['name' => $name, 'view_mode' => $view_mode, 'values' => $values]; - } - - public function build_options_key(array $selections): string { - return implode('|', array_map('sanitize_text_field', $selections)); - } - - public function resolve_option_price(int $product_id, string $options_key): float { - $base = (float) get_post_meta($product_id, '_aksrt_price', true); - $start = (float) get_post_meta($product_id, '_aksrt_start_price', true); - if ($start > 0) { - $base = $start; - } - if ($options_key === '' || $options_key === '_default') { - return $base; - } - - $groups = $this->get_product_options($product_id); - $selections = explode('|', $options_key); - foreach ($groups as $i => $group) { - $selected_label = $selections[$i] ?? ''; - foreach ($group['values'] as $val) { - if ($val['label'] === $selected_label) { - $base += $val['price_change']; - break; - } - } - } - return max(0, round($base, 2)); - } - - public function resolve_option_labels(int $product_id, string $options_key): string { - if ($options_key === '' || $options_key === '_default') { - return ''; - } - $groups = $this->get_product_options($product_id); - $selections = explode('|', $options_key); - $parts = []; - foreach ($groups as $i => $group) { - $sel = $selections[$i] ?? ''; - if ($sel !== '') { - $parts[] = $group['name'] . ':' . $sel; - } - } - return implode(', ', $parts); - } - - public function resolve_option_stock(int $product_id, string $options_key): int { - $stock_type = (string) get_post_meta($product_id, '_aksrt_stock_type', true); - if ($stock_type === 'opts' && $options_key !== '' && $options_key !== '_default') { - $stock_matrix = get_post_meta($product_id, '_aksrt_stock_opts', true); - if (is_array($stock_matrix) && isset($stock_matrix[$options_key])) { - return (int) $stock_matrix[$options_key]; - } - return 0; - } - return (int) get_post_meta($product_id, '_aksrt_stock', true); - } -} diff --git a/src/AKSRT/Shop/Part3.php b/src/AKSRT/Shop/Part3.php deleted file mode 100644 index 37127959..00000000 --- a/src/AKSRT/Shop/Part3.php +++ /dev/null @@ -1,399 +0,0 @@ - 0) { - update_post_meta($product_id, '_aksrt_stock', max(0, $stock - $qty)); - } - } - - public function get_option_image(int $product_id, string $options_key): string { - if ($options_key === '' || $options_key === '_default') { - return ''; - } - $groups = $this->get_product_options($product_id); - $selections = explode('|', $options_key); - foreach ($groups as $i => $group) { - $sel = $selections[$i] ?? ''; - foreach ($group['values'] as $val) { - if ($val['label'] === $sel && !empty($val['image'])) { - return $val['image']; - } - } - } - return ''; - } - - /* ============================================================ - * Auto-delivery (§6.2 自动发货) - * - * Storage: post_meta '_aksrt_auto_delivery' = [ - * 'type' => 'fixed'|'invit_code'|'card_pass'|'opts', - * 'content' => '固定内容文本', - * 'opts' => [ options_key => '该选项的发货内容' ], - * ] - * ============================================================ */ - - public function get_auto_delivery_config(int $product_id): array { - $raw = get_post_meta($product_id, '_aksrt_auto_delivery', true); - if (!is_array($raw)) { - return ['type' => '', 'content' => '', 'opts' => []]; - } - return [ - 'type' => sanitize_key((string) ($raw['type'] ?? '')), - 'content' => (string) ($raw['content'] ?? ''), - 'opts' => is_array($raw['opts'] ?? null) ? $raw['opts'] : [], - ]; - } - - public function execute_auto_delivery(int $product_id, string $options_key, int $qty, int $user_id, string $order_number, int $order_item_id = 0): string { - $config = $this->get_auto_delivery_config($product_id); - $type = $config['type']; - - if ($type === 'fixed') { - return (string) $config['content']; - } - if ($type === 'opts') { - $key = ($options_key !== '' && $options_key !== '_default') ? $options_key : '_default'; - return (string) ($config['opts'][$key] ?? $config['content']); - } - if ($type === 'card_pass') { - return $this->consume_card_keys($product_id, $options_key, $qty, $user_id, $order_number, $order_item_id); - } - if ($type === 'invit_code') { - return $this->create_invite_codes($qty, $user_id, $order_number); - } - return ''; - } - - private function auto_delivery_succeeded(int $product_id, string $content, int $qty): bool { - $config = $this->get_auto_delivery_config($product_id); - if (in_array($config['type'], ['card_pass', 'invit_code'], true)) { - $lines = array_values(array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $content) ?: []))); - return count($lines) === max(1, $qty) - && !in_array($content, [__('卡密已售罄', 'aksrt'), __('邀请系统不可用', 'aksrt')], true); - } - return trim($content) !== ''; - } - - private function consume_card_keys(int $product_id, string $options_key, int $qty, int $user_id, string $order_number, int $order_item_id): string { - global $wpdb; - $table = \AKSRT\Core\DBMigrator::table('card_key'); - if ($table === '' || $order_item_id <= 0 || $wpdb->query('START TRANSACTION') === false) return ''; - - $where_opt = ''; - $params = [$product_id]; - if ($options_key !== '' && $options_key !== '_default') { - $where_opt = ' AND options_key = %s'; - $params[] = $options_key; - } else { - $where_opt = " AND (options_key = '' OR options_key = '_default')"; - } - $params[] = $qty; - - $keys = $wpdb->get_results($wpdb->prepare( - "SELECT id, card_code FROM {$table} WHERE product_id = %d AND status = 'available'{$where_opt} ORDER BY id ASC LIMIT %d FOR UPDATE", - ...$params - ), ARRAY_A); - if (count((array) $keys) !== $qty) { - $wpdb->query('ROLLBACK'); - return __('卡密已售罄', 'aksrt'); - } - - $codes = []; - $ids = []; - foreach ($keys as $key) { - $codes[] = (string) $key['card_code']; - $ids[] = (int) $key['id']; - } - $placeholders = implode(',', array_fill(0, count($ids), '%d')); - $updated = $wpdb->query($wpdb->prepare( - "UPDATE {$table} SET status = 'used', order_number = %s, used_by = %d, used_at = %s WHERE status = 'available' AND id IN ({$placeholders})", - array_merge([$order_number, $user_id, current_time('mysql')], $ids) - )); - $content = implode("\n", $codes); - $item_table = $this->order_item_table(); - $persisted = $item_table !== '' ? $wpdb->update( - $item_table, - ['auto_delivery_content' => $content], - ['id' => $order_item_id, 'order_number' => $order_number], - ['%s'], - ['%d', '%s'] - ) : false; - if ($updated !== $qty || $persisted === false || $wpdb->query('COMMIT') === false) { - $wpdb->query('ROLLBACK'); - return ''; - } - return $content; - } - - private function create_invite_codes(int $qty, int $user_id, string $order_number): string { - if (!class_exists('\AKSRT\Credits\Invite')) { - return __('邀请系统不可用', 'aksrt'); - } - $invite = \AKSRT\Credits\Invite::get_instance(); - $codes = []; - for ($i = 0; $i < $qty; $i++) { - $code = $invite->generate_code($user_id); - if ($code) { - $codes[] = $code; - } - } - return implode("\n", $codes); - } - - /* ============================================================ - * Order Items (§6.5 多商品订单子表) - * ============================================================ */ - - private function order_item_table(): string { - return \AKSRT\Core\DBMigrator::table('order_item'); - } - - public function save_order_items(string $order_number, array $items): void { - global $wpdb; - $table = $this->order_item_table(); - if ($table === '') return; - - foreach ($items as $item) { - $wpdb->insert($table, [ - 'order_number' => $order_number, - 'product_id' => (int) ($item['product_id'] ?? 0), - 'product_title' => sanitize_text_field((string) ($item['title'] ?? '')), - 'options_key' => sanitize_text_field((string) ($item['options_key'] ?? '')), - 'options_label' => sanitize_text_field((string) ($item['options_label'] ?? '')), - 'quantity' => max(1, (int) ($item['quantity'] ?? 1)), - 'unit_price' => (float) ($item['unit_price'] ?? $item['price'] ?? 0), - 'total_price' => (float) ($item['total_price'] ?? $item['line_total'] ?? 0), - 'discount_amount' => (float) ($item['discount_amount'] ?? 0), - 'shipping_fee' => (float) ($item['shipping_fee'] ?? 0), - 'pay_price' => (float) ($item['pay_price'] ?? $item['line_total'] ?? 0), - 'shipping_type' => sanitize_key((string) ($item['shipping_type'] ?? 'express')), - 'auto_delivery_content'=> ($item['auto_delivery_content'] ?? null), - 'discount_hit' => isset($item['applied_discounts']) ? wp_json_encode($item['applied_discounts']) : null, - 'gift_data' => isset($item['gifts']) ? wp_json_encode($item['gifts']) : null, - 'image' => esc_url_raw((string) ($item['image'] ?? '')), - ]); - } - } - - public function get_order_items(string $order_number): array { - global $wpdb; - $table = $this->order_item_table(); - if ($table === '') return []; - - $rows = $wpdb->get_results($wpdb->prepare( - "SELECT * FROM {$table} WHERE order_number = %s ORDER BY id ASC", - $order_number - ), ARRAY_A); - - return array_map(static function ($row) { - return [ - 'id' => (int) $row['id'], - 'product_id' => (int) $row['product_id'], - 'product_title' => (string) $row['product_title'], - 'options_key' => (string) $row['options_key'], - 'options_label' => (string) $row['options_label'], - 'quantity' => (int) $row['quantity'], - 'unit_price' => (float) $row['unit_price'], - 'total_price' => (float) $row['total_price'], - 'discount_amount' => (float) $row['discount_amount'], - 'shipping_fee' => (float) $row['shipping_fee'], - 'pay_price' => (float) $row['pay_price'], - 'shipping_type' => (string) $row['shipping_type'], - 'auto_delivery_content' => $row['auto_delivery_content'], - 'discount_hit' => json_decode($row['discount_hit'] ?? '', true) ?: [], - 'gift_data' => json_decode($row['gift_data'] ?? '', true) ?: [], - 'image' => (string) $row['image'], - ]; - }, (array) $rows); - } - - /* ============================================================ - * Enhanced Fulfillment (§6.5 履约链路) - * ============================================================ */ - - public function handle_order_paid_enhanced(string $order_number, array $order): void { - $items = $this->get_order_items($order_number); - - if (empty($items)) { - $this->handle_order_paid($order_number, $order); - return; - } - - $user_id = (int) ($order['user_id'] ?? 0); - $auto_delivery_parts = []; - $auto_delivery_successes = 0; - - foreach ($items as $item) { - $pid = (int) $item['product_id']; - $qty = max(1, (int) $item['quantity']); - $opt_key = (string) $item['options_key']; - if ($pid <= 0) continue; - - $this->deduct_option_stock($pid, $opt_key, $qty); - - $sales = (int) get_post_meta($pid, '_aksrt_sales_volume', true); - update_post_meta($pid, '_aksrt_sales_volume', $sales + $qty); - - $shipping_type = (string) get_post_meta($pid, '_aksrt_shipping_type', true); - if ($shipping_type === 'auto') { - $content = $this->execute_auto_delivery($pid, $opt_key, $qty, $user_id, $order_number, (int) $item['id']); - if ($content !== '') { - $auto_delivery_parts[] = $content; - $config = $this->get_auto_delivery_config($pid); - $persisted = $config['type'] === 'card_pass' - ? true - : $this->update_order_item_delivery($item['id'], $content); - if ($persisted && $this->auto_delivery_succeeded($pid, $content, $qty)) { - $auto_delivery_successes++; - } - } - } - } - - $this->process_rebate($order_number, $order, $items); - $this->process_gifts($order_number, $order, $items); - - if (!empty($auto_delivery_parts)) { - $this->send_auto_delivery_notification($user_id, $order_number, $auto_delivery_parts); - } - - if ($auto_delivery_successes === count($items) && class_exists('\AKSRT\Payment\OrderRepository')) { - \AKSRT\Payment\OrderRepository::get_instance()->update_status( - $order_number, - \AKSRT\Payment\Order::STATUS_COMPLETED, - ['note' => __('自动发货订单已完成。', 'aksrt')] - ); - } - - do_action('aksrt_shop_fulfillment_done', $order_number, $order, $items); - } - - private function update_order_item_delivery(int $item_id, string $content): bool { - global $wpdb; - $table = $this->order_item_table(); - if ($table === '') return false; - $updated = $wpdb->update($table, ['auto_delivery_content' => $content], ['id' => $item_id], ['%s'], ['%d']); - if ($updated === false) return false; - if ($updated === 0) { - $stored = $wpdb->get_var($wpdb->prepare("SELECT auto_delivery_content FROM {$table} WHERE id = %d", $item_id)); - return is_string($stored) && hash_equals($stored, $content); - } - return true; - } - - private function send_auto_delivery_notification(int $user_id, string $order_number, array $parts): void { - if (!class_exists('AKSRT_Notification_Service')) return; - - $content = sprintf( - __("订单 %s 已自动发货:\n\n%s", 'aksrt'), - $order_number, - implode("\n---\n", $parts) - ); - - AKSRT_Notification_Service::send($user_id, 'auto_delivery', $content, [ - 'order_number' => $order_number, - ]); - - if (function_exists('aksrt_send_event_email')) { - $user = get_userdata($user_id); - if ($user && !empty($user->user_email)) { - aksrt_send_event_email($user->user_email, sprintf(__('订单 %s 自动发货通知', 'aksrt'), $order_number), $content); - } - } - } - - private function process_rebate(string $order_number, array $order, array $items): void { - $user_id = (int) ($order['user_id'] ?? 0); - if ($user_id <= 0) return; - - $referrer_id = 0; - if (class_exists('\AKSRT\Credits\Invite')) { - $referrer_id = \AKSRT\Credits\Invite::get_instance()->get_referrer($user_id); - } - if ($referrer_id <= 0) return; - - $rebate_table = \AKSRT\Core\DBMigrator::table('rebate_log'); - if ($rebate_table === '') return; - - global $wpdb; - foreach ($items as $item) { - $pid = (int) $item['product_id']; - $pay_price = (float) $item['pay_price']; - if ($pid <= 0 || $pay_price <= 0) continue; - - $rebate_rate = (float) get_post_meta($pid, '_aksrt_rebate_rate', true); - if ($rebate_rate <= 0 && class_exists('AKSRT\Shop\TermMeta')) { - $resolved = TermMeta::resolve_inheritance($pid, 'rebate'); - $rebate_rate = (float) ($resolved ?: 0); - } - if ($rebate_rate <= 0) continue; - - $amount = round($pay_price * $rebate_rate / 100, 2); - if ($amount <= 0) continue; - - $wpdb->insert($rebate_table, [ - 'user_id' => $referrer_id, - 'order_number' => $order_number, - 'source_user_id' => $user_id, - 'amount' => $amount, - 'type' => 'order', - 'status' => 'pending', - 'created_at' => current_time('mysql'), - ]); - } - } - - private function process_gifts(string $order_number, array $order, array $items): void { - $user_id = (int) ($order['user_id'] ?? 0); - if ($user_id <= 0) return; - $claim_key = '_aksrt_shop_gifts_' . sanitize_key($order_number); - if (!add_user_meta($user_id, $claim_key, 'processing', true)) return; - - foreach ($items as $item) { - $gifts = $item['gift_data'] ?? []; - if (empty($gifts)) continue; - - foreach ($gifts as $gift) { - $gift_type = (string) ($gift['type'] ?? ''); - $gift_value = $gift['value'] ?? 0; - - if ($gift_type === 'credits' && class_exists('\AKSRT\Credits\Credits') && (int) $gift_value > 0) { - \AKSRT\Credits\Credits::add((int) $gift_value, $user_id, 'shop_gift', sprintf(__('订单 %s 赠品', 'aksrt'), $order_number)); - } elseif (in_array($gift_type, ['level_integral', 'experience'], true) && class_exists('\AKSRT\User\Level') && (int) $gift_value > 0) { - \AKSRT\User\Level::add($user_id, 'shop_gift', (int) $gift_value, true); - } elseif ($gift_type === 'vip_days' && (int) $gift_value > 0) { - $current = (int) get_user_meta($user_id, 'aksrt_vip_expires', true); - $base = max($current, time()); - update_user_meta($user_id, 'aksrt_vip_expires', $base + ((int) $gift_value * DAY_IN_SECONDS)); - // 赠送 VIP 天数时确保至少 VIP1;不降级已有 VIP2。 - $level = (int) get_user_meta($user_id, 'aksrt_vip_level', true); - if ($level < 1) { - update_user_meta($user_id, 'aksrt_vip_level', 1); - } - } - } - } - update_user_meta($user_id, $claim_key, '1'); - } -} diff --git a/src/AKSRT/Shop/ProductMetaBox.php b/src/AKSRT/Shop/ProductMetaBox.php deleted file mode 100644 index f314017e..00000000 --- a/src/AKSRT/Shop/ProductMetaBox.php +++ /dev/null @@ -1,341 +0,0 @@ -ID, '_aksrt_price', true); - $stock = (int) get_post_meta($post->ID, '_aksrt_stock', true); - $image = (string) get_post_meta($post->ID, '_aksrt_product_image', true); - $gallery = get_post_meta($post->ID, '_aksrt_product_gallery', true); - $gallery = is_array($gallery) ? $gallery : []; - $shipping_mode = (string) get_post_meta($post->ID, '_aksrt_shipping_mode', true); - $shipping_fee = (float) get_post_meta($post->ID, '_aksrt_shipping_fee', true); - $shipping_fee_type = (string) (get_post_meta($post->ID, '_aksrt_shipping_fee_type', true) ?: 'fixed'); - $shipping_fee_free = (float) get_post_meta($post->ID, '_aksrt_shipping_fee_free_amount', true); - $shipping_delivery = (string) get_post_meta($post->ID, '_aksrt_shipping_delivery_desc', true); - $shipping_tip = (string) get_post_meta($post->ID, '_aksrt_shipping_tip', true); - $after_sale_policy = (string) get_post_meta($post->ID, '_aksrt_after_sale_policy', true); - $support_refund = get_post_meta($post->ID, '_aksrt_support_refund', true); - $support_refund = $support_refund === '' ? '1' : (string) $support_refund; - $service = (string) get_post_meta($post->ID, '_aksrt_service', true); - $service_tip = (string) get_post_meta($post->ID, '_aksrt_service_tip', true); - $limit_buy = get_post_meta($post->ID, '_aksrt_limit_buy', true); - $limit_buy = $limit_buy === '' ? -1 : (int) $limit_buy; - $limit_buy_note = (string) get_post_meta($post->ID, '_aksrt_limit_buy_note', true); - $stock_type = (string) (get_post_meta($post->ID, '_aksrt_stock_type', true) ?: 'all'); - $pay_modo = (string) (get_post_meta($post->ID, '_aksrt_pay_modo', true) ?: '0'); - $start_price = (float) get_post_meta($post->ID, '_aksrt_start_price', true); - $product_rebate = (string) get_post_meta($post->ID, '_aksrt_product_rebate', true); - $product_params = (string) get_post_meta($post->ID, '_aksrt_product_params', true); - $user_required = (string) get_post_meta($post->ID, '_aksrt_user_required', true); - $delivery_note = (string) get_post_meta($post->ID, '_aksrt_delivery_note', true); - $marketing = (string) get_post_meta($post->ID, '_aksrt_marketing_headline', true); - - $shipping_modes = [ - '' => __('默认(继承设置)', 'aksrt'), - 'express' => __('快递物流', 'aksrt'), - 'auto' => __('自动发货', 'aksrt'), - 'manual' => __('手动发货', 'aksrt'), - 'virtual' => __('虚拟商品', 'aksrt'), - ]; - $stock_types = [ - 'all' => __('统一库存', 'aksrt'), - 'opts' => __('按选项/变体', 'aksrt'), - ]; - $fee_types = [ - 'free' => __('包邮', 'aksrt'), - 'fixed' => __('固定运费', 'aksrt'), - 'amount_free' => __('满额包邮', 'aksrt'), - ]; - ?> - -
-

-
-
- - -
-
- - -

-
-
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -

-
-
-
- - -
-
- -
- -

-
-
- -

-
- -
- -

-
-
-
- -
- -
-
-
- -
- -
-
- -

-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
- - -
-
- - -
-
- - -
-
- -
- -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-

- -

-
- post_type !== 'aksrt_product') { - return; - } - if (!current_user_can('edit_post', $post_id)) { - return; - } - - $price = isset($_POST['aksrt_price']) ? max(0, (float) wp_unslash($_POST['aksrt_price'])) : 0; - $stock = isset($_POST['aksrt_stock']) ? (int) wp_unslash($_POST['aksrt_stock']) : 0; - if ($stock < -1) { - $stock = -1; - } - - $image = isset($_POST['aksrt_product_image']) ? esc_url_raw((string) wp_unslash($_POST['aksrt_product_image'])) : ''; - $gallery_raw = isset($_POST['aksrt_product_gallery']) ? (string) wp_unslash($_POST['aksrt_product_gallery']) : ''; - $gallery = array_values(array_filter(array_map( - static function ($line) { - return esc_url_raw(trim((string) $line)); - }, - preg_split('/\r\n|\r|\n/', $gallery_raw) ?: [] - ))); - - $shipping_mode = isset($_POST['aksrt_shipping_mode']) ? sanitize_key((string) wp_unslash($_POST['aksrt_shipping_mode'])) : ''; - $shipping_fee = isset($_POST['aksrt_shipping_fee']) ? max(0, (float) wp_unslash($_POST['aksrt_shipping_fee'])) : 0; - $shipping_fee_type = isset($_POST['aksrt_shipping_fee_type']) ? sanitize_key((string) wp_unslash($_POST['aksrt_shipping_fee_type'])) : 'fixed'; - if (!in_array($shipping_fee_type, ['free', 'fixed', 'amount_free'], true)) { - $shipping_fee_type = 'fixed'; - } - $shipping_fee_free = isset($_POST['aksrt_shipping_fee_free_amount']) ? max(0, (float) wp_unslash($_POST['aksrt_shipping_fee_free_amount'])) : 0; - $shipping_delivery = isset($_POST['aksrt_shipping_delivery_desc']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_shipping_delivery_desc'])) : ''; - $shipping_tip = isset($_POST['aksrt_shipping_tip']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_shipping_tip'])) : ''; - $policy = isset($_POST['aksrt_after_sale_policy']) ? sanitize_textarea_field((string) wp_unslash($_POST['aksrt_after_sale_policy'])) : ''; - $support_refund = !empty($_POST['aksrt_support_refund']) ? '1' : '0'; - $service = isset($_POST['aksrt_service']) ? sanitize_textarea_field((string) wp_unslash($_POST['aksrt_service'])) : ''; - $service_tip = isset($_POST['aksrt_service_tip']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_service_tip'])) : ''; - $limit_buy = isset($_POST['aksrt_limit_buy']) ? max(-1, min(9999, (int) wp_unslash($_POST['aksrt_limit_buy']))) : -1; - $limit_buy_note = isset($_POST['aksrt_limit_buy_note']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_limit_buy_note'])) : ''; - $stock_type = isset($_POST['aksrt_stock_type']) ? sanitize_key((string) wp_unslash($_POST['aksrt_stock_type'])) : 'all'; - if (!in_array($stock_type, ['all', 'opts'], true)) { - $stock_type = 'all'; - } - $pay_modo = isset($_POST['aksrt_pay_modo']) ? sanitize_key((string) wp_unslash($_POST['aksrt_pay_modo'])) : '0'; - if (!in_array($pay_modo, ['0', 'points'], true)) { - $pay_modo = '0'; - } - $start_price = isset($_POST['aksrt_start_price']) ? max(0, (float) wp_unslash($_POST['aksrt_start_price'])) : 0; - $product_rebate = isset($_POST['aksrt_product_rebate']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_product_rebate'])) : ''; - $product_params = isset($_POST['aksrt_product_params']) ? sanitize_textarea_field((string) wp_unslash($_POST['aksrt_product_params'])) : ''; - $user_required = isset($_POST['aksrt_user_required']) ? sanitize_textarea_field((string) wp_unslash($_POST['aksrt_user_required'])) : ''; - $delivery_note = isset($_POST['aksrt_delivery_note']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_delivery_note'])) : ''; - $marketing = isset($_POST['aksrt_marketing_headline']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_marketing_headline'])) : ''; - - update_post_meta($post_id, '_aksrt_price', $price); - update_post_meta($post_id, '_aksrt_stock', $stock); - update_post_meta($post_id, '_aksrt_product_image', $image); - update_post_meta($post_id, '_aksrt_product_gallery', $gallery); - update_post_meta($post_id, '_aksrt_shipping_mode', $shipping_mode); - update_post_meta($post_id, '_aksrt_shipping_type', $shipping_mode); - update_post_meta($post_id, '_aksrt_shipping_fee', $shipping_fee); - update_post_meta($post_id, '_aksrt_shipping_fee_type', $shipping_fee_type); - update_post_meta($post_id, '_aksrt_shipping_fee_free_amount', $shipping_fee_free); - update_post_meta($post_id, '_aksrt_shipping_delivery_desc', $shipping_delivery); - update_post_meta($post_id, '_aksrt_shipping_tip', $shipping_tip); - update_post_meta($post_id, '_aksrt_after_sale_policy', $policy); - update_post_meta($post_id, '_aksrt_support_refund', $support_refund); - update_post_meta($post_id, '_aksrt_service', $service); - update_post_meta($post_id, '_aksrt_service_tip', $service_tip); - update_post_meta($post_id, '_aksrt_limit_buy', $limit_buy); - update_post_meta($post_id, '_aksrt_limit_buy_note', $limit_buy_note); - update_post_meta($post_id, '_aksrt_stock_type', $stock_type); - update_post_meta($post_id, '_aksrt_pay_modo', $pay_modo); - update_post_meta($post_id, '_aksrt_start_price', $start_price); - update_post_meta($post_id, '_aksrt_product_rebate', $product_rebate); - update_post_meta($post_id, '_aksrt_product_params', $product_params); - update_post_meta($post_id, '_aksrt_user_required', $user_required); - update_post_meta($post_id, '_aksrt_delivery_note', $delivery_note); - update_post_meta($post_id, '_aksrt_marketing_headline', $marketing); - - // Keep sales counter present for list columns. - if (get_post_meta($post_id, '_aksrt_sales', true) === '') { - update_post_meta($post_id, '_aksrt_sales', 0); - } - } -} diff --git a/src/AKSRT/Shop/ProductReview.php b/src/AKSRT/Shop/ProductReview.php deleted file mode 100644 index e5d8ca40..00000000 --- a/src/AKSRT/Shop/ProductReview.php +++ /dev/null @@ -1,512 +0,0 @@ - '超赞', - 4 => '很棒', - 3 => '不错', - 2 => '一般', - 1 => '较差', - 0 => '很差', - ]; - - public static function get_instance(): self { - if (null === self::$instance) { - self::$instance = new self(); - } - return self::$instance; - } - - public function init(): void { - add_action('aksrt_daily_cron', [$this, 'run_auto_positive_cron']); - } - - public function submit_review(int $user_id, string $order_number, int $product_id, array $data): array { - if ($user_id <= 0 || $order_number === '' || $product_id <= 0) { - return ['success' => false, 'message' => __('参数错误', 'aksrt')]; - } - - if (!$this->can_review($user_id, $order_number, $product_id)) { - return ['success' => false, 'message' => __('当前订单不可评价', 'aksrt')]; - } - - if ($this->has_reviewed($user_id, $order_number, $product_id)) { - return ['success' => false, 'message' => __('已评价过该商品', 'aksrt')]; - } - - $rating_product = max(0, min(5, (int) ($data['rating_product'] ?? 5))); - $rating_service = max(0, min(5, (int) ($data['rating_service'] ?? 5))); - $rating_shipping = max(0, min(5, (int) ($data['rating_shipping'] ?? 5))); - $average = round(($rating_product + $rating_service + $rating_shipping) / 3, 1); - - $content = sanitize_textarea_field((string) ($data['content'] ?? '')); - $images = []; - $max_images = $this->get_max_images(); - foreach ((array) ($data['images'] ?? []) as $img) { - if (count($images) >= $max_images) { - break; - } - $stored = $this->normalize_review_image((string) $img, $user_id); - if ($stored !== '') { - $images[] = $stored; - } - } - - $comment_id = wp_insert_comment([ - 'comment_post_ID' => $product_id, - 'user_id' => $user_id, - 'comment_content' => $content ?: __('好评!', 'aksrt'), - 'comment_type' => self::COMMENT_TYPE, - 'comment_approved' => 1, - ]); - - if (!$comment_id) { - return ['success' => false, 'message' => __('评价提交失败', 'aksrt')]; - } - - update_comment_meta($comment_id, '_aksrt_order_number', $order_number); - update_comment_meta($comment_id, '_aksrt_rating_product', $rating_product); - update_comment_meta($comment_id, '_aksrt_rating_service', $rating_service); - update_comment_meta($comment_id, '_aksrt_rating_shipping', $rating_shipping); - update_comment_meta($comment_id, '_aksrt_rating_average', $average); - if (!empty($images)) { - update_comment_meta($comment_id, '_aksrt_review_images', $images); - } - - $scores = $this->refresh_product_scores($product_id); - update_post_meta($product_id, '_aksrt_score_average', (float) ($scores['average'] ?? $average)); - - if (class_exists('\AKSRT\Payment\OrderRepository')) { - $order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number); - $status = (string) ($order['status'] ?? ''); - if ($status === \AKSRT\Payment\Order::STATUS_PENDING_REVIEW) { - \AKSRT\Payment\OrderRepository::get_instance()->update_status( - $order_number, - \AKSRT\Payment\Order::STATUS_COMPLETED, - ['note' => __('用户已评价', 'aksrt')] - ); - } - } - - return ['success' => true, 'comment_id' => $comment_id]; - } - - public function can_review(int $user_id, string $order_number, int $product_id): bool { - if (!class_exists('\AKSRT\Payment\OrderRepository')) return false; - - $order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number); - if (!$order || (int) ($order['user_id'] ?? 0) !== $user_id) return false; - - $status = (string) ($order['status'] ?? ''); - if (!in_array($status, [ - \AKSRT\Payment\Order::STATUS_COMPLETED, - \AKSRT\Payment\Order::STATUS_PENDING_REVIEW, - \AKSRT\Payment\Order::STATUS_DELIVERED, - ], true)) { - return false; - } - - if (class_exists('\AKSRT\Payment\AfterSale')) { - $tickets = \AKSRT\Payment\AfterSale::get_instance()->get_list([ - 'order_number' => $order_number, - 'limit' => 1, - ]); - foreach (($tickets['items'] ?? []) as $ticket) { - if (in_array(($ticket['status'] ?? ''), ['pending', 'processing'], true)) { - return false; - } - } - } - - return true; - } - - public function has_reviewed(int $user_id, string $order_number, int $product_id): bool { - $comments = get_comments([ - 'post_id' => $product_id, - 'user_id' => $user_id, - 'type' => self::COMMENT_TYPE, - 'number' => 1, - 'meta_query' => [ - ['key' => '_aksrt_order_number', 'value' => $order_number], - ], - ]); - return !empty($comments); - } - - public function get_reviews(int $product_id, array $args = []): array { - $page = max(1, (int) ($args['page'] ?? 1)); - $per_page = max(1, min(50, (int) ($args['per_page'] ?? 10))); - $filter = sanitize_key((string) ($args['filter'] ?? '')); - - $query_args = [ - 'post_id' => $product_id, - 'type' => self::COMMENT_TYPE, - 'status' => 'approve', - 'number' => $per_page, - 'offset' => ($page - 1) * $per_page, - 'orderby' => 'comment_date', - 'order' => 'DESC', - ]; - - if ($filter === 'good') { - $query_args['meta_query'] = [['key' => '_aksrt_rating_average', 'value' => 4, 'compare' => '>=', 'type' => 'NUMERIC']]; - } elseif ($filter === 'bad') { - $query_args['meta_query'] = [['key' => '_aksrt_rating_average', 'value' => 3, 'compare' => '<', 'type' => 'NUMERIC']]; - } elseif ($filter === 'has_image') { - $query_args['meta_query'] = [['key' => '_aksrt_review_images', 'compare' => 'EXISTS']]; - } - - $comments = get_comments($query_args); - $total = (int) get_comments(array_merge($query_args, ['count' => true, 'number' => 0, 'offset' => 0])); - - $items = []; - foreach ($comments as $comment) { - $cid = (int) $comment->comment_ID; - $items[] = [ - 'id' => $cid, - 'user_id' => (int) $comment->user_id, - 'user_name' => (string) $comment->comment_author, - 'user_avatar' => function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url((int) $comment->user_id) : get_avatar_url((int) $comment->user_id), - 'content' => (string) $comment->comment_content, - 'date' => (string) $comment->comment_date, - 'rating_product' => (int) get_comment_meta($cid, '_aksrt_rating_product', true), - 'rating_service' => (int) get_comment_meta($cid, '_aksrt_rating_service', true), - 'rating_shipping' => (int) get_comment_meta($cid, '_aksrt_rating_shipping', true), - 'rating_average' => (float) get_comment_meta($cid, '_aksrt_rating_average', true), - 'images' => (array) (get_comment_meta($cid, '_aksrt_review_images', true) ?: []), - 'order_number' => (string) get_comment_meta($cid, '_aksrt_order_number', true), - ]; - } - - return ['items' => $items, 'total' => $total, 'page' => $page, 'per_page' => $per_page]; - } - - public function get_product_scores(int $product_id): array { - $cached = get_post_meta($product_id, '_aksrt_review_scores', true); - if (is_array($cached)) { - return $cached; - } - return $this->refresh_product_scores($product_id); - } - - public function refresh_product_scores(int $product_id): array { - global $wpdb; - - $results = $wpdb->get_row($wpdb->prepare( - "SELECT COUNT(*) as total, - AVG(CAST(m.meta_value AS DECIMAL(3,1))) as average - FROM {$wpdb->comments} c - JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_rating_average' - WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1'", - $product_id, self::COMMENT_TYPE - ), ARRAY_A); - - $total = (int) ($results['total'] ?? 0); - $average = $total > 0 ? round((float) ($results['average'] ?? 0), 1) : 0; - - $good_count = (int) $wpdb->get_var($wpdb->prepare( - "SELECT COUNT(*) FROM {$wpdb->comments} c - JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_rating_average' - WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1' - AND CAST(m.meta_value AS DECIMAL(3,1)) >= 4", - $product_id, self::COMMENT_TYPE - )); - - $bad_count = (int) $wpdb->get_var($wpdb->prepare( - "SELECT COUNT(*) FROM {$wpdb->comments} c - JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_rating_average' - WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1' - AND CAST(m.meta_value AS DECIMAL(3,1)) < 3", - $product_id, self::COMMENT_TYPE - )); - - $has_image = (int) $wpdb->get_var($wpdb->prepare( - "SELECT COUNT(*) FROM {$wpdb->comments} c - JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_review_images' - WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1'", - $product_id, self::COMMENT_TYPE - )); - - $scores = [ - 'total' => $total, - 'average' => $average, - 'good' => $good_count, - 'bad' => $bad_count, - 'has_image' => $has_image, - 'level' => self::RATING_LEVELS[min(5, max(0, (int) round($average)))] ?? '', - ]; - - update_post_meta($product_id, '_aksrt_review_scores', $scores); - return $scores; - } - - public function run_auto_positive_cron(): void { - if (!class_exists('\AKSRT\Payment\OrderRepository') || !class_exists('\AKSRT\Settings\Settings')) { - return; - } - - $shop = \AKSRT\Settings\Settings::get_shop_settings(); - $max_day = max(1, (int) ($shop['timing']['comment_max_day'] ?? 15)); - $cutoff = date('Y-m-d H:i:s', strtotime("-{$max_day} days")); - - $repo = \AKSRT\Payment\OrderRepository::get_instance(); - $orders = $repo->get_orders_admin([ - 'status' => \AKSRT\Payment\Order::STATUS_PENDING_REVIEW, - 'limit' => 200, - ]); - - foreach ($orders as $order) { - $completed_at = (string) ($order['completed_at'] ?? $order['delivered_at'] ?? ''); - if ($completed_at === '' || $completed_at > $cutoff) { - continue; - } - - $user_id = (int) ($order['user_id'] ?? 0); - $order_number = (string) ($order['order_number'] ?? ''); - if ($user_id <= 0 || $order_number === '') continue; - - $items = class_exists('AKSRT\\Shop\\Shop') - ? Shop::get_instance()->get_order_items($order_number) - : []; - - $product_ids = array_column($items, 'product_id'); - if (empty($product_ids) && !empty($order['product_id'])) { - $product_ids = [(int) $order['product_id']]; - } - - foreach ($product_ids as $pid) { - if (!$this->has_reviewed($user_id, $order_number, (int) $pid)) { - $this->submit_review($user_id, $order_number, (int) $pid, [ - 'rating_product' => 5, - 'rating_service' => 5, - 'rating_shipping' => 5, - 'content' => __('系统默认好评', 'aksrt'), - ]); - } - } - - $repo->update_status($order_number, \AKSRT\Payment\Order::STATUS_COMPLETED, [ - 'note' => sprintf(__('超过 %d 天未评价,系统自动好评。', 'aksrt'), $max_day), - ]); - } - } - - /** - * Remaining seconds until auto-positive review, or 0 if unknown/closed. - */ - public function get_auto_review_remaining_seconds(string $order_number): int { - if (!class_exists('\AKSRT\Payment\OrderRepository') || !class_exists('\AKSRT\Settings\Settings')) { - return 0; - } - $order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number); - if (!$order) { - return 0; - } - $anchor = (string) ($order['delivered_at'] ?? $order['completed_at'] ?? $order['updated_at'] ?? $order['created_at'] ?? ''); - if ($anchor === '') { - return 0; - } - $shop = \AKSRT\Settings\Settings::get_shop_settings(); - $max_day = max(1, (int) ($shop['timing']['comment_max_day'] ?? 15)); - $deadline = strtotime($anchor . ' +' . $max_day . ' days'); - if (!$deadline) { - return 0; - } - $remain = $deadline - time(); - return $remain > 0 ? (int) $remain : 0; - } - - /** - * Whether shipping score row should be shown for this order. - */ - public function order_needs_shipping_score(array $order): bool { - $method = is_array($order['shipping_method'] ?? null) ? $order['shipping_method'] : []; - $value = strtolower((string) ($method['value'] ?? $method['type'] ?? $order['delivery_type'] ?? '')); - if ($value === '') { - // Prefer showing logistics when shipping records or fee exist. - if (!empty($order['shipping_records']) || !empty($order['shipping']['count'])) { - return true; - } - $fee = (float) ($order['shipping_fee'] ?? ($order['shipping_method']['fee'] ?? 0)); - return $fee > 0; - } - if (in_array($value, ['virtual', 'auto', 'none', 'offline', 'self', 'pickup', 'no_shipping'], true)) { - return false; - } - return true; - } - - /** - * Build order review modal HTML (zibll order_comment_modal parity). - * - * @return array{success:bool,html?:string,message?:string} - */ - public function get_review_modal_html(int $user_id, string $order_number): array { - if ($user_id <= 0) { - return ['success' => false, 'message' => __('请先登录', 'aksrt')]; - } - if ($order_number === '' || !class_exists('\AKSRT\Payment\OrderRepository')) { - return ['success' => false, 'message' => __('订单不存在', 'aksrt')]; - } - - $order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number); - if (!$order || (int) ($order['user_id'] ?? 0) !== $user_id) { - return ['success' => false, 'message' => __('订单数据异常', 'aksrt')]; - } - - $product_id = 0; - $items = class_exists('\AKSRT\Shop\Shop') - ? Shop::get_instance()->get_order_items($order_number) - : []; - if (!empty($items[0]['product_id'])) { - $product_id = (int) $items[0]['product_id']; - } elseif (!empty($order['product_id'])) { - $product_id = (int) $order['product_id']; - } - - if ($product_id <= 0) { - return ['success' => false, 'message' => __('订单商品无效', 'aksrt')]; - } - - if ($this->has_reviewed($user_id, $order_number, $product_id)) { - return ['success' => false, 'message' => __('订单已评价', 'aksrt')]; - } - - if (!$this->can_review($user_id, $order_number, $product_id)) { - return ['success' => false, 'message' => __('当前订单不可评价', 'aksrt')]; - } - - $shop = class_exists('\AKSRT\Settings\Settings') - ? \AKSRT\Settings\Settings::get_shop_settings() - : []; - $placeholder = (string) ($shop['timing']['comment_placeholder'] ?? ''); - if ($placeholder === '') { - $placeholder = __('展开说说对商品的看法吧~', 'aksrt'); - } - $img_num = max(0, min(9, (int) ($shop['timing']['comment_img_num'] ?? 3))); - $need_shipping = $this->order_needs_shipping_score($order); - $remain = $this->get_auto_review_remaining_seconds($order_number); - - $score_html = ''; - $score_html .= $this->render_score_row(__('商品评分', 'aksrt'), 'product'); - if ($need_shipping) { - $score_html .= $this->render_score_row(__('物流服务', 'aksrt'), 'shipping'); - } - $score_html .= $this->render_score_row(__('服务态度', 'aksrt'), 'service'); - - $img_html = ''; - if ($img_num > 0) { - $img_html = '
'; - $img_html .= '
' . esc_html__('图片', 'aksrt') . '
'; - $img_html .= '
'; - $img_html .= '
'; - $img_html .= '
'; - $img_html .= ''; - $img_html .= '
'; - } - - $title = '' . esc_html__('评价订单', 'aksrt') . ''; - if ($remain > 0) { - $title .= '' - . '' . esc_html($this->format_countdown($remain)) . '' - . esc_html__('后自动好评', 'aksrt') - . ''; - } - - $html = '
' . $title . '
'; - $html .= ''; - $html .= '
'; - $html .= '
' . $score_html . '
'; - $html .= '
'; - $html .= $img_html; - $html .= ''; - $html .= '
'; - - return [ - 'success' => true, - 'html' => $html, - 'product_id' => $product_id, - 'need_shipping' => $need_shipping, - 'img_max' => $img_num, - 'remain_seconds' => $remain, - ]; - } - - private function render_score_row(string $label, string $key): string { - return '
' - . '
' . esc_html($label) . '
' - . '
' - . '
' - . '' - . '
'; - } - - private function format_countdown(int $seconds): string { - $seconds = max(0, $seconds); - $d = intdiv($seconds, 86400); - $h = intdiv($seconds % 86400, 3600); - $m = intdiv($seconds % 3600, 60); - $s = $seconds % 60; - if ($d > 0) { - return sprintf('%d天%02d:%02d:%02d', $d, $h, $m, $s); - } - return sprintf('%02d:%02d:%02d', $h, $m, $s); - } - - /** - * Accept https URL or data:image/*;base64,... and return a public URL. - */ - private function normalize_review_image(string $raw, int $user_id): string { - $raw = trim($raw); - if ($raw === '') { - return ''; - } - if (preg_match('#^https?://#i', $raw)) { - return esc_url_raw($raw); - } - if (!preg_match('#^data:image/(png|jpe?g|gif|webp);base64,#i', $raw, $m)) { - return ''; - } - $binary = base64_decode(substr($raw, strpos($raw, ',') + 1), true); - if ($binary === false || $binary === '') { - return ''; - } - if (strlen($binary) > 5 * 1024 * 1024) { - return ''; - } - $ext = strtolower($m[1]); - if ($ext === 'jpeg') { - $ext = 'jpg'; - } - $filename = 'review-' . $user_id . '-' . wp_generate_password(8, false) . '.' . $ext; - $upload = wp_upload_bits($filename, null, $binary); - if (!empty($upload['error']) || empty($upload['url'])) { - return ''; - } - return esc_url_raw((string) $upload['url']); - } - - private function get_max_images(): int { - if (!class_exists('\AKSRT\Settings\Settings')) { - return 9; - } - $shop = \AKSRT\Settings\Settings::get_shop_settings(); - return max(0, min(20, (int) ($shop['timing']['comment_img_num'] ?? 9))); - } -} diff --git a/src/AKSRT/Shop/ProductService.php b/src/AKSRT/Shop/ProductService.php deleted file mode 100644 index 908a1957..00000000 --- a/src/AKSRT/Shop/ProductService.php +++ /dev/null @@ -1,356 +0,0 @@ -shop = $shop; - } - - /* ============================================================ - * Product meta helpers - * ============================================================ */ - - public function get_term_options(string $taxonomy): array { - $terms = get_terms([ - 'taxonomy' => $taxonomy, - 'hide_empty' => false, - 'orderby' => 'name', - 'order' => 'ASC', - ]); - - if (is_wp_error($terms) || empty($terms)) { - return []; - } - - return array_map(static function ($term) { - return [ - 'id' => (int) $term->term_id, - 'name' => (string) $term->name, - ]; - }, $terms); - } - - public function get_product_gallery(int $product_id): array { - $gallery = get_post_meta($product_id, '_aksrt_product_gallery', true); - if (!is_array($gallery)) { - return []; - } - - return array_values(array_filter(array_map('esc_url_raw', $gallery))); - } - - public function get_product_shipping_profile(int $product_id): array { - return [ - 'mode' => sanitize_key((string) get_post_meta($product_id, '_aksrt_shipping_mode', true)) ?: 'express', - 'fee' => (float) get_post_meta($product_id, '_aksrt_shipping_fee', true), - 'delivery_note' => (string) get_post_meta($product_id, '_aksrt_delivery_note', true), - ]; - } - - public function get_product_after_sale_profile(int $product_id): array { - return [ - 'policy' => (string) get_post_meta($product_id, '_aksrt_after_sale_policy', true), - 'support_refund' => get_post_meta($product_id, '_aksrt_support_refund', true) !== '0', - ]; - } - - public function get_product_marketing_profile(int $product_id): array { - return [ - 'headline' => (string) get_post_meta($product_id, '_aksrt_marketing_headline', true), - 'shipping_tip' => (string) get_post_meta($product_id, '_aksrt_shipping_tip', true), - 'service_tip' => (string) get_post_meta($product_id, '_aksrt_service_tip', true), - ]; - } - - public function get_payment_method_options(): array { - $methods = []; - - if (class_exists('\AKSRT\Payment\Gateway')) { - foreach (\AKSRT\Payment\Gateway::get_instance()->get_all() as $id => $gateway) { - if (!method_exists($gateway, 'is_configured') || !$gateway->is_configured()) { - continue; - } - $methods[$id] = [ - 'value' => $id, - 'label' => method_exists($gateway, 'get_name') ? (string) $gateway->get_name() : $id, - 'description' => $this->gateway_method_description((string) $id), - ]; - } - } - - if (class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::credits_enabled()) { - $methods['points'] = [ - 'value' => 'points', - 'label' => __('积分兑换', 'aksrt'), - 'description' => __('使用积分购买积分商品。', 'aksrt'), - ]; - } - - // Fallback only when no configured gateways (and no points). - if (empty($methods)) { - $methods['manual_review'] = [ - 'value' => 'manual_review', - 'label' => __('人工确认', 'aksrt'), - 'description' => __('暂无可用支付网关,保留待支付订单由管理员确认。', 'aksrt'), - ]; - } - - return $methods; - } - - private function gateway_method_description(string $id): string { - switch ($id) { - case 'balance': - return __('即时完成支付并写回订单状态。', 'aksrt'); - case 'alipay': - return __('跳转支付宝完成支付。', 'aksrt'); - case 'wechatpay': - return __('微信扫码或跳转完成支付。', 'aksrt'); - case 'paypal': - return __('跳转 PayPal 完成支付。', 'aksrt'); - case 'stripe': - return __('跳转 Stripe Checkout 完成支付。', 'aksrt'); - case 'epay_alipay': - return __('经易支付跳转支付宝完成支付。', 'aksrt'); - case 'epay_wxpay': - return __('经易支付跳转微信完成支付。', 'aksrt'); - default: - return ''; - } - } - - public function get_shipping_method_options(): array { - return [ - 'express' => [ - 'value' => 'express', - 'label' => __('快递配送', 'aksrt'), - 'description' => __('标准快递,支持物流跟踪。', 'aksrt'), - 'fee' => 12.0, - ], - 'ems' => [ - 'value' => 'ems', - 'label' => __('EMS', 'aksrt'), - 'description' => __('偏远地区可选。', 'aksrt'), - 'fee' => 18.0, - ], - 'download' => [ - 'value' => 'download', - 'label' => __('自动发货', 'aksrt'), - 'description' => __('适用于数字商品。', 'aksrt'), - 'fee' => 0.0, - ], - ]; - } - - public function get_limit_buy_config(int $product_id): array { - $raw = get_post_meta($product_id, '_aksrt_limit_buy_config', true); - if (!is_array($raw)) { - $legacy = (int) get_post_meta($product_id, '_aksrt_limit_buy', true); - return ['default' => $legacy > 0 ? $legacy : -1, 'desc' => '']; - } - return [ - 'default' => (int) ($raw['default'] ?? -1), - 'vip' => (int) ($raw['vip'] ?? -1), - 'vip2' => (int) ($raw['vip2'] ?? -1), - 'auth' => (int) ($raw['auth'] ?? -1), - 'desc' => sanitize_text_field((string) ($raw['desc'] ?? '')), - ]; - } - - public function get_product_services(int $product_id): array { - $raw = get_post_meta($product_id, '_aksrt_services', true); - if (!is_array($raw)) return []; - return array_values(array_filter(array_map(static function ($s) { - $name = sanitize_text_field((string) ($s['name'] ?? '')); - if ($name === '') return null; - return [ - 'name' => $name, - 'desc' => sanitize_text_field((string) ($s['desc'] ?? '')), - 'icon' => esc_url_raw((string) ($s['icon'] ?? '')), - ]; - }, $raw))); - } - - public function get_product_params(int $product_id): array { - $raw = get_post_meta($product_id, '_aksrt_product_params', true); - if (!is_array($raw)) return []; - return array_values(array_filter(array_map(static function ($p) { - $name = sanitize_text_field((string) ($p['name'] ?? '')); - if ($name === '') return null; - return [ - 'name' => $name, - 'value' => sanitize_text_field((string) ($p['value'] ?? '')), - ]; - }, $raw))); - } - - /** - * Resolve product main image with 3-tier fallback: - * 1. _aksrt_product_image (custom main image) - * 2. WordPress featured image (post thumbnail) - * 3. First gallery image from _aksrt_product_gallery - */ - public function resolve_main_image(int $product_id): string { - $custom = (string) get_post_meta($product_id, '_aksrt_product_image', true); - if ($custom !== '') { - return esc_url_raw($custom); - } - $thumb = get_the_post_thumbnail_url($product_id, 'large'); - if ($thumb) { - return (string) $thumb; - } - $gallery = get_post_meta($product_id, '_aksrt_product_gallery', true); - if (is_array($gallery) && !empty($gallery)) { - $first = reset($gallery); - if ($first) { - return esc_url_raw((string) $first); - } - } - return ''; - } - - public function build_product_card_view(int $product_id): array { - $post = get_post($product_id); - if (!$post || $post->post_type !== 'aksrt_product') { - return []; - } - - $stock_raw = (int) get_post_meta($product_id, '_aksrt_stock', true); - $is_unlimited = $stock_raw <= -1; - - return [ - 'id' => $product_id, - 'title' => (string) get_the_title($product_id), - 'excerpt' => (string) get_the_excerpt($product_id), - 'price' => (float) get_post_meta($product_id, '_aksrt_price', true), - 'image' => $this->resolve_main_image($product_id), - 'stock' => $stock_raw, - 'is_unlimited' => $is_unlimited, - 'sales' => (int) get_post_meta($product_id, '_aksrt_sales', true), - 'status' => (string) get_post_status($product_id), - 'created' => get_the_date('Y-m-d H:i', $product_id), - 'permalink' => get_permalink($product_id) ?: '', - ]; - } - - /* ============================================================ - * Product Variants / SKU - * ============================================================ */ - - /** - * Get variants table name. - */ - public function variants_table(): string { - global $wpdb; - return $wpdb->prefix . 'aksrt_product_variant'; - } - - /** - * Get all variants for a product. - */ - public function get_variants(int $product_id): array { - global $wpdb; - $rows = $wpdb->get_results($wpdb->prepare( - "SELECT * FROM {$this->variants_table()} WHERE product_id = %d ORDER BY sort_order ASC, id ASC", - $product_id - ), ARRAY_A); - - if (!$rows) return []; - - return array_map(function ($row) { - $row['id'] = (int) $row['id']; - $row['product_id'] = (int) $row['product_id']; - $row['price'] = (float) $row['price']; - $row['stock'] = (int) $row['stock']; - $row['sort_order'] = (int) $row['sort_order']; - $row['attributes'] = json_decode($row['attributes'] ?? '[]', true) ?: []; - return $row; - }, $rows); - } - - /** - * Get a single variant by ID. - */ - public function get_variant(int $variant_id): ?array { - global $wpdb; - $row = $wpdb->get_row($wpdb->prepare( - "SELECT * FROM {$this->variants_table()} WHERE id = %d", - $variant_id - ), ARRAY_A); - if (!$row) return null; - $row['id'] = (int) $row['id']; - $row['product_id'] = (int) $row['product_id']; - $row['price'] = (float) $row['price']; - $row['stock'] = (int) $row['stock']; - $row['attributes'] = json_decode($row['attributes'] ?? '[]', true) ?: []; - return $row; - } - - /** - * Save variants for a product (replace all). - */ - public function save_variants(int $product_id, array $variants): array { - global $wpdb; - $table = $this->variants_table(); - - // Delete existing variants - $wpdb->delete($table, ['product_id' => $product_id]); - - $saved = []; - foreach ($variants as $index => $v) { - $data = [ - 'product_id' => $product_id, - 'sku' => sanitize_text_field((string) ($v['sku'] ?? '')), - 'attributes' => wp_json_encode($v['attributes'] ?? []), - 'price' => max(0, (float) ($v['price'] ?? 0)), - 'stock' => max(0, (int) ($v['stock'] ?? 0)), - 'image' => esc_url_raw((string) ($v['image'] ?? '')), - 'sort_order' => $index, - 'created_at' => current_time('mysql', true), - ]; - $wpdb->insert($table, $data); - $saved[] = array_merge($data, ['id' => (int) $wpdb->insert_id]); - } - - // Update product meta flag - update_post_meta($product_id, '_aksrt_has_variants', count($saved) > 0 ? '1' : '0'); - - return $saved; - } - - /** - * Delete a single variant. - */ - public function delete_variant(int $variant_id): bool { - global $wpdb; - $variant = $this->get_variant($variant_id); - if (!$variant) return false; - - $result = $wpdb->delete($this->variants_table(), ['id' => $variant_id]); - if ($result) { - // Update product meta flag - $remaining = $wpdb->get_var($wpdb->prepare( - "SELECT COUNT(*) FROM {$this->variants_table()} WHERE product_id = %d", - $variant['product_id'] - )); - update_post_meta($variant['product_id'], '_aksrt_has_variants', $remaining > 0 ? '1' : '0'); - } - return (bool) $result; - } - - /** - * Check if a product has variants. - */ - public function has_variants(int $product_id): bool { - return get_post_meta($product_id, '_aksrt_has_variants', true) === '1'; - } -} diff --git a/src/AKSRT/Shop/ProductVariantsMetaBox.php b/src/AKSRT/Shop/ProductVariantsMetaBox.php deleted file mode 100644 index 5d39c468..00000000 --- a/src/AKSRT/Shop/ProductVariantsMetaBox.php +++ /dev/null @@ -1,188 +0,0 @@ -get_variants((int) $post->ID); - } - - if (!$variants) { - $variants = [[ - 'sku' => '', - 'attributes' => [], - 'price' => 0, - 'stock' => 0, - 'image' => '', - ]]; - } - ?> - -

- -

- - - - - - - - - - - - $row) : - $attrs = $row['attributes'] ?? []; - $attrs_text = ''; - if (is_array($attrs)) { - $parts = []; - foreach ($attrs as $k => $v) { - if (is_int($k) && is_array($v)) { - $parts[] = ($v['name'] ?? $v['key'] ?? '') . ':' . ($v['value'] ?? ''); - } else { - $parts[] = $k . ':' . $v; - } - } - $attrs_text = implode(';', array_filter($parts)); - } elseif (is_string($attrs)) { - $attrs_text = $attrs; - } - ?> - - - - - - - - - -
-

- -

- - post_type !== 'aksrt_product' || !current_user_can('edit_post', $post_id)) { - return; - } - if (!class_exists('AKSRT\\Shop\\Shop')) { - return; - } - - $skus = isset($_POST['aksrt_variant_sku']) && is_array($_POST['aksrt_variant_sku']) ? wp_unslash($_POST['aksrt_variant_sku']) : []; - $attrs = isset($_POST['aksrt_variant_attrs']) && is_array($_POST['aksrt_variant_attrs']) ? wp_unslash($_POST['aksrt_variant_attrs']) : []; - $prices = isset($_POST['aksrt_variant_price']) && is_array($_POST['aksrt_variant_price']) ? wp_unslash($_POST['aksrt_variant_price']) : []; - $stocks = isset($_POST['aksrt_variant_stock']) && is_array($_POST['aksrt_variant_stock']) ? wp_unslash($_POST['aksrt_variant_stock']) : []; - $images = isset($_POST['aksrt_variant_image']) && is_array($_POST['aksrt_variant_image']) ? wp_unslash($_POST['aksrt_variant_image']) : []; - - $variants = []; - $count = max(count($skus), count($attrs), count($prices), count($stocks), count($images)); - for ($i = 0; $i < $count; $i++) { - $sku = sanitize_text_field((string) ($skus[$i] ?? '')); - $attr_raw = sanitize_text_field((string) ($attrs[$i] ?? '')); - $price = max(0, (float) ($prices[$i] ?? 0)); - $stock = max(0, (int) ($stocks[$i] ?? 0)); - $image = esc_url_raw((string) ($images[$i] ?? '')); - - if ($sku === '' && $attr_raw === '' && $price <= 0 && $stock <= 0 && $image === '') { - continue; - } - - $attributes = []; - if ($attr_raw !== '') { - foreach (preg_split('/\s*;\s*/', $attr_raw) ?: [] as $pair) { - if ($pair === '' || !str_contains($pair, ':')) { - continue; - } - [$k, $v] = array_map('trim', explode(':', $pair, 2)); - if ($k !== '') { - $attributes[$k] = $v; - } - } - } - - $variants[] = [ - 'sku' => $sku, - 'attributes' => $attributes, - 'price' => $price, - 'stock' => $stock, - 'image' => $image, - ]; - } - - Shop::get_instance()->save_variants($post_id, $variants); - } -} diff --git a/src/AKSRT/Shop/Shop.php b/src/AKSRT/Shop/Shop.php deleted file mode 100644 index b84e48a6..00000000 --- a/src/AKSRT/Shop/Shop.php +++ /dev/null @@ -1,397 +0,0 @@ -cart_svc) { - $this->cart_svc = new CartService($this); - } - return $this->cart_svc; - } - - private function checkout_service(): CheckoutService { - if (!$this->checkout_svc) { - $this->checkout_svc = new CheckoutService($this); - } - return $this->checkout_svc; - } - - private function address_service(): AddressService { - if (!$this->address_svc) { - $this->address_svc = new AddressService($this); - } - return $this->address_svc; - } - - private function product_service(): ProductService { - if ($this->product_svc === null) { - $this->product_svc = new ProductService($this); - } - return $this->product_svc; - } - - public function get_config(): array { - if (class_exists('\AKSRT\Settings\Settings')) { - return \AKSRT\Settings\Settings::get_shop_settings(); - } - - return ['enabled' => false]; - } - - public function get_products(array $args = []): array { - $limit = max(1, (int) ($args['limit'] ?? 20)); - $offset = max(0, (int) ($args['offset'] ?? 0)); - $search = sanitize_text_field((string) ($args['search'] ?? '')); - $status = sanitize_key((string) ($args['status'] ?? '')); - - $query_args = [ - 'post_type' => 'aksrt_product', - 'posts_per_page' => $limit, - 'offset' => $offset, - 'post_status' => $status !== '' ? $status : ['publish', 'draft', 'pending'], - 'orderby' => 'date', - 'order' => 'DESC', - ]; - - if ($search !== '') { - $query_args['s'] = $search; - } - - $query = new \WP_Query($query_args); - $products = []; - - while ($query->have_posts()) { - $query->the_post(); - $products[] = $this->build_product_card_view(get_the_ID()); - } - - wp_reset_postdata(); - - return $products; - } - - public function get_product_detail_view(int $product_id): array { - $product = get_post($product_id); - if (!$product || $product->post_type !== 'aksrt_product') { - return []; - } - - $detail = $this->build_product_card_view($product_id); - $detail['content'] = (string) $product->post_content; - $detail['excerpt'] = (string) $product->post_excerpt; - $detail['permalink'] = get_permalink($product_id) ?: ''; - $detail['status'] = (string) get_post_status($product_id); - $detail['gallery'] = $this->product_service()->get_product_gallery($product_id); - $detail['categories'] = wp_get_post_terms($product_id, 'aksrt_product_cat', ['fields' => 'all']) ?: []; - $detail['tags'] = wp_get_post_terms($product_id, 'aksrt_product_tag', ['fields' => 'all']) ?: []; - $detail['shipping'] = $this->product_service()->get_product_shipping_profile($product_id); - $detail['after_sale'] = $this->product_service()->get_product_after_sale_profile($product_id); - $detail['marketing'] = $this->product_service()->get_product_marketing_profile($product_id); - $detail['checkout_hint'] = $this->checkout_service()->get_checkout_hint($detail); - $detail['purchase_enabled'] = ($detail['is_unlimited'] || $detail['stock'] > 0) && $detail['status'] === 'publish'; - $detail['default_quantity'] = 1; - $detail['options'] = $this->get_product_options($product_id); - $detail['has_options'] = $this->has_options($product_id); - $detail['price_range'] = $this->get_price_range($product_id); - $detail['pay_modo'] = sanitize_key((string) get_post_meta($product_id, '_aksrt_pay_modo', true)) ?: '0'; - $detail['start_price'] = (float) get_post_meta($product_id, '_aksrt_start_price', true); - $detail['shipping_type'] = sanitize_key((string) get_post_meta($product_id, '_aksrt_shipping_type', true)) ?: 'express'; - $detail['auto_delivery'] = $this->get_auto_delivery_config($product_id); - $detail['limit_buy'] = $this->get_limit_buy_config($product_id); - $detail['services'] = $this->get_product_services($product_id); - $detail['params'] = $this->get_product_params($product_id); - - return $detail; - } - - public function get_product_edit_view(int $user_id, int $product_id = 0): array { - $view = [ - 'allowed' => true, - 'is_edit' => false, - 'product' => [ - 'id' => 0, - 'title' => '', - 'price' => 0, - 'stock' => 0, - 'excerpt' => '', - 'content' => '', - 'image' => '', - 'gallery' => [], - 'categories' => [], - 'tags' => [], - 'shipping_mode' => 'express', - 'shipping_fee' => 0, - 'after_sale_policy' => '', - 'support_refund' => true, - 'status' => 'draft', - 'options' => [], - 'pay_modo' => '0', - 'start_price' => 0, - 'product_video' => [], - 'product_params' => '', - ], - 'taxonomy' => [ - 'categories' => $this->product_service()->get_term_options('aksrt_product_cat'), - 'tags' => $this->product_service()->get_term_options('aksrt_product_tag'), - ], - 'shipping_modes' => $this->get_shipping_method_options(), - 'status_options' => [ - ['value' => 'draft', 'label' => __('草稿', 'aksrt')], - ['value' => 'publish', 'label' => __('发布', 'aksrt')], - ['value' => 'pending', 'label' => __('待审核', 'aksrt')], - ], - ]; - - if ($product_id <= 0) { - return $view; - } - - $product = get_post($product_id); - if (!$product || $product->post_type !== 'aksrt_product') { - return $view; - } - - $is_owner = (int) $product->post_author === $user_id || current_user_can('manage_options'); - if (!$is_owner) { - $view['allowed'] = false; - return $view; - } - - $view['is_edit'] = true; - $videos = get_post_meta($product_id, '_aksrt_product_video', true); - if (!is_array($videos)) { - $videos = []; - } - $view['product'] = [ - 'id' => $product_id, - 'title' => (string) $product->post_title, - 'price' => (float) get_post_meta($product_id, '_aksrt_price', true), - 'stock' => (int) get_post_meta($product_id, '_aksrt_stock', true), - 'excerpt' => (string) $product->post_excerpt, - 'content' => (string) $product->post_content, - 'image' => $this->resolve_main_image($product_id), - 'gallery' => $this->product_service()->get_product_gallery($product_id), - 'categories' => wp_get_post_terms($product_id, 'aksrt_product_cat', ['fields' => 'ids']) ?: [], - 'tags' => wp_get_post_terms($product_id, 'aksrt_product_tag', ['fields' => 'names']) ?: [], - 'shipping_mode' => sanitize_key((string) get_post_meta($product_id, '_aksrt_shipping_mode', true)) ?: 'express', - 'shipping_fee' => (float) get_post_meta($product_id, '_aksrt_shipping_fee', true), - 'after_sale_policy' => (string) get_post_meta($product_id, '_aksrt_after_sale_policy', true), - 'support_refund' => get_post_meta($product_id, '_aksrt_support_refund', true) !== '0', - 'status' => (string) get_post_status($product_id), - 'options' => method_exists($this, 'get_product_options') ? $this->get_product_options($product_id) : [], - 'pay_modo' => sanitize_key((string) get_post_meta($product_id, '_aksrt_pay_modo', true)) ?: '0', - 'start_price' => (float) get_post_meta($product_id, '_aksrt_start_price', true), - 'product_video' => array_values(array_filter(array_map('esc_url_raw', $videos))), - 'product_params' => (string) get_post_meta($product_id, '_aksrt_product_params', true), - ]; - - return $view; - } - - public function get_cart_page_view(int $user_id): array { - $pending_orders = $this->get_order_center_view($user_id, [ - 'page' => 1, - 'per_page' => 5, - 'status' => \AKSRT\Payment\Order::STATUS_PENDING, - ]); - - return [ - 'payment_methods' => $this->get_payment_method_options(), - 'shipping_methods' => $this->get_shipping_method_options(), - 'address_book' => $this->get_address_book($user_id), - 'coupon_enabled' => class_exists('\AKSRT\Payment\Coupon'), - 'currency_symbol' => '¥', - 'pending_orders' => $pending_orders['items'] ?? [], - 'cart_items' => $this->get_cart($user_id), - 'cart_count' => $this->get_cart_count($user_id), - ]; - } - - public function get_cart(int $user_id): array { - return $this->cart_service()->get_cart($user_id); - } - - public function get_cart_count(int $user_id): int { - return $this->cart_service()->get_cart_count($user_id); - } - - public function add_to_cart(int $user_id, int $product_id, string $options_string = '', int $count = 1): array { - return $this->cart_service()->add_to_cart($user_id, $product_id, $options_string, $count); - } - - public function reduce_from_cart(int $user_id, int $product_id, string $options_string = '', int $count = 1): array { - return $this->cart_service()->reduce_from_cart($user_id, $product_id, $options_string, $count); - } - - public function remove_from_cart(int $user_id, int $product_id, string $options_string = ''): array { - return $this->cart_service()->remove_from_cart($user_id, $product_id, $options_string); - } - - public function remove_multi_from_cart(int $user_id, array $items): array { - return $this->cart_service()->remove_multi_from_cart($user_id, $items); - } - - public function empty_cart(int $user_id): array { - return $this->cart_service()->empty_cart($user_id); - } - - public function validate_cart_item(int $product_id, string $options_string = '') { - return $this->cart_service()->validate_cart_item($product_id, $options_string); - } - - public function get_order_center_view(int $user_id, array $args = []): array { - if (class_exists('\AKSRT\Payment\UserOrders')) { - return \AKSRT\Payment\UserOrders::get_instance()->get_order_center_view($user_id, $args); - } - - return ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1, 'stats' => [], 'product_types' => []]; - } - - public function get_order_detail_view(int $user_id, string $order_number): ?array { - if (class_exists('\AKSRT\Payment\UserOrders')) { - return \AKSRT\Payment\UserOrders::get_instance()->get_order_detail_view($user_id, $order_number); - } - - return null; - } - - public function preview_checkout(int $user_id, array $payload): array { - return $this->checkout_service()->preview_checkout($user_id, $payload); - } - - public function create_checkout_order(int $user_id, array $payload): array { - return $this->checkout_service()->create_checkout_order($user_id, $payload); - } - - public function pay_order(int $user_id, string $order_number, string $payment_method): array { - return $this->checkout_service()->pay_order($user_id, $order_number, $payment_method); - } - - public function get_user_favorites(int $user_id, int $page = 1, int $per_page = 20): array { - if ($user_id <= 0) { - return ['items' => [], 'total' => 0]; - } - $fav_ids = get_user_meta($user_id, 'aksrt_product_favorites', true); - if (!is_array($fav_ids) || empty($fav_ids)) { - return ['items' => [], 'total' => 0]; - } - $total = count($fav_ids); - $paged = array_slice(array_reverse($fav_ids), ($page - 1) * $per_page, $per_page); - $items = []; - foreach ($paged as $pid) { - $card = $this->build_product_card_view((int) $pid); - if (!empty($card)) { - $items[] = $card; - } - } - return ['items' => $items, 'total' => $total]; - } - - public function toggle_favorite(int $user_id, int $product_id): array { - if ($user_id <= 0 || $product_id <= 0) { - return ['success' => false]; - } - $fav_ids = get_user_meta($user_id, 'aksrt_product_favorites', true); - if (!is_array($fav_ids)) { - $fav_ids = []; - } - $key = array_search($product_id, $fav_ids, true); - if ($key !== false) { - unset($fav_ids[$key]); - $fav_ids = array_values($fav_ids); - $is_fav = false; - } else { - $fav_ids[] = $product_id; - $is_fav = true; - } - update_user_meta($user_id, 'aksrt_product_favorites', $fav_ids); - $count = (int) get_post_meta($product_id, '_aksrt_favorite_count', true); - update_post_meta($product_id, '_aksrt_favorite_count', max(0, $count + ($is_fav ? 1 : -1))); - if ($is_fav) { - do_action('aksrt_product_favorited', $user_id, $product_id); - } else { - do_action('aksrt_product_unfavorited', $user_id, $product_id); - } - return ['success' => true, 'is_favorite' => $is_fav, 'count' => count($fav_ids)]; - } - - public function is_favorite(int $user_id, int $product_id): bool { - if ($user_id <= 0) return false; - $fav_ids = get_user_meta($user_id, 'aksrt_product_favorites', true); - return is_array($fav_ids) && in_array($product_id, $fav_ids, true); - } - - public function re_purchase(int $user_id, string $order_number): array { - $items = $this->get_order_items($order_number); - if (empty($items)) { - return ['success' => false, 'message' => __('订单无商品记录', 'aksrt')]; - } - $added = 0; - foreach ($items as $item) { - $pid = (int) $item['product_id']; - $qty = max(1, (int) $item['quantity']); - $opt_key = (string) $item['options_key']; - $result = $this->add_to_cart($user_id, $pid, $opt_key, $qty); - if (!empty($result['success'])) { - $added++; - } - } - return ['success' => $added > 0, 'added' => $added, 'cart_count' => $this->get_cart_count($user_id)]; - } - - public function cancel_order(int $user_id, string $order_number): array { - if ($user_id <= 0 || $order_number === '' || !class_exists('\AKSRT\Payment\OrderRepository')) { - return ['success' => false, 'message' => __('订单不存在。', 'aksrt')]; - } - - $repo = \AKSRT\Payment\OrderRepository::get_instance(); - $order = $repo->get_user_order($user_id, $order_number); - if (!$order) { - return ['success' => false, 'message' => __('订单不存在。', 'aksrt')]; - } - - $status = (string) ($order['status'] ?? ''); - if (!in_array($status, [\AKSRT\Payment\Order::STATUS_PENDING, \AKSRT\Payment\Order::STATUS_CREATED, \AKSRT\Payment\Order::STATUS_EXPIRED], true)) { - return ['success' => false, 'message' => __('当前订单不可取消。', 'aksrt')]; - } - - $repo->update_status($order_number, \AKSRT\Payment\Order::STATUS_CANCELLED, [ - 'force' => true, - 'note' => __('用户在前台取消订单。', 'aksrt'), - ]); - - return [ - 'success' => true, - 'data' => [ - 'order' => $this->get_order_detail_view($user_id, $order_number), - ], - ]; - } - -} diff --git a/src/AKSRT/Shop/TermMeta.php b/src/AKSRT/Shop/TermMeta.php deleted file mode 100644 index 6448639d..00000000 --- a/src/AKSRT/Shop/TermMeta.php +++ /dev/null @@ -1,748 +0,0 @@ - [ - 'name' => __('优惠活动', 'aksrt'), - 'singular_name' => __('优惠活动', 'aksrt'), - 'search_items' => __('搜索优惠活动', 'aksrt'), - 'all_items' => __('所有优惠活动', 'aksrt'), - 'edit_item' => __('编辑优惠活动', 'aksrt'), - 'add_new_item' => __('新增优惠活动', 'aksrt'), - 'menu_name' => __('优惠活动', 'aksrt'), - ], - 'public' => true, - 'hierarchical' => false, - 'show_ui' => $shop_show_ui, - 'show_in_menu' => $shop_show_menu, - 'show_in_rest' => true, - 'show_admin_column' => true, - 'rewrite' => ['slug' => 'shop-discount', 'with_front' => false], - ]); - } - - // ─── Term Meta Registration ────────────────────────────────── - - /** - * Register all term meta fields for REST API exposure. - */ - public function register_term_meta_fields(): void { - $auth = function () { - return current_user_can('manage_categories'); - }; - - // ── aksrt_product_cat ── - $cat_fields = $this->get_cat_field_definitions(); - foreach ($cat_fields as $key => $def) { - register_term_meta('aksrt_product_cat', $key, [ - 'type' => $def['type'], - 'description' => $def['description'] ?? $key, - 'single' => true, - 'default' => $def['default'] ?? '', - 'show_in_rest' => true, - 'sanitize_callback' => $def['sanitize'] ?? 'sanitize_text_field', - 'auth_callback' => $auth, - ]); - } - - // ── aksrt_product_tag ── - $tag_fields = $this->get_tag_field_definitions(); - foreach ($tag_fields as $key => $def) { - register_term_meta('aksrt_product_tag', $key, [ - 'type' => $def['type'], - 'description' => $def['description'] ?? $key, - 'single' => true, - 'default' => $def['default'] ?? '', - 'show_in_rest' => true, - 'sanitize_callback' => $def['sanitize'] ?? 'sanitize_text_field', - 'auth_callback' => $auth, - ]); - } - - // ── shop_discount ── - $discount_fields = $this->get_discount_field_definitions(); - foreach ($discount_fields as $key => $def) { - register_term_meta('shop_discount', $key, [ - 'type' => $def['type'], - 'description' => $def['description'] ?? $key, - 'single' => true, - 'default' => $def['default'] ?? '', - 'show_in_rest' => true, - 'sanitize_callback' => $def['sanitize'] ?? 'sanitize_text_field', - 'auth_callback' => $auth, - ]); - } - } - - // ─── Field Definitions ─────────────────────────────────────── - - /** - * Product category term meta field definitions. - * - * @return array - */ - private function get_cat_field_definitions(): array { - return [ - 'shop_cat_limit_buy' => [ - 'type' => 'integer', 'description' => '分类级限购 (-1 不限)', 'default' => -1, - 'sanitize' => function ($v) { return max(-1, min(9999, (int) $v)); }, - ], - 'shop_cat_shipping_fee' => [ - 'type' => 'string', 'description' => '分类级运费类型', 'default' => '', - 'sanitize' => function ($v) { - $v = sanitize_key((string) $v); - return in_array($v, ['', 'free', 'fixed', 'amount'], true) ? $v : ''; - }, - ], - 'shop_cat_guest_buy' => [ - 'type' => 'string', 'description' => '分类级免登录购买', 'default' => '', - ], - 'shop_cat_email_fill' => [ - 'type' => 'string', 'description' => '分类级邮箱必留', 'default' => '', - ], - 'shop_cat_after_sale' => [ - 'type' => 'string', 'description' => '分类级售后政策', 'default' => '', - 'sanitize' => function ($v) { - $v = sanitize_key((string) $v); - return in_array($v, ['', 'refund', 'refund_return', 'none'], true) ? $v : ''; - }, - ], - 'shop_cat_rebate' => [ - 'type' => 'string', 'description' => '分类级返佣覆盖', 'default' => '', - ], - 'shop_cat_service' => [ - 'type' => 'string', 'description' => '分类级服务保障', 'default' => '', - 'sanitize' => 'sanitize_textarea_field', - ], - 'shop_cat_single_tab' => [ - 'type' => 'string', 'description' => '分类级自定义 TAB', 'default' => '', - 'sanitize' => 'sanitize_textarea_field', - ], - 'shop_cat_content_after' => [ - 'type' => 'string', 'description' => '分类级详情页底部内容', 'default' => '', - 'sanitize' => 'wp_kses_post', - ], - 'shop_cat_content_layout' => [ - 'type' => 'string', 'description' => '分类级内容布局', 'default' => '', - 'sanitize' => function ($v) { - $v = sanitize_key((string) $v); - return in_array($v, ['', 'adapt', 'full', 'sidebar'], true) ? $v : ''; - }, - ], - 'shop_cat_content_show_bg' => [ - 'type' => 'string', 'description' => '分类级详情背景盒子', 'default' => '', - ], - 'shop_cat_list_style' => [ - 'type' => 'string', 'description' => '分类页列表样式', 'default' => '', - 'sanitize' => function ($v) { - $v = sanitize_key((string) $v); - return in_array($v, ['', 'default', 'card-lg', 'card-sm'], true) ? $v : ''; - }, - ], - ]; - } - - /** - * Product tag term meta field definitions. - * - * @return array - */ - private function get_tag_field_definitions(): array { - return [ - 'shop_tag_priority' => [ - 'type' => 'integer', 'description' => '标签优先级(影响显示顺序)', 'default' => 0, - 'sanitize' => function ($v) { return max(0, min(9999, (int) $v)); }, - ], - 'shop_tag_class' => [ - 'type' => 'string', 'description' => '标签颜色 class', 'default' => '', - 'sanitize' => 'sanitize_html_class', - ], - 'shop_tag_is_important' => [ - 'type' => 'string', 'description' => '重点标签(商品价格位着重显示)', 'default' => '', - ], - ]; - } - - /** - * Shop discount term meta field definitions. - * - * @return array - */ - private function get_discount_field_definitions(): array { - return [ - 'shop_discount_small_badge' => [ - 'type' => 'string', 'description' => '小标签文案', 'default' => '', - ], - 'shop_discount_priority' => [ - 'type' => 'integer', 'description' => '优先级(数字越大越优先)', 'default' => 0, - 'sanitize' => function ($v) { return max(0, min(9999, (int) $v)); }, - ], - 'shop_discount_is_important' => [ - 'type' => 'string', 'description' => '重点活动', 'default' => '', - ], - 'shop_discount_important_class' => [ - 'type' => 'string', 'description' => '重点活动背景色 class', 'default' => '', - 'sanitize' => 'sanitize_html_class', - ], - 'shop_discount_scope' => [ - 'type' => 'string', 'description' => '优惠范围', 'default' => 'item', - 'sanitize' => function ($v) { - $v = sanitize_key((string) $v); - return in_array($v, ['item', 'product', 'author', 'order'], true) ? $v : 'item'; - }, - ], - 'shop_discount_type' => [ - 'type' => 'string', 'description' => '优惠类型', 'default' => 'reduction', - 'sanitize' => function ($v) { - $v = sanitize_key((string) $v); - return in_array($v, ['reduction', 'discount', 'gift'], true) ? $v : 'reduction'; - }, - ], - 'shop_discount_value' => [ - 'type' => 'string', 'description' => '优惠数值(立减金额/折扣 0.01-9.99)', 'default' => '0', - 'sanitize' => function ($v) { return sanitize_text_field((string) $v); }, - ], - 'shop_discount_gift_config' => [ - 'type' => 'string', 'description' => '赠品配置 JSON', 'default' => '', - 'sanitize' => 'sanitize_textarea_field', - ], - 'shop_discount_price_limit' => [ - 'type' => 'string', 'description' => '金额门槛', 'default' => '0', - 'sanitize' => function ($v) { return max(0, (float) $v); }, - ], - 'shop_discount_user_limit' => [ - 'type' => 'string', 'description' => '用户身份限制', 'default' => '', - 'sanitize' => function ($v) { - $v = sanitize_key((string) $v); - return in_array($v, ['', 'vip', 'vip_2', 'auth'], true) ? $v : ''; - }, - ], - 'shop_discount_start' => [ - 'type' => 'string', 'description' => '活动开始时间', 'default' => '', - 'sanitize' => 'sanitize_text_field', - ], - 'shop_discount_end' => [ - 'type' => 'string', 'description' => '活动结束时间', 'default' => '', - 'sanitize' => 'sanitize_text_field', - ], - 'shop_discount_countdown' => [ - 'type' => 'string', 'description' => '倒计时显示', 'default' => '', - ], - ]; - } - - // ─── Getters ───────────────────────────────────────────────── - - /** - * Get all shop term meta for a product category. - * - * @param int $term_id - * @return array - */ - public static function get_cat_meta(int $term_id): array { - $defaults = [ - 'limit_buy' => -1, - 'shipping_fee' => '', - 'guest_buy' => '', - 'email_fill' => '', - 'after_sale' => '', - 'rebate' => '', - 'service' => '', - 'single_tab' => '', - 'content_after' => '', - 'content_layout' => '', - 'content_show_bg' => '', - 'list_style' => '', - ]; - - $result = []; - foreach ($defaults as $key => $default) { - $meta_key = "shop_cat_{$key}"; - $val = get_term_meta($term_id, $meta_key, true); - $result[$key] = ($val !== '' && $val !== false) ? $val : $default; - } - - return $result; - } - - /** - * Get all shop term meta for a product tag. - * - * @param int $term_id - * @return array - */ - public static function get_tag_meta(int $term_id): array { - $priority = get_term_meta($term_id, 'shop_tag_priority', true); - $class = get_term_meta($term_id, 'shop_tag_class', true); - $is_important = get_term_meta($term_id, 'shop_tag_is_important', true); - - return [ - 'priority' => $priority !== '' ? (int) $priority : 0, - 'class' => is_string($class) ? $class : '', - 'is_important' => !empty($is_important), - ]; - } - - /** - * Get all shop discount term meta. - * - * @param int $term_id - * @return array - */ - public static function get_discount_meta(int $term_id): array { - $defaults = [ - 'small_badge' => '', - 'priority' => 0, - 'is_important' => false, - 'important_class' => '', - 'scope' => 'item', - 'type' => 'reduction', - 'value' => '0', - 'gift_config' => '', - 'price_limit' => '0', - 'user_limit' => '', - 'start' => '', - 'end' => '', - 'countdown' => '', - ]; - - $result = []; - foreach ($defaults as $key => $default) { - $meta_key = "shop_discount_{$key}"; - $val = get_term_meta($term_id, $meta_key, true); - $result[$key] = ($val !== '' && $val !== false) ? $val : $default; - } - - $result['priority'] = (int) $result['priority']; - $result['is_important'] = !empty($result['is_important']); - $result['price_limit'] = (float) $result['price_limit']; - - return $result; - } - - /** - * Resolve parameter inheritance: product → category → theme settings. - * - * Returns the effective value for a given parameter key by checking: - * 1. Product-level override (from post meta) - * 2. Category-level override (from term meta) - * 3. Theme-level default (from \AKSRT\Settings\Settings::get_shop_settings()) - * - * @param int $product_id Product post ID - * @param string $param_key Parameter key (e.g. 'limit_buy', 'shipping_fee') - * @param mixed $product_value Product-level value (null if not set) - * @return mixed - */ - public static function resolve_inheritance(int $product_id, string $param_key, $product_value = null) { - // 1. Product-level override - if ($product_value !== null && $product_value !== '' && $product_value !== -1) { - return $product_value; - } - - // 2. Category-level override - $terms = get_the_terms($product_id, 'aksrt_product_cat'); - if (is_array($terms) && !empty($terms)) { - $cat_meta_key = "shop_cat_{$param_key}"; - foreach ($terms as $term) { - $val = get_term_meta($term->term_id, $cat_meta_key, true); - if ($val !== '' && $val !== false && $val !== -1) { - return $val; - } - } - } - - // 3. Theme-level default - $shop = \AKSRT\Settings\Settings::get_shop_settings(); - $defaults = $shop['product_defaults'] ?? []; - return $defaults[$param_key] ?? $product_value; - } - - /** - * Add form fields for shop_discount taxonomy. - */ - public function render_discount_add_fields(): void { - $fields = $this->get_discount_field_definitions(); - foreach ($fields as $key => $def) { - $label = (string) ($def['description'] ?? $key); - $default = $def['default'] ?? ''; - echo '
'; - echo ''; - $this->render_discount_field_control($key, $default, false); - echo '
'; - } - } - - /** - * Edit form fields for shop_discount taxonomy. - */ - public function render_discount_edit_fields($term): void { - if (!($term instanceof \WP_Term)) { - return; - } - $fields = $this->get_discount_field_definitions(); - foreach ($fields as $key => $def) { - $label = (string) ($def['description'] ?? $key); - $value = get_term_meta($term->term_id, $key, true); - if ($value === '' || $value === false) { - $value = $def['default'] ?? ''; - } - echo ''; - echo ''; - echo ''; - $this->render_discount_field_control($key, $value, true); - echo ''; - } - } - - /** - * @param string|int|float $value - */ - private function render_discount_field_control(string $key, $value, bool $edit): void { - $id = esc_attr($key); - $name = esc_attr($key); - $val = (string) $value; - - if ($key === 'shop_discount_type') { - $opts = [ - 'reduction' => __('立减', 'aksrt'), - 'discount' => __('折扣', 'aksrt'), - 'gift' => __('赠品', 'aksrt'), - ]; - echo ''; - return; - } - - if ($key === 'shop_discount_scope') { - $opts = [ - 'item' => __('单品', 'aksrt'), - 'product' => __('商品', 'aksrt'), - 'author' => __('商家', 'aksrt'), - 'order' => __('整单', 'aksrt'), - ]; - echo ''; - return; - } - - if ($key === 'shop_discount_user_limit') { - $opts = [ - '' => __('不限', 'aksrt'), - 'vip' => __('VIP1', 'aksrt'), - 'vip_2' => __('VIP2', 'aksrt'), - 'auth' => __('已认证', 'aksrt'), - ]; - echo ''; - return; - } - - if (in_array($key, ['shop_discount_is_important', 'shop_discount_countdown'], true)) { - echo ''; - return; - } - - if ($key === 'shop_discount_gift_config') { - echo ''; - return; - } - - $type = in_array($key, ['shop_discount_priority'], true) ? 'number' : 'text'; - if (in_array($key, ['shop_discount_start', 'shop_discount_end'], true)) { - $type = 'datetime-local'; - // Convert mysql-ish values to datetime-local if needed. - if ($val !== '' && str_contains($val, ' ')) { - $val = str_replace(' ', 'T', substr($val, 0, 16)); - } - } - - printf( - '', - esc_attr($type), - $name, - $id, - esc_attr($val), - $edit ? 'regular-text' : '' - ); - } - - public function save_discount_term_fields(int $term_id): void { - if (!current_user_can('manage_categories')) { - return; - } - - $fields = $this->get_discount_field_definitions(); - foreach ($fields as $key => $def) { - if (in_array($key, ['shop_discount_is_important', 'shop_discount_countdown'], true)) { - $raw = isset($_POST[$key]) ? '1' : ''; - } else { - if (!isset($_POST[$key])) { - continue; - } - $raw = wp_unslash($_POST[$key]); - } - - if (isset($def['sanitize']) && is_callable($def['sanitize'])) { - $value = call_user_func($def['sanitize'], $raw); - } else { - $value = sanitize_text_field((string) $raw); - } - - if (in_array($key, ['shop_discount_start', 'shop_discount_end'], true) && is_string($value) && str_contains($value, 'T')) { - $value = str_replace('T', ' ', $value); - if (strlen($value) === 16) { - $value .= ':00'; - } - } - - update_term_meta($term_id, $key, $value); - } - } - - public function render_cat_add_fields(): void { - foreach ($this->get_cat_field_definitions() as $key => $def) { - echo '
'; - echo ''; - $this->render_cat_field_control($key, $def['default'] ?? '', false); - echo '
'; - } - } - - public function render_cat_edit_fields($term): void { - if (!($term instanceof \WP_Term)) { - return; - } - foreach ($this->get_cat_field_definitions() as $key => $def) { - $value = get_term_meta($term->term_id, $key, true); - if ($value === '' || $value === false) { - $value = $def['default'] ?? ''; - } - echo ''; - $this->render_cat_field_control($key, $value, true); - echo ''; - } - } - - /** - * @param mixed $value - */ - private function render_cat_field_control(string $key, $value, bool $edit): void { - $val = (string) $value; - if (in_array($key, ['shop_cat_guest_buy', 'shop_cat_email_fill', 'shop_cat_content_show_bg'], true)) { - echo ''; - return; - } - if (in_array($key, ['shop_cat_service', 'shop_cat_single_tab', 'shop_cat_content_after'], true)) { - echo ''; - return; - } - if ($key === 'shop_cat_shipping_fee') { - $opts = ['' => __('继承', 'aksrt'), 'free' => __('包邮', 'aksrt'), 'fixed' => __('固定运费', 'aksrt'), 'amount' => __('满额包邮', 'aksrt')]; - echo ''; - return; - } - if ($key === 'shop_cat_after_sale') { - $opts = ['' => __('继承', 'aksrt'), 'refund' => __('仅退款', 'aksrt'), 'refund_return' => __('退货退款', 'aksrt'), 'none' => __('不支持', 'aksrt')]; - echo ''; - return; - } - if ($key === 'shop_cat_content_layout') { - $opts = ['' => __('继承', 'aksrt'), 'adapt' => __('自适应', 'aksrt'), 'full' => __('通栏', 'aksrt'), 'sidebar' => __('侧栏', 'aksrt')]; - echo ''; - return; - } - if ($key === 'shop_cat_list_style') { - $opts = ['' => __('继承', 'aksrt'), 'default' => __('默认', 'aksrt'), 'card-lg' => __('大卡片', 'aksrt'), 'card-sm' => __('小卡片', 'aksrt')]; - echo ''; - return; - } - $type = $key === 'shop_cat_limit_buy' ? 'number' : 'text'; - printf( - '', - esc_attr($type), - $edit ? 'regular-text' : '', - esc_attr($key), - esc_attr($key), - esc_attr($val) - ); - } - - public function save_cat_term_fields(int $term_id): void { - if (!current_user_can('manage_categories')) { - return; - } - foreach ($this->get_cat_field_definitions() as $key => $def) { - if (in_array($key, ['shop_cat_guest_buy', 'shop_cat_email_fill', 'shop_cat_content_show_bg'], true)) { - $raw = isset($_POST[$key]) ? '1' : ''; - } else { - if (!isset($_POST[$key])) { - continue; - } - $raw = wp_unslash($_POST[$key]); - } - if (isset($def['sanitize']) && is_callable($def['sanitize'])) { - $value = call_user_func($def['sanitize'], $raw); - } else { - $value = sanitize_text_field((string) $raw); - } - update_term_meta($term_id, $key, $value); - } - } - - public function render_tag_add_fields(): void { - foreach ($this->get_tag_field_definitions() as $key => $def) { - echo '
'; - echo ''; - $this->render_tag_field_control($key, $def['default'] ?? ''); - echo '
'; - } - } - - public function render_tag_edit_fields($term): void { - if (!($term instanceof \WP_Term)) { - return; - } - foreach ($this->get_tag_field_definitions() as $key => $def) { - $value = get_term_meta($term->term_id, $key, true); - if ($value === '' || $value === false) { - $value = $def['default'] ?? ''; - } - echo ''; - $this->render_tag_field_control($key, $value); - echo ''; - } - } - - /** - * @param mixed $value - */ - private function render_tag_field_control(string $key, $value): void { - $val = (string) $value; - if ($key === 'shop_tag_is_important') { - echo ''; - return; - } - $type = $key === 'shop_tag_priority' ? 'number' : 'text'; - printf( - '', - esc_attr($type), - esc_attr($key), - esc_attr($key), - esc_attr($val) - ); - } - - public function save_tag_term_fields(int $term_id): void { - if (!current_user_can('manage_categories')) { - return; - } - foreach ($this->get_tag_field_definitions() as $key => $def) { - if ($key === 'shop_tag_is_important') { - $raw = isset($_POST[$key]) ? '1' : ''; - } else { - if (!isset($_POST[$key])) { - continue; - } - $raw = wp_unslash($_POST[$key]); - } - if (isset($def['sanitize']) && is_callable($def['sanitize'])) { - $value = call_user_func($def['sanitize'], $raw); - } else { - $value = sanitize_text_field((string) $raw); - } - update_term_meta($term_id, $key, $value); - } - } -} diff --git a/src/AKSRT/Widgets/WidgetProductList.php b/src/AKSRT/Widgets/WidgetProductList.php deleted file mode 100644 index 49624b36..00000000 --- a/src/AKSRT/Widgets/WidgetProductList.php +++ /dev/null @@ -1,401 +0,0 @@ - __('AKSRT 商品列表组件:筛选、排序、AJAX 翻页。', 'aksrt')] - ); - } - - /** - * Build WP_Query args from instance + page. - * - * @param array $instance - * @param int $paged - * @return array - */ - public static function build_query_args(array $instance, int $paged = 1): array { - $per_page = max(1, min(50, (int) ($instance['per_page'] ?? 8))); - $orderby = sanitize_key($instance['orderby'] ?? 'date'); - $order = in_array(strtoupper((string) ($instance['order'] ?? '')), ['ASC', 'DESC'], true) - ? strtoupper((string) $instance['order']) - : 'DESC'; - $cat_ids = array_values(array_filter(array_map('intval', explode(',', (string) ($instance['cat_ids'] ?? ''))))); - $tag_ids = array_values(array_filter(array_map('intval', explode(',', (string) ($instance['tag_ids'] ?? ''))))); - $discount_ids = array_values(array_filter(array_map('intval', explode(',', (string) ($instance['discount_ids'] ?? ''))))); - $exclude_cat = !empty($instance['exclude_cat']); - - $query_args = [ - 'post_type' => 'aksrt_product', - 'post_status' => 'publish', - 'posts_per_page' => $per_page, - 'paged' => max(1, $paged), - 'orderby' => $orderby === 'sales' ? 'meta_value_num' : ($orderby === 'views' ? 'meta_value_num' : $orderby), - 'order' => $order, - 'ignore_sticky_posts' => true, - ]; - - if ($orderby === 'sales') { - $query_args['meta_key'] = '_aksrt_sales'; - } elseif ($orderby === 'views') { - $query_args['meta_key'] = 'aksrt_post_views'; - } - - $tax_query = []; - if (!empty($cat_ids)) { - $tax_query[] = [ - 'taxonomy' => 'aksrt_product_cat', - 'field' => 'term_id', - 'terms' => $cat_ids, - 'operator' => $exclude_cat ? 'NOT IN' : 'IN', - ]; - } - if (!empty($tag_ids)) { - $tax_query[] = [ - 'taxonomy' => 'aksrt_product_tag', - 'field' => 'term_id', - 'terms' => $tag_ids, - ]; - } - if (!empty($discount_ids)) { - $tax_query[] = [ - 'taxonomy' => 'shop_discount', - 'field' => 'term_id', - 'terms' => $discount_ids, - ]; - } - if (!empty($tax_query)) { - $query_args['tax_query'] = $tax_query; - } - return $query_args; - } - - /** - * Render product cards HTML for a query. - */ - public static function render_items_html(\WP_Query $products, string $style = 'default'): string { - if (!$products->have_posts()) { - return ''; - } - $oneline = $style === 'oneline'; - $mini = $style === 'mini'; - $list = $style === 'list'; - ob_start(); - while ($products->have_posts()) { - $products->the_post(); - $pid = get_the_ID(); - $card = class_exists('\AKSRT\Shop\Shop') - ? \AKSRT\Shop\Shop::get_instance()->build_product_card_view($pid) - : []; - $price = (float) ($card['price'] ?? 0); - $image = (string) ($card['image'] ?? ''); - $ph = get_template_directory_uri() . '/assets/img/placeholder.svg'; - if ($oneline) { - echo '
'; - } - if ($mini || $list) { - $row_cls = $list ? 'aksrt-product-widget__item aksrt-product-widget__item--list ajax-item flex ac' : 'aksrt-product-widget__item aksrt-product-widget__item--mini ajax-item flex ac'; - $thumb_cls = $list ? 'aksrt-product-widget__thumb list' : 'aksrt-product-widget__thumb mini'; - $wh = $list ? 80 : 56; - echo '
'; - echo ''; - if ($image !== '') { - echo '' . esc_attr(get_the_title($pid)) . ''; - } - echo ''; - echo '
'; - echo '' . esc_html(get_the_title($pid)) . ''; - echo '
'; - echo '¥' . esc_html(number_format_i18n($price, 2)) . ''; - if (!empty($card['sales'])) { - echo '' . esc_html((string) $card['sales']) . esc_html__(' 已售', 'aksrt') . ''; - } - echo '
'; - } elseif (function_exists('get_template_part') && !empty($card['id'])) { - get_template_part('templates/parts/shop/product', 'card', [ - 'product' => $card, - 'show_sales' => true, - ]); - } else { - // Fallback card (lazy thumbs). - echo '
'; - if ($image !== '') { - echo ''; - echo '' . esc_attr(get_the_title($pid)) . ''; - echo ''; - } - echo '
'; - echo '' . esc_html(get_the_title($pid)) . ''; - echo '
'; - echo '¥' . esc_html(number_format_i18n($price, 2)) . ''; - if (!empty($card['sales'])) { - echo '' . esc_html((string) $card['sales']) . esc_html__(' 已售', 'aksrt') . ''; - } - echo '
'; - } - if ($oneline) { - echo '
'; - } - } - wp_reset_postdata(); - return (string) ob_get_clean(); - } - - /** - * Shared payload for REST widget list. - * - * @param array $instance - * @param int $page - * @return array{html:string,paginate_html:string,has_more:bool,page:int,max_pages:int,total:int} - */ - public static function query_payload(array $instance, int $page = 1): array { - $page = max(1, $page); - $args = self::build_query_args($instance, $page); - $style = sanitize_key((string) ($instance['style'] ?? 'default')); - $paginate = sanitize_key((string) ($instance['paginate'] ?? 'ajax')); - $products = new \WP_Query($args); - $html = self::render_items_html($products, $style); - if ($html === '' && $page === 1) { - $html = function_exists('aksrt_get_ajax_null') - ? aksrt_get_ajax_null(__('暂无商品', 'aksrt')) - : '

' . esc_html__('暂无商品', 'aksrt') . '

'; - } - $max_pages = max(1, (int) $products->max_num_pages); - $total = (int) $products->found_posts; - $has_more = $page < $max_pages; - $paginate_html = ''; - if ($paginate === 'ajax' && $has_more) { - $next = add_query_arg( - array_filter([ - 'page' => $page + 1, - 'per_page' => max(1, (int) ($instance['per_page'] ?? 8)), - 'orderby' => sanitize_key((string) ($instance['orderby'] ?? 'date')), - 'order' => sanitize_key((string) ($instance['order'] ?? 'DESC')), - 'cat_ids' => (string) ($instance['cat_ids'] ?? ''), - 'tag_ids' => (string) ($instance['tag_ids'] ?? ''), - 'discount_ids' => (string) ($instance['discount_ids'] ?? ''), - 'exclude_cat' => !empty($instance['exclude_cat']) ? 1 : 0, - 'style' => sanitize_key((string) ($instance['style'] ?? 'default')), - 'paginate' => 'ajax', - ], static function ($v) { - return $v !== '' && $v !== null; - }), - rest_url('aksrt/v1/shop/frontend/widgets/products') - ); - if (function_exists('aksrt_get_ajax_next_paginate')) { - $paginate_html = aksrt_get_ajax_next_paginate( - $total, - $page, - max(1, (int) ($instance['per_page'] ?? 8)), - $next - ); - } else { - $paginate_html = ''; - } - } elseif ($paginate === 'number' && $max_pages > 1) { - $num_base = add_query_arg( - array_filter([ - 'per_page' => max(1, (int) ($instance['per_page'] ?? 8)), - 'orderby' => sanitize_key((string) ($instance['orderby'] ?? 'date')), - 'order' => sanitize_key((string) ($instance['order'] ?? 'DESC')), - 'cat_ids' => (string) ($instance['cat_ids'] ?? ''), - 'tag_ids' => (string) ($instance['tag_ids'] ?? ''), - 'discount_ids' => (string) ($instance['discount_ids'] ?? ''), - 'exclude_cat' => !empty($instance['exclude_cat']) ? 1 : 0, - 'style' => sanitize_key((string) ($instance['style'] ?? 'default')), - 'paginate' => 'number', - ], static function ($v) { - return $v !== '' && $v !== null; - }), - rest_url('aksrt/v1/shop/frontend/widgets/products') - ); - $paginate_html = function_exists('aksrt_get_ajax_number_paginate') - ? aksrt_get_ajax_number_paginate($total, $page, max(1, (int) ($instance['per_page'] ?? 8)), $num_base) - : '
' . paginate_links([ - 'base' => add_query_arg('aksrt_product_page', '%#%'), - 'format' => '', - 'current' => $page, - 'total' => $max_pages, - 'prev_text' => '«', - 'next_text' => '»', - ]) . '
'; - } else { - $paginate_html = '
'; - } - - return [ - 'html' => $html, - 'paginate_html' => $paginate_html, - 'has_more' => $has_more, - 'page' => $page, - 'max_pages' => $max_pages, - 'total' => $total, - ]; - } - - public function widget($args, $instance): void { - $title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base); - $style = sanitize_key((string) ($instance['style'] ?? 'default')); - $paginate = sanitize_key((string) ($instance['paginate'] ?? 'ajax')); - $page = max(1, (int) ($_GET['aksrt_product_page'] ?? 1)); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - - $cfg = [ - 'per_page' => max(1, min(50, (int) ($instance['per_page'] ?? 8))), - 'orderby' => sanitize_key((string) ($instance['orderby'] ?? 'date')), - 'order' => in_array(strtoupper((string) ($instance['order'] ?? '')), ['ASC', 'DESC'], true) ? strtoupper((string) $instance['order']) : 'DESC', - 'cat_ids' => (string) ($instance['cat_ids'] ?? ''), - 'tag_ids' => (string) ($instance['tag_ids'] ?? ''), - 'discount_ids' => (string) ($instance['discount_ids'] ?? ''), - 'exclude_cat' => !empty($instance['exclude_cat']) ? 1 : 0, - 'style' => $style, - 'paginate' => $paginate, - ]; - - $payload = self::query_payload($instance, $page); - $ajax_url = rest_url('aksrt/v1/shop/frontend/widgets/products'); - - echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - if ($title !== '') { - echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } - - $style_class = 'aksrt-product-widget--' . $style; - echo '
'; - - if ($style === 'oneline') { - // zibll-style horizontal swiper-scroll (shared slider.js Type A). - echo '
'; - echo '
'; - echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo '
'; - echo '
'; - echo '
'; - echo '
'; - } else { - // Items sit directly under .ajaxpager so ajax-list inject/append works (zibll product-lists-row). - echo '
'; - echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped — cards escaped upstream - echo $payload['paginate_html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo ''; - echo '
'; - } - echo '
'; - - echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } - - public function form($instance): void { - $defaults = [ - 'title' => __('商品推荐', 'aksrt'), - 'per_page' => 8, - 'orderby' => 'date', - 'order' => 'DESC', - 'cat_ids' => '', - 'tag_ids' => '', - 'discount_ids' => '', - 'exclude_cat' => false, - 'style' => 'default', - 'paginate' => 'ajax', - ]; - $instance = wp_parse_args($instance, $defaults); - ?> -

- - -

-

- - -

-

- - -

-

- - -

-

- - -

-

- -

-

- - -

-

- - -

-

- - -

-

- - -

- render_common_form_section($instance); -} - - public function update($new_instance, $old_instance): array { - $business = [ - 'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')), - 'per_page' => max(1, min(50, (int) ($new_instance['per_page'] ?? 8))), - 'orderby' => sanitize_key((string) ($new_instance['orderby'] ?? 'date')), - 'order' => in_array(strtoupper((string) ($new_instance['order'] ?? '')), ['ASC', 'DESC'], true) ? strtoupper((string) $new_instance['order']) : 'DESC', - 'cat_ids' => sanitize_text_field((string) ($new_instance['cat_ids'] ?? '')), - 'tag_ids' => sanitize_text_field((string) ($new_instance['tag_ids'] ?? '')), - 'discount_ids' => sanitize_text_field((string) ($new_instance['discount_ids'] ?? '')), - 'exclude_cat' => !empty($new_instance['exclude_cat']) ? 1 : 0, - 'style' => sanitize_key((string) ($new_instance['style'] ?? 'default')), - 'paginate' => in_array(sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')), ['none', 'ajax', 'number'], true) ? sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')) : 'ajax', - ]; return $this->update_common_fields($new_instance, $old_instance, $business); - } -} diff --git a/src/AKSRT/Widgets/WidgetProductOneline.php b/src/AKSRT/Widgets/WidgetProductOneline.php deleted file mode 100644 index d1ce7a17..00000000 --- a/src/AKSRT/Widgets/WidgetProductOneline.php +++ /dev/null @@ -1,52 +0,0 @@ - __('AKSRT 单行横滑商品(swiper-scroll,同源 product list)。', 'aksrt'), - 'classname' => 'widget_aksrt_product_oneline', - ] - ); - } - - public function widget($args, $instance): void { - $instance['style'] = 'oneline'; - // Prefer no paginate for horizontal scroller (zibll loads fixed count). - if (!isset($instance['paginate'])) { - $instance['paginate'] = 'none'; - } - parent::widget($args, $instance); - } - - public function form($instance): void { - $instance = wp_parse_args($instance, [ - 'title' => __('横滑商品', 'aksrt'), - 'style' => 'oneline', - 'paginate' => 'none', - 'per_page' => 8, - ]); - parent::form($instance); - } - - public function update($new_instance, $old_instance): array { - $out = parent::update($new_instance, $old_instance); - $out['style'] = 'oneline'; - if (($out['paginate'] ?? '') === '') { - $out['paginate'] = 'none'; - } - return $out; - } -} diff --git a/src/AKSRT/Widgets/WidgetProductTab.php b/src/AKSRT/Widgets/WidgetProductTab.php deleted file mode 100644 index 1e64e99b..00000000 --- a/src/AKSRT/Widgets/WidgetProductTab.php +++ /dev/null @@ -1,179 +0,0 @@ - __('AKSRT 多 TAB 商品列表组件(每 tab 独立筛选 + AJAX)。', 'aksrt')] - ); - } - - /** - * @return list - */ - private static function parse_tabs(string $tabs_raw): array { - $tabs = []; - foreach (array_filter(explode("\n", $tabs_raw)) as $line) { - $parts = explode('|', trim($line)); - if (count($parts) >= 1 && trim($parts[0]) !== '') { - $tabs[] = [ - 'label' => trim($parts[0]), - 'cat_ids' => trim($parts[1] ?? ''), - 'discount_ids' => trim($parts[2] ?? ''), - 'tag_ids' => trim($parts[3] ?? ''), - 'orderby' => trim($parts[4] ?? 'date'), - ]; - } - } - return $tabs; - } - - public function widget($args, $instance): void { - $title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base); - $tabs_raw = (string) ($instance['tabs'] ?? ''); - $per_page = max(1, min(20, (int) ($instance['per_page'] ?? 8))); - $style = sanitize_key((string) ($instance['style'] ?? 'default')); - $paginate = sanitize_key((string) ($instance['paginate'] ?? 'ajax')); - $tabs = self::parse_tabs($tabs_raw); - if (!$tabs) { - if (current_user_can('manage_options')) { - echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo function_exists('aksrt_get_widget_null') - ? aksrt_get_widget_null(__('请至少配置一个 TAB', 'aksrt')) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - : '

' . esc_html__('请至少配置一个 TAB', 'aksrt') . '

'; - echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } - return; - } - - echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - if ($title !== '') { - echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } - - $rest = rest_url('aksrt/v1/shop/frontend/widgets/products'); - echo '
'; - - echo '
'; - foreach ($tabs as $i => $tab) { - $active = $i === 0 ? ' is-active' : ''; - echo ''; - } - echo '
'; - - foreach ($tabs as $i => $tab) { - $hidden = $i > 0 ? ' style="display:none"' : ''; - $loaded = $i === 0 ? '1' : '0'; - echo '
'; - - if ($i === 0) { - $inst = [ - 'per_page' => $per_page, - 'orderby' => sanitize_key($tab['orderby'] ?: 'date'), - 'order' => 'DESC', - 'cat_ids' => $tab['cat_ids'], - 'tag_ids' => $tab['tag_ids'], - 'discount_ids' => $tab['discount_ids'], - 'style' => $style, - 'paginate' => $paginate, - ]; - $payload = class_exists('\AKSRT\Widgets\WidgetProductList') - ? WidgetProductList::query_payload($inst, 1) - : ['html' => '', 'paginate_html' => '']; - echo '
'; - echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo $payload['paginate_html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo ''; - echo '
'; - } else { - echo '
'; - echo '
'; - echo '
'; - for ($si = 0; $si < min(4, $per_page); $si++) { - echo '
'; - } - echo '
'; - echo '
'; - echo '
'; - } - echo '
'; - } - - echo '
'; - echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } - - public function form($instance): void { - $defaults = [ - 'title' => __('商品精选', 'aksrt'), - 'tabs' => "热门推荐|\n最新上架|", - 'per_page' => 8, - 'style' => 'default', - 'paginate' => 'ajax', - ]; - $instance = wp_parse_args($instance, $defaults); - ?> -

- - -

-

- - -

-

- - -

-

- - -

-

- - -

- render_common_form_section($instance); -} - - public function update($new_instance, $old_instance): array { - $business = [ - 'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')), - 'tabs' => sanitize_textarea_field((string) ($new_instance['tabs'] ?? '')), - 'per_page' => max(1, min(20, (int) ($new_instance['per_page'] ?? 8))), - 'style' => sanitize_key((string) ($new_instance['style'] ?? 'default')), - 'paginate' => in_array(sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')), ['none', 'ajax', 'number'], true) ? sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')) : 'ajax', - ]; return $this->update_common_fields($new_instance, $old_instance, $business); - } -} diff --git a/templates/pages/page-cart.php b/templates/pages/page-cart.php deleted file mode 100644 index d3df38b4..00000000 --- a/templates/pages/page-cart.php +++ /dev/null @@ -1,292 +0,0 @@ - [], - 'shipping_methods' => [], - 'address_book' => [], - 'coupon_enabled' => false, - 'currency_symbol' => '¥', - 'pending_orders' => [], - ]; - -$pending_orders = is_array($cart_view['pending_orders'] ?? null) ? $cart_view['pending_orders'] : []; -$shipping_methods = is_array($cart_view['shipping_methods'] ?? null) ? array_values($cart_view['shipping_methods']) : []; -$payment_methods = is_array($cart_view['payment_methods'] ?? null) ? array_values($cart_view['payment_methods']) : []; -$address_book = is_array($cart_view['address_book'] ?? null) ? $cart_view['address_book'] : []; -?> - -
- -
- - - -
- __('购物车', 'aksrt'), - 'subtitle' => __('在下单前确认地址、运费、优惠券和支付方式。', 'aksrt'), - 'active' => 'cart', - ]); - } - ?> -
- -
- -
- - - -
-
- -
- -
-
-
-
-

- -
-
-
- -

- -
-
-
- -
-
-
-

- -
-
- -
- -

-
- - $address) : ?> -
- -
- - - - - -
-
- -
-
- - - - -
-
-

-
-
-
-

-
- $method) : ?> - - -
-
- -
-

-
- $method) : ?> - - -
-
-
-
- -
-
-

-
-
- -

- - -
-
-
-
- - -
-
-
- - -
- - diff --git a/templates/pages/page-product-edit.php b/templates/pages/page-product-edit.php deleted file mode 100644 index 174ce9c0..00000000 --- a/templates/pages/page-product-edit.php +++ /dev/null @@ -1,218 +0,0 @@ - false, 'is_edit' => false, 'product' => [], 'taxonomy' => ['categories' => [], 'tags' => []], 'shipping_modes' => [], 'status_options' => []]; - -$product = (array) ($edit_view['product'] ?? []); -?> - -
- - -
- -
- -

-
- -
-
-

- -

-

-
-
- -
-
-
-

- - -
- - -
- - - - -
- -
-

- - -
- - -
-

-

- -
- - -
- - -
-
-

-
- - -
- - - - -
-
- - -
- -
-
- - diff --git a/templates/pages/page-product.php b/templates/pages/page-product.php deleted file mode 100644 index 5be01cea..00000000 --- a/templates/pages/page-product.php +++ /dev/null @@ -1,471 +0,0 @@ -get_product_detail_view($product_id) : []); - -$user_id = get_current_user_id(); -$is_fav = $shop && $user_id > 0 ? $shop->is_favorite($user_id, $product_id) : false; -$options = $product['options'] ?? []; -$has_options = !empty($options); -$price_range = $product['price_range'] ?? []; -$params = $product['params'] ?? []; -$services = $product['services'] ?? []; -$pay_modo = $product['pay_modo'] ?? '0'; -$scores = class_exists('\AKSRT\Shop\ProductReview') ? \AKSRT\Shop\ProductReview::get_instance()->get_product_scores($product_id) : []; -$reviews = class_exists('\AKSRT\Shop\ProductReview') ? \AKSRT\Shop\ProductReview::get_instance()->get_reviews($product_id, ['per_page' => 5]) : ['items' => [], 'total' => 0]; -$author_id = (int) get_post_field('post_author', $product_id); -$author = $author_id > 0 ? get_userdata($author_id) : null; -?> - -
- -
- - - - -
- -
-
-
-
- - - - -
-
- - - - - - - - - -
- -

- - -
- - - - - - - - - - - - - - - - -
- - -

- - -
- - -
- - - -
- $group) : ?> -
- -
- - - -
-
- - -
- - - -
-
-
-
-
-
-
-
-
-
-
-
0 ? '¥' . esc_html(number_format($ship_fee, 2)) : esc_html__('包邮', 'aksrt'); - ?>
-
-
- - - - - - - - - - -
- - -
- - - - - - - - - - -
- - -
- - -
-
- - - -
- -
- display_name); ?> - -
-
- -
-
- - -
- - -
-
- -
-
- -
- -
-
- - -
-
- - - - -
-
- - -
- -
-
- - - - - - - - -
-
- -
- -
- - - -
- -
- - - -

- -
-
-
- - - 'aksrt_product', - 'posts_per_page' => 6, - 'post_status' => 'publish', - 'post__not_in' => [$product_id], - 'ignore_sticky_posts' => true, - 'no_found_rows' => true, - 'update_post_meta_cache' => false, - 'update_post_term_cache' => false, - 'tax_query' => !empty($product['categories']) ? [[ - 'taxonomy' => 'aksrt_product_cat', - 'field' => 'term_id', - 'terms' => wp_list_pluck($product['categories'], 'term_id'), - ]] : [], - 'orderby' => 'rand', - ]); - while ($related_q->have_posts()) { - $related_q->the_post(); - $card = $shop->build_product_card_view((int) get_the_ID()); - if (!empty($card['id'])) { - $related_items[] = $card; - } - } - wp_reset_postdata(); - } - ?> - - - -
- - - - - - - - - - - -
-
- - - -
- - -
- - - -
- - - -
- -
-
- -

-
-
- -
- - diff --git a/templates/parts/shop/product-card.php b/templates/parts/shop/product-card.php deleted file mode 100644 index 26fc3a5f..00000000 --- a/templates/parts/shop/product-card.php +++ /dev/null @@ -1,59 +0,0 @@ - 0) || ($mode === 'threshold' && $sales >= $min_sales); -} - -$title = (string) ($product['title'] ?? ''); -$permalink = (string) ($product['permalink'] ?? '#'); -$image = (string) ($product['image'] ?? ''); -$excerpt = (string) ($product['excerpt'] ?? ''); -$price = (float) ($product['price'] ?? 0); -?> - diff --git a/templates/parts/shop/product-cat-filter.php b/templates/parts/shop/product-cat-filter.php deleted file mode 100644 index e191e21b..00000000 --- a/templates/parts/shop/product-cat-filter.php +++ /dev/null @@ -1,149 +0,0 @@ - __('最新', 'aksrt'), - 'views' => __('最热', 'aksrt'), - 'favorite_count' => __('收藏', 'aksrt'), - 'score' => __('评分', 'aksrt'), - 'sales' => __('销量', 'aksrt'), -]; - -$base_term_url = !empty($term['url']) ? (string) $term['url'] : ''; -if ($base_term_url === '' && $current_id > 0) { - $link = get_term_link($current_id, 'aksrt_product_cat'); - $base_term_url = is_wp_error($link) ? '' : (string) $link; -} - -$blog = wp_strip_all_tags(get_bloginfo('name')); -?> -
- -
- - -
  • - -
  • - - - -
  • - - -
    - - - -
    - - - - - - -
    - -
    - - - - - - - - - - - - - - -
    - - -
    - - $label) : - if ($orderby === $key) : ?> - - $key], $base_term_url) : add_query_arg(['orderby' => $key]); - ?> - - - - 'price', 'order' => $next_order], $base_term_url) - : add_query_arg(['orderby' => 'price', 'order' => $next_order]); - ?> - - - - 'price'], $base_term_url) - : add_query_arg(['orderby' => 'price']); - ?> - - - - -
    -
    diff --git a/templates/shop/product-cat.php b/templates/shop/product-cat.php deleted file mode 100644 index 3cbe6dcc..00000000 --- a/templates/shop/product-cat.php +++ /dev/null @@ -1,155 +0,0 @@ -'; - dynamic_sidebar('shop_home_top_fluid'); - echo ''; -} - -$term = get_queried_object(); -$paged = max(1, (int) get_query_var('paged')); -$orderby = ''; -$order = ''; -if (isset($_GET['orderby'])) { - $orderby = sanitize_key(wp_unslash((string) $_GET['orderby'])); -} elseif (isset($_GET['shop_orderby'])) { - $orderby = sanitize_key(wp_unslash((string) $_GET['shop_orderby'])); -} -if (isset($_GET['order'])) { - $order = sanitize_key(wp_unslash((string) $_GET['order'])); -} - -$shop = class_exists('\AKSRT\Shop\Shop') ? \AKSRT\Shop\Shop::get_instance() : null; -$view = $shop ? $shop->get_product_archive_view([ - 'taxonomy' => 'aksrt_product_cat', - 'term_id' => $term ? (int) $term->term_id : 0, - 'paged' => $paged, - 'orderby' => $orderby, - 'order' => $order, -]) : ['products' => [], 'term' => null, 'total' => 0, 'max_pages' => 0, 'orderby' => 'date', 'order' => 'DESC', 'paging' => 'number']; - -$products = $view['products'] ?? []; -$total = (int) ($view['total'] ?? 0); -$max_pages = (int) ($view['max_pages'] ?? 0); -$term_name = $term->name ?? ''; -$term_desc = $term->description ?? ''; -$active_orderby = (string) ($view['orderby'] ?? 'date'); -$active_order = strtoupper((string) ($view['order'] ?? 'DESC')); -$paging = (string) ($view['paging'] ?? 'number'); -$ias_s = !empty($view['ias_s']); -$ias_max = (int) ($view['ias_max'] ?? 5); -$style = (string) ($view['style'] ?? 'default'); -$sales_cfg = is_array($view['sales'] ?? null) ? $view['sales'] : []; -$filter = is_array($view['filter'] ?? null) ? $view['filter'] : null; -?> - -
    - - -
    - -
    -
    -

    - -

    - -

    - - -
    - -
    - - -
    - - $product, - 'sales_cfg' => $sales_cfg, - ]); - endforeach; ?> - -

    ' . esc_html__('内容空空如也', 'aksrt') . '

    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> - -
    - - 1 && $paging === 'ajax') : ?> -
    -
    - -
    -
    - 1) : ?> - - -
    - -
    - -'; - dynamic_sidebar('shop_product_bottom_fluid'); - echo ''; -} -get_footer(); diff --git a/templates/shop/product-tag.php b/templates/shop/product-tag.php deleted file mode 100644 index fd4896f2..00000000 --- a/templates/shop/product-tag.php +++ /dev/null @@ -1,179 +0,0 @@ -'; - dynamic_sidebar('shop_home_top_fluid'); - echo ''; -} - -$term = get_queried_object(); -$paged = max(1, (int) get_query_var('paged')); -$orderby = ''; -$order = ''; -if (isset($_GET['orderby'])) { - $orderby = sanitize_key(wp_unslash((string) $_GET['orderby'])); -} elseif (isset($_GET['shop_orderby'])) { - $orderby = sanitize_key(wp_unslash((string) $_GET['shop_orderby'])); -} -if (isset($_GET['order'])) { - $order = sanitize_key(wp_unslash((string) $_GET['order'])); -} - -$shop = class_exists('\AKSRT\Shop\Shop') ? \AKSRT\Shop\Shop::get_instance() : null; -$view = $shop ? $shop->get_product_archive_view([ - 'taxonomy' => 'aksrt_product_tag', - 'term_id' => $term ? (int) $term->term_id : 0, - 'paged' => $paged, - 'orderby' => $orderby, - 'order' => $order, -]) : ['products' => [], 'term' => null, 'total' => 0, 'max_pages' => 0, 'orderby' => 'date', 'order' => 'DESC', 'paging' => 'number']; - -$products = $view['products'] ?? []; -$total = (int) ($view['total'] ?? 0); -$max_pages = (int) ($view['max_pages'] ?? 0); -$term_name = $term->name ?? ''; -$term_desc = $term->description ?? ''; -$active_orderby = (string) ($view['orderby'] ?? 'date'); -$active_order = strtoupper((string) ($view['order'] ?? 'DESC')); -$paging = (string) ($view['paging'] ?? 'number'); -$ias_s = !empty($view['ias_s']); -$ias_max = (int) ($view['ias_max'] ?? 5); -$style = (string) ($view['style'] ?? 'default'); -$sales_cfg = is_array($view['sales'] ?? null) ? $view['sales'] : []; - -$order_labels = [ - 'date' => __('最新', 'aksrt'), - 'views' => __('最热', 'aksrt'), - 'favorite_count' => __('收藏', 'aksrt'), - 'score' => __('评分', 'aksrt'), - 'sales' => __('销量', 'aksrt'), -]; -$base_url = $term && !is_wp_error(get_term_link($term)) ? get_term_link($term) : ''; -?> - -
    - - -
    - -
    -
    -

    - -

    - -

    - -
    - - $label) : - if ($active_orderby === $key) : ?> - - $key], $base_url) : add_query_arg(['orderby' => $key]); - ?> - - - - 'price', 'order' => $next_order], $base_url) : add_query_arg(['orderby' => 'price', 'order' => $next_order]); - ?> - - - - 'price'], $base_url) : add_query_arg(['orderby' => 'price']); - ?> - - - - -
    -
    - -
    - - -
    - - $product, - 'sales_cfg' => $sales_cfg, - ]); - endforeach; ?> - -

    ' . esc_html__('内容空空如也', 'aksrt') . '

    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> - -
    - - 1 && $paging === 'ajax') : ?> -
    -
    - -
    -
    - 1) : ?> - - -
    - -
    - -'; - dynamic_sidebar('shop_product_bottom_fluid'); - echo ''; -} -get_footer(); diff --git a/uninstall.php b/uninstall.php index 79186837..5840fa14 100644 --- a/uninstall.php +++ b/uninstall.php @@ -35,6 +35,9 @@ $tables = [ 'aksrt_follow', 'aksrt_user_restriction', 'aksrt_product_variant', + 'aksrt_after_sale', + 'aksrt_order_shipping', + 'aksrt_card_key', 'aksrt_revenue', 'aksrt_withdrawal', 'aksrt_charge_card',