feat(admin): 新增订阅计划面板式管理并自动启用订阅
refactor(frontend): 添加我的账户订阅列表端点与渲染 docs: 更新各文档文件说明与需求清单 style: 优化后台订阅计划面板样式与交互
This commit is contained in:
parent
c2067450cb
commit
7ebf556641
34
README.md
34
README.md
|
|
@ -2,9 +2,12 @@
|
|||
|
||||
为 WooCommerce 提供基础订阅能力:在单个产品层面配置订阅计划(周期、数量、订阅价、一次性购买选项),在前端展示订阅选项,并在购物车/订单中显示订阅摘要与按订阅规则计算价格。
|
||||
|
||||
本版本新增后台“面板式订阅计划”管理(可折叠面板),支持配置多个订阅计划并保存为产品元数据;保存时如存在至少一个计划会自动启用订阅。前端当前仍基于“基础字段”(周期/数量/价格)进行渲染与计价,订阅计划的前端选择将在后续版本提供。
|
||||
|
||||
## 功能概述
|
||||
|
||||
- 产品级订阅计划:周期(月/年)、默认订阅数量、订阅价格(可选)、是否允许一次性购买;
|
||||
- 产品级订阅计划(基础字段):周期(月/年)、默认订阅数量、订阅价格(可选)、是否允许一次性购买;
|
||||
- 后台支持“多个订阅计划”的配置(名称、周期、每周期价格或折扣百分比);保存时若存在计划将自动启用订阅;
|
||||
- 前端产品页展示订阅选项,显示每周期价格与相对常规价的折扣百分比;
|
||||
- 加入购物车时记录订阅参数;
|
||||
- 购物车行项目价格 = 每周期价格 × 周期系数(年=12)× 订阅数量 × 购物车数量;
|
||||
|
|
@ -20,11 +23,14 @@
|
|||
|
||||
1. 后台 → 产品 → 编辑某个产品;
|
||||
2. 在“订阅计划”标签页:
|
||||
- 启用订阅;
|
||||
- 选择订阅周期(月/年);
|
||||
- 设置默认订阅数量(前端默认值,可修改);
|
||||
- 设置订阅价格(每周期、可选,留空则使用产品常规价);
|
||||
- 是否允许一次性购买(允许时前端可切换一次性购买或订阅购买)。
|
||||
- 启用订阅(保存时如存在订阅计划将自动开启,即使未勾选也会开启);
|
||||
- 基础字段(仍生效,当前前端基于这些字段渲染与计价):
|
||||
- 订阅周期(月/年)、默认订阅数量、订阅价格(每周期,留空表示使用产品常规价)、分级折扣规则(数量:折扣%);
|
||||
- 是否允许一次性购买(允许时前端可切换一次性/订阅购买)。
|
||||
- 订阅计划面板(新增):使用“新增订阅计划”按钮添加计划,面板内包含:
|
||||
- 计划名称、周期(月/年)、每周期价格(留空使用产品价)、折扣百分比(可选,10 表示约 9 折)。
|
||||
- 支持折叠/展开、删除计划。保存后计划以数组形式写入产品元数据。
|
||||
- 注:当前前端尚未提供“在产品页选择某个计划”的 UI,因此建议仍按需设置基础字段以保证前端行为;订阅计划的选择与联动价格将在后续版本实现。
|
||||
|
||||
## 前端效果
|
||||
|
||||
|
|
@ -33,17 +39,24 @@
|
|||
- 显示每周期订阅价与相对常规价的折扣提示;
|
||||
- 加入购物车后行项目价格按规则动态计算,购物车显示订阅摘要。
|
||||
|
||||
## 技术实现
|
||||
## 技术实现与设计
|
||||
|
||||
- 主入口:`yoone-subscriptions.php`,注册资源、加载国际化、依赖检查、加载模块;
|
||||
- 核心:`includes/class-yoone-subscriptions.php` 定义 postmeta 键名,提供 `get_config()` 与周期系数方法;
|
||||
- 后台:`includes/admin/class-yoone-subscriptions-admin.php` 在产品编辑页添加订阅面板,保存 postmeta(含 nonce 与 sanitize);
|
||||
- 后台:`includes/admin/class-yoone-subscriptions-admin.php`
|
||||
- 在产品编辑页添加订阅面板(simple/variable/yoone_bundle);
|
||||
- 渲染基础字段与“订阅计划列表”(以表格输出,`assets/js/admin.js` 动态转换为可折叠面板);
|
||||
- 保存 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` 动态设置行项目价格。
|
||||
- My Account:注册 `subscriptions` 端点与菜单项“我的订阅”,显示当前用户的订阅实例列表(数据来源于 `includes/models/class-yoone-subscriptions-db.php`)。
|
||||
- 资源:
|
||||
- 后台:`assets/css/admin.css`(面板式样式)、`assets/js/admin.js`(隐藏旧字段、计划的新增/删除与折叠交互)
|
||||
- 前端:`assets/css/frontend.css`(简版样式)、`assets/js/frontend.js`(一次性/订阅模式的样式钩子)
|
||||
|
||||
## 兼容性与支付
|
||||
|
||||
|
|
@ -54,6 +67,7 @@
|
|||
|
||||
- 后台产品编辑页 → 订阅计划面板(示例)
|
||||
- 前端产品页 → 订阅选项区块(示例)
|
||||
- My Account → 我的订阅(示例)
|
||||
|
||||
(请在实际部署后补充截图文件并更新此章节)
|
||||
|
||||
|
|
@ -67,6 +81,10 @@
|
|||
- 总价以两者乘积计算。若不希望用户在订阅场景改变购物车数量,可考虑在前端或后台强制 sold individually(可后续扩展)。
|
||||
4. 是否支持变体产品?
|
||||
- 面板在 simple/variable 产品上显示;变体粒度订阅可后续扩展至变体级别元数据。
|
||||
5. 后台新增的“多个订阅计划”前端是否可选?
|
||||
- 当前版本尚未提供选择 UI,计划数据已保存并可用于后续扩展;当前前端渲染与计价仍基于基础字段。
|
||||
6. 为什么 My Account 没有出现“我的订阅”?
|
||||
- 首次启用插件后,访问 设置 → 固定链接 → 保存 以刷新路由规则;或停用再启用插件。然后在“我的账户”页面即可看到“我的订阅”菜单。
|
||||
|
||||
## 性能与测试
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,66 @@
|
|||
/* Yoone Subscriptions 后台样式(简版) */
|
||||
#yoone_subscriptions_data .description { color: #666; display: block; margin-top: 6px; }
|
||||
#yoone_subscriptions_data .form-field { margin-bottom: 12px; }
|
||||
/*
|
||||
* Yoone Subscriptions 后台样式
|
||||
* - 兼容原有“表格式”订阅计划(由 JS 转换为面板,表格默认隐藏但保留样式)
|
||||
* - 新增“面板式”订阅计划的外观:标题、操作按钮(折叠/删除)、详情区块
|
||||
* - 风格尽量贴近 WooCommerce 后台组件,保持一致的交互与视觉
|
||||
*/
|
||||
#yoone_subscriptions_data .description { color: #666; display: block; margin-top: 6px; line-height: 1.5; }
|
||||
#yoone_subscriptions_data .form-field { margin-bottom: 12px; }
|
||||
|
||||
/* 订阅计划表格布局优化(兼容保留但默认隐藏) */
|
||||
#yoone_subscriptions_data .yoone-sub-plans-table { margin-top: 10px; width: 100%; table-layout: fixed; }
|
||||
#yoone_subscriptions_data .yoone-sub-plans-table thead th { font-weight: 600; }
|
||||
#yoone_subscriptions_data .yoone-sub-plans-table th,
|
||||
#yoone_subscriptions_data .yoone-sub-plans-table td { padding: 8px 10px; vertical-align: middle; }
|
||||
#yoone_subscriptions_data .yoone-sub-plans-table tr:nth-child(even) { background-color: #f9f9f9; }
|
||||
|
||||
/* 列宽(与 table-layout: fixed 配合) */
|
||||
#yoone_subscriptions_data .yoone-sub-plans-table colgroup col:nth-child(1) { width: 28%; }
|
||||
#yoone_subscriptions_data .yoone-sub-plans-table colgroup col:nth-child(2) { width: 16%; }
|
||||
#yoone_subscriptions_data .yoone-sub-plans-table colgroup col:nth-child(3) { width: 22%; }
|
||||
#yoone_subscriptions_data .yoone-sub-plans-table colgroup col:nth-child(4) { width: 18%; }
|
||||
#yoone_subscriptions_data .yoone-sub-plans-table colgroup col:nth-child(5) { width: 16%; }
|
||||
|
||||
/* 输入控件统一风格(参考 Woo 后台表单) */
|
||||
#yoone_subscriptions_data .yoone-sub-plans-table input[type="text"],
|
||||
#yoone_subscriptions_data .yoone-sub-plans-table input[type="number"],
|
||||
#yoone_subscriptions_data .yoone-sub-plans-table select {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
/* 行内按钮精简样式 */
|
||||
#yoone_subscriptions_data .yoone-sub-plans-table .button {
|
||||
min-height: 28px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
/* 新增计划按钮间距 */
|
||||
#yoone_subscriptions_data #yoone-sub-plan-add { margin-top: 8px; }
|
||||
|
||||
/* 面板式订阅计划样式 */
|
||||
#yoone_subscriptions_data #yoone-sub-plans-panels { margin-top: 12px; }
|
||||
#yoone_subscriptions_data .yoone-sub-plan-panel {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 1px rgba(0,0,0,.04);
|
||||
}
|
||||
#yoone_subscriptions_data .yoone-sub-plan-header {
|
||||
padding: 10px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #f6f7f7;
|
||||
border-bottom: 1px solid #e2e4e7;
|
||||
}
|
||||
#yoone_subscriptions_data .yoone-sub-plan-title {
|
||||
width: 60%;
|
||||
max-width: 420px;
|
||||
}
|
||||
#yoone_subscriptions_data .yoone-sub-plan-actions .button { margin-left: 8px; }
|
||||
#yoone_subscriptions_data .yoone-sub-plan-body { padding: 12px; }
|
||||
#yoone_subscriptions_data .yoone-sub-plan-body p { margin: 0 0 10px; }
|
||||
#yoone_subscriptions_data .yoone-sub-plan-body.collapsed { display: none; }
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
/* Yoone Subscriptions 前端样式(简版) */
|
||||
/*
|
||||
* Yoone Subscriptions 前端样式(简版)
|
||||
* - 产品页订阅选项容器(.yoone-subs-block)与基础排版
|
||||
* - 样式钩子:.yoone-subs-onetime 用于在选择一次性购买时做轻量视觉变化
|
||||
*/
|
||||
.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; }
|
||||
|
|
|
|||
|
|
@ -1,29 +1,93 @@
|
|||
/**
|
||||
* 后台 JS:订阅计划面板交互
|
||||
* - 隐藏旧的单一订阅字段(周期、默认数量、最小起订量、每周期价格、分级折扣)
|
||||
* - 将后端渲染的表格(保持字段 name 不变)转换为“可折叠面板”形式
|
||||
* - 支持新增/删除计划,折叠/展开计划详情
|
||||
* - 保持与后端 save_meta 的兼容:使用相同的 name 数组结构提交
|
||||
*/
|
||||
(function($){
|
||||
$(function(){
|
||||
// 隐藏旧的单一订阅字段
|
||||
['yoone_sub_enabled','yoone_sub_period','yoone_sub_qty_default','yoone_sub_min_qty','yoone_sub_price','yoone_sub_tier_rules'].forEach(function(id){
|
||||
var $input = $('#'+id);
|
||||
if ($input.length) {
|
||||
var $field = $input.closest('.form-field');
|
||||
if ($field.length) { $field.hide(); }
|
||||
}
|
||||
});
|
||||
|
||||
var $table = $('.yoone-sub-plans-table');
|
||||
var $tbody = $('#yoone-sub-plans-body');
|
||||
var $addBtn = $('#yoone-sub-plan-add');
|
||||
|
||||
function buildPanel(labelInput, periodSelect, priceInput, discountInput){
|
||||
var $panel = $('<div class="yoone-sub-plan-panel"/>');
|
||||
var $header = $('<div class="yoone-sub-plan-header"/>');
|
||||
var $actions = $('<div class="yoone-sub-plan-actions"/>');
|
||||
var $toggle = $('<button type="button" class="button yoone-sub-plan-toggle" aria-expanded="true"/>').text((typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.collapse : '折叠'));
|
||||
var $remove = $('<button type="button" class="button yoone-sub-plan-remove"/>').text((typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.remove : '删除'));
|
||||
var $title = $(labelInput || '<input type="text" class="yoone-sub-plan-title" name="yoone_sub_plans[label][]" placeholder="'+(typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.placeholderLabel : '例如:标准计划')+'" />');
|
||||
$title.addClass('yoone-sub-plan-title');
|
||||
$actions.append($toggle).append(' ').append($remove);
|
||||
$header.append($title).append($actions);
|
||||
|
||||
var $body = $('<div class="yoone-sub-plan-body"/>');
|
||||
var $periodRow = $('<p/>').append('<label>'+(typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.period : '周期')+'</label> ');
|
||||
var $period = $(periodSelect || [
|
||||
'<select name="yoone_sub_plans[period][]">',
|
||||
'<option value="month">', (typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.month : '月'), '</option>',
|
||||
'<option value="year">', (typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.year : '年'), '</option>',
|
||||
'</select>'
|
||||
].join(''));
|
||||
$periodRow.append($period);
|
||||
|
||||
var $priceRow = $('<p/>').append('<label>'+(typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.pricePerPeriod : '每周期价格')+'</label> ');
|
||||
var $price = $(priceInput || '<input type="text" name="yoone_sub_plans[price][]" placeholder="'+(typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.placeholderPrice : '留空表示用产品价')+'" />');
|
||||
$priceRow.append($price);
|
||||
|
||||
var $discRow = $('<p/>').append('<label>'+(typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.discountPercent : '折扣百分比(可选)')+'</label> ');
|
||||
var $disc = $(discountInput || '<input type="number" min="0" step="0.1" name="yoone_sub_plans[discount_percent][]" placeholder="'+(typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.placeholderDiscount : '如:10 表示9折')+'" />');
|
||||
$discRow.append($disc);
|
||||
|
||||
$body.append($periodRow).append($priceRow).append($discRow);
|
||||
$panel.append($header).append($body);
|
||||
return $panel;
|
||||
}
|
||||
|
||||
if ($tbody.length && $addBtn.length) {
|
||||
// 将现有表格行转换为面板
|
||||
var $container = $('<div id="yoone-sub-plans-panels"/>');
|
||||
$table.before($container);
|
||||
$tbody.find('tr').each(function(){
|
||||
var $tr = $(this);
|
||||
var $label = $tr.find('input[name="yoone_sub_plans[label][]"]').detach();
|
||||
var $period = $tr.find('select[name="yoone_sub_plans[period][]"]').detach();
|
||||
var $price = $tr.find('input[name="yoone_sub_plans[price][]"]').detach();
|
||||
var $disc = $tr.find('input[name="yoone_sub_plans[discount_percent][]"]').detach();
|
||||
var $panel = buildPanel($label, $period, $price, $disc);
|
||||
$container.append($panel);
|
||||
});
|
||||
// 隐藏表格
|
||||
$table.hide();
|
||||
|
||||
// 新增面板
|
||||
$addBtn.on('click', function(){
|
||||
var row = [
|
||||
'<tr class="yoone-sub-plan-row">',
|
||||
'<td><input type="text" name="yoone_sub_plans[label][]" placeholder="',
|
||||
(typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.placeholderLabel : '例如:标准计划'),
|
||||
'" /></td>',
|
||||
'<td><select name="yoone_sub_plans[period][]">',
|
||||
'<option value="month">', (typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.month : '月'), '</option>',
|
||||
'<option value="year">', (typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.year : '年'), '</option>',
|
||||
'</select></td>',
|
||||
'<td><input type="text" name="yoone_sub_plans[price][]" placeholder="', (typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.placeholderPrice : '留空表示用产品价'), '" /></td>',
|
||||
'<td><input type="number" min="0" step="0.1" name="yoone_sub_plans[discount_percent][]" placeholder="', (typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.placeholderDiscount : '如:10 表示9折'), '" /></td>',
|
||||
'<td><button type="button" class="button yoone-sub-plan-remove">', (typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.remove : '删除'), '</button></td>',
|
||||
'</tr>'
|
||||
].join('');
|
||||
$tbody.append(row);
|
||||
$container.append(buildPanel());
|
||||
});
|
||||
|
||||
$tbody.on('click', '.yoone-sub-plan-remove', function(){
|
||||
$(this).closest('tr').remove();
|
||||
// 删除面板
|
||||
$(document).on('click', '.yoone-sub-plan-remove', function(){
|
||||
$(this).closest('.yoone-sub-plan-panel').remove();
|
||||
});
|
||||
|
||||
// 折叠/展开
|
||||
$(document).on('click', '.yoone-sub-plan-toggle', function(){
|
||||
var $btn = $(this);
|
||||
var $body = $btn.closest('.yoone-sub-plan-panel').find('.yoone-sub-plan-body');
|
||||
var expanded = $btn.attr('aria-expanded') === 'true';
|
||||
$btn.attr('aria-expanded', expanded ? 'false' : 'true');
|
||||
$btn.text(expanded ? (typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.expand : '展开') : (typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.collapse : '折叠'));
|
||||
$body.toggleClass('collapsed', expanded);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
/**
|
||||
* 前端 JS:订阅选项的轻量交互
|
||||
* - 当用户切换“一次性购买/订阅购买”时,为容器添加/移除样式钩子,便于样式控制
|
||||
*/
|
||||
(function($){
|
||||
// 切换一次性购买/订阅购买时,简单控制 UI(可按需扩展隐藏/显示订阅字段)。
|
||||
$(document).on('change', 'input[name="yoone_sub_purchase_mode"]', function(){
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
# 实现订阅功能
|
||||
<!-- 文件说明:技术文档,记录插件的实现细节、模块职责与开发约定 -->
|
||||
# 实现订阅功能
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
<!-- 文件说明:数据库模型文档,描述订阅实例表结构与 CRUD 接口设计 -->
|
||||
# 数据库模型(Yoone Subscriptions)
|
||||
|
||||
本插件新增一个独立的数据库模型目录 `includes/models/`,用于管理用户订阅实例的持久化数据。当前实现增加了基础表结构与 CRUD 接口,方便未来接入续订、暂停、取消等业务。
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
<!-- 文件说明:需求清单,记录业务侧的功能点与用户期望 -->
|
||||
- 扣款失败需要发邮件给客户
|
||||
- 后台有用户订阅列表管理页面
|
||||
- 数据存储在数据库
|
||||
- 前端用户登陆后可以在 my account 页面中查看自己已设置的订阅
|
||||
- 配置订阅计划的产品显示订阅计划的选项 可以加入购物车
|
||||
- 支付手段可以使用 woocommerce 的payments 进行订阅配置
|
||||
- 订阅生成的订单应该包括订阅计划的信息
|
||||
- 订阅在第一次之后,定时自动续费并生成正确的订单
|
||||
-
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
<!-- 文件说明:项目新增需求文档,列出插件需要实现的功能与规范 -->
|
||||
开发一个名为`yoone-subscriptions`的WordPress插件,实现订阅功能。
|
||||
|
||||
1. 插件基础框架:
|
||||
|
|
@ -14,6 +15,7 @@
|
|||
- 产品页
|
||||
- 设置了订阅计划的产品页显示订阅计划的选项(以及金额折扣)
|
||||
- 可以加购
|
||||
- 帮我生成订阅
|
||||
- 购物车
|
||||
- 可以在购物车中添加订阅项目(显示订阅标识)
|
||||
- 购物车中订阅产品的行项目价格 = 订阅价格 × 订阅周期 × 订阅数量
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
<?php
|
||||
/**
|
||||
* 后台:产品编辑页订阅计划配置面板。
|
||||
* 后台:产品编辑页订阅计划配置面板
|
||||
*
|
||||
* 主要职责:
|
||||
* - 在 WooCommerce 产品数据区添加“订阅计划”标签页(simple/variable/yoone_bundle)
|
||||
* - 渲染基础字段:启用、周期、默认订阅量、最小起订量、每周期价格、分级折扣、允许一次性购买
|
||||
* - 渲染“订阅计划列表”表格(保持字段 name 结构),并由 admin.js 在前端转换为可折叠面板
|
||||
* - 保存元数据(含 nonce 校验与 sanitize),当存在至少一个有效订阅计划时自动启用订阅
|
||||
* - 在 WooCommerce 菜单下注册 Subscriptions 子菜单并渲染订阅列表页(简版)
|
||||
*/
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
|
|
@ -125,7 +132,8 @@ class Yoone_Subscriptions_Admin {
|
|||
echo '<p class="description">' . esc_html__('可以为该产品配置多个订阅计划,前端用户可在购买时选择其一。每个计划可设置周期与每周期价格,或以折扣百分比表示相对产品价的优惠。', 'yoone-subscriptions') . '</p>';
|
||||
|
||||
$plans = isset($cfg['plans']) && is_array($cfg['plans']) ? $cfg['plans'] : array();
|
||||
echo '<table class="widefat fixed" style="margin-top:10px;">';
|
||||
echo '<table class="wp-list-table widefat fixed striped yoone-sub-plans-table" style="margin-top:10px; table-layout:fixed;">';
|
||||
echo '<colgroup><col/><col/><col/><col/><col/></colgroup>';
|
||||
echo '<thead><tr>'
|
||||
. '<th>' . esc_html__('计划名称', 'yoone-subscriptions') . '</th>'
|
||||
. '<th>' . esc_html__('周期', 'yoone-subscriptions') . '</th>'
|
||||
|
|
@ -186,7 +194,7 @@ class Yoone_Subscriptions_Admin {
|
|||
$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);
|
||||
update_post_meta($post_id, Yoone_Subscriptions::META_MIN_QTY, max(1, $min_qty));
|
||||
|
|
@ -228,6 +236,10 @@ class Yoone_Subscriptions_Admin {
|
|||
}
|
||||
}
|
||||
update_post_meta($post_id, Yoone_Subscriptions::META_PLANS, $plans);
|
||||
|
||||
// 如果存在至少一个有效的订阅计划,则自动启用订阅
|
||||
$auto_enabled = ! empty($plans);
|
||||
update_post_meta($post_id, Yoone_Subscriptions::META_ENABLED, ($enabled || $auto_enabled) ? '1' : '');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -13,6 +13,14 @@ class Yoone_Subscriptions_Frontend {
|
|||
}
|
||||
|
||||
private function __construct() {
|
||||
// 注册“我的账户 → 订阅”端点与菜单
|
||||
add_action('init', array($this, 'register_account_endpoint'));
|
||||
add_filter('query_vars', array($this, 'add_query_var'));
|
||||
add_filter('woocommerce_get_query_vars', array($this, 'register_wc_query_vars'));
|
||||
add_filter('woocommerce_endpoint_subscriptions_title', array($this, 'endpoint_title'));
|
||||
add_filter('woocommerce_account_menu_items', array($this, 'add_account_menu_item'));
|
||||
add_action('woocommerce_account_subscriptions_endpoint', array($this, 'render_account_subscriptions'));
|
||||
|
||||
// 在简单产品的 add-to-cart 区域前渲染订阅选项
|
||||
add_action('woocommerce_before_add_to_cart_button', array($this, 'render_subscription_options'));
|
||||
|
||||
|
|
@ -27,6 +35,108 @@ class Yoone_Subscriptions_Frontend {
|
|||
add_action('woocommerce_before_calculate_totals', array($this, 'adjust_subscription_price'), 20, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册 My Account 的“subscriptions”端点。
|
||||
* - 访问地址示例:/my-account/subscriptions/
|
||||
*/
|
||||
public function register_account_endpoint() {
|
||||
// 为 WP 添加 rewrite 端点(仅需注册一次;在插件激活时会 flush rules)
|
||||
add_rewrite_endpoint('subscriptions', EP_ROOT | EP_PAGES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将自定义端点加入 WP 的 query vars,避免部分站点未识别导致 404。
|
||||
*/
|
||||
public function add_query_var($vars) {
|
||||
$vars[] = 'subscriptions';
|
||||
return $vars;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将端点注册到 WooCommerce 的 endpoint 映射中(用于标题与模板路由)。
|
||||
*/
|
||||
public function register_wc_query_vars($vars) {
|
||||
$vars['subscriptions'] = 'subscriptions';
|
||||
return $vars;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置端点页面标题。
|
||||
*/
|
||||
public function endpoint_title($title) {
|
||||
return __('我的订阅', 'yoone-subscriptions');
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 My Account 菜单中新增“我的订阅”。
|
||||
*/
|
||||
public function add_account_menu_item($items) {
|
||||
// 在“订单”后插入“订阅”菜单
|
||||
$new = array();
|
||||
foreach ($items as $key => $label) {
|
||||
$new[$key] = $label;
|
||||
if ('orders' === $key) {
|
||||
$new['subscriptions'] = __('我的订阅', 'yoone-subscriptions');
|
||||
}
|
||||
}
|
||||
// 如果没有“订单”菜单,则直接追加
|
||||
if (! isset($new['subscriptions'])) {
|
||||
$new['subscriptions'] = __('我的订阅', 'yoone-subscriptions');
|
||||
}
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染 My Account → 订阅 列表。
|
||||
*/
|
||||
public function render_account_subscriptions() {
|
||||
if (! is_user_logged_in()) {
|
||||
echo '<p>' . esc_html__('请先登录以查看您的订阅。', 'yoone-subscriptions') . '</p>';
|
||||
return;
|
||||
}
|
||||
$uid = get_current_user_id();
|
||||
if (! class_exists('Yoone_Subscriptions_DB')) {
|
||||
echo '<p>' . esc_html__('订阅数据模块未加载。', 'yoone-subscriptions') . '</p>';
|
||||
return;
|
||||
}
|
||||
$subs = Yoone_Subscriptions_DB::get_by_user($uid, array('limit' => 50, 'offset' => 0));
|
||||
|
||||
echo '<h2>' . esc_html__('我的订阅', 'yoone-subscriptions') . '</h2>';
|
||||
if (empty($subs)) {
|
||||
echo '<p>' . esc_html__('您目前还没有订阅。', 'yoone-subscriptions') . '</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
echo '<table class="shop_table shop_table_responsive my_account_subscriptions">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>' . esc_html__('商品', 'yoone-subscriptions') . '</th>
|
||||
<th>' . esc_html__('周期', 'yoone-subscriptions') . '</th>
|
||||
<th>' . esc_html__('数量', 'yoone-subscriptions') . '</th>
|
||||
<th>' . esc_html__('每周期金额', 'yoone-subscriptions') . '</th>
|
||||
<th>' . esc_html__('状态', 'yoone-subscriptions') . '</th>
|
||||
<th>' . esc_html__('开始时间', 'yoone-subscriptions') . '</th>
|
||||
<th>' . esc_html__('下次续费', 'yoone-subscriptions') . '</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>';
|
||||
foreach ($subs as $row) {
|
||||
$product = wc_get_product(intval($row['product_id']));
|
||||
$product_name = $product ? $product->get_name() : ('#' . intval($row['product_id']));
|
||||
$period_label = $row['period'] === 'year' ? __('年', 'yoone-subscriptions') : __('月', 'yoone-subscriptions');
|
||||
echo '<tr>
|
||||
<td>' . esc_html($product_name) . '</td>
|
||||
<td>' . esc_html($period_label) . '</td>
|
||||
<td>' . esc_html(intval($row['qty'])) . '</td>
|
||||
<td>' . wc_price(floatval($row['price_per_cycle'])) . '</td>
|
||||
<td>' . esc_html($row['status']) . '</td>
|
||||
<td>' . esc_html($row['start_date']) . '</td>
|
||||
<td>' . esc_html($row['next_renewal_date']) . '</td>
|
||||
</tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染订阅选择 UI(产品页)。
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<?php
|
||||
/**
|
||||
* 简单日志封装:使用 WooCommerce 的 WC_Logger。
|
||||
* 日志封装:使用 WooCommerce 的 WC_Logger
|
||||
* - 提供 info / warning / error 三个级别的简洁封装
|
||||
* - 统一设置 source = 'yoone-subscriptions',便于在日志中筛选来源
|
||||
*/
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,14 @@
|
|||
* Text Domain: yoone-subscriptions
|
||||
*/
|
||||
|
||||
/**
|
||||
* 文件说明:插件主入口
|
||||
* - 注册国际化、激活/卸载钩子
|
||||
* - WooCommerce 依赖检查,加载核心、后台、前端、日志、数据库模型模块
|
||||
* - 资源注册:
|
||||
* - 后台:assets/css/admin.css、assets/js/admin.js(产品编辑页)
|
||||
* - 前端:assets/css/frontend.css、assets/js/frontend.js(产品页与购物车交互)
|
||||
*/
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
// 常量
|
||||
|
|
@ -31,6 +39,8 @@ register_activation_hook(__FILE__, function() {
|
|||
if (class_exists('Yoone_Subscriptions_DB')) {
|
||||
Yoone_Subscriptions_DB::install();
|
||||
}
|
||||
// 刷新 rewrite 规则,确保 My Account 的 subscriptions 端点生效
|
||||
flush_rewrite_rules();
|
||||
});
|
||||
|
||||
// 卸载钩子:注意不可使用匿名函数(Closure),因为 WordPress 会序列化回调,PHP 不允许序列化 Closure。
|
||||
|
|
|
|||
Loading…
Reference in New Issue