子比主题开发文档
使用指南Codestar Framework主题扩展在线部署AI 功能推荐插件赞助打赏

商城模块

扩展子比主题商城商品、购物车、商品分类、优惠、售后、物流、订单联动、商品页和商城小工具。

模块入口

商城模块入口是 inc/functions/shop/shop.php。它先注册基础类和初始化类:

define('ZIB_SHOP_ASSETS_URI', ZIB_TEMPLATE_DIRECTORY_URI . '/inc/functions/shop/assets');
define('ZIB_SHOP_REQUIRE_URI', '/inc/functions/shop/');

zib_require(array(
    'inc/public',
    'inc/class.setup',
    'inc/class.init',
), false, ZIB_SHOP_REQUIRE_URI);

function zib_shop_instance()
{
    $GLOBALS['zib_shop'] = zib_shop::instance();
}
add_action('after_setup_theme', 'zib_shop_instance');

商城前台能力由主题设置控制:

if (_pz('shop_s')) {
    zib_require(array(
        'inc/functions',
        'widgets/widgets',
        'action/action',
    ), false, ZIB_SHOP_REQUIRE_URI);

    do_action('zib_shop_init');
}

扩展商城时先判断 _pz('shop_s')。商城关闭时,主题不会加载商城前台函数、动作和小工具。

目录地图

