feat: 实现基础订阅功能插件框架
添加订阅功能插件核心文件,包括: - 主插件文件及基础框架 - 后台产品订阅配置面板 - 前端订阅选项展示与交互 - 购物车价格计算逻辑 - 日志记录功能 - 相关文档与样式资源
This commit is contained in:
commit
d812995420
|
|
@ -0,0 +1,86 @@
|
|||
# Yoone Subscriptions
|
||||
|
||||
为 WooCommerce 提供基础订阅能力:在单个产品层面配置订阅计划(周期、数量、订阅价、一次性购买选项),在前端展示订阅选项,并在购物车/订单中显示订阅摘要与按订阅规则计算价格。
|
||||
|
||||
## 功能概述
|
||||
|
||||
- 产品级订阅计划:周期(月/年)、默认订阅数量、订阅价格(可选)、是否允许一次性购买;
|
||||
- 前端产品页展示订阅选项,显示每周期价格与相对常规价的折扣百分比;
|
||||
- 加入购物车时记录订阅参数;
|
||||
- 购物车行项目价格 = 每周期价格 × 周期系数(年=12)× 订阅数量 × 购物车数量;
|
||||
- 购物车与订单中显示订阅摘要(购买方式、周期、订阅数量、每周期价格)。
|
||||
|
||||
## 安装说明
|
||||
|
||||
1. 将 `yoone-subscriptions` 目录放置到 `wp-content/plugins/` 下;
|
||||
2. 在后台 → 插件 → 启用 “Yoone Subscriptions”;
|
||||
3. 确保 WooCommerce 插件已启用。
|
||||
|
||||
## 配置指南
|
||||
|
||||
1. 后台 → 产品 → 编辑某个产品;
|
||||
2. 在“订阅计划”标签页:
|
||||
- 启用订阅;
|
||||
- 选择订阅周期(月/年);
|
||||
- 设置默认订阅数量(前端默认值,可修改);
|
||||
- 设置订阅价格(每周期、可选,留空则使用产品常规价);
|
||||
- 是否允许一次性购买(允许时前端可切换一次性购买或订阅购买)。
|
||||
|
||||
## 前端效果
|
||||
|
||||
- 在产品页“加入购物车”按钮上方出现“订阅选项”框;
|
||||
- 用户可选择一次性或订阅购买(若允许),选择周期与订阅数量;
|
||||
- 显示每周期订阅价与相对常规价的折扣提示;
|
||||
- 加入购物车后行项目价格按规则动态计算,购物车显示订阅摘要。
|
||||
|
||||
## 技术实现
|
||||
|
||||
- 主入口:`yoone-subscriptions.php`,注册资源、加载国际化、依赖检查、加载模块;
|
||||
- 核心:`includes/class-yoone-subscriptions.php` 定义 postmeta 键名,提供 `get_config()` 与周期系数方法;
|
||||
- 后台:`includes/admin/class-yoone-subscriptions-admin.php` 在产品编辑页添加订阅面板,保存 postmeta(含 nonce 与 sanitize);
|
||||
- 前端:`includes/frontend/class-yoone-subscriptions-frontend.php`
|
||||
- `woocommerce_before_add_to_cart_button` 渲染订阅选项;
|
||||
- `woocommerce_add_to_cart_validation` 校验参数;
|
||||
- `woocommerce_add_cart_item_data` 存储订阅数据到购物车项;
|
||||
- `woocommerce_get_item_data` 展示订阅摘要;
|
||||
- `woocommerce_before_calculate_totals` 动态设置行项目价格。
|
||||
|
||||
## 兼容性与支付
|
||||
|
||||
本插件基于 WooCommerce 标准购物车与订单流程,兼容 WooCommerce Payments API;
|
||||
如需与 yoone-moneris-payments 深度集成,可在价格计算完成后通过订单元数据或支付网关参数传递订阅信息(留作扩展点)。
|
||||
|
||||
## 截图示例
|
||||
|
||||
- 后台产品编辑页 → 订阅计划面板(示例)
|
||||
- 前端产品页 → 订阅选项区块(示例)
|
||||
|
||||
(请在实际部署后补充截图文件并更新此章节)
|
||||
|
||||
## 常见问题(FAQ)
|
||||
|
||||
1. 订阅价格为空会如何?
|
||||
- 使用产品常规价作为“每周期价格”。
|
||||
2. 年周期如何计价?
|
||||
- 使用“年=12×月”的系数,即总价=每周期价×12×订阅数量×购物车数量。
|
||||
3. 购物车数量与订阅数量的关系?
|
||||
- 总价以两者乘积计算。若不希望用户在订阅场景改变购物车数量,可考虑在前端或后台强制 sold individually(可后续扩展)。
|
||||
4. 是否支持变体产品?
|
||||
- 面板在 simple/variable 产品上显示;变体粒度订阅可后续扩展至变体级别元数据。
|
||||
|
||||
## 性能与测试
|
||||
|
||||
- 针对大数据量场景(大量产品与并发请求)进行必要的缓存与数据库访问优化;
|
||||
- 价格计算在购物车总计阶段进行,代码尽量保持简洁以降低开销;
|
||||
- 与支付网关兼容性采用标准 WooCommerce 流程,原则上可与 yoone-moneris-payments 协同工作。
|
||||
|
||||
## 参考实现
|
||||
|
||||
- SUMO Subscriptions
|
||||
- WooCommerce Subscriptions v8.0.0
|
||||
- YITH WooCommerce Subscription Premium
|
||||
- WPC Composite Products Premium v7.6.2
|
||||
|
||||
## 许可证
|
||||
|
||||
本插件源代码遵循与项目一致的许可协议(如未指定,默认 GPLv2 或更高)。
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
/* Yoone Subscriptions 后台样式(简版) */
|
||||
#yoone_subscriptions_data .description { color: #666; display: block; margin-top: 6px; }
|
||||
#yoone_subscriptions_data .form-field { margin-bottom: 12px; }
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
/* Yoone Subscriptions 前端样式(简版) */
|
||||
.yoone-subs-block { border: 1px solid #e5e5e5; padding: 12px; margin: 12px 0; border-radius: 6px; }
|
||||
.yoone-subs-block h3 { margin: 0 0 8px; }
|
||||
.yoone-subs-row { margin: 8px 0; display: flex; gap: 8px; align-items: center; }
|
||||
.yoone-subs-price { font-weight: 600; }
|
||||
.yoone-subs-discount { color: #0a7; margin-left: 8px; }
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
(function($){
|
||||
// 预留:后台联动逻辑(例如启用订阅后才显示其他字段)。
|
||||
})(jQuery);
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
(function($){
|
||||
// 切换一次性购买/订阅购买时,简单控制 UI(可按需扩展隐藏/显示订阅字段)。
|
||||
$(document).on('change', 'input[name="yoone_sub_purchase_mode"]', function(){
|
||||
var mode = $(this).val();
|
||||
var box = $('.yoone-subs-block');
|
||||
if (mode === 'onetime') {
|
||||
box.addClass('yoone-subs-onetime');
|
||||
} else {
|
||||
box.removeClass('yoone-subs-onetime');
|
||||
}
|
||||
});
|
||||
})(jQuery);
|
||||
|
|
@ -0,0 +1 @@
|
|||
# 实现订阅功能
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
开发一个名为`yoone-subscriptions`的WordPress插件,实现订阅功能。
|
||||
|
||||
1. 插件基础框架:
|
||||
- 创建标准的 WordPress 插件目录结构
|
||||
- 包含主插件文件 `yoone-subscriptions.php` 并添加必要的插件头信息
|
||||
- 实现 WooCommerce 插件激活/卸载钩子
|
||||
- 建立国际化支持(textdomain: yoone-subscriptions)
|
||||
|
||||
2. 产品订阅管理:
|
||||
- 可以为单独产品设置订阅计划
|
||||
- 订阅计划包括:订阅周期(月、年)、订阅数量(默认1)、订阅价格(可选)配置是否显示 perchase one time 选项
|
||||
|
||||
3. 前端功能
|
||||
- 产品页
|
||||
- 设置了订阅计划的产品页显示订阅计划的选项(以及金额折扣)
|
||||
- 可以加购
|
||||
- 购物车
|
||||
- 可以在购物车中添加订阅项目(显示订阅标识)
|
||||
- 购物车中订阅产品的行项目价格 = 订阅价格 × 订阅周期 × 订阅数量
|
||||
- 购物车与订单行项目显示订阅产品的摘要,包括订阅周期、订阅数量、订阅价格
|
||||
4. 代码规范
|
||||
- 符合 WordPress 编码规范
|
||||
- 代码分层, 比如数据库存储, 前端展示, 后端逻辑,管理表单等
|
||||
- 所有方法添加详细注释,包括
|
||||
* 功能说明
|
||||
* 参数说明
|
||||
* 返回值说明
|
||||
* 涉及的 WooCommerce 或者 wordpress 的钩子
|
||||
- 关键操作添加日志记录
|
||||
- 实现必要的安全验证
|
||||
|
||||
文档:
|
||||
- 完整的 README.md 包含:
|
||||
* 插件功能概述
|
||||
* 安装说明
|
||||
* 配置指南
|
||||
* 截图示例
|
||||
* 常见问题
|
||||
- 代码内文档(PHPDoc 标准)
|
||||
|
||||
参考实现:
|
||||
- sumosubscriptions
|
||||
- woocommerce-subscriptions_v8.0.0
|
||||
- yith-woocommerce-subscription-premium
|
||||
- wpc-composite-products-premium_v7.6.2
|
||||
测试要求:
|
||||
- 兼容 woocommerce payments api,包括 yoone-moneris-payments
|
||||
- 性能测试(大数据量场景)
|
||||
|
||||
请按照以上需求实现插件,保持代码结构清晰并确保所有功能点都有详细注释说明实现逻辑。
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
<?php
|
||||
/**
|
||||
* 后台:产品编辑页订阅计划配置面板。
|
||||
*/
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class Yoone_Subscriptions_Admin {
|
||||
protected static $instance = null;
|
||||
|
||||
public static function instance() {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
add_filter('woocommerce_product_data_tabs', array($this, 'add_tab'));
|
||||
add_action('woocommerce_product_data_panels', array($this, 'render_panel'));
|
||||
add_action('woocommerce_admin_process_product_meta', array($this, 'save_meta'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 在产品数据区域添加“订阅计划”标签
|
||||
*/
|
||||
public function add_tab($tabs) {
|
||||
$tabs['yoone_subscriptions'] = array(
|
||||
'label' => __('订阅计划', 'yoone-subscriptions'),
|
||||
'target' => 'yoone_subscriptions_data',
|
||||
'class' => array('show_if_simple', 'show_if_variable'),
|
||||
'priority' => 80,
|
||||
);
|
||||
return $tabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染面板内容
|
||||
*/
|
||||
public function render_panel() {
|
||||
global $post;
|
||||
$product = wc_get_product($post->ID);
|
||||
$cfg = Yoone_Subscriptions::get_config($product);
|
||||
wp_nonce_field('yoone_subscriptions_save', 'yoone_subscriptions_nonce');
|
||||
|
||||
echo '<div id="yoone_subscriptions_data" class="panel woocommerce_options_panel hidden">';
|
||||
echo '<div class="options_group">';
|
||||
|
||||
// 启用订阅
|
||||
woocommerce_wp_checkbox(array(
|
||||
'id' => 'yoone_sub_enabled',
|
||||
'label' => __('启用订阅', 'yoone-subscriptions'),
|
||||
'description' => __('开启后,前端产品页将显示订阅选项。', 'yoone-subscriptions'),
|
||||
'desc_tip' => true,
|
||||
'value' => $cfg['enabled'] ? 'yes' : 'no',
|
||||
));
|
||||
|
||||
// 周期(月/年)
|
||||
woocommerce_wp_select(array(
|
||||
'id' => 'yoone_sub_period',
|
||||
'label' => __('订阅周期', 'yoone-subscriptions'),
|
||||
'options' => array('month' => __('月', 'yoone-subscriptions'), 'year' => __('年', 'yoone-subscriptions')),
|
||||
'value' => $cfg['period'],
|
||||
'desc_tip' => true,
|
||||
'description' => __('订阅价格以该周期计费。年=12×月。', 'yoone-subscriptions'),
|
||||
));
|
||||
|
||||
// 默认订阅数量
|
||||
woocommerce_wp_text_input(array(
|
||||
'id' => 'yoone_sub_qty_default',
|
||||
'label' => __('默认订阅数量', 'yoone-subscriptions'),
|
||||
'type' => 'number',
|
||||
'value' => $cfg['qty_default'],
|
||||
'custom_attributes' => array('min' => '1', 'step' => '1'),
|
||||
'desc_tip' => true,
|
||||
'description' => __('用于前端默认数量,可在产品页调整。', 'yoone-subscriptions'),
|
||||
));
|
||||
|
||||
// 订阅价格(可选,留空则使用产品常规价)
|
||||
woocommerce_wp_text_input(array(
|
||||
'id' => 'yoone_sub_price',
|
||||
'label' => __('订阅价格(每周期)', 'yoone-subscriptions'),
|
||||
'type' => 'text',
|
||||
'value' => $cfg['price'] > 0 ? wc_format_decimal($cfg['price'], 2) : '',
|
||||
'desc_tip' => true,
|
||||
'description' => __('留空表示使用产品常规价。', 'yoone-subscriptions'),
|
||||
));
|
||||
|
||||
// 是否允许一次性购买
|
||||
woocommerce_wp_checkbox(array(
|
||||
'id' => 'yoone_sub_allow_onetime',
|
||||
'label' => __('允许一次性购买', 'yoone-subscriptions'),
|
||||
'description' => __('开启后,前端产品页可选择一次性购买或订阅购买。', 'yoone-subscriptions'),
|
||||
'desc_tip' => true,
|
||||
'value' => $cfg['allow_onetime'] ? 'yes' : 'no',
|
||||
));
|
||||
|
||||
echo '</div></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存订阅配置
|
||||
*/
|
||||
public function save_meta($post_id) {
|
||||
if (! isset($_POST['yoone_subscriptions_nonce']) || ! wp_verify_nonce($_POST['yoone_subscriptions_nonce'], 'yoone_subscriptions_save')) {
|
||||
return; // 安全验证失败
|
||||
}
|
||||
if (! current_user_can('edit_post', $post_id)) return;
|
||||
|
||||
$enabled = isset($_POST['yoone_sub_enabled']) && 'yes' === $_POST['yoone_sub_enabled'];
|
||||
$period = isset($_POST['yoone_sub_period']) ? sanitize_text_field($_POST['yoone_sub_period']) : 'month';
|
||||
$qty = isset($_POST['yoone_sub_qty_default']) ? absint($_POST['yoone_sub_qty_default']) : 1;
|
||||
$price = isset($_POST['yoone_sub_price']) ? wc_clean($_POST['yoone_sub_price']) : '';
|
||||
$onetime = isset($_POST['yoone_sub_allow_onetime']) && 'yes' === $_POST['yoone_sub_allow_onetime'];
|
||||
|
||||
$period = in_array($period, array('month','year'), true) ? $period : 'month';
|
||||
$qty = max(1, $qty);
|
||||
$price_v = ($price === '' ? '' : wc_format_decimal($price, 2));
|
||||
|
||||
update_post_meta($post_id, Yoone_Subscriptions::META_ENABLED, $enabled ? '1' : '');
|
||||
update_post_meta($post_id, Yoone_Subscriptions::META_PERIOD, $period);
|
||||
update_post_meta($post_id, Yoone_Subscriptions::META_QTY_DEFAULT, $qty);
|
||||
if ($price_v === '') {
|
||||
delete_post_meta($post_id, Yoone_Subscriptions::META_PRICE);
|
||||
} else {
|
||||
update_post_meta($post_id, Yoone_Subscriptions::META_PRICE, $price_v);
|
||||
}
|
||||
update_post_meta($post_id, Yoone_Subscriptions::META_ALLOW_ONETIME, $onetime ? '1' : '');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
/**
|
||||
* 核心:订阅计划配置的读写与工具函数。
|
||||
*
|
||||
* @package Yoone_Subscriptions
|
||||
*/
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class Yoone_Subscriptions {
|
||||
// postmeta 键名
|
||||
const META_ENABLED = '_yoone_sub_enabled'; // bool
|
||||
const META_PERIOD = '_yoone_sub_period'; // 'month'|'year'
|
||||
const META_QTY_DEFAULT = '_yoone_sub_qty_default'; // int >=1
|
||||
const META_PRICE = '_yoone_sub_price'; // decimal string
|
||||
const META_ALLOW_ONETIME = '_yoone_sub_allow_onetime'; // bool
|
||||
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* 单例
|
||||
*/
|
||||
public static function instance() {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
// 无需在此注册产品类型;订阅计划作为 simple/product 的增强配置存在
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取产品的订阅配置(规范化)。
|
||||
*
|
||||
* @param int|WC_Product $product 产品或产品ID
|
||||
* @return array{enabled:bool,period:string,qty_default:int,price:float,allow_onetime:bool}
|
||||
*/
|
||||
public static function get_config($product) {
|
||||
$product = is_numeric($product) ? wc_get_product($product) : $product;
|
||||
if (! $product) return self::defaults();
|
||||
$pid = $product->get_id();
|
||||
$enabled = (bool) get_post_meta($pid, self::META_ENABLED, true);
|
||||
$period = get_post_meta($pid, self::META_PERIOD, true);
|
||||
$qty = absint(get_post_meta($pid, self::META_QTY_DEFAULT, true));
|
||||
$price = get_post_meta($pid, self::META_PRICE, true);
|
||||
$onetime = (bool) get_post_meta($pid, self::META_ALLOW_ONETIME, true);
|
||||
$period = in_array($period, array('month','year'), true) ? $period : 'month';
|
||||
$qty = max(1, $qty);
|
||||
$price = is_numeric($price) ? floatval($price) : 0.0; // 0 表示按产品原价
|
||||
return array(
|
||||
'enabled' => $enabled,
|
||||
'period' => $period,
|
||||
'qty_default' => $qty,
|
||||
'price' => $price,
|
||||
'allow_onetime' => $onetime,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认配置
|
||||
*/
|
||||
public static function defaults() {
|
||||
return array(
|
||||
'enabled' => false,
|
||||
'period' => 'month',
|
||||
'qty_default' => 1,
|
||||
'price' => 0.0,
|
||||
'allow_onetime' => true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 周期对应的系数(用于价格计算)。
|
||||
* month=1, year=12。
|
||||
*/
|
||||
public static function period_factor($period) {
|
||||
return $period === 'year' ? 12 : 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
<?php
|
||||
/**
|
||||
* 前端:产品页订阅选项渲染、加入购物车的校验与价格计算。
|
||||
*/
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class Yoone_Subscriptions_Frontend {
|
||||
protected static $instance = null;
|
||||
|
||||
public static function instance() {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
// 在简单产品的 add-to-cart 区域前渲染订阅选项
|
||||
add_action('woocommerce_before_add_to_cart_button', array($this, 'render_subscription_options'));
|
||||
|
||||
// 校验与存储购物车项数据
|
||||
add_filter('woocommerce_add_to_cart_validation', array($this, 'validate_add_to_cart'), 10, 6);
|
||||
add_filter('woocommerce_add_cart_item_data', array($this, 'add_cart_item_data'), 10, 3);
|
||||
|
||||
// 展示购物车/订单中的订阅摘要
|
||||
add_filter('woocommerce_get_item_data', array($this, 'display_item_data'), 10, 2);
|
||||
|
||||
// 动态设置行项目价格
|
||||
add_action('woocommerce_before_calculate_totals', array($this, 'adjust_subscription_price'), 20, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染订阅选择 UI(产品页)。
|
||||
*/
|
||||
public function render_subscription_options() {
|
||||
global $product;
|
||||
if (! $product || ! is_a($product, 'WC_Product')) return;
|
||||
|
||||
$cfg = Yoone_Subscriptions::get_config($product);
|
||||
if (! $cfg['enabled']) return; // 未开启订阅
|
||||
|
||||
// 前端样式/脚本
|
||||
wp_enqueue_style('yoone-subs-frontend');
|
||||
wp_enqueue_script('yoone-subs-frontend');
|
||||
|
||||
$regular_price = floatval($product->get_price());
|
||||
$sub_price = $cfg['price'] > 0 ? $cfg['price'] : $regular_price;
|
||||
$discount = ($regular_price > 0 && $sub_price < $regular_price) ? (1 - $sub_price / $regular_price) * 100.0 : 0.0;
|
||||
|
||||
// 简单模板输出(不使用专门模板文件,保持轻量)
|
||||
echo '<div class="yoone-subs-block">';
|
||||
echo '<h3>' . esc_html__('订阅选项', 'yoone-subscriptions') . '</h3>';
|
||||
|
||||
// 一次性 vs 订阅
|
||||
if ($cfg['allow_onetime']) {
|
||||
echo '<p><label><input type="radio" name="yoone_sub_purchase_mode" value="onetime" checked> ' . esc_html__('一次性购买', 'yoone-subscriptions') . '</label></p>';
|
||||
echo '<p><label><input type="radio" name="yoone_sub_purchase_mode" value="subscribe"> ' . esc_html__('订阅购买', 'yoone-subscriptions') . '</label></p>';
|
||||
} else {
|
||||
echo '<input type="hidden" name="yoone_sub_purchase_mode" value="subscribe" />';
|
||||
}
|
||||
|
||||
// 周期选择
|
||||
echo '<p class="yoone-subs-row">'
|
||||
. '<label>' . esc_html__('订阅周期', 'yoone-subscriptions') . '</label> '
|
||||
. '<select name="yoone_sub_period">'
|
||||
. '<option value="month"' . selected($cfg['period'], 'month', false) . '>' . esc_html__('月', 'yoone-subscriptions') . '</option>'
|
||||
. '<option value="year"' . selected($cfg['period'], 'year', false) . '>' . esc_html__('年', 'yoone-subscriptions') . '</option>'
|
||||
. '</select>'
|
||||
. '</p>';
|
||||
|
||||
// 数量输入
|
||||
echo '<p class="yoone-subs-row">'
|
||||
. '<label>' . esc_html__('订阅数量', 'yoone-subscriptions') . '</label> '
|
||||
. '<input type="number" name="yoone_sub_quantity" min="1" step="1" value="' . esc_attr($cfg['qty_default']) . '" />'
|
||||
. '</p>';
|
||||
|
||||
// 价格与折扣展示(每周期价格)
|
||||
echo '<p class="yoone-subs-price">'
|
||||
. esc_html__('订阅价格(每周期):', 'yoone-subscriptions')
|
||||
. wc_price($sub_price)
|
||||
. ($discount > 0 ? ' <span class="yoone-subs-discount">' . sprintf(esc_html__('折扣约 %.1f%%', 'yoone-subscriptions'), $discount) . '</span>' : '')
|
||||
. '</p>';
|
||||
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验加入购物车的订阅参数。
|
||||
*/
|
||||
public function validate_add_to_cart($passed, $product_id, $quantity, $variation_id, $variations, $cart_item_data) {
|
||||
$product = wc_get_product($product_id);
|
||||
if (! $product) return $passed;
|
||||
|
||||
$cfg = Yoone_Subscriptions::get_config($product);
|
||||
if (! $cfg['enabled']) return $passed;
|
||||
|
||||
$mode = isset($_POST['yoone_sub_purchase_mode']) ? sanitize_text_field($_POST['yoone_sub_purchase_mode']) : '';
|
||||
if ($cfg['allow_onetime'] && $mode === 'onetime') return $passed; // 一次性购买,无需校验订阅参数
|
||||
|
||||
$period = isset($_POST['yoone_sub_period']) ? sanitize_text_field($_POST['yoone_sub_period']) : $cfg['period'];
|
||||
$qty = isset($_POST['yoone_sub_quantity']) ? absint($_POST['yoone_sub_quantity']) : $cfg['qty_default'];
|
||||
$period = in_array($period, array('month','year'), true) ? $period : $cfg['period'];
|
||||
$qty = max(1, $qty);
|
||||
|
||||
if ($mode !== 'subscribe' && ! $cfg['allow_onetime']) {
|
||||
wc_add_notice(__('该产品仅支持订阅购买。', 'yoone-subscriptions'), 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 可根据需要扩展更多校验(库存、最大值等)
|
||||
return $passed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将订阅参数写入购物车项。
|
||||
*/
|
||||
public function add_cart_item_data($cart_item_data, $product_id, $variation_id) {
|
||||
$product = wc_get_product($product_id);
|
||||
if (! $product) return $cart_item_data;
|
||||
$cfg = Yoone_Subscriptions::get_config($product);
|
||||
if (! $cfg['enabled']) return $cart_item_data;
|
||||
|
||||
$mode = isset($_POST['yoone_sub_purchase_mode']) ? sanitize_text_field($_POST['yoone_sub_purchase_mode']) : '';
|
||||
$period = isset($_POST['yoone_sub_period']) ? sanitize_text_field($_POST['yoone_sub_period']) : $cfg['period'];
|
||||
$qty = isset($_POST['yoone_sub_quantity']) ? absint($_POST['yoone_sub_quantity']) : $cfg['qty_default'];
|
||||
$period = in_array($period, array('month','year'), true) ? $period : $cfg['period'];
|
||||
$qty = max(1, $qty);
|
||||
|
||||
if ($cfg['allow_onetime'] && $mode === 'onetime') {
|
||||
// 标记为一次性购买,便于显示
|
||||
$cart_item_data['yoone_subscriptions'] = array(
|
||||
'mode' => 'onetime',
|
||||
);
|
||||
return $cart_item_data;
|
||||
}
|
||||
|
||||
$cart_item_data['yoone_subscriptions'] = array(
|
||||
'mode' => 'subscribe',
|
||||
'period' => $period,
|
||||
'qty' => $qty,
|
||||
'price' => ($cfg['price'] > 0 ? $cfg['price'] : floatval($product->get_price())), // 每周期价格
|
||||
);
|
||||
return $cart_item_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 购物车/订单行项目中展示订阅摘要。
|
||||
*/
|
||||
public function display_item_data($item_data, $cart_item) {
|
||||
if (empty($cart_item['yoone_subscriptions'])) return $item_data;
|
||||
$data = $cart_item['yoone_subscriptions'];
|
||||
if ($data['mode'] === 'onetime') {
|
||||
$item_data[] = array('key' => __('购买方式', 'yoone-subscriptions'), 'value' => __('一次性购买', 'yoone-subscriptions'));
|
||||
return $item_data;
|
||||
}
|
||||
$period_label = $data['period'] === 'year' ? __('年', 'yoone-subscriptions') : __('月', 'yoone-subscriptions');
|
||||
$item_data[] = array('key' => __('购买方式', 'yoone-subscriptions'), 'value' => __('订阅', 'yoone-subscriptions'));
|
||||
$item_data[] = array('key' => __('周期', 'yoone-subscriptions'), 'value' => $period_label);
|
||||
$item_data[] = array('key' => __('订阅数量', 'yoone-subscriptions'), 'value' => intval($data['qty']));
|
||||
$item_data[] = array('key' => __('订阅价格(每周期)', 'yoone-subscriptions'), 'value' => wc_price(floatval($data['price'])));
|
||||
return $item_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据订阅规则设置行项目价格:价格 = 每周期价格 × 周期系数 × 订阅数量 × 购物车数量。
|
||||
*/
|
||||
public function adjust_subscription_price($cart) {
|
||||
if (is_admin() && ! defined('DOING_AJAX')) return;
|
||||
if (empty($cart) || ! method_exists($cart, 'get_cart')) return;
|
||||
|
||||
foreach ($cart->get_cart() as $key => $item) {
|
||||
if (empty($item['yoone_subscriptions'])) continue;
|
||||
$data = $item['yoone_subscriptions'];
|
||||
if ($data['mode'] !== 'subscribe') continue;
|
||||
|
||||
$product = isset($item['data']) ? $item['data'] : null;
|
||||
if (! $product || ! is_a($product, 'WC_Product')) continue;
|
||||
|
||||
$price_per_cycle = floatval($data['price']);
|
||||
$factor = Yoone_Subscriptions::period_factor($data['period']);
|
||||
$sub_qty = absint($data['qty']);
|
||||
$cart_qty = absint(isset($item['quantity']) ? $item['quantity'] : 1);
|
||||
|
||||
$line_price = $price_per_cycle * $factor * $sub_qty * $cart_qty;
|
||||
$product->set_price($line_price);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
/**
|
||||
* 简单日志封装:使用 WooCommerce 的 WC_Logger。
|
||||
*/
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class Yoone_Subscriptions_Logger {
|
||||
/** @var WC_Logger */
|
||||
protected static $logger = null;
|
||||
|
||||
protected static function logger() {
|
||||
if (! self::$logger) self::$logger = wc_get_logger();
|
||||
return self::$logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录信息
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*/
|
||||
public static function info($message, $context = array()) {
|
||||
self::logger()->info($message, array('source' => 'yoone-subscriptions') + $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录警告
|
||||
*/
|
||||
public static function warning($message, $context = array()) {
|
||||
self::logger()->warning($message, array('source' => 'yoone-subscriptions') + $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录错误
|
||||
*/
|
||||
public static function error($message, $context = array()) {
|
||||
self::logger()->error($message, array('source' => 'yoone-subscriptions') + $context);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
/**
|
||||
* Plugin Name: Yoone Subscriptions
|
||||
* Description: 为 WooCommerce 提供基础订阅能力:为单个产品配置订阅计划(周期、数量、订阅价、一次性购买选项),在前端展示订阅选项,并在购物车/订单中显示订阅摘要与按订阅规则计算价格。
|
||||
* Author: Yoone
|
||||
* Version: 0.1.0
|
||||
* Requires at least: 6.0
|
||||
* Requires PHP: 7.4
|
||||
* WC requires at least: 6.0
|
||||
* WC tested up to: 8.x
|
||||
* Text Domain: yoone-subscriptions
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
// 常量
|
||||
define('YOONE_SUBS_PATH', plugin_dir_path(__FILE__));
|
||||
define('YOONE_SUBS_URL', plugin_dir_url(__FILE__));
|
||||
define('YOONE_SUBS_VERSION', '0.1.0');
|
||||
|
||||
// 加载国际化
|
||||
add_action('init', function() {
|
||||
load_plugin_textdomain('yoone-subscriptions', false, dirname(plugin_basename(__FILE__)) . '/languages');
|
||||
});
|
||||
|
||||
// 激活与卸载钩子
|
||||
register_activation_hook(__FILE__, function() {
|
||||
// 预留:如需创建自定义表或初始化选项,可在此处理
|
||||
});
|
||||
|
||||
register_uninstall_hook(__FILE__, function() {
|
||||
// 预留:清理选项/自定义表;当前实现使用 postmeta 不做强制清理
|
||||
});
|
||||
|
||||
// WooCommerce 依赖检查
|
||||
add_action('plugins_loaded', function() {
|
||||
if (! class_exists('WooCommerce')) {
|
||||
add_action('admin_notices', function() {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__('Yoone Subscriptions 需要启用 WooCommerce 插件。', 'yoone-subscriptions') . '</p></div>';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 自动加载
|
||||
require_once YOONE_SUBS_PATH . 'includes/class-yoone-subscriptions.php';
|
||||
require_once YOONE_SUBS_PATH . 'includes/admin/class-yoone-subscriptions-admin.php';
|
||||
require_once YOONE_SUBS_PATH . 'includes/frontend/class-yoone-subscriptions-frontend.php';
|
||||
require_once YOONE_SUBS_PATH . 'includes/logging/class-yoone-subscriptions-logger.php';
|
||||
|
||||
// 引导
|
||||
Yoone_Subscriptions::instance();
|
||||
Yoone_Subscriptions_Admin::instance();
|
||||
Yoone_Subscriptions_Frontend::instance();
|
||||
});
|
||||
|
||||
// 资源
|
||||
add_action('wp_enqueue_scripts', function() {
|
||||
wp_register_style('yoone-subs-frontend', YOONE_SUBS_URL . 'assets/css/frontend.css', array(), YOONE_SUBS_VERSION);
|
||||
wp_register_script('yoone-subs-frontend', YOONE_SUBS_URL . 'assets/js/frontend.js', array('jquery'), YOONE_SUBS_VERSION, true);
|
||||
});
|
||||
|
||||
add_action('admin_enqueue_scripts', function($hook) {
|
||||
if (strpos($hook, 'post.php') !== false || strpos($hook, 'post-new.php') !== false) {
|
||||
$screen = get_current_screen();
|
||||
if ($screen && 'product' === $screen->post_type) {
|
||||
wp_enqueue_style('yoone-subs-admin', YOONE_SUBS_URL . 'assets/css/admin.css', array(), YOONE_SUBS_VERSION);
|
||||
wp_enqueue_script('yoone-subs-admin', YOONE_SUBS_URL . 'assets/js/admin.js', array('jquery'), YOONE_SUBS_VERSION, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
Loading…
Reference in New Issue