diff --git a/README.md b/README.md index e5c3729..341f712 100644 --- a/README.md +++ b/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 没有出现“我的订阅”? + - 首次启用插件后,访问 设置 → 固定链接 → 保存 以刷新路由规则;或停用再启用插件。然后在“我的账户”页面即可看到“我的订阅”菜单。 ## 性能与测试 diff --git a/assets/css/admin.css b/assets/css/admin.css index 8431487..e3f335f 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -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; } \ No newline at end of file +/* + * 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; } \ No newline at end of file diff --git a/assets/css/frontend.css b/assets/css/frontend.css index 98d5c9b..41acd63 100644 --- a/assets/css/frontend.css +++ b/assets/css/frontend.css @@ -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; } diff --git a/assets/js/admin.js b/assets/js/admin.js index 9709fd8..d12aa31 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -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 = $('
'); + var $header = $('
'); + var $actions = $('
'); + var $toggle = $('', - '' - ].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); }); } }); diff --git a/assets/js/frontend.js b/assets/js/frontend.js index 5e18cb6..2b75b1a 100644 --- a/assets/js/frontend.js +++ b/assets/js/frontend.js @@ -1,3 +1,7 @@ +/** + * 前端 JS:订阅选项的轻量交互 + * - 当用户切换“一次性购买/订阅购买”时,为容器添加/移除样式钩子,便于样式控制 + */ (function($){ // 切换一次性购买/订阅购买时,简单控制 UI(可按需扩展隐藏/显示订阅字段)。 $(document).on('change', 'input[name="yoone_sub_purchase_mode"]', function(){ diff --git a/docs/技术文档.md b/docs/技术文档.md index 9e890fc..c7606de 100644 --- a/docs/技术文档.md +++ b/docs/技术文档.md @@ -1 +1,2 @@ -# 实现订阅功能 + +# 实现订阅功能 \ No newline at end of file diff --git a/docs/数据库模型.md b/docs/数据库模型.md index 23c1e09..37e3e9b 100644 --- a/docs/数据库模型.md +++ b/docs/数据库模型.md @@ -1,3 +1,4 @@ + # 数据库模型(Yoone Subscriptions) 本插件新增一个独立的数据库模型目录 `includes/models/`,用于管理用户订阅实例的持久化数据。当前实现增加了基础表结构与 CRUD 接口,方便未来接入续订、暂停、取消等业务。 diff --git a/docs/需求.md b/docs/需求.md index bde0b88..bb95f51 100644 --- a/docs/需求.md +++ b/docs/需求.md @@ -1,4 +1,10 @@ + - 扣款失败需要发邮件给客户 - 后台有用户订阅列表管理页面 - 数据存储在数据库 - 前端用户登陆后可以在 my account 页面中查看自己已设置的订阅 +- 配置订阅计划的产品显示订阅计划的选项 可以加入购物车 +- 支付手段可以使用 woocommerce 的payments 进行订阅配置 +- 订阅生成的订单应该包括订阅计划的信息 +- 订阅在第一次之后,定时自动续费并生成正确的订单 +- \ No newline at end of file diff --git a/docs/项目新增.md b/docs/项目新增.md index c0b8e06..218a6cc 100644 --- a/docs/项目新增.md +++ b/docs/项目新增.md @@ -1,3 +1,4 @@ + 开发一个名为`yoone-subscriptions`的WordPress插件,实现订阅功能。 1. 插件基础框架: @@ -14,6 +15,7 @@ - 产品页 - 设置了订阅计划的产品页显示订阅计划的选项(以及金额折扣) - 可以加购 + - 帮我生成订阅 - 购物车 - 可以在购物车中添加订阅项目(显示订阅标识) - 购物车中订阅产品的行项目价格 = 订阅价格 × 订阅周期 × 订阅数量 diff --git a/includes/admin/class-yoone-subscriptions-admin.php b/includes/admin/class-yoone-subscriptions-admin.php index e5e84e8..2bc27c2 100644 --- a/includes/admin/class-yoone-subscriptions-admin.php +++ b/includes/admin/class-yoone-subscriptions-admin.php @@ -1,6 +1,13 @@ ' . esc_html__('可以为该产品配置多个订阅计划,前端用户可在购买时选择其一。每个计划可设置周期与每周期价格,或以折扣百分比表示相对产品价的优惠。', 'yoone-subscriptions') . '

'; $plans = isset($cfg['plans']) && is_array($cfg['plans']) ? $cfg['plans'] : array(); - echo ''; + echo '
'; + echo ''; echo '' . '' . '' @@ -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' : ''); } /** diff --git a/includes/frontend/class-yoone-subscriptions-frontend.php b/includes/frontend/class-yoone-subscriptions-frontend.php index 43e2110..5c650a4 100644 --- a/includes/frontend/class-yoone-subscriptions-frontend.php +++ b/includes/frontend/class-yoone-subscriptions-frontend.php @@ -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 '

' . esc_html__('请先登录以查看您的订阅。', 'yoone-subscriptions') . '

'; + return; + } + $uid = get_current_user_id(); + if (! class_exists('Yoone_Subscriptions_DB')) { + echo '

' . esc_html__('订阅数据模块未加载。', 'yoone-subscriptions') . '

'; + return; + } + $subs = Yoone_Subscriptions_DB::get_by_user($uid, array('limit' => 50, 'offset' => 0)); + + echo '

' . esc_html__('我的订阅', 'yoone-subscriptions') . '

'; + if (empty($subs)) { + echo '

' . esc_html__('您目前还没有订阅。', 'yoone-subscriptions') . '

'; + return; + } + + echo '
' . esc_html__('计划名称', 'yoone-subscriptions') . '' . esc_html__('周期', 'yoone-subscriptions') . '
+ + + + + + + + + + + + '; + 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 ' + + + + + + + + '; + } + echo ''; + } + /** * 渲染订阅选择 UI(产品页)。 */ diff --git a/includes/logging/class-yoone-subscriptions-logger.php b/includes/logging/class-yoone-subscriptions-logger.php index 4e5d02b..f4fa6f5 100644 --- a/includes/logging/class-yoone-subscriptions-logger.php +++ b/includes/logging/class-yoone-subscriptions-logger.php @@ -1,6 +1,8 @@