目录作用
inc/functions/shop/inc/class.init.php注册商品 post type、分类法、重写规则、查询和模板路由
inc/functions/shop/inc/functions.php商城公共函数、搜索接入、分享海报
inc/functions/shop/inc/product.php商品信息、商品卡片、销量、购买统计缓存
inc/functions/shop/inc/cart.php购物车按钮和购物车数据
inc/functions/shop/inc/pay.php商城订单与 Zibpay 的创建、关闭、退款、支付成功联动
inc/functions/shop/inc/order.php用户订单展示、订单详情和收货地址修改入口;用户中心与通知链路见 商城用户中心与消息通知
inc/functions/shop/inc/after-sale.php售后申请、处理和记录
inc/functions/shop/inc/shipping.php发货、物流、自动发货内容
inc/functions/shop/inc/comment.php商品评价和评分统计
inc/functions/shop/action/action.php商城前台 Ajax 动作,包含联系商家弹窗、订单地址修改、评价、售后和购物车交互
inc/functions/shop/admin/*商城后台设置、商品 Meta、分类 Meta、发货和售后管理;后台列表与排查见 商城后台与订单排查
inc/functions/shop/page/*商品页、购物车页、分类页、优惠页、标签页

数据模型

商城核心对象由 inc/functions/shop/inc/class.init.php 注册:

类型注册方式说明
shop_productregister_post_type()商品
shop_catregister_taxonomy('shop_cat', array('shop_product'), ...)商品分类
shop_discountregister_taxonomy('shop_discount', array('shop_product'), ...)优惠活动
shop_tagregister_taxonomy('shop_tag', array('shop_product'), ...)商品标签

商品配置和分类配置由 Codestar 提供:

配置文件保存位置
商品 Metainc/functions/shop/admin/options/meta-option.phpproduct_config 等 post meta
商品分类/标签/优惠 Metainc/functions/shop/admin/options/term-option.phpterm meta
商城全局设置inc/functions/shop/admin/options/admin-option.phpzibll_options

商城首页与分类入口

子比主题的商城首页不是一个单独的 shop_home post type。后台“商城首页链接”字段保存为 shop_home_url,用于面包屑、导航和商城入口跳转;页面本身通常是一个普通页面,再通过页面专属小工具容器、商城小工具和短代码组装出来。

后台配置入口在 admin/options/admin-option.php

array(
    'title'       => __('商城首页链接', 'zib_language'),
    'id'          => 'shop_home_url',
    'default'     => '',
    'type'        => 'text',
    'placeholder' => 'https://',
)

因此创建商城首页的推荐路径是:

步骤说明
新建普通页面页面标题可以叫商城、产品中心或资源商城,正文可留空
开启页面模块容器使用页面专属 page_top_fluid_{ID}page_top_content_{ID} 等位置承载模块
添加商城小工具使用 [商城]商品列表[商城]多栏目商品列表[商城]单行商品列表 等模块
配置商城首页链接把该页面 URL 写入 shop_home_url,供面包屑和商城入口使用
配置商品分类分类页、标签页和优惠页仍走主题内置 taxonomy 归档,不需要为每个分类新建页面

商城系统的能力边界可以这样理解:

层级主要对象入口
首页/频道普通页面、小工具、短代码页面容器、Zib_CFSwidget[productbox]
商品归档shop_catshop_tagshop_discounttaxonomy 路由、shop_list_opt、Ajax 分页
商品详情shop_productpage/product.phpshop_product_page_headershop_product_page_content
交易链路购物车、地址、订单、优惠、发货、售后、评价action/action.phpinc/pay.php、用户中心

不要把商城首页做成硬编码模板。使用普通页面加模块更符合主题设计:页面可以导入导出模块,分类页仍保留原生 taxonomy 查询、排序、筛选、分页和搜索能力。

商品列表配置

全局商品列表配置保存在 shop_list_opt,会影响分类页、搜索页和用户页的默认列表样式:

字段作用
orderby默认排序方式
count单页商品数量
paginateajax 追加加载或 number 数字分页
ias_s / ias_maxAjax 翻页自动加载开关和自动加载页数
list_style商品卡片 UI 配置

分类、标签和优惠活动主查询会读取这组配置。入口在 inc/functions/shop/inc/class.init.php::main_post_query()

if ($query->is_main_query() && (is_tax('shop_cat') || is_tax('shop_discount') || is_tax('shop_tag'))) {
    $shop_list_opt  = _pz('shop_list_opt');
    $posts_per_page = $shop_list_opt['count'] ?? 12;
    $orderby        = $shop_list_opt['orderby'] ?? '';

    $query->set('post_type', 'shop_product');
    $query->set('posts_per_page', $posts_per_page);
}

排序字段并不是直接塞进 SQL。主题会先判断它是不是数值型 meta、字符串 meta,最后才当作 WordPress 原生 orderby

$orderby_keys      = zib_get_query_mate_orderby_keys();
$mate_orderbys     = $orderby_keys['value'];
$mate_orderbys_num = $orderby_keys['value_num'];

if (in_array($orderby, $mate_orderbys_num)) {
    $query->set('orderby', 'meta_value_num');
    $query->set('meta_key', $orderby);
} elseif (in_array($orderby, $mate_orderbys)) {
    $query->set('orderby', 'meta_value');
    $query->set('meta_key', $orderby);
} else {
    $query->set('orderby', $orderby);
}

商城排序选项由 zib_shop_csf_module::product_orderby_options() 提供:

含义
modified更新时间
date发布时间
views浏览量,数值型 meta
comment_count评论量
favorite_count收藏数量,数值型 meta
zibpay_price售价,数值型 meta
score评分,数值型 meta
sales_volume销量,数值型 meta
rand随机

如果要新增排序字段,优先通过 query_mate_orderby_keys 把自己的 meta key 加到 value_numvalue,再加入对应的后台选项。不要在模板里手写 ORDER BY

分类页还会输出面包屑、同级分类、子分类和排序条。链接上带有 ajax-replace="true"route="1"ajax-next,所以前端会走主题 Ajax 替换,而不是整页刷新:

<a ajax-replace="true" route="1" class="ajax-next" href="...">分类名称</a>

分类页筛选逻辑在 inc/functions/shop/page/cat.php::zib_shop_get_cat_filter(),它会按当前分类查:

场景输出
当前分类有子分类顶部显示同级分类 Tab,下方显示“全部 + 子分类”
当前分类无子分类但有父级顶部显示父级的同级分类,下方显示“全部 + 当前同级分类”
一级分类且无子分类只显示一级分类 Tab

排序条由 zib_shop_get_the_trem_orderby_lists() 读取当前 orderorderby 后生成。shop_discountshop_tag 页面没有层级筛选,但同样输出排序条,并把排序块标记为:

win-ajax-replace="orderby"

主列表输出在 zib_shop_get_main_product_lists()。它会用 shop_list_opt.list_style 合并分类级样式,然后输出商品卡片、分页和加载占位:

$shop_list_opt = _pz('shop_list_opt');
$default       = $shop_list_opt['list_style'] ?? array();
$card_args     = array_merge($default, $card_args);

while (have_posts()) {
    the_post();
    $lists .= zib_shop_get_product_list_card($card_args);
}

$lists .= zib_shop_get_paginate($wp_query->found_posts);
$lists .= '<div class="post_ajax_loader" style="display:none;">' . zib_shop_get_lists_card_placeholder($card_args) . '</div>';

扩展分类页筛选时,保留这些协议:

协议作用
.ajaxpager.product-lists-rowAjax 替换和追加列表的目标容器
.ajax-item-headerAjax 列表头部区域
.ajax-next主题前端接管链接
ajax-replace="true"使用局部替换而不是整页跳转
route="1" / route-title路由标题和浏览器地址同步
.post_ajax_loader下一页加载时的商品骨架屏

不要把商品分类、优惠活动、标签页改成独立 REST 拉取后整块重绘。主题的 URL、分页、搜索、排序、标题和局部替换已经通过这些属性串起来,重写后很容易出现浏览器后退、标题、分页或目录状态不同步。

归档页共用 Term 能力

分类、标签、优惠活动的共用函数在 inc/functions/shop/inc/term.php。这层不是后台字段定义,而是前台归档页和前台设置用到的辅助能力。

函数用途
zib_shop_get_the_trem_orderby_lists()从当前查询和 shop_list_opt.orderby 生成排序按钮
zib_shop_get_term_header_more_btn()输出归档页右上角更多按钮,目前主要是搜索
zib_shop_get_term_search_btn()生成限定当前 term 的商品搜索按钮
zib_shop_get_taxonomy_name()shop_catshop_tagshop_discount 转成分类、标签、优惠活动
zib_shop_get_term_config()读取 {taxonomy}_config term meta
zib_shop_term_frontend_set_input_array()前台设置弹窗追加可编辑字段
zib_shop_term_frontend_set_save()保存前台设置字段到 term meta

归档页右上角搜索按钮最终走主题搜索弹窗:

$args = array(
    'class'       => $class,
    'trem'        => $term->term_id,
    'trem_name'   => zib_str_cut($title, 0, 8),
    'type'        => 'product',
    'placeholder' => __('在', 'zib_language') . zib_shop_get_taxonomy_name($term->taxonomy) . '[' . $title . ']中搜索商品',
);

return zib_get_search_link($args);

这里源码参数名是 trem。二开时不要随手改成 term,除非同时确认前端搜索脚本和搜索请求读取的参数名已经一起改掉。

前台设置会按 taxonomy 追加不同字段:

Taxonomy字段说明
shop_catlist_style_style分类页列表样式,支持默认、竖向大卡片、横向小卡片
shop_discountsmall_badge优惠活动小标签
shop_discountpriority优惠活动优先级
shop_discountis_important重点活动,商品金额位置着重显示
shop_tagpriority标签优先级
shop_tagis_important重点标签,重要位置着重显示

保存时只处理这几个 key:

$keys = array('list_style_style', 'small_badge', 'priority', 'is_important');
foreach ($keys as $key) {
    if (isset($_POST[$key])) {
        $config[$key] = $_POST[$key];
    }
}

update_term_meta($object_data->term_id, $taxonomy . '_config', $config);

如果要新增前台可编辑的 term 字段,应同时补输入项和保存白名单。只在输入数组里加字段,不加保存 key,前台设置看起来能改,刷新后仍会丢失。

优惠活动归档页

优惠活动页使用 shop_discount taxonomy,对应页面文件是 inc/functions/shop/page/dis.php。它不是单纯输出商品列表,而是先用 zib_shop_get_dis_header($dis_id) 渲染活动头部,再进入同一套商品列表:

function zib_shop_dis_page_content()
{
    global $wp_query;
    $cat_id = $wp_query->get_queried_object_id();
    $header = zib_shop_get_dis_header($cat_id);

    $html = '<div class="shop-term-main mb20"><div class="ajaxpager product-lists-row">';
    $html .= $header;
    $html .= zib_shop_get_main_product_lists();
    $html .= '</div></div>';

    echo $html;
}
add_action('shop_dis_page_content', 'zib_shop_dis_page_content');

活动头部读取的是 zib_shop_get_discount_data($dis),会把优惠政策转换成用户可读信息:

数据展示
small_badge角标,和活动名相同则不重复显示
discount_type=reduction立减{reduction_amount}
discount_type=discount{discount_amount}折
term 描述.page-desc 活动说明
price_limit + discount_scope单价、商品、店铺、跨店满额可用
user_limitVIP、VIP2、认证用户可用
time_limit_config.countdown“活动仅剩”倒计时
gift_config“赠送”赠品摘要
discount_error未开始、已结束或配置错误提示

配置错误的提示只对超级管理员显示:

if ($dis_data['discount_error'] === 'config_error' && is_super_admin()) {
    $error = sprintf(__('配置错误:%s', 'zib_language'), $dis_data['discount_error_msg']);
}

因此不要在前台给普通用户输出后台配置细节。普通用户只需要看到活动是否可用、门槛和商品列表;管理员才需要看到配置错误原因。

优惠活动的实际数据不是直接读取 term meta 后原样使用。主题会先通过 zib_shop_get_discount_data() 合并 term 基础信息和 shop_discount_config,再调用 zib_shop_get_discount_policy() 做有效性判断:

$product_discount_config = zib_shop_get_discount_config($discount->term_id) ?: array();
$data = array_merge(array(
    'id'             => $discount->term_id,
    'name'           => $discount->name,
    'small_badge'    => !empty($product_discount_config['small_badge']) ? $product_discount_config['small_badge'] : $discount->name,
    'desc'           => $discount->description,
    'link'           => get_term_link($discount),
    'price_limit'    => $product_discount_config['price_limit'] ?? 0,
    'user_limit'     => $product_discount_config['user_limit'] ?? 0,
    'time_limit'     => $product_discount_config['time_limit'] ?? 0,
    'priority'       => $product_discount_config['priority'] ?? 50,
    'discount_scope' => $product_discount_config['discount_scope'] ?? 'item',
), zib_shop_get_discount_policy($product_discount_config));

zib_shop_get_discount_policy() 会先判断 discount_type,再校验每种策略:

策略校验
reductionreduction_amount 必须大于 0
discountdiscount_amount 必须在 0.019.99 之间
giftgift_config 过滤后至少要有一个有效赠品
time_limit未开始返回 time_limit_start,已结束返回 time_limit_end
is_important写入 important_class,供商品价格区域突出显示

商品读取优惠活动时使用 zib_shop_get_product_discount($post_id, $is_valid)。默认只返回有效活动;如果要做后台排查或活动预览,才传 false 拿到包含未开始、已结束和配置错误在内的全部活动。返回结果会走 zib_shop_discount_sort(),先按 priority 升序,再按 term ID 升序,所以数值越小越靠前。

usort($discounts, function ($a, $b) {
    $priority_a = isset($a['priority']) ? intval($a['priority']) : 0;
    $priority_b = isset($b['priority']) ? intval($b['priority']) : 0;

    if ($priority_a !== $priority_b) {
        return $priority_a - $priority_b;
    }

    return intval($a['id'] ?? 0) - intval($b['id'] ?? 0);
});

商品页的重点活动只取第一个有效的 is_important 活动。运营上如果给同一商品挂多个重点活动,前台不会都作为主价格氛围展示;其余活动仍可在优惠卡片、结算明细或活动归档里展示。

后台优惠活动列表也复用同一份解析结果。discount_columns() 会追加“活动、限制、优先级”列,discount_custom_column() 会把小标签、重点标记、配置错误、立减、折扣、赠品摘要和限制条件展示出来。后台看到的错误和前台看到的活动状态来自同一套数据,不要另写一套后台列计算逻辑。

结算计算时,优惠命中分两步:先检查金额门槛和用户限制,再计算优惠金额。作用范围 discount_scope 决定满额和分摊方式:

discount_scope满额判断立减分摊
item当前规格单价立减金额乘购买数量
product当前商品总额按当前规格数量占商品总数量比例分摊
author当前商家总额按当前规格数量占商家商品总数量比例分摊
order整单总额按当前规格数量占整单总数量比例分摊

折扣类优惠会直接按当前已优惠后的价格继续打折;立减金额不会超过当前价格。也就是说多个优惠会按排序结果依次作用,后一个优惠面对的是前一个优惠处理后的价格。二开结算页时不要只在前端改展示价格,最终订单里的 discount_hitprices.total_discountgift_data 和支付金额都依赖这套计算链路。

优惠活动页的排序条仍然走 zib_shop_get_the_trem_orderby_lists(),并保留局部替换标记:

win-ajax-replace="orderby"

活动页底部仍由 zib_shop_get_main_product_lists() 输出商品卡片和分页。二开活动页头部时,适合追加运营说明、规则链接或活动客服入口;不要把 shop_discount_config 里的政策改写到页面模板里,否则确认下单和活动展示会出现两套规则。

商城小工具

商城首页最常用的小工具在 widgets/widgets-product.php

小工具id适用场景
商品列表zib_shop_widget_ui_product_lists首页分区、分类推荐、优惠商品区
单行商品列表zib_shop_widget_ui_oneline_product_lists横向滚动展示、首屏推荐
多栏目商品列表zib_shop_widget_ui_tab_product热门推荐、最新上架、销量榜等 Tab

这些小工具都支持按 shop_catshop_discountshop_tag 筛选,并通过 zib_query_orderby_filter() 统一处理排序。筛选字段的含义:

字段作用
exclude_cat排除分类,优先级高于 include_cat
include_cat仅包含分类,只有未设置 exclude_cat 时生效
include_dis仅包含指定优惠活动
include_tag仅包含指定特色标签
orderby排序方式
count每次查询数量
paginateajax 追加、number 数字分页或不分页
list_style商品卡片样式

商品列表小工具第一次渲染时不直接查询商品,而是输出 zib_get_ias_ajaxpager() 占位,让前端通过 ajax_widget_ui 拉取保存的小工具实例:

$ias_args = array(
    'type'   => 'ias',
    'class'  => 'product-lists-row',
    'loader' => zib_shop_get_lists_card_placeholder($list_style),
    'query'  => array(
        'action' => 'ajax_widget_ui',
        'id'     => $id_base,
        'index'  => $index,
    ),
);

Ajax 回调 zib_shop_widget_ui_product_lists_ajax($instance) 才会读取实例里的筛选条件,组装 tax_query

if (!empty($instance['exclude_cat'])) {
    $tax_query[] = array(
        'taxonomy' => 'shop_cat',
        'field'    => 'term_id',
        'terms'    => $instance['exclude_cat'],
        'operator' => 'NOT IN',
    );
} elseif (!empty($instance['include_cat'])) {
    $tax_query[] = array(
        'taxonomy' => 'shop_cat',
        'field'    => 'term_id',
        'terms'    => $instance['include_cat'],
    );
}

然后构建商品查询:

$query_args = array(
    'ignore_sticky_posts' => 1,
    'post_type'           => 'shop_product',
    'post_status'         => 'publish',
    'tax_query'           => $tax_query,
    'posts_per_page'      => $count,
);

$query_args = zib_query_orderby_filter($orderby, $query_args);

如果 paginate 为空,会设置 no_found_rows=true,减少不需要总数的查询;如果开启 ajaxnumber,才会查询 found_posts 并输出 .ajax-pag。这也是为什么自定义小工具不要无条件分页,首页多模块同时查询时成本会明显增加。

多栏目商品列表的非首个 Tab 会输出隐藏触发器:

<span class="post_ajax_trigger hide">
    <a class="ajax_load ajax-next ajax-open" no-scroll="true"></a>
</span>

所以新增商城类 Tab 模块时,不需要重写前端 Ajax Tab。保持 data-ajax.post_ajax_trigger.ajax-next.ajaxpager 即可。

[商城]单行商品列表 不走 Ajax 懒加载,它会直接查询商品并输出 Swiper 结构:

<div class="swiper-container swiper-scroll mb20">
    <div class="swiper-wrapper swiper-wrapper-product-lists">
        ...
    </div>
    <div class="swiper-button-prev"></div>
    <div class="swiper-button-next"></div>
</div>

因此单行商品列表适合首屏少量推荐,不适合大量商品分页。需要分页、Tab 或懒加载时,应使用商品列表或多栏目商品列表。

商品短代码

正文或模块内容里可以插入单个商品卡片:

[productbox id="123"]

短代码会检查 _pz('shop_s'),再读取商品和全局列表样式,强制用小卡片展示:

$card_args               = $shop_list_opt['list_style'] ?? array();
$card_args['style']      = 'small';
$card_args['show_price'] = 1;
$card_args['show_desc']  = 0;

短代码适合在文章、专题页或产品说明中插入商品入口。不要把短代码当作下单接口,它只负责展示商品卡片;价格、优惠、库存和订单仍由商品详情、购物车和下单链路服务端计算。

搜索与用户中心

商城开启后会把商品加入主题搜索:

add_filter('search_types', 'zib_shop_search_types_filter');
add_filter('search_main_tabs_array', 'zib_shop_search_main_tabs_array_filter');
add_filter('main_search_tab_content_product', 'zib_shop_get_search_product', 10);

商城也会接入用户中心:

add_filter('user_center_page_sidebar', 'zib_shop_user_center_page_sidebar_order');
add_filter('user_ctnter_main_tabs_array', 'zib_shop_user_ctnter_main_tabs_array_filter_order', 20);
add_filter('user_page_order_tabs', 'zib_shop_user_order_tabs');
add_filter('user_order_list_card', 'zib_shop_user_order_list_card', 10, 3);
add_filter('user_order_details_modal', 'zib_shop_user_order_details_modal', 10, 3);

新增用户中心商城功能时,要跟随用户中心现有 Tab、订单卡片、详情弹窗和权限判断,不要新建一套孤立入口。

商品页面模板 Hook

商品页在 inc/functions/shop/page/product.php 中触发:

do_action('shop_locate_template');
do_action('shop_locate_template_' . $page_type);
do_action('shop_' . $page_type . '_page_header');
do_action('shop_' . $page_type . '_page_content');

默认商品页内容挂载在:

add_action('shop_product_page_header', 'zib_shop_single_header');
add_action('shop_product_page_content', 'zib_shop_single_content');

商品正文后还有:

do_action('shop_product_page_content_after');

追加购买说明或售后说明可以这样写:

add_action('shop_product_page_content_after', 'zib_docs_shop_product_after_sale_notice', 20);

function zib_docs_shop_product_after_sale_notice()
{
    if (!is_singular('shop_product')) {
        return;
    }

    echo '<div class="muted-box mt20">' . esc_html__('下单前请确认商品规格和收货信息。', 'zib_language') . '</div>';
}

商品详情页布局

商品详情页不是一个静态模板文件拼完的页面,而是 page/product.php 触发 Hook,inc/single.php 挂载头部、内容区、移动端按钮、评价和小工具位置。

页面入口会先根据布局重写 body_class

function zib_shop_product_body_class($classes)
{
    $content_layout = zib_shop_get_product_content_layout();
    if ($content_layout === 'side') {
        $classes[] = 'site-layout-2';
    } else {
        $classes[] = 'site-layout-1';
    }

    $classes[] = 'content-layout-' . $content_layout;
    $classes[] = 'shop';
    return $classes;
}

三种布局值的行为如下:

页面结构适用场景
full详情 Tab 全宽显示,相关推荐和 shop_product_page_content_after 放回 .container商品介绍主要由大图、长图、模块化区块构成
box内容区包在 .container > .content-wrap > .content-layout 内,无侧栏常规商品详情,正文宽度需要保持可读
side内容区同 box,额外输出 .sidebarshop_product_sidebar需要右侧目录、店铺说明、热门商品或补充模块

布局配置按“商品 > 分类 > 主题设置”依次继承:

function zib_shop_get_product_in_turn_config($product_id, $key, $default = '')
{
    $config = zib_shop_get_product_config($product_id, $key, '');
    if (!$config) {
        $config = zib_shop_get_product_cat_config($product_id, $key);
    }
    if (!$config) {
        $config = _pz('shop_' . $key, $default);
    }

    return $config;
}

zib_shop_get_product_cat_config() 会先读当前商品分类,再向父级分类查找。扩展配置时不要只读 _pz('shop_content_layout'),否则会绕过商品和分类级覆盖。

头部购买区

商品页头部挂在 shop_product_page_header

add_action('shop_product_page_header', 'zib_shop_single_header');

默认头部结构是:

区块函数说明
面包屑zib_shop_get_breadcrumbs()PC 端按首页、商城首页、商品分类、商品名称输出
商品封面zib_shop_get_product_cover($post)读取 cover_imagescover_videos、选项图片并交给 zib_new_slider()
商品选项zib_shop_get_product_detail($post)输出 Vue 挂载点,包含标题、简介、标签、价格、优惠、规格、服务、数量和按钮
移动评价zib_shop_single_mobile_comment_drawer()移动端把评价做成抽屉入口,避免详情页首屏过长

封面轮播会把商品封面图转成 slides,如果没有封面图,会回退到商品缩略图或主题占位图。配置了选项图片时,主题还会把选项图片追加到轮播,并写入 product-opt-index,用于规格切换时联动封面。

商品封面图片与视频

商品封面字段在 inc/functions/shop/admin/options/meta-option.phpproduct_config 里。图片和视频不是同一个字段:

字段类型说明
cover_imagesgallery商品封面图片,保存附件 ID 列表
cover_videosrepeater商品封面视频,每项只有 url,可上传视频或填写视频地址
main_imageupload自定义商品主图,不等同于轮播封面列表

后台字段说明里有一个关键约束:封面视频会和封面图片同步显示,顺序保持一致,所以必须先设置图片封面,并且图片数量要大于等于视频数量。前台 zib_shop_get_product_cover() 正是按图片索引去取同位置视频:

$cover_images = explode(',', ($product_configs['cover_images'] ?? ''));
$cover_videos = $product_configs['cover_videos'] ?? array();

foreach ($cover_images as $index => $cover_id) {
    $img_url = zib_get_attachment_image_src((int) $cover_id, 'full');
    if (!empty($img_url[0])) {
        $slides[] = array(
            'video'      => $cover_videos[$index]['url'] ?? '',
            'background' => $img_url[0],
        );
    }
}

如果没有有效封面图片,主题会回退到商品缩略图或主题占位图,同时只尝试使用 cover_videos[0].url

$slides = array(
    array(
        'background' => zib_shop_get_product_thumbnail_url($post, 'full') ?: ZIB_TEMPLATE_DIRECTORY_URI . '/img/thumbnail.svg',
        'video'      => $cover_videos[0]['url'] ?? '',
    ),
);

随后规格选项图片会继续追加到同一个轮播,并写入 product-opt-index

$slides[] = array(
    'background' => $opt_value['image'],
    'attr'       => 'product-opt-index="' . $opt_key . '_' . $opt_value_key . '"',
);

因此扩展商品封面视频时,不要把商品视频写到文章的 featured_video,也不要写到 Zibpay 付费视频的 posts_zibpay.video_url。商品封面视频只服务商品详情页头部轮播;付费播放、购买权限、剧集内容仍应走 Zibpay 付费视频链路。

如果要通过代码补充商品封面视频,应保持 product_config.cover_videos 的 repeater 结构:

function zib_docs_shop_append_cover_video($product_id, $video_url)
{
    $product_id = (int) $product_id;
    if (!$product_id || !$video_url) {
        return false;
    }

    $config = zib_shop_get_product_config($product_id);
    $config['cover_videos'] = $config['cover_videos'] ?? array();
    $config['cover_videos'][] = array(
        'url' => esc_url_raw($video_url),
    );

    zib_shop_save_product_config($product_id, null, $config);

    return true;
}

这个示例只追加封面视频,不创建订单权限,也不改变商品主图。实际使用时还要确保 cover_images 至少有同位置图片作为封面兜底。

内容区与 Tab

内容区挂在 shop_product_page_content

add_action('shop_product_page_content', 'zib_shop_single_content');

默认内容先生成“详情”Tab,再按条件追加“评价”和自定义 Tab:

Tab来源说明
articlezib_shop_single_content_article()商品正文,经过 the_content,支持分页和正文模块
commentzib_shop_single_content_comment()PC 端显示,移动端转为评价抽屉
tab-*zib_shop_get_single_tabs()商品、分类或主题设置里的自定义栏目

自定义 Tab 的继承顺序同样是商品、分类、主题设置。商品设置为 disable 会关闭自定义 Tab;设置为 custom 会只使用当前商品的栏目;分类设置也可以覆盖主题默认栏目。

源码会把自定义 Tab 内容套进:

'<div class="zib-widget article product-article"><div class="wp-posts-content">' . $content . '</div></div>'

所以 Tab 内容可以使用正文短代码和古腾堡输出,但不要在里面再包整套页面容器。适合放“安装须知”“规格说明”“授权范围”“售后规则”“常见问题”等补充说明。

商品页模块位置

商城小工具注册在 inc/functions/shop/widgets/widgets.php。商品详情页有三个位置:

Sidebar id后台名称输出位置
shop_product_top_fluid[商城]商品详情页-上方全宽度商品详情内容上方,适合公告、横幅、组合模块
shop_product_sidebar[商城]商品详情页-侧边栏side 布局下输出,适合窄模块
shop_product_bottom_fluid[商城]商品详情页-底部全宽度商品页最底部,适合推荐、保障、FAQ、专题模块

内容区输出顺序是:

dynamic_sidebar('shop_product_top_fluid');

// content tabs + related + shop_product_page_content_after

dynamic_sidebar('shop_product_bottom_fluid');

用模块搭建产品详情页时,优先把主视觉、卖点、对比、FAQ、评价摘要、服务保障这些展示区放进 shop_product_top_fluidshop_product_bottom_fluid;商品正文只保留真正属于当前商品的详情内容。这样模块可以通过小工具导入导出复用,商品正文仍能随商品单独维护。

如果选择 side,侧栏只在电脑端显示,源码注册说明也明确侧栏位置较小。不要把宽封面、横向轮播、复杂组合模块塞进 shop_product_sidebar,它更适合商品目录、卖家联系、热门商品、服务说明、标签云等窄内容。

背景盒子与正文承载

商品正文外层由 content_show_bg 控制:

输出类名建议
onarticle product-article zib-widget正文是文字说明、表格、短内容,或页面是侧栏布局
offproduct-article-full mb20正文主要是长图、视觉模块或自定义大段排版

这个配置也按商品、分类、主题设置继承,并可通过前台设置保存到 product_config

add_action('zib_frontend_set_save', 'zib_shop_single_frontend_set_save', 10, 2);

扩展前台设置时,跟随 zib_frontend_set_input_arrayzib_frontend_set_save 的模式,把可编辑字段写回 product_config,不要另建一个和商品配置脱节的 meta。

商品评价与评分统计

商城评价不是普通文章评论入口。它复用 WordPress comment 表存储内容,但入口、状态、评分和订单关系都由商城模块接管:

文件负责
inc/functions/shop/action/action.php打开评价弹窗、提交评价 Ajax
inc/functions/shop/inc/comment.php评价状态、自动好评、创建评论、评分统计、缓存清理
inc/functions/shop/template/comments.php商品详情页评价头部、筛选、列表和商家回复
inc/functions/shop/inc/product.php商品 score_data 增量评分
inc/functions/shop/inc/shipping.php确认收货后初始化评价状态
inc/functions/shop/inc/after-sale.php售后期间锁定或恢复评价状态

订单评价状态写在 Zibpay 订单 meta comment_status

含义触发点
-1商品未开启评价确认收货时商品评价开关为关闭
0待评价确认收货后初始化,或售后结束恢复
1已评价用户提交评价,或超过评价时效自动好评
2售后中无法评价待评价订单申请售后,或退款退货后不再允许评价

确认收货统一走 zib_shop_order_receive_confirm(),它会写入 order_data.shipping_data.receive_time,然后调用:

zib_shop_init_order_comment_status($order);

评价时效由 _pz('shop_comment_max_day', 15) 控制。zib_shop_get_order_comment_status() 读取到 comment_status === 0 时,会继续检查 zib_shop_get_order_comment_over_time();如果已超过确认收货后的可评价天数,会调用 zib_shop_order_auto_comment() 自动创建 5 分好评并返回已评价状态。快递订单会同时写入 shipping=5,虚拟或无需物流订单只写商品和服务评分。

用户点击评价按钮时,订单页通过 zib_shop_get_order_comment_link() 打开 order_comment_modal

add_action('wp_ajax_order_comment_modal', 'zib_shop_ajax_comment_modal');
add_action('wp_ajax_shop_order_comment', 'zib_shop_order_comment');

提交评价时必须保留这些校验:

  1. zib_ajax_verify_nonce() 验证 shop_order_comment
  2. 当前用户必须是订单所属用户。
  3. 订单必须是商城订单,且商品评价开关未关闭。
  4. comment_status 必须仍为 0
  5. 商品评分和服务评分必填;快递订单还要校验物流评分。
  6. 图片上传走主题上传链路,数量受 _pz('shop_comment_img_num', 6) 控制。

统一落库函数是 zib_shop_order_comment_handle()。它会允许同一商品下重复 comment,因为同一用户可以购买多次;同时会强制本次评价直接审核通过:

add_filter('duplicate_comment_id', '__return_false');
add_filter('pre_comment_approved', function () {
    return 1;
});

评价成功后会写这些 comment meta:

Meta内容
score本次评价平均分,用于评论查询筛选
score_dataproductserviceshippingaveragehas_imageis_autoimg_ids
shop_has_image有图评价标记,只有有图时写入
order_data购买规格 options_active_name、数量 count、订单 ID

商品本身会维护两份评分数据:

Meta内容
score_data商品综合评分、分项评分、评价总数、好评/中差评/有图数量
score综合评分的扁平值,后台列表和排序更容易读取

评分增量由 zib_shop_update_product_score() 完成。新增评价时按旧平均分和旧数量重新计算 averageproductserviceshipping,并把 average >= 3.5 归入好评,否则归入中/差评。评论审核、删除或状态变化时,transition_comment_status 会触发 zib_shop_update_product_score_counts() 反向增减统计;如果没有评价了,商品 score_data 会恢复为 false

评价数量不是直接使用 WordPress comment_count。zib_shop_get_comment_count() 只统计商品下一级、已审核评论,并把结果缓存到 shop_comment_count。创建评价、评论状态变化时都会删除这个缓存。

评价列表头部由 zib_shop_comment_header() 输出,前台筛选参数如下:

参数说明
ctypeimage只看有图评价,查询 shop_has_image=1
ctypegood只看 score >= 3.5 的评价
ctypebad只看 score < 3.5 的评价
corderbycomment_date_gmt最新评价
corderbycomment_like最热评价

评价头部带有 win-ajax-replace="shop-comment-header",筛选和翻页时会替换评分摘要、筛选数量和排序按钮。商品评论模板还会关闭普通评论的置顶、编辑和回复按钮;如果某条子评论的 user_id 等于商品作者 ID,则渲染为“商家回复”。

移动端评价不放在 Tab 内。zib_shop_single_mobile_comment_drawer() 会筛选评分不低于 3.5、正文长度大于 3 的最新评价,最多展示 3 条精选摘要,并把完整评价列表放进抽屉;PC 端则通过 zib_shop_single_comment_is_show_tab() 把评价作为商品详情 Tab。

二开商品评价时,优先复用订单评价链路,不要自己 wp_insert_comment() 后只写一条评论。否则订单按钮、售后锁定、商品评分、好评数量、有图数量、自动好评和评论缓存都会脱节。

商品详情相关推荐

商品详情页底部相关推荐由全局开关 _pz('shop_single_related_s', true) 控制,参数保存在 _pz('shop_single_related_opt')

配置默认说明
title推荐推荐区标题
typecatdiscounttag关联分类、活动、标签
orderbyviews使用商城列表排序选项
count12每页或一次加载数量
paginate可选 ajax 追加翻页或 number 数字翻页
list_style商品卡片样式配置

页面首次输出只放 Ajax 容器:

$ias_args = array(
    'type'   => 'ias',
    'class'  => 'product-lists-row',
    'loader' => zib_shop_get_lists_card_placeholder($related_config['list_style']),
    'query'  => array(
        'action'  => 'shop_single_related',
        'post_id' => $product_id,
    ),
);

zib_get_ias_ajaxpager($ias_args);

真正的查询在 zib_shop_ajax_single_related()。它会读取当前商品的 shop_catshop_discountshop_tag,按启用的关联类型组装 tax_query,关系为 OR,并排除当前商品:

$query_args = array(
    'ignore_sticky_posts' => 1,
    'post_type'           => 'shop_product',
    'post_status'         => 'publish',
    'tax_query'           => $tax_query,
    'posts_per_page'      => $count,
    'post__not_in'        => array($post_id),
);

排序继续交给 zib_query_orderby_filter($orderby, $query_args),卡片输出继续使用 zib_shop_get_product_list_card($list_card_args)。如果开启 Ajax 翻页,返回内容会追加 zib_get_ajax_next_paginate();数字翻页则使用 zib_get_ajax_number_paginate();不翻页时仍会输出隐藏分页节点,方便前端列表组件保持一致结构。

扩展相关推荐时,建议只调整 shop_single_related_opt 或在查询前后挂过滤逻辑。不要把推荐商品硬编码进正文 Tab,否则商品分类、活动标签、列表样式、分页和占位加载都会和主题原有列表系统脱节。

扩展示例

在商品详情 Tab 后追加一个轻量模块,可以使用 shop_product_page_content_after。示例保持主题的命名函数、array() 和转义风格:

add_action('shop_product_page_content_after', 'zib_docs_shop_product_extra_service', 30);

function zib_docs_shop_product_extra_service()
{
    if (!is_singular('shop_product')) {
        return;
    }

    $service = zib_shop_get_product_in_turn_config(get_the_ID(), 'service', array());
    if (empty($service) || !is_array($service)) {
        return;
    }

    echo '<div class="zib-widget product-extra-service">';
    echo '<div class="title-theme">' . esc_html__('服务说明', 'zib_language') . '</div>';
    echo '<div class="muted-color">';
    foreach ($service as $item) {
        if (empty($item['name'])) {
            continue;
        }
        echo '<span class="badg badg-sm mr6 mb6">' . esc_html($item['name']) . '</span>';
    }
    echo '</div>';
    echo '</div>';
}

这类扩展只读主题已经计算好的商品继承配置,不参与价格、库存和订单创建。如果要改变购买条件,应接入购物车、下单或库存校验链路,而不是只在页面上隐藏按钮。

前台 Ajax 动作

商城前台动作集中在 inc/functions/shop/action/action.php

Action回调说明
cart_addzib_shop_ajax_cart_add加入购物车
update_cartzib_shop_ajax_update_cart更新购物车
shop_single_relatedzib_shop_ajax_single_related商品详情页相关推荐
shop_confirm_modalzib_shop_ajax_shop_confirm_modal下单确认弹窗
shop_submit_orderzib_shop_ajax_submit_order提交商城订单
shop_get_user_addresseszib_shop_ajax_get_user_addresses获取收货地址
shop_save_user_addresszib_shop_ajax_save_user_address保存收货地址
shop_delete_user_addresszib_shop_ajax_delete_user_address删除收货地址
order_comment_modalzib_shop_ajax_comment_modal评价弹窗
shop_order_commentzib_shop_order_comment提交评价
shipping_express_datazib_shop_ajax_shipping_express_data查询物流
order_receive_confirmzib_shop_ajax_order_receive_confirm确认收货
order_modify_addresszib_shop_ajax_order_modify_address修改收货地址
modify_address_applyzib_shop_ajax_modify_address_apply申请修改地址
order_after_sale_applyzib_shop_ajax_order_after_sale_apply申请售后
order_after_sale_cancelzib_shop_ajax_order_after_sale_cancel取消售后
favorite_productzib_shop_ajax_favorite_product收藏商品
author_shop_productzib_shop_ajax_user_shop_product作者主页商品列表
author_contact_modalzib_shop_ajax_author_contact_modal联系卖家弹窗

这些动作会同时关心商品状态、订单状态、当前用户、订单归属、售后时效和 nonce。不要绕过它们直接写订单 meta。

后台发货与售后动作

后台管理动作在 inc/functions/shop/admin/actions/ajax.php

Action回调说明
admin_shipping_submitzib_shop_ajax_admin_shipping_submit单个订单发货
admin_batch_shipping_submitzib_shop_ajax_admin_batch_shipping_submit批量发货
admin_after_sale_handle_submitzib_shop_ajax_admin_after_sale_handle_submit处理售后
admin_after_sale_refund_return_handlezib_shop_ajax_admin_after_sale_refund_return_handle处理退货退款
after_sale_express_datazib_shop_ajax_after_sale_express_data查询售后物流

后台动作要保留管理员权限、nonce、订单状态校验和操作记录。涉及退款、退货、发货时要和 Zibpay 订单状态同步。

与 Zibpay 的联动

商城购买最终会进入 Zibpay 订单流程。inc/functions/shop/inc/pay.php 挂了这些关键 Hook:

add_filter('zibpay_is_allow_balance_pay', 'zib_shop_is_allow_balance_pay_filter', 10, 2);
add_filter('zibpay_is_allow_card_pass_pay', 'zib_shop_is_allow_card_pass_pay_filter', 10, 2);
add_action('order_created', 'zib_shop_order_created', 10, 1);
add_action('order_closed', 'zib_shop_order_closed', 10, 1);
add_action('order_refunded', 'zib_shop_order_closed', 10, 1);
add_action('payment_order_success', 'zib_shop_order_payment_success', 10, 2);

边界很清楚:

层级负责
商城层商品、购物车、收货地址、发货、售后、评价
Zibpay 层创建订单、支付、关闭、退款、余额、积分、会员、分佣
支付渠道层发起支付、回调验签、金额校验、订单状态通知

新增商城能力时,不要自己拼支付回调和资产变动。需要发放权益或关闭订单时,应接入 Zibpay 的订单状态流程。

商城提交订单的 Ajax 是 shop_submit_order。源码会重新计算购物车、商品价格、优惠、收货地址、支付方式和应付金额,然后调用 zibpay::add_payment()zibpay::add_order()。所以扩展商城下单时,正确位置是:

需求建议入口
下单前补充字段下单确认弹窗或提交前的服务端校验
改变商品可购买条件商品状态、库存、权限和购买限制相关 filter
支付成功后发放权益payment_order_success
订单创建后写业务 metaorder_created 或订单创建前的服务端数据组装
用户中心展示订单扩展信息user_order_list_carduser_order_details_modal

不要从前端提交 pay_priceorder_pricepost_author 后直接落库。前端只能提交用户选择,价格、优惠、订单归属和支付数据必须由服务端重新计算。

购物车地址管理、确认下单优惠徽章、优惠明细弹窗和服务端下单校验见 商城购物车与确认下单。商家主页并不是独立店铺系统,而是复用作者主页商品 Tab;相关入口见 作者主页与用户中心

缓存与统计

商品购买、关闭、退款会清理用户已购商品数量缓存:

add_action('order_created', 'zib_shop_get_user_bought_product_count_cache_del');
add_action('order_closed', 'zib_shop_get_user_bought_product_count_cache_del');
add_action('order_refunded', 'zib_shop_get_user_bought_product_count_cache_del');

商品 Meta 变化会触发:

add_action('updated_post_meta', 'zib_shop_update_postmeta_add_action', 99, 4);
add_action('added_post_meta', 'zib_shop_update_postmeta_add_action', 99, 4);

评价状态变化会更新商品评分统计:

add_action('transition_comment_status', 'zib_shop_update_product_score_counts', 10, 3);

收藏商品后会触发:

do_action('shop_favorite_product', $id, $user_id);

示例:商品收藏后写入轻量日志:

add_action('shop_favorite_product', 'zib_docs_shop_favorite_log', 10, 2);

function zib_docs_shop_favorite_log($product_id, $user_id)
{
    $product_id = (int) $product_id;
    $user_id    = (int) $user_id;

    if (!$product_id || !$user_id || get_post_type($product_id) !== 'shop_product') {
        return;
    }

    update_user_meta($user_id, '_docs_last_favorite_product', $product_id);
}

风险边界

  • 不要从前端提交价格、库存、优惠后直接创建订单。
  • 不要把收货地址、售后申请、评价提交只放前端校验。
  • 不要绕过 shop_submit_orderorder_createdpayment_order_success 这些状态链路。
  • 商品页、购物车、订单页、售后页不应被全页缓存固定。
  • 发货、售后、退款、确认收货要与订单状态联动,并保留操作记录。
  • 商品投稿、商品编辑、后台商品管理要区分普通用户、作者和管理员权限。

On this page