feat: 添加订阅系统核心组件和测试框架
新增支付网关接口、订阅接口和抽象数据类 添加测试配置、支付集成测试和订阅流程测试 实现日志系统和混装产品前端模板 完善README文档说明系统架构和功能
This commit is contained in:
commit
b676ae7469
|
|
@ -0,0 +1,210 @@
|
||||||
|
# Yoone Subscriptions 插件
|
||||||
|
|
||||||
|
Yoone Subscriptions 是一个面向 WooCommerce 的订阅管理插件,用于实现订阅商品、自动续费、支付令牌管理、捆绑产品订阅、日志与分析、以及前后台管理页面等完整功能。
|
||||||
|
|
||||||
|
## 功能总览
|
||||||
|
|
||||||
|
- 订阅生命周期管理:创建、激活、暂停、恢复、取消、过期处理
|
||||||
|
- 续费与账期:续费订单创建、支付处理、下一次支付日期计算
|
||||||
|
- 支付集成:Moneris 网关(令牌化支付)、支付令牌管理
|
||||||
|
- 捆绑产品:支持将多个商品组合为订阅捆绑,前后台配置与展示
|
||||||
|
- 前台:我的订阅、订阅详情、订阅选项等模板
|
||||||
|
- 后台:订阅列表与编辑、捆绑产品列表与编辑、日志查看与分析
|
||||||
|
- 定时任务:自动处理续费、过期与清理任务
|
||||||
|
- 日志与分析:结构化日志记录、错误与告警分析
|
||||||
|
- API 与 AJAX:提供必要的接口与管理操作
|
||||||
|
- 测试:内置测试套件与场景测试脚本,辅助验证完整流程
|
||||||
|
|
||||||
|
## 环境与依赖
|
||||||
|
|
||||||
|
- WordPress 6.x 及以上
|
||||||
|
- WooCommerce 6.x 及以上(需启用)
|
||||||
|
- PHP 7.4 及以上
|
||||||
|
- Moneris 支付网关(可选,启用令牌化支付)
|
||||||
|
|
||||||
|
## 安装与启用
|
||||||
|
|
||||||
|
1. 将 yoone-subscriptions 插件拷贝到 `wp-content/plugins/yoone-subscriptions` 目录。
|
||||||
|
2. 在 WordPress 后台的“插件”页面启用该插件。
|
||||||
|
3. 在 WooCommerce 设置中启用并配置 Moneris 网关(如使用令牌化支付与自动续费)。
|
||||||
|
4. 访问后台的“订阅管理”和“捆绑产品”页面进行配置。
|
||||||
|
|
||||||
|
## 代码结构与架构
|
||||||
|
|
||||||
|
插件主入口文件:`yoone-subscriptions.php`
|
||||||
|
- 负责加载类文件、初始化前台与后台逻辑、注册网关与产品类型、挂载 AJAX 与定时任务。
|
||||||
|
|
||||||
|
核心目录:`includes/`
|
||||||
|
- abstracts/
|
||||||
|
- `abstract-yoone-data.php`:数据抽象基类,统一提供读写、保存、删除、属性管理等通用能力。
|
||||||
|
- interfaces/
|
||||||
|
- `interface-yoone-payment-gateway.php`:支付网关接口(扩展新网关的契约)。
|
||||||
|
- `interface-yoone-subscription.php`:订阅接口(统一订阅对象的行为约束)。
|
||||||
|
- subscription/
|
||||||
|
- `class-yoone-subscription.php`:订阅核心类,包含生命周期方法(activate/pause/resume/cancel)、续费处理(process_renewal / create_renewal_order / process_renewal_payment)、账期计算(calculate_next_payment_date / add_billing_period)、校验与日志、数据库操作(read/create/update/delete)。
|
||||||
|
- payment/
|
||||||
|
- `class-yoone-moneris-gateway.php`:Moneris 支付网关集成,处理支付与令牌化逻辑。
|
||||||
|
- `class-yoone-payment-token.php`:支付令牌模型,定义字段(customer_id、gateway_id、token、card_type、last_four、expiry 等)、默认令牌管理、有效性与过期判断、数据库读写、静态查询与清理。
|
||||||
|
- bundle/
|
||||||
|
- `class-yoone-bundle.php`:捆绑产品核心逻辑。
|
||||||
|
- `class-yoone-bundle-frontend.php`:捆绑产品的前端展示逻辑。
|
||||||
|
- admin/
|
||||||
|
- `class-yoone-admin.php`:后台初始化与页面管理。
|
||||||
|
- `class-yoone-admin-logs.php`:后台日志页面与工具。
|
||||||
|
- `class-yoone-bundles-list-table.php`、`class-yoone-subscriptions-list-table.php`:列表页表格展示。
|
||||||
|
- frontend/
|
||||||
|
- `class-yoone-frontend.php`:前端初始化与路由。
|
||||||
|
- 其他核心类
|
||||||
|
- `class-yoone-install.php`:安装与升级(创建数据库表)。
|
||||||
|
- `class-yoone-ajax.php`:AJAX 操作处理。
|
||||||
|
- `class-yoone-api.php`:后端 API(如获取支付令牌、订阅数据等)。
|
||||||
|
- `class-yoone-cron.php`:定时任务注册与处理。
|
||||||
|
- `class-yoone-logger.php`:日志记录。
|
||||||
|
- `class-yoone-log-analyzer.php`:日志分析与错误告警。
|
||||||
|
- `class-yoone-helper.php`:通用帮助方法。
|
||||||
|
|
||||||
|
静态资源:`assets/`
|
||||||
|
- css/
|
||||||
|
- `yoone-admin.css`、`yoone-frontend.css`:后台与前端样式。
|
||||||
|
- js/
|
||||||
|
- `yoone-admin.js`、`yoone-frontend.js`、`admin-logs.js`、`bundle-frontend.js`:交互脚本。
|
||||||
|
|
||||||
|
模板视图:`templates/`
|
||||||
|
- admin/
|
||||||
|
- `bundle-add.php`、`bundle-edit.php`、`bundle-list.php`:捆绑产品后台页面。
|
||||||
|
- `subscription-edit.php`、`subscription-list.php`:订阅后台页面。
|
||||||
|
- frontend/
|
||||||
|
- `bundle-product.php`、`subscription-options.php`、`subscription-details.php`、`my-subscriptions.php` 等:前端展示页与用户中心。
|
||||||
|
- emails/
|
||||||
|
- `subscription-renewal.php`、`subscription-cancelled.php`:邮件模板。
|
||||||
|
- subscription/
|
||||||
|
- `subscription-options.php`:订阅商品配置信息展示。
|
||||||
|
- myaccount/
|
||||||
|
- `subscriptions.php`:我的订阅入口页面。
|
||||||
|
|
||||||
|
测试:`tests/`
|
||||||
|
- `class-yoone-test-suite.php`:测试套件(AJAX 驱动,支持单独运行订阅/支付/捆绑/定时任务测试)。
|
||||||
|
- `test-config.php`:测试配置与辅助方法(创建测试用户与产品、环境信息等)。
|
||||||
|
- `test-subscription-flow.php`:订阅全流程场景测试(创建/激活/暂停/恢复/续费/取消)。
|
||||||
|
- `test-payment-integration.php`:支付集成测试(令牌创建与验证、网关配置检查、支付数据校验)。
|
||||||
|
- `test-bundle-products.php`:捆绑产品场景测试(创建、价格计算、订阅建立、库存与子品管理)。
|
||||||
|
- `test-cron-jobs.php`:定时任务测试(续费/过期/清理任务的调度与模拟)。
|
||||||
|
- `run-tests.php`:浏览器端测试运行入口页面(展示环境信息与测试结果)。
|
||||||
|
|
||||||
|
## 数据模型与数据库表
|
||||||
|
|
||||||
|
由 `class-yoone-install.php` 管理数据库结构(安装/升级时创建),主要表包括:
|
||||||
|
- `wp_yoone_subscriptions`:订阅主表,包含客户、状态、账期、下一次支付日期等字段。
|
||||||
|
- `wp_yoone_payment_tokens`:支付令牌表,包含令牌、网关 ID、卡类型、后四位、过期信息、是否默认等。
|
||||||
|
- `wp_yoone_logs`:日志记录表(包含类型、消息、时间等)。
|
||||||
|
- 其他可能的辅助表(如订阅项目)按实现情况存在。
|
||||||
|
|
||||||
|
数据抽象层通过 `Abstract_Yoone_Data` 统一提供:
|
||||||
|
- `read/create/update/delete` 数据库操作
|
||||||
|
- `get_prop/set_prop/set_props` 属性管理
|
||||||
|
- `save()` 保存与触发钩子
|
||||||
|
|
||||||
|
## 订阅生命周期与续费流程
|
||||||
|
|
||||||
|
- 生命周期方法:
|
||||||
|
- `activate()` 激活订阅(更新状态、保存、记录日志、触发动作)。
|
||||||
|
- `pause()` 暂停订阅;`resume()` 恢复订阅。
|
||||||
|
- `cancel()` 取消订阅(支持 pending-cancel 与彻底取消策略)。
|
||||||
|
- 续费与账期:
|
||||||
|
- `process_renewal()` 续费流程入口。
|
||||||
|
- `create_renewal_order()` 创建续费订单(用于 WooCommerce 支付)。
|
||||||
|
- `process_renewal_payment()` 处理续费支付(结合令牌与网关)。
|
||||||
|
- `calculate_next_payment_date()`、`add_billing_period()` 负责下一次支付日期计算。
|
||||||
|
- 校验与辅助:
|
||||||
|
- `can_be_activated/can_be_renewed/can_be_paused/can_be_cancelled/is_trial` 等校验方法。
|
||||||
|
- `add_log/get_logs` 日志记录与读取。
|
||||||
|
- 钩子与扩展:
|
||||||
|
- 如 `yoone_subscription_updated`、`yoone_subscription_cancelled` 等动作。
|
||||||
|
|
||||||
|
## 支付集成与令牌化
|
||||||
|
|
||||||
|
- Moneris 网关(`class-yoone-moneris-gateway.php`):
|
||||||
|
- 负责与 Moneris 的对接、令牌化与交易处理。
|
||||||
|
- 后台配置项(Store ID、API Token 等),通过 WooCommerce 网关设置管理。
|
||||||
|
- 支付令牌(`class-yoone-payment-token.php`):
|
||||||
|
- 字段:`customer_id`、`gateway_id`、`token`、`token_type`、`card_type`、`last_four`、`expiry_month/year`、`is_default`、`expires_at` 等。
|
||||||
|
- 方法:`is_valid()`、`is_expired()`、`is_card_expired()`;`set_default()` 会自动取消其他令牌默认状态。
|
||||||
|
- 静态查询:`get_customer_tokens($customer_id, $gateway_id = '')`、`get_default_token($customer_id, $gateway_id)`、`get_by_token($token, $gateway_id)`、`cleanup_expired_tokens()`。
|
||||||
|
|
||||||
|
> 与《订阅支付全流程.md》对齐:续费时优先使用客户默认令牌,若令牌无效或过期则记录日志并标记失败,支持重试或通知用户更新支付方式。
|
||||||
|
|
||||||
|
## 捆绑产品
|
||||||
|
|
||||||
|
- 后台模板:`templates/admin/bundle-add.php`, `bundle-edit.php`, `bundle-list.php`。
|
||||||
|
- 前端模板:`templates/frontend/bundle-product.php`、`templates/bundle/bundle-options.php`。
|
||||||
|
- 逻辑类:`class-yoone-bundle.php`(组合商品与定价策略)、`class-yoone-bundle-frontend.php`(展示与交互)。
|
||||||
|
- 常见场景:
|
||||||
|
- 在捆绑产品中配置子产品及折扣,订阅价格与账期通过产品元数据(如 `_yoone_subscription_price`、`_yoone_subscription_period`、`_yoone_subscription_period_interval`)进行管理。
|
||||||
|
|
||||||
|
## 定时任务(Cron)
|
||||||
|
|
||||||
|
由 `class-yoone-cron.php` 注册与维护,典型钩子:
|
||||||
|
- `yoone_process_subscription_renewals`:检查并处理到期续费。
|
||||||
|
- `yoone_process_expired_subscriptions`:过期订阅处理。
|
||||||
|
- `yoone_cleanup_logs`:日志清理与归档。
|
||||||
|
- `yoone_cleanup_expired_tokens`:清理过期支付令牌。
|
||||||
|
|
||||||
|
> 与《流程跑通.md》对齐:确保在站点激活后按预设频率调度,考虑 WooCommerce 与 WordPress Cron 的时差与站点请求触发机制。
|
||||||
|
|
||||||
|
## 日志与分析
|
||||||
|
|
||||||
|
- `Yoone_Logger`:统一日志记录,支持 info/warning/error 等级。
|
||||||
|
- `Yoone_Log_Analyzer`:分析日志数据、聚合错误与告警,辅助排查问题与生成报告。
|
||||||
|
- 后台“日志”页面:支持筛选、搜索与简单可视化(由 `admin-logs.js` 与相关模板驱动)。
|
||||||
|
|
||||||
|
## API 与 AJAX
|
||||||
|
|
||||||
|
- `class-yoone-api.php`:提供订阅数据与支付令牌相关接口(示例:获取用户令牌列表)。
|
||||||
|
- `class-yoone-ajax.php`:处理后台或前台触发的 AJAX 操作,如运行测试套件、更新订阅状态等。
|
||||||
|
|
||||||
|
## 前后台页面与模板
|
||||||
|
|
||||||
|
- 后台:订阅列表/编辑、捆绑产品列表/编辑、日志分析。
|
||||||
|
- 前台:我的订阅、订阅详情、订阅选项、捆绑商品展示。
|
||||||
|
- 模板可通过主题覆盖机制进行自定义,建议保持字段与钩子兼容。
|
||||||
|
|
||||||
|
## 测试与质量保障
|
||||||
|
|
||||||
|
- 浏览器运行入口:`wp-content/plugins/yoone-subscriptions/run-tests.php`。
|
||||||
|
- 测试套件:
|
||||||
|
- 订阅流程(`test-subscription-flow.php`)
|
||||||
|
- 支付集成(`test-payment-integration.php`)
|
||||||
|
- 捆绑产品(`test-bundle-products.php`)
|
||||||
|
- 定时任务(`test-cron-jobs.php`)
|
||||||
|
- 支持通过 AJAX 在后台界面运行不同测试类型,并生成概要与详细报告。
|
||||||
|
|
||||||
|
## 扩展指南
|
||||||
|
|
||||||
|
- 新增支付网关:实现 `interface-yoone-payment-gateway.php` 并在主入口注册,参考 Moneris 实现。
|
||||||
|
- 自定义订阅行为:扩展 `class-yoone-subscription.php` 的钩子与动作,或在子类中覆盖方法。
|
||||||
|
- 自定义模板:将模板文件复制到主题并保持关键字段与钩子一致。
|
||||||
|
- 数据与日志:通过 `Abstract_Yoone_Data` 统一操作数据模型,并记录关键流程的日志,便于分析与测试。
|
||||||
|
|
||||||
|
## 与内部文档的对应关系
|
||||||
|
|
||||||
|
- 《订阅需求.md》:覆盖订阅状态、账期、试用期、取消策略与用户交互;对应 `class-yoone-subscription.php` 与相关模板。
|
||||||
|
- 《订阅支付全流程.md》:覆盖令牌化支付、续费订单与失败重试;对应 `class-yoone-moneris-gateway.php` 与 `class-yoone-payment-token.php`。
|
||||||
|
- 《流程跑通.md》:覆盖从创建订阅到取消的全流程、定时任务调度与测试验证;对应 `tests/` 目录与 `class-yoone-cron.php`。
|
||||||
|
- 《技术需求与实现参考.md》《自实现技术设计文档.md》:对应整体架构、抽象层与扩展点;对应 `includes/` 目录下的抽象类、接口与核心模块。
|
||||||
|
|
||||||
|
## 常见问题与排查
|
||||||
|
|
||||||
|
- 令牌无效或过期:检查 `yoone_payment_tokens` 表、令牌过期字段、默认令牌设置,查看日志分析页面。
|
||||||
|
- 续费失败:查看续费订单与支付响应,确认网关配置(Store ID / API Token),检查定时任务是否被正确调度。
|
||||||
|
- 模板未生效:确认主题覆盖路径与文件名、清理缓存、检查钩子绑定是否正确。
|
||||||
|
- 测试失败:使用 `run-tests.php` 查看详细报错,定位对应类与方法进行修复。
|
||||||
|
|
||||||
|
## 开发建议
|
||||||
|
|
||||||
|
- 遵循 WooCommerce 与 WordPress 的编码规范及钩子机制。
|
||||||
|
- 对关键流程(创建、续费、取消)添加日志记录,辅助运维与测试。
|
||||||
|
- 编写场景测试用例,覆盖主要业务路径与异常路径。
|
||||||
|
- 在开发环境中开启调试模式与错误日志,结合 `Yoone_Log_Analyzer` 分析问题。
|
||||||
|
|
||||||
|
---
|
||||||
|
如需进一步信息或与内部文档对齐的详细说明,请参考项目目录 `/Users/zksu/Developer/work/work-yoone/实际开发/订阅制` 下的规范文档,并在代码中搜索对应模块的实现以获取最新内容。
|
||||||
|
|
@ -0,0 +1,855 @@
|
||||||
|
/**
|
||||||
|
* Yoone Subscriptions 后台管理样式
|
||||||
|
*
|
||||||
|
* 混装产品和订阅功能的后台管理样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
通用后台样式
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.yoone-admin-page {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-admin-page .wrap h1 {
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
border-bottom: 2px solid #3498db;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-notice {
|
||||||
|
margin: 15px 0;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-left: 4px solid #3498db;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-notice.notice-success {
|
||||||
|
border-left-color: #27ae60;
|
||||||
|
background: #d5f4e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-notice.notice-error {
|
||||||
|
border-left-color: #e74c3c;
|
||||||
|
background: #fadbd8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-notice.notice-warning {
|
||||||
|
border-left-color: #f39c12;
|
||||||
|
background: #fef9e7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-loading {
|
||||||
|
position: relative;
|
||||||
|
opacity: 0.6;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-loading::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin: -10px 0 0 -10px;
|
||||||
|
border: 2px solid #ccc;
|
||||||
|
border-top-color: #3498db;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: yoone-spin 1s linear infinite;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes yoone-spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
订阅管理页面样式
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.yoone-subscriptions-page .tablenav {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscriptions-page .wp-list-table {
|
||||||
|
border: 1px solid #e1e1e1;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscriptions-page .wp-list-table th {
|
||||||
|
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
|
||||||
|
color: #2c3e50;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 15px 12px;
|
||||||
|
border-bottom: 2px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscriptions-page .wp-list-table td {
|
||||||
|
padding: 15px 12px;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-bottom: 1px solid #f1f3f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscriptions-page .wp-list-table tr:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-status-badge.status-active {
|
||||||
|
background: linear-gradient(135deg, #27ae60, #2ecc71);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(39, 174, 96, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-status-badge.status-paused {
|
||||||
|
background: linear-gradient(135deg, #f39c12, #e67e22);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(243, 156, 18, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-status-badge.status-cancelled {
|
||||||
|
background: linear-gradient(135deg, #e74c3c, #c0392b);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(231, 76, 60, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-status-badge.status-expired {
|
||||||
|
background: linear-gradient(135deg, #95a5a6, #7f8c8d);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(149, 165, 166, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-customer-link {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-customer-link:hover {
|
||||||
|
color: #2980b9;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-billing-period {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-total {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #27ae60;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-actions .button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-actions .button-primary {
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-actions .button-primary:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-actions .button-secondary {
|
||||||
|
background: #95a5a6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-actions .button-secondary:hover {
|
||||||
|
background: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-actions .button-danger {
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-actions .button-danger:hover {
|
||||||
|
background: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
混装产品管理页面样式
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.yoone-bundles-page .wp-list-table th,
|
||||||
|
.yoone-bundles-page .wp-list-table td {
|
||||||
|
padding: 15px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-name-link {
|
||||||
|
color: #2c3e50;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-name-link:hover {
|
||||||
|
color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-status-badge.status-active {
|
||||||
|
background: linear-gradient(135deg, #27ae60, #2ecc71);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(39, 174, 96, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-status-badge.status-inactive {
|
||||||
|
background: linear-gradient(135deg, #95a5a6, #7f8c8d);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(149, 165, 166, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-discount-info {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #e74c3c;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-quantity-info {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
产品编辑页面样式
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.yoone-product-data-panel {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-product-data-panel h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 1px solid #e1e1e1;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-field-group {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-field-group h4 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-field {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-field label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-field input[type="text"],
|
||||||
|
.yoone-field input[type="number"],
|
||||||
|
.yoone-field select,
|
||||||
|
.yoone-field textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #bdc3c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-field input[type="text"]:focus,
|
||||||
|
.yoone-field input[type="number"]:focus,
|
||||||
|
.yoone-field select:focus,
|
||||||
|
.yoone-field textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3498db;
|
||||||
|
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-field input[type="checkbox"] {
|
||||||
|
margin-right: 8px;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-field .description {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-checkbox-field {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-checkbox-field label {
|
||||||
|
margin: 0 0 0 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 混装产品项目管理 */
|
||||||
|
.yoone-bundle-items {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-items h4 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-search {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-search input {
|
||||||
|
width: 70%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #bdc3c7;
|
||||||
|
border-radius: 4px 0 0 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-search .button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: 1px solid #3498db;
|
||||||
|
border-left: none;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-search .button:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-items-list {
|
||||||
|
border: 1px solid #e1e1e1;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px;
|
||||||
|
border-bottom: 1px solid #f1f3f4;
|
||||||
|
background: white;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-row:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-image {
|
||||||
|
flex: 0 0 60px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-image img {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-details {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-price {
|
||||||
|
color: #27ae60;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-quantity {
|
||||||
|
flex: 0 0 100px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-quantity input {
|
||||||
|
width: 70px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #bdc3c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-actions {
|
||||||
|
flex: 0 0 80px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-remove {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-remove:hover {
|
||||||
|
background: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-search-results {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e1e1e1;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 6px 6px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-search-result {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-search-result:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-search-result img {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-search-result-details {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-search-result-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-search-result-price {
|
||||||
|
color: #27ae60;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订阅设置样式 */
|
||||||
|
.subscription-periods-list {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-period-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-period-row input[type="number"] {
|
||||||
|
width: 80px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-period-row select {
|
||||||
|
width: 120px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-period-remove {
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-period-remove:hover {
|
||||||
|
background: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-add-period {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #27ae60;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-add-period:hover {
|
||||||
|
background: #229954;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
订单管理页面样式
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.yoone-order-meta-box {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-order-meta-box h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 1px solid #e1e1e1;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-subscription-info {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 4px solid #9b59b6;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-subscription-info h4 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info-value {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-bundle-info {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 4px solid #3498db;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-bundle-info h4 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-items-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-info:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-info img {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-details {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-quantity,
|
||||||
|
.bundle-item-price {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-price {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
支付设置页面样式
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.yoone-payment-settings {
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-payment-settings .form-table {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e1e1e1;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-payment-settings .form-table th {
|
||||||
|
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
|
||||||
|
color: #2c3e50;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 20px;
|
||||||
|
width: 200px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-payment-settings .form-table td {
|
||||||
|
padding: 20px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-payment-settings input[type="text"],
|
||||||
|
.yoone-payment-settings input[type="password"],
|
||||||
|
.yoone-payment-settings select,
|
||||||
|
.yoone-payment-settings textarea {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #bdc3c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-payment-settings input[type="text"]:focus,
|
||||||
|
.yoone-payment-settings input[type="password"]:focus,
|
||||||
|
.yoone-payment-settings select:focus,
|
||||||
|
.yoone-payment-settings textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3498db;
|
||||||
|
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-payment-settings .description {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-test-mode {
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-test-mode h4 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #856404;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-test-mode p {
|
||||||
|
margin: 0;
|
||||||
|
color: #856404;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
响应式设计
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.yoone-admin-page .wrap h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-actions,
|
||||||
|
.bundle-item-row {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-image,
|
||||||
|
.bundle-item-details,
|
||||||
|
.bundle-item-quantity,
|
||||||
|
.bundle-item-actions {
|
||||||
|
margin: 5px 0;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-search input {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item-search .button {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-period-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-period-row input[type="number"],
|
||||||
|
.subscription-period-row select {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-payment-settings .form-table,
|
||||||
|
.yoone-payment-settings .form-table tbody,
|
||||||
|
.yoone-payment-settings .form-table tr,
|
||||||
|
.yoone-payment-settings .form-table th,
|
||||||
|
.yoone-payment-settings .form-table td {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-payment-settings .form-table th {
|
||||||
|
padding: 15px 15px 5px 15px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-payment-settings .form-table td {
|
||||||
|
padding: 5px 15px 15px 15px;
|
||||||
|
border-bottom: 1px solid #e1e1e1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.yoone-field-group {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-order-meta-box,
|
||||||
|
.yoone-product-data-panel {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-actions .button {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,894 @@
|
||||||
|
/**
|
||||||
|
* Yoone Subscriptions 前端样式
|
||||||
|
*
|
||||||
|
* 混装产品和订阅功能的前端样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
通用样式
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.yoone-notice {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
z-index: 9999;
|
||||||
|
max-width: 300px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-notice-success {
|
||||||
|
background-color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-notice-error {
|
||||||
|
background-color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-loading {
|
||||||
|
opacity: 0.6;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-loading::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin: -10px 0 0 -10px;
|
||||||
|
border: 2px solid #ccc;
|
||||||
|
border-top-color: #333;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: yoone-spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes yoone-spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
混装产品样式
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.yoone-bundle-options {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 25px;
|
||||||
|
border: 2px solid #e1e1e1;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||||
|
box-shadow: 0 2px 15px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-options h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 2px solid #3498db;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-info {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-description {
|
||||||
|
color: #555;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-discount {
|
||||||
|
text-align: center;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discount-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: linear-gradient(135deg, #e74c3c, #c0392b);
|
||||||
|
color: white;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
box-shadow: 0 2px 10px rgba(231, 76, 60, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-items {
|
||||||
|
margin: 25px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item:hover {
|
||||||
|
border-color: #3498db;
|
||||||
|
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .item-image {
|
||||||
|
flex: 0 0 80px;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .item-image img {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .item-details {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .item-name {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .item-price {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #e74c3c;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .item-description {
|
||||||
|
margin: 0;
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .item-quantity {
|
||||||
|
flex: 0 0 120px;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .item-quantity label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #34495e;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .bundle-quantity-input {
|
||||||
|
width: 70px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 2px solid #bdc3c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .bundle-quantity-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3498db;
|
||||||
|
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .item-subtotal {
|
||||||
|
flex: 0 0 120px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .subtotal-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .subtotal-amount {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-summary {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
box-shadow: 0 2px 15px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-total .total-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-total .total-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-total .total-amount {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-total .discount {
|
||||||
|
color: #e74c3c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-total .final-total {
|
||||||
|
border-top: 2px solid #ecf0f1;
|
||||||
|
padding-top: 15px;
|
||||||
|
margin-top: 15px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-total .final-total .total-amount {
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-actions {
|
||||||
|
margin-top: 25px;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-actions .button {
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-actions .yoone-calculate-bundle {
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-actions .yoone-calculate-bundle:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-actions .yoone-add-bundle-to-cart {
|
||||||
|
background: #27ae60;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-actions .yoone-add-bundle-to-cart:hover:not(:disabled) {
|
||||||
|
background: #229954;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(39, 174, 96, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-actions .button:disabled {
|
||||||
|
background: #bdc3c7;
|
||||||
|
color: #7f8c8d;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-quantity-info {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #ecf0f1;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 4px solid #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-quantity-info p {
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #34495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-quantity-error {
|
||||||
|
margin: 15px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background: #fadbd8;
|
||||||
|
border: 1px solid #e74c3c;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
订阅选项样式
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.yoone-subscription-options {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 25px;
|
||||||
|
border: 2px solid #e1e1e1;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||||
|
box-shadow: 0 2px 15px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 2px solid #9b59b6;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-purchase-type {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-purchase-type label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-purchase-type label:hover {
|
||||||
|
border-color: #9b59b6;
|
||||||
|
box-shadow: 0 2px 10px rgba(155, 89, 182, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-purchase-type input[type="radio"] {
|
||||||
|
margin-right: 10px;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-purchase-type input[type="radio"]:checked + label,
|
||||||
|
.subscription-purchase-type label:has(input[type="radio"]:checked) {
|
||||||
|
border-color: #9b59b6;
|
||||||
|
background: linear-gradient(135deg, #f4f1f7, #ffffff);
|
||||||
|
box-shadow: 0 2px 15px rgba(155, 89, 182, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-purchase-type .price {
|
||||||
|
float: right;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-discount {
|
||||||
|
color: #e74c3c;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-periods {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-periods label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-periods select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border: 2px solid #bdc3c7;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
background: white;
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-periods select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #9b59b6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(155, 89, 182, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-benefits h4 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-benefits ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-benefits li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #555;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-benefits li::marker {
|
||||||
|
color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-details {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 4px solid #9b59b6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-details p {
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-details strong {
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-total .billing-cycle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-management {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.management-info {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.management-info a {
|
||||||
|
color: #9b59b6;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.management-info a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-trial {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #d5f4e6, #ffffff);
|
||||||
|
border: 2px solid #27ae60;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-info h4 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #27ae60;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-info p {
|
||||||
|
margin: 0;
|
||||||
|
color: #27ae60;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
我的账户订阅样式
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.yoone-my-subscriptions {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-my-subscriptions h2 {
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
border-bottom: 2px solid #3498db;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-subscriptions {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
background: linear-gradient(135deg, #f8f9fa, #ffffff);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-subscriptions p {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-item {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 25px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 2px 15px rgba(0,0,0,0.05);
|
||||||
|
transition: box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-item:hover {
|
||||||
|
box-shadow: 0 4px 25px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 2px solid #ecf0f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-id h3 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-status {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-status.status-active {
|
||||||
|
background: linear-gradient(135deg, #27ae60, #2ecc71);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(39, 174, 96, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-status.status-paused {
|
||||||
|
background: linear-gradient(135deg, #f39c12, #e67e22);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(243, 156, 18, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-status.status-cancelled {
|
||||||
|
background: linear-gradient(135deg, #e74c3c, #c0392b);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(231, 76, 60, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-status.status-expired {
|
||||||
|
background: linear-gradient(135deg, #95a5a6, #7f8c8d);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(149, 165, 166, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-actions .button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-pause {
|
||||||
|
background: #f39c12;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-pause:hover {
|
||||||
|
background: #e67e22;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-resume {
|
||||||
|
background: #27ae60;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-resume:hover {
|
||||||
|
background: #229954;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-cancel {
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-cancel:hover {
|
||||||
|
background: #c0392b;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-details {
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-details:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 25px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-products h4,
|
||||||
|
.subscription-details-info h4 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
border-bottom: 1px solid #ecf0f1;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-items {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-item-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-item-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-item-row .item-image {
|
||||||
|
flex: 0 0 50px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-item-row .item-image img {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-item-row .item-details {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-item-row .item-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-item-row .item-quantity,
|
||||||
|
.subscription-item-row .item-price {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-item-row .item-price {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
color: #555;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-management {
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 2px solid #ecf0f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-management h4 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.management-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.management-actions .button {
|
||||||
|
padding: 10px 18px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid #bdc3c7;
|
||||||
|
background: white;
|
||||||
|
color: #2c3e50;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.management-actions .button:hover {
|
||||||
|
border-color: #3498db;
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
响应式设计
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.bundle-item {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .item-image,
|
||||||
|
.bundle-item .item-details,
|
||||||
|
.bundle-item .item-quantity,
|
||||||
|
.bundle-item .item-subtotal {
|
||||||
|
margin: 10px 0;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-actions .button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.management-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.management-actions .button {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-options,
|
||||||
|
.yoone-subscription-options {
|
||||||
|
padding: 15px;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-options h3,
|
||||||
|
.yoone-subscription-options h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.subscription-purchase-type .price {
|
||||||
|
float: none;
|
||||||
|
display: block;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-total .total-row {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-total .final-total {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-total .final-total .total-amount {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,501 @@
|
||||||
|
/**
|
||||||
|
* 日志管理界面JavaScript
|
||||||
|
*
|
||||||
|
* 处理日志页面的交互功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志管理器
|
||||||
|
*/
|
||||||
|
var LogManager = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化
|
||||||
|
*/
|
||||||
|
init: function() {
|
||||||
|
this.bindEvents();
|
||||||
|
this.setupAutoRefresh();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定事件
|
||||||
|
*/
|
||||||
|
bindEvents: function() {
|
||||||
|
// 刷新日志
|
||||||
|
$('#refresh-logs').on('click', this.refreshLogs.bind(this));
|
||||||
|
|
||||||
|
// 清空日志
|
||||||
|
$('#clear-logs').on('click', this.clearLogs.bind(this));
|
||||||
|
|
||||||
|
// 下载日志
|
||||||
|
$('#download-logs').on('click', this.downloadLogs.bind(this));
|
||||||
|
|
||||||
|
// 日志行点击展开详情
|
||||||
|
$(document).on('click', '.log-table tbody tr', this.toggleLogDetails.bind(this));
|
||||||
|
|
||||||
|
// 实时搜索
|
||||||
|
$('input[name="search"]').on('input', this.debounce(this.performSearch.bind(this), 500));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新日志
|
||||||
|
*/
|
||||||
|
refreshLogs: function() {
|
||||||
|
location.reload();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空日志
|
||||||
|
*/
|
||||||
|
clearLogs: function() {
|
||||||
|
if (!confirm(yoone_logs_params.i18n.confirm_clear)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var $button = $('#clear-logs');
|
||||||
|
var originalText = $button.text();
|
||||||
|
|
||||||
|
$button.text(yoone_logs_params.i18n.clearing).prop('disabled', true);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: yoone_logs_params.ajax_url,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'yoone_clear_logs',
|
||||||
|
nonce: yoone_logs_params.nonce
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert(response.data.message || '清空失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
alert('请求失败,请重试');
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
$button.text(originalText).prop('disabled', false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载日志
|
||||||
|
*/
|
||||||
|
downloadLogs: function() {
|
||||||
|
var $button = $('#download-logs');
|
||||||
|
var originalText = $button.text();
|
||||||
|
|
||||||
|
$button.text(yoone_logs_params.i18n.downloading).prop('disabled', true);
|
||||||
|
|
||||||
|
// 创建隐藏的下载链接
|
||||||
|
var downloadUrl = yoone_logs_params.ajax_url + '?action=yoone_download_logs&nonce=' + yoone_logs_params.nonce;
|
||||||
|
var $link = $('<a>').attr({
|
||||||
|
href: downloadUrl,
|
||||||
|
download: 'yoone-subscriptions-' + new Date().toISOString().split('T')[0] + '.log'
|
||||||
|
}).appendTo('body');
|
||||||
|
|
||||||
|
$link[0].click();
|
||||||
|
$link.remove();
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
$button.text(originalText).prop('disabled', false);
|
||||||
|
}, 2000);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换日志详情显示
|
||||||
|
*/
|
||||||
|
toggleLogDetails: function(e) {
|
||||||
|
var $row = $(e.currentTarget);
|
||||||
|
var $detailRow = $row.next('.log-detail-row');
|
||||||
|
|
||||||
|
if ($detailRow.length) {
|
||||||
|
$detailRow.toggle();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建详情行
|
||||||
|
var context = $row.find('.log-context').text();
|
||||||
|
if (!context) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var $newDetailRow = $('<tr class="log-detail-row">').html(
|
||||||
|
'<td colspan="4"><div class="log-detail">' +
|
||||||
|
'<h4>详细信息:</h4>' +
|
||||||
|
'<pre>' + this.formatJSON(context) + '</pre>' +
|
||||||
|
'</div></td>'
|
||||||
|
);
|
||||||
|
|
||||||
|
$row.after($newDetailRow);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化JSON
|
||||||
|
*/
|
||||||
|
formatJSON: function(jsonString) {
|
||||||
|
try {
|
||||||
|
var obj = JSON.parse(jsonString);
|
||||||
|
return JSON.stringify(obj, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
return jsonString;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行搜索
|
||||||
|
*/
|
||||||
|
performSearch: function() {
|
||||||
|
var searchTerm = $('input[name="search"]').val();
|
||||||
|
var $rows = $('.log-table tbody tr:not(.log-detail-row)');
|
||||||
|
|
||||||
|
if (!searchTerm) {
|
||||||
|
$rows.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows.each(function() {
|
||||||
|
var $row = $(this);
|
||||||
|
var text = $row.text().toLowerCase();
|
||||||
|
var match = text.indexOf(searchTerm.toLowerCase()) !== -1;
|
||||||
|
|
||||||
|
$row.toggle(match);
|
||||||
|
|
||||||
|
// 隐藏对应的详情行
|
||||||
|
var $detailRow = $row.next('.log-detail-row');
|
||||||
|
if ($detailRow.length) {
|
||||||
|
$detailRow.toggle(match);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置自动刷新
|
||||||
|
*/
|
||||||
|
setupAutoRefresh: function() {
|
||||||
|
// 每30秒自动刷新一次(仅在错误标签页)
|
||||||
|
if (this.getCurrentTab() === 'error') {
|
||||||
|
setInterval(function() {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前标签页
|
||||||
|
*/
|
||||||
|
getCurrentTab: function() {
|
||||||
|
var urlParams = new URLSearchParams(window.location.search);
|
||||||
|
return urlParams.get('tab') || 'recent';
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防抖函数
|
||||||
|
*/
|
||||||
|
debounce: function(func, wait) {
|
||||||
|
var timeout;
|
||||||
|
return function executedFunction() {
|
||||||
|
var context = this;
|
||||||
|
var args = arguments;
|
||||||
|
var later = function() {
|
||||||
|
timeout = null;
|
||||||
|
func.apply(context, args);
|
||||||
|
};
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志统计图表
|
||||||
|
*/
|
||||||
|
var LogChart = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化图表
|
||||||
|
*/
|
||||||
|
init: function() {
|
||||||
|
this.createLevelChart();
|
||||||
|
this.createTimelineChart();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建级别分布图表
|
||||||
|
*/
|
||||||
|
createLevelChart: function() {
|
||||||
|
var $container = $('#log-level-chart');
|
||||||
|
if (!$container.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计各级别日志数量
|
||||||
|
var levels = {
|
||||||
|
error: 0,
|
||||||
|
warning: 0,
|
||||||
|
info: 0,
|
||||||
|
debug: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
$('.log-level').each(function() {
|
||||||
|
var level = $(this).text().toLowerCase();
|
||||||
|
if (levels.hasOwnProperty(level)) {
|
||||||
|
levels[level]++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建简单的条形图
|
||||||
|
var total = Object.values(levels).reduce((a, b) => a + b, 0);
|
||||||
|
var html = '<div class="log-chart-title">日志级别分布</div>';
|
||||||
|
|
||||||
|
Object.keys(levels).forEach(function(level) {
|
||||||
|
var count = levels[level];
|
||||||
|
var percentage = total > 0 ? (count / total * 100).toFixed(1) : 0;
|
||||||
|
|
||||||
|
html += '<div class="chart-bar">' +
|
||||||
|
'<span class="bar-label">' + level + '</span>' +
|
||||||
|
'<div class="bar-container">' +
|
||||||
|
'<div class="bar-fill ' + level + '" style="width: ' + percentage + '%"></div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<span class="bar-value">' + count + ' (' + percentage + '%)</span>' +
|
||||||
|
'</div>';
|
||||||
|
});
|
||||||
|
|
||||||
|
$container.html(html);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间线图表
|
||||||
|
*/
|
||||||
|
createTimelineChart: function() {
|
||||||
|
var $container = $('#log-timeline-chart');
|
||||||
|
if (!$container.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按小时统计日志数量
|
||||||
|
var hourlyStats = {};
|
||||||
|
var now = new Date();
|
||||||
|
|
||||||
|
// 初始化最近24小时
|
||||||
|
for (var i = 23; i >= 0; i--) {
|
||||||
|
var hour = new Date(now.getTime() - i * 60 * 60 * 1000);
|
||||||
|
var key = hour.getHours().toString().padStart(2, '0') + ':00';
|
||||||
|
hourlyStats[key] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计实际日志
|
||||||
|
$('.log-table tbody tr:not(.log-detail-row)').each(function() {
|
||||||
|
var timestamp = $(this).find('td:first').text();
|
||||||
|
var date = new Date(timestamp);
|
||||||
|
var hour = date.getHours().toString().padStart(2, '0') + ':00';
|
||||||
|
|
||||||
|
if (hourlyStats.hasOwnProperty(hour)) {
|
||||||
|
hourlyStats[hour]++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建时间线图表
|
||||||
|
var maxCount = Math.max(...Object.values(hourlyStats));
|
||||||
|
var html = '<div class="log-chart-title">24小时日志趋势</div><div class="timeline-chart">';
|
||||||
|
|
||||||
|
Object.keys(hourlyStats).forEach(function(hour) {
|
||||||
|
var count = hourlyStats[hour];
|
||||||
|
var height = maxCount > 0 ? (count / maxCount * 100) : 0;
|
||||||
|
|
||||||
|
html += '<div class="timeline-bar" title="' + hour + ': ' + count + ' 条日志">' +
|
||||||
|
'<div class="timeline-fill" style="height: ' + height + '%"></div>' +
|
||||||
|
'<span class="timeline-label">' + hour + '</span>' +
|
||||||
|
'</div>';
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
$container.html(html);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志导出功能
|
||||||
|
*/
|
||||||
|
var LogExporter = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出为CSV
|
||||||
|
*/
|
||||||
|
exportCSV: function() {
|
||||||
|
var csv = 'Timestamp,Level,Message,Context\n';
|
||||||
|
|
||||||
|
$('.log-table tbody tr:not(.log-detail-row)').each(function() {
|
||||||
|
var $row = $(this);
|
||||||
|
var cells = $row.find('td').map(function() {
|
||||||
|
return '"' + $(this).text().replace(/"/g, '""') + '"';
|
||||||
|
}).get();
|
||||||
|
|
||||||
|
csv += cells.join(',') + '\n';
|
||||||
|
});
|
||||||
|
|
||||||
|
this.downloadFile(csv, 'yoone-logs-' + new Date().toISOString().split('T')[0] + '.csv', 'text/csv');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出为JSON
|
||||||
|
*/
|
||||||
|
exportJSON: function() {
|
||||||
|
var logs = [];
|
||||||
|
|
||||||
|
$('.log-table tbody tr:not(.log-detail-row)').each(function() {
|
||||||
|
var $row = $(this);
|
||||||
|
var $cells = $row.find('td');
|
||||||
|
|
||||||
|
logs.push({
|
||||||
|
timestamp: $cells.eq(0).text(),
|
||||||
|
level: $cells.eq(1).text(),
|
||||||
|
message: $cells.eq(2).text(),
|
||||||
|
context: $cells.eq(3).text()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var json = JSON.stringify(logs, null, 2);
|
||||||
|
this.downloadFile(json, 'yoone-logs-' + new Date().toISOString().split('T')[0] + '.json', 'application/json');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载文件
|
||||||
|
*/
|
||||||
|
downloadFile: function(content, filename, contentType) {
|
||||||
|
var blob = new Blob([content], { type: contentType });
|
||||||
|
var url = window.URL.createObjectURL(blob);
|
||||||
|
var $link = $('<a>').attr({
|
||||||
|
href: url,
|
||||||
|
download: filename
|
||||||
|
}).appendTo('body');
|
||||||
|
|
||||||
|
$link[0].click();
|
||||||
|
$link.remove();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 文档就绪时初始化
|
||||||
|
$(document).ready(function() {
|
||||||
|
LogManager.init();
|
||||||
|
LogChart.init();
|
||||||
|
|
||||||
|
// 添加导出按钮事件
|
||||||
|
$(document).on('click', '#export-csv', LogExporter.exportCSV.bind(LogExporter));
|
||||||
|
$(document).on('click', '#export-json', LogExporter.exportJSON.bind(LogExporter));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加样式
|
||||||
|
$('<style>').text(`
|
||||||
|
.log-detail {
|
||||||
|
background: #f9f9f9;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-detail h4 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-detail pre {
|
||||||
|
background: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-table tbody tr:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-chart-title {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-label {
|
||||||
|
width: 80px;
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-container {
|
||||||
|
flex: 1;
|
||||||
|
height: 20px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 0 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-fill.error { background: #dc3545; }
|
||||||
|
.bar-fill.warning { background: #ffc107; }
|
||||||
|
.bar-fill.info { background: #17a2b8; }
|
||||||
|
.bar-fill.debug { background: #6c757d; }
|
||||||
|
|
||||||
|
.bar-value {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-chart {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
height: 100px;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-bar {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-fill {
|
||||||
|
width: 100%;
|
||||||
|
background: #007cba;
|
||||||
|
border-radius: 2px 2px 0 0;
|
||||||
|
transition: height 0.3s ease;
|
||||||
|
min-height: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 5px;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
}
|
||||||
|
`).appendTo('head');
|
||||||
|
|
||||||
|
})(jQuery);
|
||||||
|
|
@ -0,0 +1,326 @@
|
||||||
|
/**
|
||||||
|
* 混装产品前端脚本
|
||||||
|
*
|
||||||
|
* 处理混装产品的用户交互和价格计算
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 混装产品处理器
|
||||||
|
*/
|
||||||
|
var YooneBundleHandler = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化
|
||||||
|
*/
|
||||||
|
init: function() {
|
||||||
|
this.bindEvents();
|
||||||
|
this.updatePricing();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定事件
|
||||||
|
*/
|
||||||
|
bindEvents: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// 商品选择变化
|
||||||
|
$(document).on('change', '.bundle-item-checkbox', function() {
|
||||||
|
self.onItemSelectionChange($(this));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数量变化
|
||||||
|
$(document).on('change', '#bundle-quantity', function() {
|
||||||
|
self.updatePricing();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数量按钮
|
||||||
|
$(document).on('click', '.qty-btn.plus', function() {
|
||||||
|
self.increaseQuantity();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.qty-btn.minus', function() {
|
||||||
|
self.decreaseQuantity();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表单提交验证
|
||||||
|
$('form.cart').on('submit', function(e) {
|
||||||
|
if (!self.validateBundleSelection()) {
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商品选择变化处理
|
||||||
|
*/
|
||||||
|
onItemSelectionChange: function($checkbox) {
|
||||||
|
var $item = $checkbox.closest('.bundle-item');
|
||||||
|
|
||||||
|
if ($checkbox.is(':checked')) {
|
||||||
|
$item.addClass('selected');
|
||||||
|
} else {
|
||||||
|
$item.removeClass('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updatePricing();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 增加数量
|
||||||
|
*/
|
||||||
|
increaseQuantity: function() {
|
||||||
|
var $input = $('#bundle-quantity');
|
||||||
|
var current = parseInt($input.val()) || 1;
|
||||||
|
var max = parseInt($input.attr('max'));
|
||||||
|
|
||||||
|
if (!max || current < max) {
|
||||||
|
$input.val(current + 1).trigger('change');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 减少数量
|
||||||
|
*/
|
||||||
|
decreaseQuantity: function() {
|
||||||
|
var $input = $('#bundle-quantity');
|
||||||
|
var current = parseInt($input.val()) || 1;
|
||||||
|
var min = parseInt($input.attr('min')) || 1;
|
||||||
|
|
||||||
|
if (current > min) {
|
||||||
|
$input.val(current - 1).trigger('change');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新价格显示
|
||||||
|
*/
|
||||||
|
updatePricing: function() {
|
||||||
|
var self = this;
|
||||||
|
var selectedItems = this.getSelectedItems();
|
||||||
|
var quantity = parseInt($('#bundle-quantity').val()) || 1;
|
||||||
|
|
||||||
|
if (selectedItems.length === 0) {
|
||||||
|
this.clearPricing();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示计算中状态
|
||||||
|
$('#bundle-original-total, #bundle-final-total').text(yoone_bundle_params.i18n.calculating);
|
||||||
|
|
||||||
|
// AJAX 请求计算价格
|
||||||
|
$.ajax({
|
||||||
|
url: yoone_bundle_params.ajax_url,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'yoone_calculate_bundle_price',
|
||||||
|
nonce: yoone_bundle_params.nonce,
|
||||||
|
bundle_id: $('.yoone-bundle-options').data('bundle-id'),
|
||||||
|
selected_items: selectedItems,
|
||||||
|
quantity: quantity
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
self.displayPricing(response.data);
|
||||||
|
} else {
|
||||||
|
self.showError(response.data.message || yoone_bundle_params.i18n.error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
self.showError(yoone_bundle_params.i18n.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取选中的商品
|
||||||
|
*/
|
||||||
|
getSelectedItems: function() {
|
||||||
|
var items = [];
|
||||||
|
|
||||||
|
$('.bundle-item-checkbox:checked').each(function() {
|
||||||
|
var $checkbox = $(this);
|
||||||
|
var productId = $checkbox.closest('.bundle-item').data('product-id');
|
||||||
|
var quantity = parseInt($checkbox.val()) || 1;
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
product_id: productId,
|
||||||
|
quantity: quantity
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示价格信息
|
||||||
|
*/
|
||||||
|
displayPricing: function(data) {
|
||||||
|
$('#bundle-original-total').text(data.original_total_formatted);
|
||||||
|
$('#bundle-final-total').text(data.final_total_formatted);
|
||||||
|
|
||||||
|
if (data.savings > 0) {
|
||||||
|
$('#bundle-savings .savings-amount').text(data.savings_formatted);
|
||||||
|
$('#bundle-savings').show();
|
||||||
|
} else {
|
||||||
|
$('#bundle-savings').hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新产品价格(用于WooCommerce)
|
||||||
|
this.updateProductPrice(data.final_total);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空价格显示
|
||||||
|
*/
|
||||||
|
clearPricing: function() {
|
||||||
|
$('#bundle-original-total, #bundle-final-total').text('-');
|
||||||
|
$('#bundle-savings').hide();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新产品价格
|
||||||
|
*/
|
||||||
|
updateProductPrice: function(price) {
|
||||||
|
// 更新产品页面的价格显示
|
||||||
|
var formattedPrice = this.formatPrice(price);
|
||||||
|
$('.summary .price .amount').html(formattedPrice);
|
||||||
|
|
||||||
|
// 触发价格更新事件
|
||||||
|
$(document.body).trigger('yoone_bundle_price_updated', [price]);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化价格
|
||||||
|
*/
|
||||||
|
formatPrice: function(price) {
|
||||||
|
// 这里应该使用WooCommerce的价格格式化
|
||||||
|
// 简单实现,实际应该从服务器获取格式化后的价格
|
||||||
|
return '$' + parseFloat(price).toFixed(2);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证混装选择
|
||||||
|
*/
|
||||||
|
validateBundleSelection: function() {
|
||||||
|
var selectedItems = this.getSelectedItems();
|
||||||
|
|
||||||
|
if (selectedItems.length === 0) {
|
||||||
|
this.showError(yoone_bundle_params.i18n.select_items);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查必选商品
|
||||||
|
var hasError = false;
|
||||||
|
$('.bundle-item').each(function() {
|
||||||
|
var $item = $(this);
|
||||||
|
var $checkbox = $item.find('.bundle-item-checkbox');
|
||||||
|
|
||||||
|
if ($checkbox.prop('disabled') && !$checkbox.is(':checked')) {
|
||||||
|
hasError = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
this.showError('请选择所有必需的商品');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示错误信息
|
||||||
|
*/
|
||||||
|
showError: function(message) {
|
||||||
|
var $messages = $('.validation-messages');
|
||||||
|
$messages.html('<p>' + message + '</p>').show();
|
||||||
|
|
||||||
|
// 3秒后自动隐藏
|
||||||
|
setTimeout(function() {
|
||||||
|
$messages.fadeOut();
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏错误信息
|
||||||
|
*/
|
||||||
|
hideError: function() {
|
||||||
|
$('.validation-messages').hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 价格计算器
|
||||||
|
*/
|
||||||
|
var PriceCalculator = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算原始总价
|
||||||
|
*/
|
||||||
|
calculateOriginalTotal: function(items) {
|
||||||
|
// 这个方法在实际使用中应该通过AJAX从服务器获取
|
||||||
|
// 这里只是示例实现
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用折扣
|
||||||
|
*/
|
||||||
|
applyDiscount: function(total, discountType, discountValue) {
|
||||||
|
if (discountType === 'percentage') {
|
||||||
|
return total * (1 - discountValue / 100);
|
||||||
|
} else if (discountType === 'fixed') {
|
||||||
|
return Math.max(0, total - discountValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 库存检查器
|
||||||
|
*/
|
||||||
|
var StockChecker = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查商品库存
|
||||||
|
*/
|
||||||
|
checkStock: function(productId, quantity, callback) {
|
||||||
|
$.ajax({
|
||||||
|
url: yoone_bundle_params.ajax_url,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'yoone_check_bundle_stock',
|
||||||
|
nonce: yoone_bundle_params.nonce,
|
||||||
|
product_id: productId,
|
||||||
|
quantity: quantity
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (callback) {
|
||||||
|
callback(response.success, response.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档就绪时初始化
|
||||||
|
*/
|
||||||
|
$(document).ready(function() {
|
||||||
|
if ($('.yoone-bundle-options').length > 0) {
|
||||||
|
YooneBundleHandler.init();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 暴露到全局作用域
|
||||||
|
window.YooneBundleHandler = YooneBundleHandler;
|
||||||
|
window.YoonePriceCalculator = PriceCalculator;
|
||||||
|
window.YooneStockChecker = StockChecker;
|
||||||
|
|
||||||
|
})(jQuery);
|
||||||
|
|
@ -0,0 +1,733 @@
|
||||||
|
/**
|
||||||
|
* Yoone Subscriptions 后台管理脚本
|
||||||
|
*
|
||||||
|
* 处理混装产品和订阅功能的后台管理交互
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// 全局变量
|
||||||
|
var YooneAdmin = {
|
||||||
|
// 初始化
|
||||||
|
init: function() {
|
||||||
|
this.initBundleManagement();
|
||||||
|
this.initSubscriptionManagement();
|
||||||
|
this.initProductDataPanels();
|
||||||
|
this.initPaymentSettings();
|
||||||
|
this.bindEvents();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 初始化混装产品管理
|
||||||
|
initBundleManagement: function() {
|
||||||
|
this.initBundleItemSearch();
|
||||||
|
this.initBundleItemActions();
|
||||||
|
this.initBundleValidation();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 初始化订阅管理
|
||||||
|
initSubscriptionManagement: function() {
|
||||||
|
this.initSubscriptionActions();
|
||||||
|
this.initSubscriptionFilters();
|
||||||
|
this.initSubscriptionBulkActions();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 初始化产品数据面板
|
||||||
|
initProductDataPanels: function() {
|
||||||
|
this.initBundleProductPanel();
|
||||||
|
this.initSubscriptionProductPanel();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 初始化支付设置
|
||||||
|
initPaymentSettings: function() {
|
||||||
|
this.initMonerisSettings();
|
||||||
|
this.initTestModeToggle();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
bindEvents: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// 混装产品搜索
|
||||||
|
$(document).on('input', '.bundle-item-search input', function() {
|
||||||
|
self.searchProducts($(this));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加混装产品项目
|
||||||
|
$(document).on('click', '.bundle-search-result', function() {
|
||||||
|
self.addBundleItem($(this));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移除混装产品项目
|
||||||
|
$(document).on('click', '.bundle-item-remove', function() {
|
||||||
|
self.removeBundleItem($(this));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新混装产品数量
|
||||||
|
$(document).on('change', '.bundle-item-quantity input', function() {
|
||||||
|
self.updateBundleItemQuantity($(this));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 订阅周期管理
|
||||||
|
$(document).on('click', '.subscription-add-period', function() {
|
||||||
|
self.addSubscriptionPeriod();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.subscription-period-remove', function() {
|
||||||
|
self.removeSubscriptionPeriod($(this));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 订阅操作
|
||||||
|
$(document).on('click', '.subscription-action', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
self.handleSubscriptionAction($(this));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 批量操作
|
||||||
|
$(document).on('change', '#bulk-action-selector-top, #bulk-action-selector-bottom', function() {
|
||||||
|
self.handleBulkActionChange($(this));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 支付设置
|
||||||
|
$(document).on('change', '#yoone_moneris_test_mode', function() {
|
||||||
|
self.toggleTestMode($(this));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 产品类型切换
|
||||||
|
$(document).on('change', '#product-type', function() {
|
||||||
|
self.handleProductTypeChange($(this));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表单验证
|
||||||
|
$(document).on('submit', '.yoone-admin-form', function(e) {
|
||||||
|
return self.validateForm($(this));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 混装产品项目搜索
|
||||||
|
initBundleItemSearch: function() {
|
||||||
|
var searchTimeout;
|
||||||
|
var $searchContainer = $('.bundle-item-search');
|
||||||
|
|
||||||
|
if ($searchContainer.length) {
|
||||||
|
$searchContainer.append('<div class="bundle-search-results"></div>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 搜索产品
|
||||||
|
searchProducts: function($input) {
|
||||||
|
var self = this;
|
||||||
|
var query = $input.val().trim();
|
||||||
|
var $results = $input.closest('.bundle-item-search').find('.bundle-search-results');
|
||||||
|
|
||||||
|
// 清除之前的搜索超时
|
||||||
|
clearTimeout(this.searchTimeout);
|
||||||
|
|
||||||
|
if (query.length < 2) {
|
||||||
|
$results.hide().empty();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置新的搜索超时
|
||||||
|
this.searchTimeout = setTimeout(function() {
|
||||||
|
self.performProductSearch(query, $results);
|
||||||
|
}, 300);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 执行产品搜索
|
||||||
|
performProductSearch: function(query, $results) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
$results.html('<div class="yoone-loading">搜索中...</div>').show();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: yoone_admin_ajax.ajax_url,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'yoone_search_products',
|
||||||
|
query: query,
|
||||||
|
nonce: yoone_admin_ajax.nonce
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success && response.data.length > 0) {
|
||||||
|
self.displaySearchResults(response.data, $results);
|
||||||
|
} else {
|
||||||
|
$results.html('<div class="no-results">未找到相关产品</div>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$results.html('<div class="search-error">搜索失败,请重试</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 显示搜索结果
|
||||||
|
displaySearchResults: function(products, $results) {
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
$.each(products, function(index, product) {
|
||||||
|
html += '<div class="bundle-search-result" data-product-id="' + product.id + '">';
|
||||||
|
html += '<img src="' + product.image + '" alt="' + product.name + '">';
|
||||||
|
html += '<div class="bundle-search-result-details">';
|
||||||
|
html += '<div class="bundle-search-result-name">' + product.name + '</div>';
|
||||||
|
html += '<div class="bundle-search-result-price">' + product.price + '</div>';
|
||||||
|
html += '</div>';
|
||||||
|
html += '</div>';
|
||||||
|
});
|
||||||
|
|
||||||
|
$results.html(html);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 添加混装产品项目
|
||||||
|
addBundleItem: function($element) {
|
||||||
|
var productId = $element.data('product-id');
|
||||||
|
var productName = $element.find('.bundle-search-result-name').text();
|
||||||
|
var productPrice = $element.find('.bundle-search-result-price').text();
|
||||||
|
var productImage = $element.find('img').attr('src');
|
||||||
|
|
||||||
|
// 检查是否已存在
|
||||||
|
if ($('.bundle-item-row[data-product-id="' + productId + '"]').length > 0) {
|
||||||
|
this.showNotice('该产品已添加到混装中', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = '<div class="bundle-item-row" data-product-id="' + productId + '">';
|
||||||
|
html += '<div class="bundle-item-image"><img src="' + productImage + '" alt="' + productName + '"></div>';
|
||||||
|
html += '<div class="bundle-item-details">';
|
||||||
|
html += '<div class="bundle-item-name">' + productName + '</div>';
|
||||||
|
html += '<div class="bundle-item-price">' + productPrice + '</div>';
|
||||||
|
html += '</div>';
|
||||||
|
html += '<div class="bundle-item-quantity">';
|
||||||
|
html += '<input type="number" name="bundle_items[' + productId + '][quantity]" value="1" min="1" max="999">';
|
||||||
|
html += '</div>';
|
||||||
|
html += '<div class="bundle-item-actions">';
|
||||||
|
html += '<button type="button" class="bundle-item-remove">移除</button>';
|
||||||
|
html += '</div>';
|
||||||
|
html += '<input type="hidden" name="bundle_items[' + productId + '][product_id]" value="' + productId + '">';
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
$('.bundle-items-list').append(html);
|
||||||
|
|
||||||
|
// 清空搜索
|
||||||
|
$('.bundle-item-search input').val('');
|
||||||
|
$('.bundle-search-results').hide().empty();
|
||||||
|
|
||||||
|
this.showNotice('产品已添加到混装中', 'success');
|
||||||
|
},
|
||||||
|
|
||||||
|
// 移除混装产品项目
|
||||||
|
removeBundleItem: function($button) {
|
||||||
|
var $row = $button.closest('.bundle-item-row');
|
||||||
|
var productName = $row.find('.bundle-item-name').text();
|
||||||
|
|
||||||
|
if (confirm('确定要移除 "' + productName + '" 吗?')) {
|
||||||
|
$row.fadeOut(300, function() {
|
||||||
|
$(this).remove();
|
||||||
|
});
|
||||||
|
this.showNotice('产品已从混装中移除', 'success');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新混装产品数量
|
||||||
|
updateBundleItemQuantity: function($input) {
|
||||||
|
var quantity = parseInt($input.val());
|
||||||
|
var min = parseInt($input.attr('min')) || 1;
|
||||||
|
var max = parseInt($input.attr('max')) || 999;
|
||||||
|
|
||||||
|
if (quantity < min) {
|
||||||
|
$input.val(min);
|
||||||
|
this.showNotice('数量不能小于 ' + min, 'warning');
|
||||||
|
} else if (quantity > max) {
|
||||||
|
$input.val(max);
|
||||||
|
this.showNotice('数量不能大于 ' + max, 'warning');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 初始化混装产品操作
|
||||||
|
initBundleItemActions: function() {
|
||||||
|
// 拖拽排序
|
||||||
|
if ($.fn.sortable) {
|
||||||
|
$('.bundle-items-list').sortable({
|
||||||
|
handle: '.bundle-item-image',
|
||||||
|
placeholder: 'bundle-item-placeholder',
|
||||||
|
update: function(event, ui) {
|
||||||
|
// 更新排序
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 初始化混装验证
|
||||||
|
initBundleValidation: function() {
|
||||||
|
// 实时验证混装设置
|
||||||
|
},
|
||||||
|
|
||||||
|
// 添加订阅周期
|
||||||
|
addSubscriptionPeriod: function() {
|
||||||
|
var html = '<div class="subscription-period-row">';
|
||||||
|
html += '<input type="number" name="subscription_periods[][interval]" value="1" min="1" max="365" placeholder="间隔">';
|
||||||
|
html += '<select name="subscription_periods[][period]">';
|
||||||
|
html += '<option value="day">天</option>';
|
||||||
|
html += '<option value="week">周</option>';
|
||||||
|
html += '<option value="month" selected>月</option>';
|
||||||
|
html += '<option value="year">年</option>';
|
||||||
|
html += '</select>';
|
||||||
|
html += '<button type="button" class="subscription-period-remove">移除</button>';
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
$('.subscription-periods-list').append(html);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 移除订阅周期
|
||||||
|
removeSubscriptionPeriod: function($button) {
|
||||||
|
var $row = $button.closest('.subscription-period-row');
|
||||||
|
|
||||||
|
if ($('.subscription-period-row').length > 1) {
|
||||||
|
$row.fadeOut(300, function() {
|
||||||
|
$(this).remove();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.showNotice('至少需要保留一个订阅周期', 'warning');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 初始化订阅操作
|
||||||
|
initSubscriptionActions: function() {
|
||||||
|
// 订阅状态切换
|
||||||
|
$('.subscription-status-toggle').on('change', function() {
|
||||||
|
var $this = $(this);
|
||||||
|
var subscriptionId = $this.data('subscription-id');
|
||||||
|
var newStatus = $this.is(':checked') ? 'active' : 'paused';
|
||||||
|
|
||||||
|
// 发送AJAX请求更新状态
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 处理订阅操作
|
||||||
|
handleSubscriptionAction: function($button) {
|
||||||
|
var action = $button.data('action');
|
||||||
|
var subscriptionId = $button.data('subscription-id');
|
||||||
|
var confirmMessage = $button.data('confirm');
|
||||||
|
|
||||||
|
if (confirmMessage && !confirm(confirmMessage)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.performSubscriptionAction(action, subscriptionId, $button);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 执行订阅操作
|
||||||
|
performSubscriptionAction: function(action, subscriptionId, $button) {
|
||||||
|
var self = this;
|
||||||
|
var originalText = $button.text();
|
||||||
|
|
||||||
|
$button.text('处理中...').prop('disabled', true);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: yoone_admin_ajax.ajax_url,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'yoone_subscription_action',
|
||||||
|
subscription_action: action,
|
||||||
|
subscription_id: subscriptionId,
|
||||||
|
nonce: yoone_admin_ajax.nonce
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
self.showNotice(response.data.message, 'success');
|
||||||
|
// 刷新页面或更新状态
|
||||||
|
if (response.data.reload) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
self.updateSubscriptionStatus(subscriptionId, response.data.status);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.showNotice(response.data.message || '操作失败', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
self.showNotice('网络错误,请重试', 'error');
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
$button.text(originalText).prop('disabled', false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新订阅状态
|
||||||
|
updateSubscriptionStatus: function(subscriptionId, newStatus) {
|
||||||
|
var $row = $('tr[data-subscription-id="' + subscriptionId + '"]');
|
||||||
|
var $statusBadge = $row.find('.subscription-status-badge');
|
||||||
|
|
||||||
|
// 更新状态徽章
|
||||||
|
$statusBadge.removeClass().addClass('subscription-status-badge status-' + newStatus);
|
||||||
|
$statusBadge.text(this.getStatusText(newStatus));
|
||||||
|
|
||||||
|
// 更新操作按钮
|
||||||
|
this.updateActionButtons($row, newStatus);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
getStatusText: function(status) {
|
||||||
|
var statusTexts = {
|
||||||
|
'active': '活跃',
|
||||||
|
'paused': '暂停',
|
||||||
|
'cancelled': '已取消',
|
||||||
|
'expired': '已过期'
|
||||||
|
};
|
||||||
|
return statusTexts[status] || status;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新操作按钮
|
||||||
|
updateActionButtons: function($row, status) {
|
||||||
|
var $actions = $row.find('.subscription-actions');
|
||||||
|
var subscriptionId = $row.data('subscription-id');
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 'active':
|
||||||
|
html += '<a href="#" class="button subscription-action" data-action="pause" data-subscription-id="' + subscriptionId + '">暂停</a>';
|
||||||
|
html += '<a href="#" class="button subscription-action" data-action="cancel" data-subscription-id="' + subscriptionId + '" data-confirm="确定要取消这个订阅吗?">取消</a>';
|
||||||
|
break;
|
||||||
|
case 'paused':
|
||||||
|
html += '<a href="#" class="button subscription-action" data-action="resume" data-subscription-id="' + subscriptionId + '">恢复</a>';
|
||||||
|
html += '<a href="#" class="button subscription-action" data-action="cancel" data-subscription-id="' + subscriptionId + '" data-confirm="确定要取消这个订阅吗?">取消</a>';
|
||||||
|
break;
|
||||||
|
case 'cancelled':
|
||||||
|
case 'expired':
|
||||||
|
html += '<a href="#" class="button subscription-action" data-action="reactivate" data-subscription-id="' + subscriptionId + '">重新激活</a>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '<a href="' + yoone_admin_ajax.subscription_edit_url.replace('%d', subscriptionId) + '" class="button">编辑</a>';
|
||||||
|
|
||||||
|
$actions.html(html);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 初始化订阅过滤器
|
||||||
|
initSubscriptionFilters: function() {
|
||||||
|
// 状态过滤器
|
||||||
|
$('.subscription-status-filter').on('change', function() {
|
||||||
|
var status = $(this).val();
|
||||||
|
var url = new URL(window.location);
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
url.searchParams.set('status', status);
|
||||||
|
} else {
|
||||||
|
url.searchParams.delete('status');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location = url.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 日期范围过滤器
|
||||||
|
if ($.fn.datepicker) {
|
||||||
|
$('.date-filter').datepicker({
|
||||||
|
dateFormat: 'yy-mm-dd',
|
||||||
|
changeMonth: true,
|
||||||
|
changeYear: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 初始化批量操作
|
||||||
|
initSubscriptionBulkActions: function() {
|
||||||
|
// 全选/取消全选
|
||||||
|
$('#cb-select-all-1, #cb-select-all-2').on('change', function() {
|
||||||
|
var checked = $(this).is(':checked');
|
||||||
|
$('.subscription-checkbox').prop('checked', checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 单个复选框
|
||||||
|
$(document).on('change', '.subscription-checkbox', function() {
|
||||||
|
var totalCheckboxes = $('.subscription-checkbox').length;
|
||||||
|
var checkedCheckboxes = $('.subscription-checkbox:checked').length;
|
||||||
|
|
||||||
|
$('#cb-select-all-1, #cb-select-all-2').prop('checked', totalCheckboxes === checkedCheckboxes);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 处理批量操作变化
|
||||||
|
handleBulkActionChange: function($select) {
|
||||||
|
var action = $select.val();
|
||||||
|
var $form = $select.closest('form');
|
||||||
|
|
||||||
|
if (action === 'delete') {
|
||||||
|
$form.attr('onsubmit', 'return confirm("确定要删除选中的订阅吗?此操作不可撤销。");');
|
||||||
|
} else {
|
||||||
|
$form.removeAttr('onsubmit');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 初始化混装产品面板
|
||||||
|
initBundleProductPanel: function() {
|
||||||
|
var $panel = $('#yoone_bundle_product_data');
|
||||||
|
|
||||||
|
if ($panel.length) {
|
||||||
|
// 折扣类型切换
|
||||||
|
$panel.find('select[name="_bundle_discount_type"]').on('change', function() {
|
||||||
|
var type = $(this).val();
|
||||||
|
var $valueField = $panel.find('input[name="_bundle_discount_value"]');
|
||||||
|
|
||||||
|
if (type === 'percentage') {
|
||||||
|
$valueField.attr('max', '100').attr('placeholder', '例如:10 (表示10%)');
|
||||||
|
} else {
|
||||||
|
$valueField.removeAttr('max').attr('placeholder', '例如:50.00');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数量限制切换
|
||||||
|
$panel.find('input[name="_bundle_enable_quantity_limits"]').on('change', function() {
|
||||||
|
var $limits = $panel.find('.bundle-quantity-limits');
|
||||||
|
|
||||||
|
if ($(this).is(':checked')) {
|
||||||
|
$limits.show();
|
||||||
|
} else {
|
||||||
|
$limits.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 初始化订阅产品面板
|
||||||
|
initSubscriptionProductPanel: function() {
|
||||||
|
var $panel = $('#yoone_subscription_product_data');
|
||||||
|
|
||||||
|
if ($panel.length) {
|
||||||
|
// 订阅启用切换
|
||||||
|
$panel.find('input[name="_subscription_enabled"]').on('change', function() {
|
||||||
|
var $options = $panel.find('.subscription-options');
|
||||||
|
|
||||||
|
if ($(this).is(':checked')) {
|
||||||
|
$options.show();
|
||||||
|
} else {
|
||||||
|
$options.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 折扣类型切换
|
||||||
|
$panel.find('select[name="_subscription_discount_type"]').on('change', function() {
|
||||||
|
var type = $(this).val();
|
||||||
|
var $valueField = $panel.find('input[name="_subscription_discount_value"]');
|
||||||
|
|
||||||
|
if (type === 'percentage') {
|
||||||
|
$valueField.attr('max', '100').attr('placeholder', '例如:15 (表示15%)');
|
||||||
|
} else {
|
||||||
|
$valueField.removeAttr('max').attr('placeholder', '例如:25.00');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 试用期切换
|
||||||
|
$panel.find('input[name="_subscription_trial_enabled"]').on('change', function() {
|
||||||
|
var $trialOptions = $panel.find('.subscription-trial-options');
|
||||||
|
|
||||||
|
if ($(this).is(':checked')) {
|
||||||
|
$trialOptions.show();
|
||||||
|
} else {
|
||||||
|
$trialOptions.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 处理产品类型变化
|
||||||
|
handleProductTypeChange: function($select) {
|
||||||
|
var productType = $select.val();
|
||||||
|
|
||||||
|
// 显示/隐藏相关面板
|
||||||
|
$('.yoone-product-data-panel').hide();
|
||||||
|
|
||||||
|
if (productType === 'yoone_bundle') {
|
||||||
|
$('#yoone_bundle_product_data').show();
|
||||||
|
} else if (productType === 'yoone_subscription') {
|
||||||
|
$('#yoone_subscription_product_data').show();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 初始化Moneris设置
|
||||||
|
initMonerisSettings: function() {
|
||||||
|
var $form = $('.yoone-payment-settings');
|
||||||
|
|
||||||
|
if ($form.length) {
|
||||||
|
// 测试连接
|
||||||
|
$form.find('.test-connection').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var $button = $(this);
|
||||||
|
|
||||||
|
$button.text('测试中...').prop('disabled', true);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: yoone_admin_ajax.ajax_url,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'yoone_test_moneris_connection',
|
||||||
|
store_id: $form.find('input[name="yoone_moneris_store_id"]').val(),
|
||||||
|
api_token: $form.find('input[name="yoone_moneris_api_token"]').val(),
|
||||||
|
test_mode: $form.find('input[name="yoone_moneris_test_mode"]').is(':checked'),
|
||||||
|
nonce: yoone_admin_ajax.nonce
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
alert('连接测试成功!');
|
||||||
|
} else {
|
||||||
|
alert('连接测试失败:' + (response.data.message || '未知错误'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
alert('网络错误,请检查网络连接后重试');
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
$button.text('测试连接').prop('disabled', false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 切换测试模式
|
||||||
|
toggleTestMode: function($checkbox) {
|
||||||
|
var $form = $checkbox.closest('form');
|
||||||
|
var $testNotice = $form.find('.payment-test-mode');
|
||||||
|
|
||||||
|
if ($checkbox.is(':checked')) {
|
||||||
|
if ($testNotice.length === 0) {
|
||||||
|
var html = '<div class="payment-test-mode">';
|
||||||
|
html += '<h4>测试模式已启用</h4>';
|
||||||
|
html += '<p>当前处于测试模式,所有交易都是模拟的,不会产生实际费用。</p>';
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
$form.prepend(html);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$testNotice.remove();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 表单验证
|
||||||
|
validateForm: function($form) {
|
||||||
|
var isValid = true;
|
||||||
|
var errors = [];
|
||||||
|
|
||||||
|
// 混装产品验证
|
||||||
|
if ($form.hasClass('bundle-form')) {
|
||||||
|
var bundleItems = $form.find('.bundle-item-row').length;
|
||||||
|
|
||||||
|
if (bundleItems < 2) {
|
||||||
|
errors.push('混装产品至少需要包含2个产品');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var discountValue = parseFloat($form.find('input[name="_bundle_discount_value"]').val());
|
||||||
|
var discountType = $form.find('select[name="_bundle_discount_type"]').val();
|
||||||
|
|
||||||
|
if (discountType === 'percentage' && (discountValue < 0 || discountValue > 100)) {
|
||||||
|
errors.push('折扣百分比必须在0-100之间');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订阅产品验证
|
||||||
|
if ($form.hasClass('subscription-form')) {
|
||||||
|
var subscriptionEnabled = $form.find('input[name="_subscription_enabled"]').is(':checked');
|
||||||
|
|
||||||
|
if (subscriptionEnabled) {
|
||||||
|
var periods = $form.find('.subscription-period-row').length;
|
||||||
|
|
||||||
|
if (periods === 0) {
|
||||||
|
errors.push('启用订阅功能时至少需要设置一个订阅周期');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支付设置验证
|
||||||
|
if ($form.hasClass('payment-settings-form')) {
|
||||||
|
var storeId = $form.find('input[name="yoone_moneris_store_id"]').val().trim();
|
||||||
|
var apiToken = $form.find('input[name="yoone_moneris_api_token"]').val().trim();
|
||||||
|
|
||||||
|
if (!storeId) {
|
||||||
|
errors.push('请输入Moneris商店ID');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!apiToken) {
|
||||||
|
errors.push('请输入Moneris API令牌');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误信息
|
||||||
|
if (!isValid) {
|
||||||
|
var errorMessage = '请修正以下错误:\n\n' + errors.join('\n');
|
||||||
|
alert(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 显示通知
|
||||||
|
showNotice: function(message, type) {
|
||||||
|
type = type || 'info';
|
||||||
|
|
||||||
|
var $notice = $('<div class="yoone-notice notice-' + type + '">' + message + '</div>');
|
||||||
|
|
||||||
|
// 移除现有通知
|
||||||
|
$('.yoone-notice').remove();
|
||||||
|
|
||||||
|
// 添加新通知
|
||||||
|
$('.wrap').prepend($notice);
|
||||||
|
|
||||||
|
// 自动隐藏
|
||||||
|
setTimeout(function() {
|
||||||
|
$notice.fadeOut(300, function() {
|
||||||
|
$(this).remove();
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 工具函数:格式化价格
|
||||||
|
formatPrice: function(price) {
|
||||||
|
return parseFloat(price).toFixed(2);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 工具函数:格式化日期
|
||||||
|
formatDate: function(date) {
|
||||||
|
if (typeof date === 'string') {
|
||||||
|
date = new Date(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 工具函数:防抖
|
||||||
|
debounce: function(func, wait) {
|
||||||
|
var timeout;
|
||||||
|
return function executedFunction() {
|
||||||
|
var context = this;
|
||||||
|
var args = arguments;
|
||||||
|
|
||||||
|
var later = function() {
|
||||||
|
timeout = null;
|
||||||
|
func.apply(context, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 文档就绪时初始化
|
||||||
|
$(document).ready(function() {
|
||||||
|
YooneAdmin.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 导出到全局作用域
|
||||||
|
window.YooneAdmin = YooneAdmin;
|
||||||
|
|
||||||
|
})(jQuery);
|
||||||
|
|
@ -0,0 +1,379 @@
|
||||||
|
/**
|
||||||
|
* Yoone Subscriptions 前端 JavaScript
|
||||||
|
*
|
||||||
|
* 处理混装产品和订阅功能的前端交互
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var YooneSubscriptions = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化
|
||||||
|
*/
|
||||||
|
init: function() {
|
||||||
|
this.initBundleOptions();
|
||||||
|
this.initSubscriptionOptions();
|
||||||
|
this.initCartUpdates();
|
||||||
|
this.bindEvents();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化混装选项
|
||||||
|
*/
|
||||||
|
initBundleOptions: function() {
|
||||||
|
var $bundleOptions = $('.yoone-bundle-options');
|
||||||
|
|
||||||
|
if ($bundleOptions.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定数量变化事件
|
||||||
|
$bundleOptions.on('change', '.bundle-quantity-input', this.updateBundleItemSubtotal);
|
||||||
|
|
||||||
|
// 绑定计算价格按钮
|
||||||
|
$bundleOptions.on('click', '#calculate-bundle-price', this.calculateBundlePrice);
|
||||||
|
|
||||||
|
// 绑定添加到购物车按钮
|
||||||
|
$bundleOptions.on('click', '#add-bundle-to-cart', this.addBundleToCart);
|
||||||
|
|
||||||
|
// 初始计算
|
||||||
|
this.calculateBundlePrice();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化订阅选项
|
||||||
|
*/
|
||||||
|
initSubscriptionOptions: function() {
|
||||||
|
var $subscriptionOptions = $('.yoone-subscription-options');
|
||||||
|
|
||||||
|
if ($subscriptionOptions.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定购买类型切换
|
||||||
|
$subscriptionOptions.on('change', 'input[name="purchase_type"]', this.toggleSubscriptionOptions);
|
||||||
|
|
||||||
|
// 绑定订阅周期切换
|
||||||
|
$subscriptionOptions.on('change', '#subscription_period', this.updateSubscriptionDetails);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化购物车更新
|
||||||
|
*/
|
||||||
|
initCartUpdates: function() {
|
||||||
|
// 监听购物车更新事件
|
||||||
|
$(document.body).on('updated_wc_div', this.onCartUpdated);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定事件
|
||||||
|
*/
|
||||||
|
bindEvents: function() {
|
||||||
|
// 绑定订阅管理操作
|
||||||
|
$('.yoone-my-subscriptions').on('click', '.subscription-pause, .subscription-resume, .subscription-cancel', this.handleSubscriptionAction);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新混装项目小计
|
||||||
|
*/
|
||||||
|
updateBundleItemSubtotal: function() {
|
||||||
|
var $input = $(this);
|
||||||
|
var $item = $input.closest('.bundle-item');
|
||||||
|
var $subtotal = $item.find('.subtotal-amount');
|
||||||
|
var basePrice = parseFloat($subtotal.data('base-price'));
|
||||||
|
var quantity = parseInt($input.val()) || 0;
|
||||||
|
var subtotal = basePrice * quantity;
|
||||||
|
|
||||||
|
$subtotal.html(YooneSubscriptions.formatPrice(subtotal));
|
||||||
|
|
||||||
|
// 启用计算按钮
|
||||||
|
$('#calculate-bundle-price').prop('disabled', false);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算混装价格
|
||||||
|
*/
|
||||||
|
calculateBundlePrice: function() {
|
||||||
|
var $bundleOptions = $('.yoone-bundle-options');
|
||||||
|
var productId = $bundleOptions.data('product-id');
|
||||||
|
var quantities = {};
|
||||||
|
|
||||||
|
// 收集数量数据
|
||||||
|
$bundleOptions.find('.bundle-quantity-input').each(function() {
|
||||||
|
var $input = $(this);
|
||||||
|
var itemProductId = $input.closest('.bundle-item').data('product-id');
|
||||||
|
var quantity = parseInt($input.val()) || 0;
|
||||||
|
quantities[itemProductId] = quantity;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 发送AJAX请求计算价格
|
||||||
|
$.ajax({
|
||||||
|
url: yoone_ajax.ajax_url,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'yoone_calculate_bundle_price',
|
||||||
|
nonce: yoone_ajax.nonce,
|
||||||
|
product_id: productId,
|
||||||
|
quantities: quantities
|
||||||
|
},
|
||||||
|
beforeSend: function() {
|
||||||
|
$('#calculate-bundle-price').prop('disabled', true).text(yoone_ajax.calculating_text);
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
var data = response.data;
|
||||||
|
|
||||||
|
// 更新价格显示
|
||||||
|
$('#bundle-original-total').html(YooneSubscriptions.formatPrice(data.original_total));
|
||||||
|
$('#bundle-discount-amount').html('-' + YooneSubscriptions.formatPrice(data.discount_amount));
|
||||||
|
$('#bundle-final-total').html(YooneSubscriptions.formatPrice(data.final_total));
|
||||||
|
|
||||||
|
// 启用添加到购物车按钮
|
||||||
|
$('#add-bundle-to-cart').prop('disabled', false);
|
||||||
|
|
||||||
|
// 验证数量限制
|
||||||
|
if (data.quantity_valid) {
|
||||||
|
$('.bundle-quantity-error').remove();
|
||||||
|
} else {
|
||||||
|
YooneSubscriptions.showQuantityError(data.quantity_message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
YooneSubscriptions.showError(response.data.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
YooneSubscriptions.showError(yoone_ajax.error_message);
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
$('#calculate-bundle-price').prop('disabled', false).text(yoone_ajax.calculate_text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加混装到购物车
|
||||||
|
*/
|
||||||
|
addBundleToCart: function() {
|
||||||
|
var $bundleOptions = $('.yoone-bundle-options');
|
||||||
|
var productId = $bundleOptions.data('product-id');
|
||||||
|
var quantities = {};
|
||||||
|
|
||||||
|
// 收集数量数据
|
||||||
|
$bundleOptions.find('.bundle-quantity-input').each(function() {
|
||||||
|
var $input = $(this);
|
||||||
|
var itemProductId = $input.closest('.bundle-item').data('product-id');
|
||||||
|
var quantity = parseInt($input.val()) || 0;
|
||||||
|
quantities[itemProductId] = quantity;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 发送AJAX请求添加到购物车
|
||||||
|
$.ajax({
|
||||||
|
url: yoone_ajax.ajax_url,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'yoone_add_bundle_to_cart',
|
||||||
|
nonce: yoone_ajax.nonce,
|
||||||
|
product_id: productId,
|
||||||
|
quantities: quantities
|
||||||
|
},
|
||||||
|
beforeSend: function() {
|
||||||
|
$('#add-bundle-to-cart').prop('disabled', true).text(yoone_ajax.adding_text);
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
// 显示成功消息
|
||||||
|
YooneSubscriptions.showSuccess(response.data.message);
|
||||||
|
|
||||||
|
// 更新购物车计数
|
||||||
|
if (response.data.cart_count) {
|
||||||
|
$('.cart-contents-count').text(response.data.cart_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发购物车更新事件
|
||||||
|
$(document.body).trigger('added_to_cart', [response.data.fragments, response.data.cart_hash]);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
YooneSubscriptions.showError(response.data.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
YooneSubscriptions.showError(yoone_ajax.error_message);
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
$('#add-bundle-to-cart').prop('disabled', false).text(yoone_ajax.add_to_cart_text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换订阅选项显示
|
||||||
|
*/
|
||||||
|
toggleSubscriptionOptions: function() {
|
||||||
|
var $input = $(this);
|
||||||
|
var purchaseType = $input.val();
|
||||||
|
var $subscriptionOptions = $input.closest('.yoone-subscription-options');
|
||||||
|
|
||||||
|
if (purchaseType === 'subscription') {
|
||||||
|
$subscriptionOptions.find('.subscription-periods').show();
|
||||||
|
$subscriptionOptions.find('.subscription-info').show();
|
||||||
|
$subscriptionOptions.find('.subscription-trial').show();
|
||||||
|
YooneSubscriptions.updateSubscriptionDetails();
|
||||||
|
} else {
|
||||||
|
$subscriptionOptions.find('.subscription-periods').hide();
|
||||||
|
$subscriptionOptions.find('.subscription-info').hide();
|
||||||
|
$subscriptionOptions.find('.subscription-trial').hide();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新订阅详情
|
||||||
|
*/
|
||||||
|
updateSubscriptionDetails: function() {
|
||||||
|
var $select = $('#subscription_period');
|
||||||
|
var $selectedOption = $select.find(':selected');
|
||||||
|
var period = $selectedOption.data('period');
|
||||||
|
var interval = $selectedOption.data('interval');
|
||||||
|
var price = $selectedOption.data('price');
|
||||||
|
|
||||||
|
// 更新价格显示
|
||||||
|
$('#subscription-price').html(YooneSubscriptions.formatPrice(price));
|
||||||
|
|
||||||
|
// 更新计费周期
|
||||||
|
var billingCycle = YooneSubscriptions.formatBillingCycle(period, interval);
|
||||||
|
$('#billing-cycle').text(billingCycle);
|
||||||
|
|
||||||
|
// 计算下次配送日期
|
||||||
|
var nextDate = YooneSubscriptions.calculateNextDeliveryDate(period, interval);
|
||||||
|
$('#next-delivery-date').text(nextDate);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理订阅操作
|
||||||
|
*/
|
||||||
|
handleSubscriptionAction: function(e) {
|
||||||
|
var $button = $(this);
|
||||||
|
var action = '';
|
||||||
|
|
||||||
|
if ($button.hasClass('subscription-pause')) {
|
||||||
|
action = 'pause';
|
||||||
|
} else if ($button.hasClass('subscription-resume')) {
|
||||||
|
action = 'resume';
|
||||||
|
} else if ($button.hasClass('subscription-cancel')) {
|
||||||
|
action = 'cancel';
|
||||||
|
if (!confirm(yoone_ajax.confirm_cancel)) {
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这里可以添加AJAX处理逻辑
|
||||||
|
// 目前使用链接跳转的方式
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 购物车更新回调
|
||||||
|
*/
|
||||||
|
onCartUpdated: function() {
|
||||||
|
// 购物车更新后的处理逻辑
|
||||||
|
console.log('Cart updated');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化价格
|
||||||
|
*/
|
||||||
|
formatPrice: function(price) {
|
||||||
|
return yoone_ajax.currency_symbol + parseFloat(price).toFixed(2);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化计费周期
|
||||||
|
*/
|
||||||
|
formatBillingCycle: function(period, interval) {
|
||||||
|
var periods = {
|
||||||
|
'day': yoone_ajax.period_day,
|
||||||
|
'week': yoone_ajax.period_week,
|
||||||
|
'month': yoone_ajax.period_month,
|
||||||
|
'year': yoone_ajax.period_year
|
||||||
|
};
|
||||||
|
|
||||||
|
var periodName = periods[period] || period;
|
||||||
|
|
||||||
|
if (interval > 1) {
|
||||||
|
return '/ ' + yoone_ajax.every_text + ' ' + interval + ' ' + periodName;
|
||||||
|
} else {
|
||||||
|
return '/ ' + yoone_ajax.every_text + periodName;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算下次配送日期
|
||||||
|
*/
|
||||||
|
calculateNextDeliveryDate: function(period, interval) {
|
||||||
|
var now = new Date();
|
||||||
|
var nextDate = new Date(now);
|
||||||
|
|
||||||
|
switch (period) {
|
||||||
|
case 'day':
|
||||||
|
nextDate.setDate(now.getDate() + interval);
|
||||||
|
break;
|
||||||
|
case 'week':
|
||||||
|
nextDate.setDate(now.getDate() + (interval * 7));
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
nextDate.setMonth(now.getMonth() + interval);
|
||||||
|
break;
|
||||||
|
case 'year':
|
||||||
|
nextDate.setFullYear(now.getFullYear() + interval);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextDate.toLocaleDateString();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示成功消息
|
||||||
|
*/
|
||||||
|
showSuccess: function(message) {
|
||||||
|
this.showNotice(message, 'success');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示错误消息
|
||||||
|
*/
|
||||||
|
showError: function(message) {
|
||||||
|
this.showNotice(message, 'error');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示数量错误
|
||||||
|
*/
|
||||||
|
showQuantityError: function(message) {
|
||||||
|
$('.bundle-quantity-error').remove();
|
||||||
|
$('.yoone-bundle-summary').before('<div class="bundle-quantity-error notice notice-error"><p>' + message + '</p></div>');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示通知
|
||||||
|
*/
|
||||||
|
showNotice: function(message, type) {
|
||||||
|
var $notice = $('<div class="yoone-notice yoone-notice-' + type + '">' + message + '</div>');
|
||||||
|
|
||||||
|
$('body').prepend($notice);
|
||||||
|
|
||||||
|
$notice.fadeIn().delay(3000).fadeOut(function() {
|
||||||
|
$(this).remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 文档就绪时初始化
|
||||||
|
$(document).ready(function() {
|
||||||
|
YooneSubscriptions.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 导出到全局
|
||||||
|
window.YooneSubscriptions = YooneSubscriptions;
|
||||||
|
|
||||||
|
})(jQuery);
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 抽象数据类
|
||||||
|
*
|
||||||
|
* 为所有数据对象提供基础功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抽象数据类
|
||||||
|
*/
|
||||||
|
abstract class Abstract_Yoone_Data {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据存储
|
||||||
|
*/
|
||||||
|
protected $data = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 元数据存储
|
||||||
|
*/
|
||||||
|
protected $meta_data = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象ID
|
||||||
|
*/
|
||||||
|
protected $id = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象类型
|
||||||
|
*/
|
||||||
|
protected $object_type = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据变更标记
|
||||||
|
*/
|
||||||
|
protected $changes = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
public function __construct($id = 0) {
|
||||||
|
if ($id > 0) {
|
||||||
|
$this->set_id($id);
|
||||||
|
$this->read();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取ID
|
||||||
|
*/
|
||||||
|
public function get_id() {
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置ID
|
||||||
|
*/
|
||||||
|
public function set_id($id) {
|
||||||
|
$this->id = absint($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取对象类型
|
||||||
|
*/
|
||||||
|
public function get_object_type() {
|
||||||
|
return $this->object_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据
|
||||||
|
*/
|
||||||
|
public function get_data() {
|
||||||
|
return array_merge($this->data, array('id' => $this->get_id()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取变更
|
||||||
|
*/
|
||||||
|
public function get_changes() {
|
||||||
|
return $this->changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置属性
|
||||||
|
*/
|
||||||
|
protected function set_prop($prop, $value) {
|
||||||
|
if (array_key_exists($prop, $this->data)) {
|
||||||
|
if ($this->data[$prop] !== $value) {
|
||||||
|
$this->changes[$prop] = $value;
|
||||||
|
$this->data[$prop] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取属性
|
||||||
|
*/
|
||||||
|
protected function get_prop($prop, $context = 'view') {
|
||||||
|
$value = null;
|
||||||
|
|
||||||
|
if (array_key_exists($prop, $this->changes)) {
|
||||||
|
$value = $this->changes[$prop];
|
||||||
|
} elseif (array_key_exists($prop, $this->data)) {
|
||||||
|
$value = $this->data[$prop];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('view' === $context) {
|
||||||
|
$value = apply_filters($this->get_hook_prefix() . $prop, $value, $this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取钩子前缀
|
||||||
|
*/
|
||||||
|
protected function get_hook_prefix() {
|
||||||
|
return 'yoone_' . $this->object_type . '_get_';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取元数据
|
||||||
|
*/
|
||||||
|
public function get_meta($key = '', $single = true, $context = 'view') {
|
||||||
|
if (!$this->get_id()) {
|
||||||
|
return $single ? '' : array();
|
||||||
|
}
|
||||||
|
|
||||||
|
return get_metadata($this->object_type, $this->get_id(), $key, $single);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置元数据
|
||||||
|
*/
|
||||||
|
public function set_meta($key, $value) {
|
||||||
|
if (!$this->get_id()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return update_metadata($this->object_type, $this->get_id(), $key, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除元数据
|
||||||
|
*/
|
||||||
|
public function delete_meta($key) {
|
||||||
|
if (!$this->get_id()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return delete_metadata($this->object_type, $this->get_id(), $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用变更
|
||||||
|
*/
|
||||||
|
protected function apply_changes() {
|
||||||
|
$this->data = array_replace_recursive($this->data, $this->changes);
|
||||||
|
$this->changes = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存数据
|
||||||
|
*/
|
||||||
|
public function save() {
|
||||||
|
if (!$this->get_id()) {
|
||||||
|
$this->create();
|
||||||
|
} else {
|
||||||
|
$this->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->apply_changes();
|
||||||
|
|
||||||
|
return $this->get_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除数据
|
||||||
|
*/
|
||||||
|
public function delete($force_delete = false) {
|
||||||
|
if (!$this->get_id()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
do_action('yoone_before_delete_' . $this->object_type, $this->get_id(), $this);
|
||||||
|
|
||||||
|
$result = $this->delete_from_database($force_delete);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
$this->set_id(0);
|
||||||
|
do_action('yoone_delete_' . $this->object_type, $this->get_id(), $this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抽象方法 - 从数据库读取
|
||||||
|
*/
|
||||||
|
abstract protected function read();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抽象方法 - 创建记录
|
||||||
|
*/
|
||||||
|
abstract protected function create();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抽象方法 - 更新记录
|
||||||
|
*/
|
||||||
|
abstract protected function update();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抽象方法 - 从数据库删除
|
||||||
|
*/
|
||||||
|
abstract protected function delete_from_database($force_delete = false);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,491 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 日志管理界面类
|
||||||
|
*
|
||||||
|
* 处理后台日志查看和管理功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志管理界面类
|
||||||
|
*/
|
||||||
|
class Yoone_Admin_Logs {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
add_action('admin_menu', array($this, 'add_menu_page'));
|
||||||
|
add_action('wp_ajax_yoone_clear_logs', array($this, 'ajax_clear_logs'));
|
||||||
|
add_action('wp_ajax_yoone_download_logs', array($this, 'ajax_download_logs'));
|
||||||
|
add_action('admin_enqueue_scripts', array($this, 'enqueue_scripts'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加菜单页面
|
||||||
|
*/
|
||||||
|
public function add_menu_page() {
|
||||||
|
add_submenu_page(
|
||||||
|
'yoone-subscriptions',
|
||||||
|
__('日志管理', 'yoone-subscriptions'),
|
||||||
|
__('日志', 'yoone-subscriptions'),
|
||||||
|
'manage_options',
|
||||||
|
'yoone-logs',
|
||||||
|
array($this, 'display_logs_page')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示日志页面
|
||||||
|
*/
|
||||||
|
public function display_logs_page() {
|
||||||
|
$current_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'recent';
|
||||||
|
$log_level = isset($_GET['level']) ? sanitize_text_field($_GET['level']) : 'all';
|
||||||
|
$search = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '';
|
||||||
|
|
||||||
|
?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php _e('日志管理', 'yoone-subscriptions'); ?></h1>
|
||||||
|
|
||||||
|
<nav class="nav-tab-wrapper">
|
||||||
|
<a href="<?php echo esc_url(add_query_arg('tab', 'recent')); ?>"
|
||||||
|
class="nav-tab <?php echo $current_tab === 'recent' ? 'nav-tab-active' : ''; ?>">
|
||||||
|
<?php _e('最近日志', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url(add_query_arg('tab', 'subscription')); ?>"
|
||||||
|
class="nav-tab <?php echo $current_tab === 'subscription' ? 'nav-tab-active' : ''; ?>">
|
||||||
|
<?php _e('订阅日志', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url(add_query_arg('tab', 'payment')); ?>"
|
||||||
|
class="nav-tab <?php echo $current_tab === 'payment' ? 'nav-tab-active' : ''; ?>">
|
||||||
|
<?php _e('支付日志', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url(add_query_arg('tab', 'error')); ?>"
|
||||||
|
class="nav-tab <?php echo $current_tab === 'error' ? 'nav-tab-active' : ''; ?>">
|
||||||
|
<?php _e('错误日志', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo esc_url(add_query_arg('tab', 'settings')); ?>"
|
||||||
|
class="nav-tab <?php echo $current_tab === 'settings' ? 'nav-tab-active' : ''; ?>">
|
||||||
|
<?php _e('设置', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="log-filters">
|
||||||
|
<form method="get">
|
||||||
|
<input type="hidden" name="page" value="yoone-logs" />
|
||||||
|
<input type="hidden" name="tab" value="<?php echo esc_attr($current_tab); ?>" />
|
||||||
|
|
||||||
|
<select name="level">
|
||||||
|
<option value="all" <?php selected($log_level, 'all'); ?>><?php _e('所有级别', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="error" <?php selected($log_level, 'error'); ?>><?php _e('错误', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="warning" <?php selected($log_level, 'warning'); ?>><?php _e('警告', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="info" <?php selected($log_level, 'info'); ?>><?php _e('信息', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="debug" <?php selected($log_level, 'debug'); ?>><?php _e('调试', 'yoone-subscriptions'); ?></option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input type="text" name="search" value="<?php echo esc_attr($search); ?>"
|
||||||
|
placeholder="<?php _e('搜索日志...', 'yoone-subscriptions'); ?>" />
|
||||||
|
|
||||||
|
<input type="submit" class="button" value="<?php _e('筛选', 'yoone-subscriptions'); ?>" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="log-actions">
|
||||||
|
<button type="button" class="button" id="refresh-logs">
|
||||||
|
<?php _e('刷新', 'yoone-subscriptions'); ?>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="button" id="download-logs">
|
||||||
|
<?php _e('下载日志', 'yoone-subscriptions'); ?>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="button button-secondary" id="clear-logs">
|
||||||
|
<?php _e('清空日志', 'yoone-subscriptions'); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="log-content">
|
||||||
|
<?php
|
||||||
|
switch ($current_tab) {
|
||||||
|
case 'recent':
|
||||||
|
$this->display_recent_logs($log_level, $search);
|
||||||
|
break;
|
||||||
|
case 'subscription':
|
||||||
|
$this->display_subscription_logs($log_level, $search);
|
||||||
|
break;
|
||||||
|
case 'payment':
|
||||||
|
$this->display_payment_logs($log_level, $search);
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
$this->display_error_logs($search);
|
||||||
|
break;
|
||||||
|
case 'settings':
|
||||||
|
$this->display_log_settings();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.log-filters {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-filters form {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-content {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-table th,
|
||||||
|
.log-table td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-table th {
|
||||||
|
background: #f5f5f5;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-level {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-level.error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-level.warning {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-level.info {
|
||||||
|
background: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-level.debug {
|
||||||
|
background: #e2e3e5;
|
||||||
|
color: #383d41;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-message {
|
||||||
|
max-width: 400px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-context {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #666;
|
||||||
|
max-width: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-logs {
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示最近日志
|
||||||
|
*/
|
||||||
|
private function display_recent_logs($level = 'all', $search = '') {
|
||||||
|
$logs = $this->get_filtered_logs($level, $search, 100);
|
||||||
|
$this->render_log_table($logs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示订阅日志
|
||||||
|
*/
|
||||||
|
private function display_subscription_logs($level = 'all', $search = '') {
|
||||||
|
$logs = $this->get_filtered_logs($level, $search, 100, 'subscription');
|
||||||
|
$this->render_log_table($logs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示支付日志
|
||||||
|
*/
|
||||||
|
private function display_payment_logs($level = 'all', $search = '') {
|
||||||
|
$logs = $this->get_filtered_logs($level, $search, 100, 'payment');
|
||||||
|
$this->render_log_table($logs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示错误日志
|
||||||
|
*/
|
||||||
|
private function display_error_logs($search = '') {
|
||||||
|
$logs = $this->get_filtered_logs('error', $search, 100);
|
||||||
|
$this->render_log_table($logs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示日志设置
|
||||||
|
*/
|
||||||
|
private function display_log_settings() {
|
||||||
|
$log_level = get_option('yoone_log_level', 'info');
|
||||||
|
$log_retention = get_option('yoone_log_retention_days', 30);
|
||||||
|
$enable_debug = get_option('yoone_enable_debug_logging', false);
|
||||||
|
|
||||||
|
if (isset($_POST['save_log_settings'])) {
|
||||||
|
check_admin_referer('yoone_log_settings');
|
||||||
|
|
||||||
|
update_option('yoone_log_level', sanitize_text_field($_POST['log_level']));
|
||||||
|
update_option('yoone_log_retention_days', absint($_POST['log_retention']));
|
||||||
|
update_option('yoone_enable_debug_logging', isset($_POST['enable_debug']));
|
||||||
|
|
||||||
|
echo '<div class="notice notice-success"><p>' . __('设置已保存', 'yoone-subscriptions') . '</p></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<form method="post" class="log-settings-form">
|
||||||
|
<?php wp_nonce_field('yoone_log_settings'); ?>
|
||||||
|
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php _e('日志级别', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td>
|
||||||
|
<select name="log_level">
|
||||||
|
<option value="error" <?php selected($log_level, 'error'); ?>><?php _e('仅错误', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="warning" <?php selected($log_level, 'warning'); ?>><?php _e('警告及以上', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="info" <?php selected($log_level, 'info'); ?>><?php _e('信息及以上', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="debug" <?php selected($log_level, 'debug'); ?>><?php _e('所有日志', 'yoone-subscriptions'); ?></option>
|
||||||
|
</select>
|
||||||
|
<p class="description"><?php _e('选择要记录的最低日志级别', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php _e('日志保留天数', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="log_retention" value="<?php echo esc_attr($log_retention); ?>" min="1" max="365" />
|
||||||
|
<p class="description"><?php _e('超过此天数的日志将被自动删除', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php _e('调试模式', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="enable_debug" <?php checked($enable_debug); ?> />
|
||||||
|
<?php _e('启用调试日志记录', 'yoone-subscriptions'); ?>
|
||||||
|
</label>
|
||||||
|
<p class="description"><?php _e('启用后将记录详细的调试信息,可能会产生大量日志', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p class="submit">
|
||||||
|
<input type="submit" name="save_log_settings" class="button-primary" value="<?php _e('保存设置', 'yoone-subscriptions'); ?>" />
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染日志表格
|
||||||
|
*/
|
||||||
|
private function render_log_table($logs) {
|
||||||
|
if (empty($logs)) {
|
||||||
|
echo '<div class="no-logs">' . __('没有找到日志记录', 'yoone-subscriptions') . '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<table class="log-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('时间', 'yoone-subscriptions'); ?></th>
|
||||||
|
<th><?php _e('级别', 'yoone-subscriptions'); ?></th>
|
||||||
|
<th><?php _e('消息', 'yoone-subscriptions'); ?></th>
|
||||||
|
<th><?php _e('上下文', 'yoone-subscriptions'); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($logs as $log): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo esc_html($log['timestamp']); ?></td>
|
||||||
|
<td><span class="log-level <?php echo esc_attr($log['level']); ?>"><?php echo esc_html($log['level']); ?></span></td>
|
||||||
|
<td class="log-message"><?php echo esc_html($log['message']); ?></td>
|
||||||
|
<td class="log-context"><?php echo esc_html($log['context']); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取过滤后的日志
|
||||||
|
*/
|
||||||
|
private function get_filtered_logs($level = 'all', $search = '', $limit = 100, $type = '') {
|
||||||
|
$logs = Yoone_Logger::get_recent_logs($limit * 2); // 获取更多以便过滤
|
||||||
|
$filtered_logs = array();
|
||||||
|
|
||||||
|
foreach ($logs as $log_line) {
|
||||||
|
$parsed_log = $this->parse_log_line($log_line);
|
||||||
|
|
||||||
|
if (!$parsed_log) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 级别过滤
|
||||||
|
if ($level !== 'all' && $parsed_log['level'] !== $level) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型过滤
|
||||||
|
if ($type && strpos($parsed_log['message'], $type) === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索过滤
|
||||||
|
if ($search && stripos($parsed_log['message'], $search) === false && stripos($parsed_log['context'], $search) === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filtered_logs[] = $parsed_log;
|
||||||
|
|
||||||
|
if (count($filtered_logs) >= $limit) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $filtered_logs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析日志行
|
||||||
|
*/
|
||||||
|
private function parse_log_line($log_line) {
|
||||||
|
// 简单的日志解析,实际应该根据WooCommerce日志格式调整
|
||||||
|
if (preg_match('/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2})\s+(\w+)\s+(.+)$/', $log_line, $matches)) {
|
||||||
|
$context = '';
|
||||||
|
$message = $matches[3];
|
||||||
|
|
||||||
|
// 提取JSON上下文
|
||||||
|
if (preg_match('/^(.+?)\s+(\{.+\})$/', $message, $msg_matches)) {
|
||||||
|
$message = $msg_matches[1];
|
||||||
|
$context = $msg_matches[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'timestamp' => $matches[1],
|
||||||
|
'level' => strtolower($matches[2]),
|
||||||
|
'message' => $message,
|
||||||
|
'context' => $context
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX清空日志
|
||||||
|
*/
|
||||||
|
public function ajax_clear_logs() {
|
||||||
|
check_ajax_referer('yoone_admin_nonce', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_die(__('权限不足', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$log_file = Yoone_Logger::get_log_file_path();
|
||||||
|
|
||||||
|
if (file_exists($log_file)) {
|
||||||
|
file_put_contents($log_file, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success(array('message' => __('日志已清空', 'yoone-subscriptions')));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX下载日志
|
||||||
|
*/
|
||||||
|
public function ajax_download_logs() {
|
||||||
|
check_ajax_referer('yoone_admin_nonce', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_die(__('权限不足', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$log_file = Yoone_Logger::get_log_file_path();
|
||||||
|
|
||||||
|
if (!file_exists($log_file)) {
|
||||||
|
wp_die(__('日志文件不存在', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
header('Content-Disposition: attachment; filename="yoone-subscriptions-' . date('Y-m-d') . '.log"');
|
||||||
|
header('Content-Length: ' . filesize($log_file));
|
||||||
|
|
||||||
|
readfile($log_file);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载脚本
|
||||||
|
*/
|
||||||
|
public function enqueue_scripts($hook) {
|
||||||
|
if ($hook !== 'yoone-subscriptions_page_yoone-logs') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_enqueue_script(
|
||||||
|
'yoone-admin-logs',
|
||||||
|
YOONE_SUBSCRIPTIONS_PLUGIN_URL . 'assets/js/admin-logs.js',
|
||||||
|
array('jquery'),
|
||||||
|
YOONE_SUBSCRIPTIONS_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_localize_script('yoone-admin-logs', 'yoone_logs_params', array(
|
||||||
|
'ajax_url' => admin_url('admin-ajax.php'),
|
||||||
|
'nonce' => wp_create_nonce('yoone_admin_nonce'),
|
||||||
|
'i18n' => array(
|
||||||
|
'confirm_clear' => __('确定要清空所有日志吗?此操作不可恢复。', 'yoone-subscriptions'),
|
||||||
|
'clearing' => __('正在清空...', 'yoone-subscriptions'),
|
||||||
|
'downloading' => __('正在下载...', 'yoone-subscriptions')
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
new Yoone_Admin_Logs();
|
||||||
|
|
@ -0,0 +1,754 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 后台管理类
|
||||||
|
*
|
||||||
|
* 处理WordPress后台管理功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台管理类
|
||||||
|
*/
|
||||||
|
class Yoone_Admin {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
$this->init_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化钩子
|
||||||
|
*/
|
||||||
|
private function init_hooks() {
|
||||||
|
// 管理菜单
|
||||||
|
add_action('admin_menu', array($this, 'add_admin_menu'));
|
||||||
|
|
||||||
|
// 产品数据面板
|
||||||
|
add_filter('woocommerce_product_data_tabs', array($this, 'add_product_data_tabs'));
|
||||||
|
add_action('woocommerce_product_data_panels', array($this, 'add_product_data_panels'));
|
||||||
|
add_action('woocommerce_process_product_meta', array($this, 'save_product_data'));
|
||||||
|
|
||||||
|
// 产品类型
|
||||||
|
add_filter('product_type_selector', array($this, 'add_product_types'));
|
||||||
|
|
||||||
|
// 订单管理
|
||||||
|
add_action('add_meta_boxes', array($this, 'add_order_meta_boxes'));
|
||||||
|
add_action('woocommerce_admin_order_data_after_billing_address', array($this, 'display_order_subscription_info'));
|
||||||
|
|
||||||
|
// 脚本和样式
|
||||||
|
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));
|
||||||
|
|
||||||
|
// AJAX 处理
|
||||||
|
add_action('wp_ajax_yoone_search_products', array($this, 'ajax_search_products'));
|
||||||
|
add_action('wp_ajax_yoone_get_bundle_items', array($this, 'ajax_get_bundle_items'));
|
||||||
|
add_action('wp_ajax_yoone_save_bundle_items', array($this, 'ajax_save_bundle_items'));
|
||||||
|
|
||||||
|
// 列表页面自定义列
|
||||||
|
add_filter('manage_product_posts_columns', array($this, 'add_product_columns'));
|
||||||
|
add_action('manage_product_posts_custom_column', array($this, 'display_product_columns'), 10, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载管理脚本
|
||||||
|
*/
|
||||||
|
public function enqueue_admin_scripts($hook) {
|
||||||
|
global $post_type;
|
||||||
|
|
||||||
|
if ($post_type === 'product' || strpos($hook, 'yoone-subscriptions') !== false) {
|
||||||
|
wp_enqueue_script(
|
||||||
|
'yoone-admin',
|
||||||
|
YOONE_SUBSCRIPTIONS_PLUGIN_URL . 'assets/js/admin.js',
|
||||||
|
array('jquery', 'jquery-ui-sortable'),
|
||||||
|
YOONE_SUBSCRIPTIONS_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_enqueue_style(
|
||||||
|
'yoone-admin',
|
||||||
|
YOONE_SUBSCRIPTIONS_PLUGIN_URL . 'assets/css/admin.css',
|
||||||
|
array(),
|
||||||
|
YOONE_SUBSCRIPTIONS_VERSION
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_localize_script('yoone-admin', 'yoone_admin_params', array(
|
||||||
|
'ajax_url' => admin_url('admin-ajax.php'),
|
||||||
|
'nonce' => wp_create_nonce('yoone_admin_nonce'),
|
||||||
|
'i18n' => array(
|
||||||
|
'loading' => __('加载中...', 'yoone-subscriptions'),
|
||||||
|
'error' => __('发生错误', 'yoone-subscriptions'),
|
||||||
|
'confirm_delete' => __('确定要删除吗?', 'yoone-subscriptions'),
|
||||||
|
'select_product' => __('选择产品', 'yoone-subscriptions'),
|
||||||
|
'add_item' => __('添加项目', 'yoone-subscriptions'),
|
||||||
|
'remove_item' => __('移除项目', 'yoone-subscriptions'),
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 管理菜单
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加管理菜单
|
||||||
|
*/
|
||||||
|
public function add_admin_menu() {
|
||||||
|
add_menu_page(
|
||||||
|
__('Yoone 订阅', 'yoone-subscriptions'),
|
||||||
|
__('Yoone 订阅', 'yoone-subscriptions'),
|
||||||
|
'manage_woocommerce',
|
||||||
|
'yoone-subscriptions',
|
||||||
|
array($this, 'subscriptions_page'),
|
||||||
|
'dashicons-update',
|
||||||
|
56
|
||||||
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'yoone-subscriptions',
|
||||||
|
__('所有订阅', 'yoone-subscriptions'),
|
||||||
|
__('所有订阅', 'yoone-subscriptions'),
|
||||||
|
'manage_woocommerce',
|
||||||
|
'yoone-subscriptions',
|
||||||
|
array($this, 'subscriptions_page')
|
||||||
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'yoone-subscriptions',
|
||||||
|
__('套装管理', 'yoone-subscriptions'),
|
||||||
|
__('套装管理', 'yoone-subscriptions'),
|
||||||
|
'manage_woocommerce',
|
||||||
|
'yoone-bundles',
|
||||||
|
array($this, 'bundles_page')
|
||||||
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'yoone-subscriptions',
|
||||||
|
__('支付设置', 'yoone-subscriptions'),
|
||||||
|
__('支付设置', 'yoone-subscriptions'),
|
||||||
|
'manage_woocommerce',
|
||||||
|
'yoone-payment-settings',
|
||||||
|
array($this, 'payment_settings_page')
|
||||||
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'yoone-subscriptions',
|
||||||
|
__('报告', 'yoone-subscriptions'),
|
||||||
|
__('报告', 'yoone-subscriptions'),
|
||||||
|
'manage_woocommerce',
|
||||||
|
'yoone-reports',
|
||||||
|
array($this, 'reports_page')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅管理页面
|
||||||
|
*/
|
||||||
|
public function subscriptions_page() {
|
||||||
|
$list_table = new Yoone_Subscriptions_List_Table();
|
||||||
|
$list_table->prepare_items();
|
||||||
|
|
||||||
|
echo '<div class="wrap">';
|
||||||
|
echo '<h1>' . __('订阅管理', 'yoone-subscriptions') . '</h1>';
|
||||||
|
|
||||||
|
$list_table->display();
|
||||||
|
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 套装管理页面
|
||||||
|
*/
|
||||||
|
public function bundles_page() {
|
||||||
|
$action = isset($_GET['action']) ? $_GET['action'] : 'list';
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'edit':
|
||||||
|
case 'add':
|
||||||
|
$this->bundle_edit_page();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$this->bundle_list_page();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 套装列表页面
|
||||||
|
*/
|
||||||
|
private function bundle_list_page() {
|
||||||
|
$list_table = new Yoone_Bundles_List_Table();
|
||||||
|
$list_table->prepare_items();
|
||||||
|
|
||||||
|
echo '<div class="wrap">';
|
||||||
|
echo '<h1>' . __('套装管理', 'yoone-subscriptions');
|
||||||
|
echo '<a href="' . admin_url('admin.php?page=yoone-bundles&action=add') . '" class="page-title-action">' . __('添加套装', 'yoone-subscriptions') . '</a>';
|
||||||
|
echo '</h1>';
|
||||||
|
|
||||||
|
$list_table->display();
|
||||||
|
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 套装编辑页面
|
||||||
|
*/
|
||||||
|
private function bundle_edit_page() {
|
||||||
|
$bundle_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
|
||||||
|
$bundle = new Yoone_Bundle($bundle_id);
|
||||||
|
|
||||||
|
if (isset($_POST['save_bundle'])) {
|
||||||
|
$this->save_bundle_data($bundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
include YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/admin/views/bundle-edit.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付设置页面
|
||||||
|
*/
|
||||||
|
public function payment_settings_page() {
|
||||||
|
if (isset($_POST['save_settings'])) {
|
||||||
|
$this->save_payment_settings();
|
||||||
|
}
|
||||||
|
|
||||||
|
include YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/admin/views/payment-settings.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 报告页面
|
||||||
|
*/
|
||||||
|
public function reports_page() {
|
||||||
|
include YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/admin/views/reports.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 产品数据面板
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加产品数据标签
|
||||||
|
*/
|
||||||
|
public function add_product_data_tabs($tabs) {
|
||||||
|
$tabs['yoone_bundle'] = array(
|
||||||
|
'label' => __('套装设置', 'yoone-subscriptions'),
|
||||||
|
'target' => 'yoone_bundle_product_data',
|
||||||
|
'class' => array('show_if_yoone_bundle'),
|
||||||
|
'priority' => 25,
|
||||||
|
);
|
||||||
|
|
||||||
|
$tabs['yoone_subscription'] = array(
|
||||||
|
'label' => __('订阅设置', 'yoone-subscriptions'),
|
||||||
|
'target' => 'yoone_subscription_product_data',
|
||||||
|
'class' => array(),
|
||||||
|
'priority' => 26,
|
||||||
|
);
|
||||||
|
|
||||||
|
return $tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加产品数据面板
|
||||||
|
*/
|
||||||
|
public function add_product_data_panels() {
|
||||||
|
global $post;
|
||||||
|
|
||||||
|
// 套装设置面板
|
||||||
|
echo '<div id="yoone_bundle_product_data" class="panel woocommerce_options_panel">';
|
||||||
|
|
||||||
|
woocommerce_wp_select(array(
|
||||||
|
'id' => '_yoone_bundle_discount_type',
|
||||||
|
'label' => __('折扣类型', 'yoone-subscriptions'),
|
||||||
|
'options' => array(
|
||||||
|
'' => __('无折扣', 'yoone-subscriptions'),
|
||||||
|
'percentage' => __('百分比折扣', 'yoone-subscriptions'),
|
||||||
|
'fixed' => __('固定金额折扣', 'yoone-subscriptions'),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
woocommerce_wp_text_input(array(
|
||||||
|
'id' => '_yoone_bundle_discount_value',
|
||||||
|
'label' => __('折扣值', 'yoone-subscriptions'),
|
||||||
|
'description' => __('输入折扣值(百分比或固定金额)', 'yoone-subscriptions'),
|
||||||
|
'type' => 'number',
|
||||||
|
'custom_attributes' => array(
|
||||||
|
'step' => '0.01',
|
||||||
|
'min' => '0',
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
woocommerce_wp_text_input(array(
|
||||||
|
'id' => '_yoone_bundle_min_quantity',
|
||||||
|
'label' => __('最小数量', 'yoone-subscriptions'),
|
||||||
|
'description' => __('套装中产品的最小总数量', 'yoone-subscriptions'),
|
||||||
|
'type' => 'number',
|
||||||
|
'custom_attributes' => array(
|
||||||
|
'min' => '0',
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
woocommerce_wp_text_input(array(
|
||||||
|
'id' => '_yoone_bundle_max_quantity',
|
||||||
|
'label' => __('最大数量', 'yoone-subscriptions'),
|
||||||
|
'description' => __('套装中产品的最大总数量(0表示无限制)', 'yoone-subscriptions'),
|
||||||
|
'type' => 'number',
|
||||||
|
'custom_attributes' => array(
|
||||||
|
'min' => '0',
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
echo '<div class="options_group">';
|
||||||
|
echo '<h4>' . __('套装产品', 'yoone-subscriptions') . '</h4>';
|
||||||
|
echo '<div id="yoone-bundle-items">';
|
||||||
|
echo '<div id="yoone-bundle-items-list"></div>';
|
||||||
|
echo '<button type="button" class="button" id="add-bundle-item">' . __('添加产品', 'yoone-subscriptions') . '</button>';
|
||||||
|
echo '</div>';
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
// 订阅设置面板
|
||||||
|
echo '<div id="yoone_subscription_product_data" class="panel woocommerce_options_panel">';
|
||||||
|
|
||||||
|
woocommerce_wp_checkbox(array(
|
||||||
|
'id' => '_yoone_subscription_enabled',
|
||||||
|
'label' => __('启用订阅', 'yoone-subscriptions'),
|
||||||
|
'description' => __('将此产品设为订阅产品', 'yoone-subscriptions'),
|
||||||
|
));
|
||||||
|
|
||||||
|
woocommerce_wp_select(array(
|
||||||
|
'id' => '_yoone_subscription_period',
|
||||||
|
'label' => __('订阅周期', 'yoone-subscriptions'),
|
||||||
|
'options' => array(
|
||||||
|
'day' => __('天', 'yoone-subscriptions'),
|
||||||
|
'week' => __('周', 'yoone-subscriptions'),
|
||||||
|
'month' => __('月', 'yoone-subscriptions'),
|
||||||
|
'year' => __('年', 'yoone-subscriptions'),
|
||||||
|
),
|
||||||
|
'value' => get_post_meta($post->ID, '_yoone_subscription_period', true) ?: 'month',
|
||||||
|
));
|
||||||
|
|
||||||
|
woocommerce_wp_text_input(array(
|
||||||
|
'id' => '_yoone_subscription_interval',
|
||||||
|
'label' => __('订阅间隔', 'yoone-subscriptions'),
|
||||||
|
'description' => __('每隔多少个周期收费一次', 'yoone-subscriptions'),
|
||||||
|
'type' => 'number',
|
||||||
|
'value' => get_post_meta($post->ID, '_yoone_subscription_interval', true) ?: '1',
|
||||||
|
'custom_attributes' => array(
|
||||||
|
'min' => '1',
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
woocommerce_wp_text_input(array(
|
||||||
|
'id' => '_yoone_subscription_length',
|
||||||
|
'label' => __('订阅长度', 'yoone-subscriptions'),
|
||||||
|
'description' => __('订阅持续多少个周期(0表示永久)', 'yoone-subscriptions'),
|
||||||
|
'type' => 'number',
|
||||||
|
'value' => get_post_meta($post->ID, '_yoone_subscription_length', true) ?: '0',
|
||||||
|
'custom_attributes' => array(
|
||||||
|
'min' => '0',
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
woocommerce_wp_text_input(array(
|
||||||
|
'id' => '_yoone_subscription_trial_length',
|
||||||
|
'label' => __('试用期长度', 'yoone-subscriptions'),
|
||||||
|
'description' => __('免费试用期天数(0表示无试用期)', 'yoone-subscriptions'),
|
||||||
|
'type' => 'number',
|
||||||
|
'value' => get_post_meta($post->ID, '_yoone_subscription_trial_length', true) ?: '0',
|
||||||
|
'custom_attributes' => array(
|
||||||
|
'min' => '0',
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
woocommerce_wp_text_input(array(
|
||||||
|
'id' => '_yoone_subscription_signup_fee',
|
||||||
|
'label' => __('注册费', 'yoone-subscriptions'),
|
||||||
|
'description' => __('一次性注册费用', 'yoone-subscriptions'),
|
||||||
|
'type' => 'number',
|
||||||
|
'value' => get_post_meta($post->ID, '_yoone_subscription_signup_fee', true) ?: '0',
|
||||||
|
'custom_attributes' => array(
|
||||||
|
'step' => '0.01',
|
||||||
|
'min' => '0',
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存产品数据
|
||||||
|
*/
|
||||||
|
public function save_product_data($post_id) {
|
||||||
|
// 套装设置
|
||||||
|
$bundle_fields = array(
|
||||||
|
'_yoone_bundle_discount_type',
|
||||||
|
'_yoone_bundle_discount_value',
|
||||||
|
'_yoone_bundle_min_quantity',
|
||||||
|
'_yoone_bundle_max_quantity',
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($bundle_fields as $field) {
|
||||||
|
if (isset($_POST[$field])) {
|
||||||
|
update_post_meta($post_id, $field, sanitize_text_field($_POST[$field]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订阅设置
|
||||||
|
$subscription_fields = array(
|
||||||
|
'_yoone_subscription_enabled',
|
||||||
|
'_yoone_subscription_period',
|
||||||
|
'_yoone_subscription_interval',
|
||||||
|
'_yoone_subscription_length',
|
||||||
|
'_yoone_subscription_trial_length',
|
||||||
|
'_yoone_subscription_signup_fee',
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($subscription_fields as $field) {
|
||||||
|
if (isset($_POST[$field])) {
|
||||||
|
update_post_meta($post_id, $field, sanitize_text_field($_POST[$field]));
|
||||||
|
} else {
|
||||||
|
delete_post_meta($post_id, $field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是套装产品,创建或更新套装记录
|
||||||
|
if (isset($_POST['product-type']) && $_POST['product-type'] === 'yoone_bundle') {
|
||||||
|
$this->save_bundle_product_data($post_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加产品类型
|
||||||
|
*/
|
||||||
|
public function add_product_types($types) {
|
||||||
|
$types['yoone_bundle'] = __('套装产品', 'yoone-subscriptions');
|
||||||
|
return $types;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 订单管理
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加订单元框
|
||||||
|
*/
|
||||||
|
public function add_order_meta_boxes() {
|
||||||
|
add_meta_box(
|
||||||
|
'yoone-order-subscriptions',
|
||||||
|
__('相关订阅', 'yoone-subscriptions'),
|
||||||
|
array($this, 'order_subscriptions_meta_box'),
|
||||||
|
'shop_order',
|
||||||
|
'normal',
|
||||||
|
'default'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单订阅元框
|
||||||
|
*/
|
||||||
|
public function order_subscriptions_meta_box($post) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$subscriptions = $wpdb->get_results($wpdb->prepare("
|
||||||
|
SELECT * FROM {$wpdb->prefix}yoone_subscriptions
|
||||||
|
WHERE parent_order_id = %d
|
||||||
|
", $post->ID));
|
||||||
|
|
||||||
|
if (empty($subscriptions)) {
|
||||||
|
echo '<p>' . __('此订单没有相关订阅', 'yoone-subscriptions') . '</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<table class="widefat">';
|
||||||
|
echo '<thead>';
|
||||||
|
echo '<tr>';
|
||||||
|
echo '<th>' . __('订阅ID', 'yoone-subscriptions') . '</th>';
|
||||||
|
echo '<th>' . __('状态', 'yoone-subscriptions') . '</th>';
|
||||||
|
echo '<th>' . __('金额', 'yoone-subscriptions') . '</th>';
|
||||||
|
echo '<th>' . __('下次付款', 'yoone-subscriptions') . '</th>';
|
||||||
|
echo '<th>' . __('操作', 'yoone-subscriptions') . '</th>';
|
||||||
|
echo '</tr>';
|
||||||
|
echo '</thead>';
|
||||||
|
echo '<tbody>';
|
||||||
|
|
||||||
|
foreach ($subscriptions as $subscription) {
|
||||||
|
echo '<tr>';
|
||||||
|
echo '<td>#' . $subscription->id . '</td>';
|
||||||
|
echo '<td>' . ucfirst($subscription->status) . '</td>';
|
||||||
|
echo '<td>' . wc_price($subscription->total) . '</td>';
|
||||||
|
echo '<td>' . ($subscription->next_payment_date ? date('Y-m-d', strtotime($subscription->next_payment_date)) : '-') . '</td>';
|
||||||
|
echo '<td>';
|
||||||
|
echo '<a href="' . admin_url('admin.php?page=yoone-subscriptions&action=edit&id=' . $subscription->id) . '" class="button button-small">' . __('查看', 'yoone-subscriptions') . '</a>';
|
||||||
|
echo '</td>';
|
||||||
|
echo '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</tbody>';
|
||||||
|
echo '</table>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示订单订阅信息
|
||||||
|
*/
|
||||||
|
public function display_order_subscription_info($order) {
|
||||||
|
$has_subscription = false;
|
||||||
|
|
||||||
|
foreach ($order->get_items() as $item) {
|
||||||
|
$product = $item->get_product();
|
||||||
|
if ($product && $product->get_meta('_yoone_subscription_enabled') === 'yes') {
|
||||||
|
$has_subscription = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($has_subscription) {
|
||||||
|
echo '<div class="yoone-order-subscription-notice">';
|
||||||
|
echo '<p><strong>' . __('此订单包含订阅产品', 'yoone-subscriptions') . '</strong></p>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 产品列表自定义列
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加产品列
|
||||||
|
*/
|
||||||
|
public function add_product_columns($columns) {
|
||||||
|
$new_columns = array();
|
||||||
|
|
||||||
|
foreach ($columns as $key => $column) {
|
||||||
|
$new_columns[$key] = $column;
|
||||||
|
|
||||||
|
if ($key === 'product_type') {
|
||||||
|
$new_columns['yoone_subscription'] = __('订阅', 'yoone-subscriptions');
|
||||||
|
$new_columns['yoone_bundle'] = __('套装', 'yoone-subscriptions');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $new_columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示产品列内容
|
||||||
|
*/
|
||||||
|
public function display_product_columns($column, $post_id) {
|
||||||
|
switch ($column) {
|
||||||
|
case 'yoone_subscription':
|
||||||
|
$enabled = get_post_meta($post_id, '_yoone_subscription_enabled', true);
|
||||||
|
if ($enabled === 'yes') {
|
||||||
|
echo '<span class="dashicons dashicons-yes-alt" style="color: green;"></span>';
|
||||||
|
} else {
|
||||||
|
echo '<span class="dashicons dashicons-minus" style="color: #ccc;"></span>';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'yoone_bundle':
|
||||||
|
$product = wc_get_product($post_id);
|
||||||
|
if ($product && $product->get_type() === 'yoone_bundle') {
|
||||||
|
echo '<span class="dashicons dashicons-products" style="color: blue;"></span>';
|
||||||
|
} else {
|
||||||
|
echo '<span class="dashicons dashicons-minus" style="color: #ccc;"></span>';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| AJAX 处理
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX 搜索产品
|
||||||
|
*/
|
||||||
|
public function ajax_search_products() {
|
||||||
|
check_ajax_referer('yoone_admin_nonce', 'nonce');
|
||||||
|
|
||||||
|
$term = sanitize_text_field($_POST['term']);
|
||||||
|
|
||||||
|
$products = wc_get_products(array(
|
||||||
|
'limit' => 20,
|
||||||
|
'status' => 'publish',
|
||||||
|
's' => $term,
|
||||||
|
));
|
||||||
|
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
foreach ($products as $product) {
|
||||||
|
$results[] = array(
|
||||||
|
'id' => $product->get_id(),
|
||||||
|
'text' => $product->get_name() . ' (#' . $product->get_id() . ')',
|
||||||
|
'price' => $product->get_price(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json($results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX 获取套装项目
|
||||||
|
*/
|
||||||
|
public function ajax_get_bundle_items() {
|
||||||
|
check_ajax_referer('yoone_admin_nonce', 'nonce');
|
||||||
|
|
||||||
|
$product_id = intval($_POST['product_id']);
|
||||||
|
|
||||||
|
$bundle = new Yoone_Bundle();
|
||||||
|
$bundle_data = $bundle->get_bundle_by_product_id($product_id);
|
||||||
|
|
||||||
|
if ($bundle_data) {
|
||||||
|
$items = $bundle->get_bundle_items($bundle_data['id']);
|
||||||
|
wp_send_json_success($items);
|
||||||
|
} else {
|
||||||
|
wp_send_json_success(array());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX 保存套装项目
|
||||||
|
*/
|
||||||
|
public function ajax_save_bundle_items() {
|
||||||
|
check_ajax_referer('yoone_admin_nonce', 'nonce');
|
||||||
|
|
||||||
|
$product_id = intval($_POST['product_id']);
|
||||||
|
$items = isset($_POST['items']) ? $_POST['items'] : array();
|
||||||
|
|
||||||
|
if (!$product_id) {
|
||||||
|
wp_send_json_error(__('无效的产品ID', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取或创建套装
|
||||||
|
$bundle = new Yoone_Bundle();
|
||||||
|
$bundle_data = $bundle->get_bundle_by_product_id($product_id);
|
||||||
|
|
||||||
|
if ($bundle_data) {
|
||||||
|
$bundle = new Yoone_Bundle($bundle_data['id']);
|
||||||
|
} else {
|
||||||
|
$bundle->set_product_id($product_id);
|
||||||
|
$bundle->set_name(get_the_title($product_id));
|
||||||
|
$bundle->set_status('active');
|
||||||
|
$bundle->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除现有项目
|
||||||
|
$bundle->clear_items();
|
||||||
|
|
||||||
|
// 添加新项目
|
||||||
|
foreach ($items as $index => $item) {
|
||||||
|
$bundle->add_item(
|
||||||
|
intval($item['product_id']),
|
||||||
|
intval($item['quantity']),
|
||||||
|
floatval($item['discount']),
|
||||||
|
$index
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success(__('套装项目已保存', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 辅助方法
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存套装产品数据
|
||||||
|
*/
|
||||||
|
private function save_bundle_product_data($product_id) {
|
||||||
|
$bundle = new Yoone_Bundle();
|
||||||
|
$bundle_data = $bundle->get_bundle_by_product_id($product_id);
|
||||||
|
|
||||||
|
if ($bundle_data) {
|
||||||
|
$bundle = new Yoone_Bundle($bundle_data['id']);
|
||||||
|
} else {
|
||||||
|
$bundle->set_product_id($product_id);
|
||||||
|
$bundle->set_name(get_the_title($product_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新套装设置
|
||||||
|
$bundle->set_discount_type(sanitize_text_field($_POST['_yoone_bundle_discount_type'] ?? ''));
|
||||||
|
$bundle->set_discount_value(floatval($_POST['_yoone_bundle_discount_value'] ?? 0));
|
||||||
|
$bundle->set_min_quantity(intval($_POST['_yoone_bundle_min_quantity'] ?? 0));
|
||||||
|
$bundle->set_max_quantity(intval($_POST['_yoone_bundle_max_quantity'] ?? 0));
|
||||||
|
$bundle->set_status('active');
|
||||||
|
|
||||||
|
$bundle->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存套装数据
|
||||||
|
*/
|
||||||
|
private function save_bundle_data($bundle) {
|
||||||
|
if (!wp_verify_nonce($_POST['yoone_bundle_nonce'], 'save_bundle')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bundle->set_name(sanitize_text_field($_POST['bundle_name']));
|
||||||
|
$bundle->set_description(sanitize_textarea_field($_POST['bundle_description']));
|
||||||
|
$bundle->set_discount_type(sanitize_text_field($_POST['discount_type']));
|
||||||
|
$bundle->set_discount_value(floatval($_POST['discount_value']));
|
||||||
|
$bundle->set_min_quantity(intval($_POST['min_quantity']));
|
||||||
|
$bundle->set_max_quantity(intval($_POST['max_quantity']));
|
||||||
|
$bundle->set_status(sanitize_text_field($_POST['status']));
|
||||||
|
|
||||||
|
$bundle->save();
|
||||||
|
|
||||||
|
// 保存套装项目
|
||||||
|
if (isset($_POST['bundle_items'])) {
|
||||||
|
$bundle->clear_items();
|
||||||
|
|
||||||
|
foreach ($_POST['bundle_items'] as $index => $item) {
|
||||||
|
$bundle->add_item(
|
||||||
|
intval($item['product_id']),
|
||||||
|
intval($item['quantity']),
|
||||||
|
floatval($item['discount']),
|
||||||
|
$index
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_redirect(admin_url('admin.php?page=yoone-bundles&message=saved'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存支付设置
|
||||||
|
*/
|
||||||
|
private function save_payment_settings() {
|
||||||
|
if (!wp_verify_nonce($_POST['yoone_payment_nonce'], 'save_payment_settings')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = array(
|
||||||
|
'moneris_enabled' => sanitize_text_field($_POST['moneris_enabled'] ?? ''),
|
||||||
|
'moneris_testmode' => sanitize_text_field($_POST['moneris_testmode'] ?? ''),
|
||||||
|
'moneris_store_id' => sanitize_text_field($_POST['moneris_store_id'] ?? ''),
|
||||||
|
'moneris_api_token' => sanitize_text_field($_POST['moneris_api_token'] ?? ''),
|
||||||
|
'moneris_country' => sanitize_text_field($_POST['moneris_country'] ?? 'CA'),
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($settings as $key => $value) {
|
||||||
|
update_option('yoone_' . $key, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_redirect(admin_url('admin.php?page=yoone-payment-settings&message=saved'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,389 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 混装产品列表表格类
|
||||||
|
*
|
||||||
|
* 处理后台混装产品列表显示
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!class_exists('WP_List_Table')) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 混装产品列表表格类
|
||||||
|
*/
|
||||||
|
class Yoone_Bundles_List_Table extends WP_List_Table {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
parent::__construct(array(
|
||||||
|
'singular' => 'bundle',
|
||||||
|
'plural' => 'bundles',
|
||||||
|
'ajax' => false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取列
|
||||||
|
*/
|
||||||
|
public function get_columns() {
|
||||||
|
return array(
|
||||||
|
'cb' => '<input type="checkbox" />',
|
||||||
|
'id' => __('ID', 'yoone-subscriptions'),
|
||||||
|
'name' => __('名称', 'yoone-subscriptions'),
|
||||||
|
'status' => __('状态', 'yoone-subscriptions'),
|
||||||
|
'discount_type' => __('折扣类型', 'yoone-subscriptions'),
|
||||||
|
'discount_value'=> __('折扣值', 'yoone-subscriptions'),
|
||||||
|
'min_quantity' => __('最小数量', 'yoone-subscriptions'),
|
||||||
|
'max_quantity' => __('最大数量', 'yoone-subscriptions'),
|
||||||
|
'items_count' => __('产品数量', 'yoone-subscriptions'),
|
||||||
|
'created_date' => __('创建日期', 'yoone-subscriptions'),
|
||||||
|
'actions' => __('操作', 'yoone-subscriptions'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可排序列
|
||||||
|
*/
|
||||||
|
public function get_sortable_columns() {
|
||||||
|
return array(
|
||||||
|
'id' => array('id', false),
|
||||||
|
'name' => array('name', false),
|
||||||
|
'status' => array('status', false),
|
||||||
|
'discount_value'=> array('discount_value', false),
|
||||||
|
'created_date' => array('created_at', false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取批量操作
|
||||||
|
*/
|
||||||
|
public function get_bulk_actions() {
|
||||||
|
return array(
|
||||||
|
'activate' => __('激活', 'yoone-subscriptions'),
|
||||||
|
'deactivate' => __('停用', 'yoone-subscriptions'),
|
||||||
|
'delete' => __('删除', 'yoone-subscriptions'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 准备项目
|
||||||
|
*/
|
||||||
|
public function prepare_items() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$per_page = 20;
|
||||||
|
$current_page = $this->get_pagenum();
|
||||||
|
|
||||||
|
// 处理搜索
|
||||||
|
$search = isset($_REQUEST['s']) ? sanitize_text_field($_REQUEST['s']) : '';
|
||||||
|
|
||||||
|
// 处理状态过滤
|
||||||
|
$status_filter = isset($_REQUEST['status']) ? sanitize_text_field($_REQUEST['status']) : '';
|
||||||
|
|
||||||
|
// 构建查询
|
||||||
|
$where_conditions = array('1=1');
|
||||||
|
$where_values = array();
|
||||||
|
|
||||||
|
if (!empty($search)) {
|
||||||
|
$where_conditions[] = "(b.name LIKE %s OR b.description LIKE %s)";
|
||||||
|
$where_values[] = '%' . $wpdb->esc_like($search) . '%';
|
||||||
|
$where_values[] = '%' . $wpdb->esc_like($search) . '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($status_filter)) {
|
||||||
|
$where_conditions[] = "b.status = %s";
|
||||||
|
$where_values[] = $status_filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
$where_clause = implode(' AND ', $where_conditions);
|
||||||
|
|
||||||
|
// 排序
|
||||||
|
$allowed_orderby = array('id', 'name', 'status', 'discount_value', 'created_at');
|
||||||
|
$orderby = isset($_REQUEST['orderby']) && in_array($_REQUEST['orderby'], $allowed_orderby) ? $_REQUEST['orderby'] : 'id';
|
||||||
|
$order = isset($_REQUEST['order']) && $_REQUEST['order'] === 'asc' ? 'ASC' : 'DESC';
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
$total_query = "
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM {$wpdb->prefix}yoone_bundles b
|
||||||
|
WHERE {$where_clause}
|
||||||
|
";
|
||||||
|
|
||||||
|
if (!empty($where_values)) {
|
||||||
|
$total_items = $wpdb->get_var($wpdb->prepare($total_query, $where_values));
|
||||||
|
} else {
|
||||||
|
$total_items = $wpdb->get_var($total_query);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
$offset = ($current_page - 1) * $per_page;
|
||||||
|
|
||||||
|
$items_query = "
|
||||||
|
SELECT b.*,
|
||||||
|
COUNT(bi.id) as items_count
|
||||||
|
FROM {$wpdb->prefix}yoone_bundles b
|
||||||
|
LEFT JOIN {$wpdb->prefix}yoone_bundle_items bi ON b.id = bi.bundle_id
|
||||||
|
WHERE {$where_clause}
|
||||||
|
GROUP BY b.id
|
||||||
|
ORDER BY b.{$orderby} {$order}
|
||||||
|
LIMIT %d OFFSET %d
|
||||||
|
";
|
||||||
|
|
||||||
|
$query_values = array_merge($where_values, array($per_page, $offset));
|
||||||
|
$this->items = $wpdb->get_results($wpdb->prepare($items_query, $query_values));
|
||||||
|
|
||||||
|
// 设置分页
|
||||||
|
$this->set_pagination_args(array(
|
||||||
|
'total_items' => $total_items,
|
||||||
|
'per_page' => $per_page,
|
||||||
|
'total_pages' => ceil($total_items / $per_page),
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->_column_headers = array(
|
||||||
|
$this->get_columns(),
|
||||||
|
array(),
|
||||||
|
$this->get_sortable_columns(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认列显示
|
||||||
|
*/
|
||||||
|
public function column_default($item, $column_name) {
|
||||||
|
switch ($column_name) {
|
||||||
|
case 'id':
|
||||||
|
return '#' . $item->id;
|
||||||
|
|
||||||
|
case 'name':
|
||||||
|
$edit_url = admin_url('admin.php?page=yoone-bundles&action=edit&id=' . $item->id);
|
||||||
|
return '<a href="' . $edit_url . '"><strong>' . esc_html($item->name) . '</strong></a>';
|
||||||
|
|
||||||
|
case 'status':
|
||||||
|
return $this->get_status_badge($item->status);
|
||||||
|
|
||||||
|
case 'discount_type':
|
||||||
|
$types = array(
|
||||||
|
'percentage' => __('百分比', 'yoone-subscriptions'),
|
||||||
|
'fixed' => __('固定金额', 'yoone-subscriptions'),
|
||||||
|
);
|
||||||
|
return isset($types[$item->discount_type]) ? $types[$item->discount_type] : $item->discount_type;
|
||||||
|
|
||||||
|
case 'discount_value':
|
||||||
|
if ($item->discount_type === 'percentage') {
|
||||||
|
return $item->discount_value . '%';
|
||||||
|
} else {
|
||||||
|
return wc_price($item->discount_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'min_quantity':
|
||||||
|
return $item->min_quantity ?: '-';
|
||||||
|
|
||||||
|
case 'max_quantity':
|
||||||
|
return $item->max_quantity ?: '-';
|
||||||
|
|
||||||
|
case 'items_count':
|
||||||
|
return $item->items_count;
|
||||||
|
|
||||||
|
case 'created_date':
|
||||||
|
$date = new DateTime($item->created_at);
|
||||||
|
return $date->format('Y-m-d H:i');
|
||||||
|
|
||||||
|
case 'actions':
|
||||||
|
return $this->get_row_actions($item);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return isset($item->$column_name) ? $item->$column_name : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复选框列
|
||||||
|
*/
|
||||||
|
public function column_cb($item) {
|
||||||
|
return sprintf('<input type="checkbox" name="bundle[]" value="%s" />', $item->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态徽章
|
||||||
|
*/
|
||||||
|
private function get_status_badge($status) {
|
||||||
|
$statuses = array(
|
||||||
|
'active' => array('label' => __('活跃', 'yoone-subscriptions'), 'color' => 'green'),
|
||||||
|
'inactive' => array('label' => __('停用', 'yoone-subscriptions'), 'color' => 'red'),
|
||||||
|
'draft' => array('label' => __('草稿', 'yoone-subscriptions'), 'color' => 'gray'),
|
||||||
|
);
|
||||||
|
|
||||||
|
$status_info = isset($statuses[$status]) ? $statuses[$status] : array('label' => $status, 'color' => 'gray');
|
||||||
|
|
||||||
|
return sprintf(
|
||||||
|
'<span class="yoone-status-badge yoone-status-%s" style="background-color: %s; color: white; padding: 2px 8px; border-radius: 3px; font-size: 11px;">%s</span>',
|
||||||
|
$status,
|
||||||
|
$status_info['color'],
|
||||||
|
$status_info['label']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取行操作
|
||||||
|
*/
|
||||||
|
private function get_row_actions($item) {
|
||||||
|
$actions = array();
|
||||||
|
|
||||||
|
$edit_url = admin_url('admin.php?page=yoone-bundles&action=edit&id=' . $item->id);
|
||||||
|
$actions['edit'] = '<a href="' . $edit_url . '">' . __('编辑', 'yoone-subscriptions') . '</a>';
|
||||||
|
|
||||||
|
if ($item->status === 'active') {
|
||||||
|
$deactivate_url = wp_nonce_url(
|
||||||
|
admin_url('admin.php?page=yoone-bundles&action=deactivate&id=' . $item->id),
|
||||||
|
'deactivate_bundle_' . $item->id
|
||||||
|
);
|
||||||
|
$actions['deactivate'] = '<a href="' . $deactivate_url . '">' . __('停用', 'yoone-subscriptions') . '</a>';
|
||||||
|
} else {
|
||||||
|
$activate_url = wp_nonce_url(
|
||||||
|
admin_url('admin.php?page=yoone-bundles&action=activate&id=' . $item->id),
|
||||||
|
'activate_bundle_' . $item->id
|
||||||
|
);
|
||||||
|
$actions['activate'] = '<a href="' . $activate_url . '">' . __('激活', 'yoone-subscriptions') . '</a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$duplicate_url = wp_nonce_url(
|
||||||
|
admin_url('admin.php?page=yoone-bundles&action=duplicate&id=' . $item->id),
|
||||||
|
'duplicate_bundle_' . $item->id
|
||||||
|
);
|
||||||
|
$actions['duplicate'] = '<a href="' . $duplicate_url . '">' . __('复制', 'yoone-subscriptions') . '</a>';
|
||||||
|
|
||||||
|
$delete_url = wp_nonce_url(
|
||||||
|
admin_url('admin.php?page=yoone-bundles&action=delete&id=' . $item->id),
|
||||||
|
'delete_bundle_' . $item->id
|
||||||
|
);
|
||||||
|
$actions['delete'] = '<a href="' . $delete_url . '" style="color: red;" onclick="return confirm(\'' . __('确定要删除这个混装产品吗?', 'yoone-subscriptions') . '\')">' . __('删除', 'yoone-subscriptions') . '</a>';
|
||||||
|
|
||||||
|
return implode(' | ', $actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示状态过滤器
|
||||||
|
*/
|
||||||
|
protected function get_views() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$status_counts = $wpdb->get_results("
|
||||||
|
SELECT status, COUNT(*) as count
|
||||||
|
FROM {$wpdb->prefix}yoone_bundles
|
||||||
|
GROUP BY status
|
||||||
|
", ARRAY_A);
|
||||||
|
|
||||||
|
$total_count = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}yoone_bundles");
|
||||||
|
|
||||||
|
$views = array();
|
||||||
|
$current_status = isset($_REQUEST['status']) ? $_REQUEST['status'] : '';
|
||||||
|
|
||||||
|
// 全部
|
||||||
|
$class = empty($current_status) ? 'current' : '';
|
||||||
|
$views['all'] = sprintf(
|
||||||
|
'<a href="%s" class="%s">%s <span class="count">(%d)</span></a>',
|
||||||
|
admin_url('admin.php?page=yoone-bundles'),
|
||||||
|
$class,
|
||||||
|
__('全部', 'yoone-subscriptions'),
|
||||||
|
$total_count
|
||||||
|
);
|
||||||
|
|
||||||
|
// 各状态
|
||||||
|
$status_labels = array(
|
||||||
|
'active' => __('活跃', 'yoone-subscriptions'),
|
||||||
|
'inactive' => __('停用', 'yoone-subscriptions'),
|
||||||
|
'draft' => __('草稿', 'yoone-subscriptions'),
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($status_counts as $status_data) {
|
||||||
|
$status = $status_data['status'];
|
||||||
|
$count = $status_data['count'];
|
||||||
|
|
||||||
|
if (isset($status_labels[$status])) {
|
||||||
|
$class = $current_status === $status ? 'current' : '';
|
||||||
|
$views[$status] = sprintf(
|
||||||
|
'<a href="%s" class="%s">%s <span class="count">(%d)</span></a>',
|
||||||
|
admin_url('admin.php?page=yoone-bundles&status=' . $status),
|
||||||
|
$class,
|
||||||
|
$status_labels[$status],
|
||||||
|
$count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $views;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理批量操作
|
||||||
|
*/
|
||||||
|
public function process_bulk_action() {
|
||||||
|
$action = $this->current_action();
|
||||||
|
|
||||||
|
if (!$action) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bundle_ids = isset($_REQUEST['bundle']) ? array_map('intval', $_REQUEST['bundle']) : array();
|
||||||
|
|
||||||
|
if (empty($bundle_ids)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($bundle_ids as $bundle_id) {
|
||||||
|
$bundle = new Yoone_Bundle($bundle_id);
|
||||||
|
|
||||||
|
if (!$bundle->get_id()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'activate':
|
||||||
|
$bundle->set_status('active');
|
||||||
|
$bundle->save();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'deactivate':
|
||||||
|
$bundle->set_status('inactive');
|
||||||
|
$bundle->save();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
$bundle->delete();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_redirect(admin_url('admin.php?page=yoone-bundles&bulk_action=' . $action . '&processed=' . count($bundle_ids)));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示批量操作通知
|
||||||
|
*/
|
||||||
|
public function admin_notices() {
|
||||||
|
if (isset($_GET['bulk_action']) && isset($_GET['processed'])) {
|
||||||
|
$action = sanitize_text_field($_GET['bulk_action']);
|
||||||
|
$processed = intval($_GET['processed']);
|
||||||
|
|
||||||
|
$messages = array(
|
||||||
|
'activate' => __('已激活 %d 个混装产品', 'yoone-subscriptions'),
|
||||||
|
'deactivate' => __('已停用 %d 个混装产品', 'yoone-subscriptions'),
|
||||||
|
'delete' => __('已删除 %d 个混装产品', 'yoone-subscriptions'),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isset($messages[$action])) {
|
||||||
|
echo '<div class="notice notice-success is-dismissible">';
|
||||||
|
echo '<p>' . sprintf($messages[$action], $processed) . '</p>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,409 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 订阅列表表格类
|
||||||
|
*
|
||||||
|
* 处理后台订阅列表显示
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!class_exists('WP_List_Table')) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅列表表格类
|
||||||
|
*/
|
||||||
|
class Yoone_Subscriptions_List_Table extends WP_List_Table {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
parent::__construct(array(
|
||||||
|
'singular' => 'subscription',
|
||||||
|
'plural' => 'subscriptions',
|
||||||
|
'ajax' => false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取列
|
||||||
|
*/
|
||||||
|
public function get_columns() {
|
||||||
|
return array(
|
||||||
|
'cb' => '<input type="checkbox" />',
|
||||||
|
'id' => __('ID', 'yoone-subscriptions'),
|
||||||
|
'customer' => __('客户', 'yoone-subscriptions'),
|
||||||
|
'status' => __('状态', 'yoone-subscriptions'),
|
||||||
|
'total' => __('金额', 'yoone-subscriptions'),
|
||||||
|
'billing_period' => __('计费周期', 'yoone-subscriptions'),
|
||||||
|
'next_payment' => __('下次付款', 'yoone-subscriptions'),
|
||||||
|
'created_date' => __('创建日期', 'yoone-subscriptions'),
|
||||||
|
'actions' => __('操作', 'yoone-subscriptions'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可排序列
|
||||||
|
*/
|
||||||
|
public function get_sortable_columns() {
|
||||||
|
return array(
|
||||||
|
'id' => array('id', false),
|
||||||
|
'status' => array('status', false),
|
||||||
|
'total' => array('total', false),
|
||||||
|
'next_payment' => array('next_payment_date', false),
|
||||||
|
'created_date' => array('created_at', false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取批量操作
|
||||||
|
*/
|
||||||
|
public function get_bulk_actions() {
|
||||||
|
return array(
|
||||||
|
'activate' => __('激活', 'yoone-subscriptions'),
|
||||||
|
'pause' => __('暂停', 'yoone-subscriptions'),
|
||||||
|
'cancel' => __('取消', 'yoone-subscriptions'),
|
||||||
|
'delete' => __('删除', 'yoone-subscriptions'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 准备项目
|
||||||
|
*/
|
||||||
|
public function prepare_items() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$per_page = 20;
|
||||||
|
$current_page = $this->get_pagenum();
|
||||||
|
|
||||||
|
// 处理搜索
|
||||||
|
$search = isset($_REQUEST['s']) ? sanitize_text_field($_REQUEST['s']) : '';
|
||||||
|
|
||||||
|
// 处理状态过滤
|
||||||
|
$status_filter = isset($_REQUEST['status']) ? sanitize_text_field($_REQUEST['status']) : '';
|
||||||
|
|
||||||
|
// 构建查询
|
||||||
|
$where_conditions = array('1=1');
|
||||||
|
$where_values = array();
|
||||||
|
|
||||||
|
if (!empty($search)) {
|
||||||
|
$where_conditions[] = "(s.id LIKE %s OR u.display_name LIKE %s OR u.user_email LIKE %s)";
|
||||||
|
$where_values[] = '%' . $wpdb->esc_like($search) . '%';
|
||||||
|
$where_values[] = '%' . $wpdb->esc_like($search) . '%';
|
||||||
|
$where_values[] = '%' . $wpdb->esc_like($search) . '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($status_filter)) {
|
||||||
|
$where_conditions[] = "s.status = %s";
|
||||||
|
$where_values[] = $status_filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
$where_clause = implode(' AND ', $where_conditions);
|
||||||
|
|
||||||
|
// 排序
|
||||||
|
$allowed_orderby = array('id', 'status', 'total', 'next_payment_date', 'created_at');
|
||||||
|
$orderby = isset($_REQUEST['orderby']) && in_array($_REQUEST['orderby'], $allowed_orderby) ? $_REQUEST['orderby'] : 'id';
|
||||||
|
$order = isset($_REQUEST['order']) && $_REQUEST['order'] === 'asc' ? 'ASC' : 'DESC';
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
$total_query = "
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM {$wpdb->prefix}yoone_subscriptions s
|
||||||
|
LEFT JOIN {$wpdb->users} u ON s.customer_id = u.ID
|
||||||
|
WHERE {$where_clause}
|
||||||
|
";
|
||||||
|
|
||||||
|
if (!empty($where_values)) {
|
||||||
|
$total_items = $wpdb->get_var($wpdb->prepare($total_query, $where_values));
|
||||||
|
} else {
|
||||||
|
$total_items = $wpdb->get_var($total_query);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
$offset = ($current_page - 1) * $per_page;
|
||||||
|
|
||||||
|
$items_query = "
|
||||||
|
SELECT s.*, u.display_name, u.user_email
|
||||||
|
FROM {$wpdb->prefix}yoone_subscriptions s
|
||||||
|
LEFT JOIN {$wpdb->users} u ON s.customer_id = u.ID
|
||||||
|
WHERE {$where_clause}
|
||||||
|
ORDER BY s.{$orderby} {$order}
|
||||||
|
LIMIT %d OFFSET %d
|
||||||
|
";
|
||||||
|
|
||||||
|
$query_values = array_merge($where_values, array($per_page, $offset));
|
||||||
|
$this->items = $wpdb->get_results($wpdb->prepare($items_query, $query_values));
|
||||||
|
|
||||||
|
// 设置分页
|
||||||
|
$this->set_pagination_args(array(
|
||||||
|
'total_items' => $total_items,
|
||||||
|
'per_page' => $per_page,
|
||||||
|
'total_pages' => ceil($total_items / $per_page),
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->_column_headers = array(
|
||||||
|
$this->get_columns(),
|
||||||
|
array(),
|
||||||
|
$this->get_sortable_columns(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认列显示
|
||||||
|
*/
|
||||||
|
public function column_default($item, $column_name) {
|
||||||
|
switch ($column_name) {
|
||||||
|
case 'id':
|
||||||
|
return '#' . $item->id;
|
||||||
|
|
||||||
|
case 'customer':
|
||||||
|
$customer_link = admin_url('user-edit.php?user_id=' . $item->customer_id);
|
||||||
|
return '<a href="' . $customer_link . '">' . $item->display_name . '</a><br><small>' . $item->user_email . '</small>';
|
||||||
|
|
||||||
|
case 'status':
|
||||||
|
return $this->get_status_badge($item->status);
|
||||||
|
|
||||||
|
case 'total':
|
||||||
|
return wc_price($item->total);
|
||||||
|
|
||||||
|
case 'billing_period':
|
||||||
|
return $this->format_billing_period($item->billing_period, $item->billing_interval);
|
||||||
|
|
||||||
|
case 'next_payment':
|
||||||
|
if ($item->next_payment_date && $item->status === 'active') {
|
||||||
|
$date = new DateTime($item->next_payment_date);
|
||||||
|
return $date->format('Y-m-d H:i');
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
|
||||||
|
case 'created_date':
|
||||||
|
$date = new DateTime($item->created_at);
|
||||||
|
return $date->format('Y-m-d H:i');
|
||||||
|
|
||||||
|
case 'actions':
|
||||||
|
return $this->get_row_actions($item);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return isset($item->$column_name) ? $item->$column_name : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复选框列
|
||||||
|
*/
|
||||||
|
public function column_cb($item) {
|
||||||
|
return sprintf('<input type="checkbox" name="subscription[]" value="%s" />', $item->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态徽章
|
||||||
|
*/
|
||||||
|
private function get_status_badge($status) {
|
||||||
|
$statuses = array(
|
||||||
|
'active' => array('label' => __('活跃', 'yoone-subscriptions'), 'color' => 'green'),
|
||||||
|
'paused' => array('label' => __('暂停', 'yoone-subscriptions'), 'color' => 'orange'),
|
||||||
|
'cancelled' => array('label' => __('已取消', 'yoone-subscriptions'), 'color' => 'red'),
|
||||||
|
'expired' => array('label' => __('已过期', 'yoone-subscriptions'), 'color' => 'gray'),
|
||||||
|
'pending' => array('label' => __('待处理', 'yoone-subscriptions'), 'color' => 'blue'),
|
||||||
|
);
|
||||||
|
|
||||||
|
$status_info = isset($statuses[$status]) ? $statuses[$status] : array('label' => $status, 'color' => 'gray');
|
||||||
|
|
||||||
|
return sprintf(
|
||||||
|
'<span class="yoone-status-badge yoone-status-%s" style="background-color: %s; color: white; padding: 2px 8px; border-radius: 3px; font-size: 11px;">%s</span>',
|
||||||
|
$status,
|
||||||
|
$status_info['color'],
|
||||||
|
$status_info['label']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化计费周期
|
||||||
|
*/
|
||||||
|
private function format_billing_period($period, $interval) {
|
||||||
|
$periods = array(
|
||||||
|
'day' => __('天', 'yoone-subscriptions'),
|
||||||
|
'week' => __('周', 'yoone-subscriptions'),
|
||||||
|
'month' => __('月', 'yoone-subscriptions'),
|
||||||
|
'year' => __('年', 'yoone-subscriptions'),
|
||||||
|
);
|
||||||
|
|
||||||
|
$period_name = isset($periods[$period]) ? $periods[$period] : $period;
|
||||||
|
|
||||||
|
if ($interval > 1) {
|
||||||
|
return sprintf(__('每 %d %s', 'yoone-subscriptions'), $interval, $period_name);
|
||||||
|
} else {
|
||||||
|
return sprintf(__('每%s', 'yoone-subscriptions'), $period_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取行操作
|
||||||
|
*/
|
||||||
|
private function get_row_actions($item) {
|
||||||
|
$actions = array();
|
||||||
|
|
||||||
|
$edit_url = admin_url('admin.php?page=yoone-subscriptions&action=edit&id=' . $item->id);
|
||||||
|
$actions['edit'] = '<a href="' . $edit_url . '">' . __('编辑', 'yoone-subscriptions') . '</a>';
|
||||||
|
|
||||||
|
if ($item->status === 'active') {
|
||||||
|
$pause_url = wp_nonce_url(
|
||||||
|
admin_url('admin.php?page=yoone-subscriptions&action=pause&id=' . $item->id),
|
||||||
|
'pause_subscription_' . $item->id
|
||||||
|
);
|
||||||
|
$actions['pause'] = '<a href="' . $pause_url . '">' . __('暂停', 'yoone-subscriptions') . '</a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($item->status === 'paused') {
|
||||||
|
$resume_url = wp_nonce_url(
|
||||||
|
admin_url('admin.php?page=yoone-subscriptions&action=resume&id=' . $item->id),
|
||||||
|
'resume_subscription_' . $item->id
|
||||||
|
);
|
||||||
|
$actions['resume'] = '<a href="' . $resume_url . '">' . __('恢复', 'yoone-subscriptions') . '</a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($item->status, array('active', 'paused'))) {
|
||||||
|
$cancel_url = wp_nonce_url(
|
||||||
|
admin_url('admin.php?page=yoone-subscriptions&action=cancel&id=' . $item->id),
|
||||||
|
'cancel_subscription_' . $item->id
|
||||||
|
);
|
||||||
|
$actions['cancel'] = '<a href="' . $cancel_url . '" style="color: red;">' . __('取消', 'yoone-subscriptions') . '</a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$delete_url = wp_nonce_url(
|
||||||
|
admin_url('admin.php?page=yoone-subscriptions&action=delete&id=' . $item->id),
|
||||||
|
'delete_subscription_' . $item->id
|
||||||
|
);
|
||||||
|
$actions['delete'] = '<a href="' . $delete_url . '" style="color: red;" onclick="return confirm(\'' . __('确定要删除这个订阅吗?', 'yoone-subscriptions') . '\')">' . __('删除', 'yoone-subscriptions') . '</a>';
|
||||||
|
|
||||||
|
return implode(' | ', $actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示状态过滤器
|
||||||
|
*/
|
||||||
|
protected function get_views() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$status_counts = $wpdb->get_results("
|
||||||
|
SELECT status, COUNT(*) as count
|
||||||
|
FROM {$wpdb->prefix}yoone_subscriptions
|
||||||
|
GROUP BY status
|
||||||
|
", ARRAY_A);
|
||||||
|
|
||||||
|
$total_count = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}yoone_subscriptions");
|
||||||
|
|
||||||
|
$views = array();
|
||||||
|
$current_status = isset($_REQUEST['status']) ? $_REQUEST['status'] : '';
|
||||||
|
|
||||||
|
// 全部
|
||||||
|
$class = empty($current_status) ? 'current' : '';
|
||||||
|
$views['all'] = sprintf(
|
||||||
|
'<a href="%s" class="%s">%s <span class="count">(%d)</span></a>',
|
||||||
|
admin_url('admin.php?page=yoone-subscriptions'),
|
||||||
|
$class,
|
||||||
|
__('全部', 'yoone-subscriptions'),
|
||||||
|
$total_count
|
||||||
|
);
|
||||||
|
|
||||||
|
// 各状态
|
||||||
|
$status_labels = array(
|
||||||
|
'active' => __('活跃', 'yoone-subscriptions'),
|
||||||
|
'paused' => __('暂停', 'yoone-subscriptions'),
|
||||||
|
'cancelled' => __('已取消', 'yoone-subscriptions'),
|
||||||
|
'expired' => __('已过期', 'yoone-subscriptions'),
|
||||||
|
'pending' => __('待处理', 'yoone-subscriptions'),
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($status_counts as $status_data) {
|
||||||
|
$status = $status_data['status'];
|
||||||
|
$count = $status_data['count'];
|
||||||
|
|
||||||
|
if (isset($status_labels[$status])) {
|
||||||
|
$class = $current_status === $status ? 'current' : '';
|
||||||
|
$views[$status] = sprintf(
|
||||||
|
'<a href="%s" class="%s">%s <span class="count">(%d)</span></a>',
|
||||||
|
admin_url('admin.php?page=yoone-subscriptions&status=' . $status),
|
||||||
|
$class,
|
||||||
|
$status_labels[$status],
|
||||||
|
$count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $views;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理批量操作
|
||||||
|
*/
|
||||||
|
public function process_bulk_action() {
|
||||||
|
$action = $this->current_action();
|
||||||
|
|
||||||
|
if (!$action) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscription_ids = isset($_REQUEST['subscription']) ? array_map('intval', $_REQUEST['subscription']) : array();
|
||||||
|
|
||||||
|
if (empty($subscription_ids)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($subscription_ids as $subscription_id) {
|
||||||
|
$subscription = new Yoone_Subscription($subscription_id);
|
||||||
|
|
||||||
|
if (!$subscription->get_id()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'activate':
|
||||||
|
$subscription->activate();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'pause':
|
||||||
|
$subscription->pause();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'cancel':
|
||||||
|
$subscription->cancel();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
$subscription->delete();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_redirect(admin_url('admin.php?page=yoone-subscriptions&bulk_action=' . $action . '&processed=' . count($subscription_ids)));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示批量操作通知
|
||||||
|
*/
|
||||||
|
public function admin_notices() {
|
||||||
|
if (isset($_GET['bulk_action']) && isset($_GET['processed'])) {
|
||||||
|
$action = sanitize_text_field($_GET['bulk_action']);
|
||||||
|
$processed = intval($_GET['processed']);
|
||||||
|
|
||||||
|
$messages = array(
|
||||||
|
'activate' => __('已激活 %d 个订阅', 'yoone-subscriptions'),
|
||||||
|
'pause' => __('已暂停 %d 个订阅', 'yoone-subscriptions'),
|
||||||
|
'cancel' => __('已取消 %d 个订阅', 'yoone-subscriptions'),
|
||||||
|
'delete' => __('已删除 %d 个订阅', 'yoone-subscriptions'),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isset($messages[$action])) {
|
||||||
|
echo '<div class="notice notice-success is-dismissible">';
|
||||||
|
echo '<p>' . sprintf($messages[$action], $processed) . '</p>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,318 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 混装产品前端展示类
|
||||||
|
*
|
||||||
|
* 处理混装产品的前端显示和用户交互
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 混装产品前端展示类
|
||||||
|
*/
|
||||||
|
class Yoone_Bundle_Frontend {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
$this->init_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化钩子
|
||||||
|
*/
|
||||||
|
private function init_hooks() {
|
||||||
|
// 在产品页面显示混装选项
|
||||||
|
add_action('woocommerce_single_product_summary', array($this, 'display_bundle_options'), 25);
|
||||||
|
|
||||||
|
// 处理混装产品添加到购物车
|
||||||
|
add_filter('woocommerce_add_to_cart_validation', array($this, 'validate_bundle_add_to_cart'), 10, 3);
|
||||||
|
add_action('woocommerce_add_to_cart', array($this, 'add_bundle_to_cart'), 10, 6);
|
||||||
|
|
||||||
|
// 在购物车中显示混装信息
|
||||||
|
add_filter('woocommerce_get_item_data', array($this, 'display_bundle_cart_item_data'), 10, 2);
|
||||||
|
|
||||||
|
// 计算混装价格
|
||||||
|
add_action('woocommerce_before_calculate_totals', array($this, 'calculate_bundle_cart_item_price'));
|
||||||
|
|
||||||
|
// 加载前端脚本和样式
|
||||||
|
add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示混装选项
|
||||||
|
*/
|
||||||
|
public function display_bundle_options() {
|
||||||
|
global $product;
|
||||||
|
|
||||||
|
if (!$product || !$this->is_bundle_product($product->get_id())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bundle = $this->get_product_bundle($product->get_id());
|
||||||
|
if (!$bundle || !$bundle->is_available()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = $bundle->get_items();
|
||||||
|
if (empty($items)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载模板
|
||||||
|
wc_get_template(
|
||||||
|
'single-product/bundle-options.php',
|
||||||
|
array(
|
||||||
|
'bundle' => $bundle,
|
||||||
|
'items' => $items,
|
||||||
|
'product' => $product
|
||||||
|
),
|
||||||
|
'',
|
||||||
|
YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'templates/frontend/'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证混装产品添加到购物车
|
||||||
|
*/
|
||||||
|
public function validate_bundle_add_to_cart($passed, $product_id, $quantity) {
|
||||||
|
if (!$this->is_bundle_product($product_id)) {
|
||||||
|
return $passed;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bundle = $this->get_product_bundle($product_id);
|
||||||
|
if (!$bundle) {
|
||||||
|
wc_add_notice(__('混装产品不存在', 'yoone-subscriptions'), 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证数量
|
||||||
|
if (!$bundle->validate_quantity($quantity)) {
|
||||||
|
$min = $bundle->get_min_quantity();
|
||||||
|
$max = $bundle->get_max_quantity();
|
||||||
|
|
||||||
|
if ($max) {
|
||||||
|
$message = sprintf(__('数量必须在 %d 到 %d 之间', 'yoone-subscriptions'), $min, $max);
|
||||||
|
} else {
|
||||||
|
$message = sprintf(__('最小数量为 %d', 'yoone-subscriptions'), $min);
|
||||||
|
}
|
||||||
|
|
||||||
|
wc_add_notice($message, 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证选择的商品
|
||||||
|
$selected_items = isset($_POST['bundle_items']) ? $_POST['bundle_items'] : array();
|
||||||
|
if (empty($selected_items)) {
|
||||||
|
wc_add_notice(__('请选择混装商品', 'yoone-subscriptions'), 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证必需商品
|
||||||
|
$bundle_items = $bundle->get_items();
|
||||||
|
foreach ($bundle_items as $item) {
|
||||||
|
if ($item['is_required'] && !isset($selected_items[$item['product_id']])) {
|
||||||
|
$product = wc_get_product($item['product_id']);
|
||||||
|
$message = sprintf(__('必须选择商品: %s', 'yoone-subscriptions'), $product->get_name());
|
||||||
|
wc_add_notice($message, 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证库存
|
||||||
|
foreach ($selected_items as $item_product_id => $item_quantity) {
|
||||||
|
$item_product = wc_get_product($item_product_id);
|
||||||
|
if (!$item_product) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$item_product->has_enough_stock($item_quantity * $quantity)) {
|
||||||
|
$message = sprintf(__('商品 %s 库存不足', 'yoone-subscriptions'), $item_product->get_name());
|
||||||
|
wc_add_notice($message, 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $passed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加混装产品到购物车
|
||||||
|
*/
|
||||||
|
public function add_bundle_to_cart($cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data) {
|
||||||
|
if (!$this->is_bundle_product($product_id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$selected_items = isset($_POST['bundle_items']) ? $_POST['bundle_items'] : array();
|
||||||
|
if (empty($selected_items)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存混装信息到购物车项目
|
||||||
|
WC()->cart->cart_contents[$cart_item_key]['bundle_items'] = $selected_items;
|
||||||
|
WC()->cart->cart_contents[$cart_item_key]['is_bundle'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在购物车中显示混装信息
|
||||||
|
*/
|
||||||
|
public function display_bundle_cart_item_data($item_data, $cart_item) {
|
||||||
|
if (!isset($cart_item['is_bundle']) || !$cart_item['is_bundle']) {
|
||||||
|
return $item_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($cart_item['bundle_items']) || empty($cart_item['bundle_items'])) {
|
||||||
|
return $item_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$item_data[] = array(
|
||||||
|
'key' => __('混装商品', 'yoone-subscriptions'),
|
||||||
|
'value' => $this->format_bundle_items($cart_item['bundle_items'])
|
||||||
|
);
|
||||||
|
|
||||||
|
return $item_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算混装购物车项目价格
|
||||||
|
*/
|
||||||
|
public function calculate_bundle_cart_item_price($cart) {
|
||||||
|
if (is_admin() && !defined('DOING_AJAX')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
|
||||||
|
if (!isset($cart_item['is_bundle']) || !$cart_item['is_bundle']) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$product_id = $cart_item['product_id'];
|
||||||
|
$bundle = $this->get_product_bundle($product_id);
|
||||||
|
|
||||||
|
if (!$bundle) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$selected_items = isset($cart_item['bundle_items']) ? $cart_item['bundle_items'] : array();
|
||||||
|
$bundle_price = $bundle->calculate_price($this->format_selected_items($selected_items));
|
||||||
|
|
||||||
|
// 设置新价格
|
||||||
|
$cart_item['data']->set_price($bundle_price);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载前端脚本和样式
|
||||||
|
*/
|
||||||
|
public function enqueue_scripts() {
|
||||||
|
if (is_product()) {
|
||||||
|
wp_enqueue_script(
|
||||||
|
'yoone-bundle-frontend',
|
||||||
|
YOONE_SUBSCRIPTIONS_PLUGIN_URL . 'assets/js/bundle-frontend.js',
|
||||||
|
array('jquery'),
|
||||||
|
YOONE_SUBSCRIPTIONS_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_enqueue_style(
|
||||||
|
'yoone-bundle-frontend',
|
||||||
|
YOONE_SUBSCRIPTIONS_PLUGIN_URL . 'assets/css/bundle-frontend.css',
|
||||||
|
array(),
|
||||||
|
YOONE_SUBSCRIPTIONS_VERSION
|
||||||
|
);
|
||||||
|
|
||||||
|
// 本地化脚本
|
||||||
|
wp_localize_script('yoone-bundle-frontend', 'yoone_bundle_params', array(
|
||||||
|
'ajax_url' => admin_url('admin-ajax.php'),
|
||||||
|
'nonce' => wp_create_nonce('yoone_bundle_nonce'),
|
||||||
|
'i18n' => array(
|
||||||
|
'select_items' => __('请选择商品', 'yoone-subscriptions'),
|
||||||
|
'calculating' => __('计算中...', 'yoone-subscriptions'),
|
||||||
|
'error' => __('发生错误', 'yoone-subscriptions')
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 辅助方法
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为混装产品
|
||||||
|
*/
|
||||||
|
private function is_bundle_product($product_id) {
|
||||||
|
$bundle = $this->get_product_bundle($product_id);
|
||||||
|
return $bundle && $bundle->is_available();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取产品的混装配置
|
||||||
|
*/
|
||||||
|
private function get_product_bundle($product_id) {
|
||||||
|
static $bundles = array();
|
||||||
|
|
||||||
|
if (!isset($bundles[$product_id])) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$bundle_id = $wpdb->get_var($wpdb->prepare("
|
||||||
|
SELECT id FROM {$wpdb->prefix}yoone_bundles
|
||||||
|
WHERE product_id = %d AND status = 'active'
|
||||||
|
LIMIT 1
|
||||||
|
", $product_id));
|
||||||
|
|
||||||
|
if ($bundle_id) {
|
||||||
|
$bundles[$product_id] = new Yoone_Bundle($bundle_id);
|
||||||
|
} else {
|
||||||
|
$bundles[$product_id] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $bundles[$product_id];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化混装商品显示
|
||||||
|
*/
|
||||||
|
private function format_bundle_items($bundle_items) {
|
||||||
|
$formatted = array();
|
||||||
|
|
||||||
|
foreach ($bundle_items as $product_id => $quantity) {
|
||||||
|
$product = wc_get_product($product_id);
|
||||||
|
if ($product) {
|
||||||
|
$formatted[] = sprintf('%s × %d', $product->get_name(), $quantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(', ', $formatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化选择的商品为计算格式
|
||||||
|
*/
|
||||||
|
private function format_selected_items($selected_items) {
|
||||||
|
$formatted = array();
|
||||||
|
|
||||||
|
foreach ($selected_items as $product_id => $quantity) {
|
||||||
|
$formatted[] = array(
|
||||||
|
'product_id' => $product_id,
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'discount_type' => 'none',
|
||||||
|
'discount_value' => 0,
|
||||||
|
'is_required' => false,
|
||||||
|
'sort_order' => 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $formatted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
new Yoone_Bundle_Frontend();
|
||||||
|
|
@ -0,0 +1,613 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 混装产品类
|
||||||
|
*
|
||||||
|
* 处理产品组合的创建、管理和计算
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 混装产品类
|
||||||
|
*/
|
||||||
|
class Yoone_Bundle extends Abstract_Yoone_Data {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象类型
|
||||||
|
*/
|
||||||
|
protected $object_type = 'yoone_bundle';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据结构
|
||||||
|
*/
|
||||||
|
protected $data = array(
|
||||||
|
'name' => '',
|
||||||
|
'description' => '',
|
||||||
|
'status' => 'active',
|
||||||
|
'discount_type' => 'percentage',
|
||||||
|
'discount_value' => 0.00,
|
||||||
|
'min_quantity' => 1,
|
||||||
|
'max_quantity' => null,
|
||||||
|
'created_at' => null,
|
||||||
|
'updated_at' => null
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 混装商品
|
||||||
|
*/
|
||||||
|
protected $items = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
public function __construct($id = 0) {
|
||||||
|
parent::__construct($id);
|
||||||
|
|
||||||
|
if ($this->get_id() > 0) {
|
||||||
|
$this->load_items();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Getters
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取名称
|
||||||
|
*/
|
||||||
|
public function get_name($context = 'view') {
|
||||||
|
return $this->get_prop('name', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取描述
|
||||||
|
*/
|
||||||
|
public function get_description($context = 'view') {
|
||||||
|
return $this->get_prop('description', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态
|
||||||
|
*/
|
||||||
|
public function get_status($context = 'view') {
|
||||||
|
return $this->get_prop('status', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取折扣类型
|
||||||
|
*/
|
||||||
|
public function get_discount_type($context = 'view') {
|
||||||
|
return $this->get_prop('discount_type', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取折扣值
|
||||||
|
*/
|
||||||
|
public function get_discount_value($context = 'view') {
|
||||||
|
return $this->get_prop('discount_value', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最小数量
|
||||||
|
*/
|
||||||
|
public function get_min_quantity($context = 'view') {
|
||||||
|
return $this->get_prop('min_quantity', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最大数量
|
||||||
|
*/
|
||||||
|
public function get_max_quantity($context = 'view') {
|
||||||
|
return $this->get_prop('max_quantity', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取创建时间
|
||||||
|
*/
|
||||||
|
public function get_created_at($context = 'view') {
|
||||||
|
return $this->get_prop('created_at', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取更新时间
|
||||||
|
*/
|
||||||
|
public function get_updated_at($context = 'view') {
|
||||||
|
return $this->get_prop('updated_at', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Setters
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置名称
|
||||||
|
*/
|
||||||
|
public function set_name($name) {
|
||||||
|
$this->set_prop('name', sanitize_text_field($name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置描述
|
||||||
|
*/
|
||||||
|
public function set_description($description) {
|
||||||
|
$this->set_prop('description', wp_kses_post($description));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置状态
|
||||||
|
*/
|
||||||
|
public function set_status($status) {
|
||||||
|
$valid_statuses = array('active', 'inactive', 'draft');
|
||||||
|
if (in_array($status, $valid_statuses)) {
|
||||||
|
$this->set_prop('status', $status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置折扣类型
|
||||||
|
*/
|
||||||
|
public function set_discount_type($type) {
|
||||||
|
$valid_types = array('percentage', 'fixed', 'none');
|
||||||
|
if (in_array($type, $valid_types)) {
|
||||||
|
$this->set_prop('discount_type', $type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置折扣值
|
||||||
|
*/
|
||||||
|
public function set_discount_value($value) {
|
||||||
|
$this->set_prop('discount_value', floatval($value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置最小数量
|
||||||
|
*/
|
||||||
|
public function set_min_quantity($quantity) {
|
||||||
|
$this->set_prop('min_quantity', absint($quantity));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置最大数量
|
||||||
|
*/
|
||||||
|
public function set_max_quantity($quantity) {
|
||||||
|
$this->set_prop('max_quantity', $quantity ? absint($quantity) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 商品管理
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取混装商品
|
||||||
|
*/
|
||||||
|
public function get_items() {
|
||||||
|
return $this->items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加商品
|
||||||
|
*/
|
||||||
|
public function add_item($product_id, $quantity = 1, $args = array()) {
|
||||||
|
$defaults = array(
|
||||||
|
'discount_type' => 'none',
|
||||||
|
'discount_value' => 0.00,
|
||||||
|
'is_required' => false,
|
||||||
|
'sort_order' => 0
|
||||||
|
);
|
||||||
|
|
||||||
|
$args = wp_parse_args($args, $defaults);
|
||||||
|
|
||||||
|
$item = array(
|
||||||
|
'product_id' => absint($product_id),
|
||||||
|
'quantity' => absint($quantity),
|
||||||
|
'discount_type' => sanitize_text_field($args['discount_type']),
|
||||||
|
'discount_value' => floatval($args['discount_value']),
|
||||||
|
'is_required' => (bool) $args['is_required'],
|
||||||
|
'sort_order' => absint($args['sort_order'])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->items[] = $item;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除商品
|
||||||
|
*/
|
||||||
|
public function remove_item($index) {
|
||||||
|
if (isset($this->items[$index])) {
|
||||||
|
unset($this->items[$index]);
|
||||||
|
$this->items = array_values($this->items); // 重新索引
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空商品
|
||||||
|
*/
|
||||||
|
public function clear_items() {
|
||||||
|
$this->items = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载商品
|
||||||
|
*/
|
||||||
|
protected function load_items() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
if (!$this->get_id()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = $wpdb->get_results($wpdb->prepare("
|
||||||
|
SELECT product_id, quantity, discount_type, discount_value, is_required, sort_order
|
||||||
|
FROM {$wpdb->prefix}yoone_bundle_items
|
||||||
|
WHERE bundle_id = %d
|
||||||
|
ORDER BY sort_order ASC, id ASC
|
||||||
|
", $this->get_id()), ARRAY_A);
|
||||||
|
|
||||||
|
$this->items = $items ?: array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存商品
|
||||||
|
*/
|
||||||
|
protected function save_items() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
if (!$this->get_id()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除现有商品
|
||||||
|
$wpdb->delete(
|
||||||
|
$wpdb->prefix . 'yoone_bundle_items',
|
||||||
|
array('bundle_id' => $this->get_id()),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
// 插入新商品
|
||||||
|
foreach ($this->items as $item) {
|
||||||
|
$wpdb->insert(
|
||||||
|
$wpdb->prefix . 'yoone_bundle_items',
|
||||||
|
array(
|
||||||
|
'bundle_id' => $this->get_id(),
|
||||||
|
'product_id' => $item['product_id'],
|
||||||
|
'quantity' => $item['quantity'],
|
||||||
|
'discount_type' => $item['discount_type'],
|
||||||
|
'discount_value' => $item['discount_value'],
|
||||||
|
'is_required' => $item['is_required'] ? 1 : 0,
|
||||||
|
'sort_order' => $item['sort_order']
|
||||||
|
),
|
||||||
|
array('%d', '%d', '%d', '%s', '%f', '%d', '%d')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 价格计算
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算混装价格
|
||||||
|
*/
|
||||||
|
public function calculate_price($selected_items = array()) {
|
||||||
|
$total = 0;
|
||||||
|
$items = $this->get_items();
|
||||||
|
|
||||||
|
if (empty($selected_items)) {
|
||||||
|
$selected_items = $items;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($selected_items as $item) {
|
||||||
|
$product = wc_get_product($item['product_id']);
|
||||||
|
if (!$product) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$item_price = $product->get_price() * $item['quantity'];
|
||||||
|
|
||||||
|
// 应用商品级别折扣
|
||||||
|
if ($item['discount_type'] === 'percentage' && $item['discount_value'] > 0) {
|
||||||
|
$item_price = $item_price * (1 - $item['discount_value'] / 100);
|
||||||
|
} elseif ($item['discount_type'] === 'fixed' && $item['discount_value'] > 0) {
|
||||||
|
$item_price = max(0, $item_price - $item['discount_value']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$total += $item_price;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用混装级别折扣
|
||||||
|
if ($this->get_discount_type() === 'percentage' && $this->get_discount_value() > 0) {
|
||||||
|
$total = $total * (1 - $this->get_discount_value() / 100);
|
||||||
|
} elseif ($this->get_discount_type() === 'fixed' && $this->get_discount_value() > 0) {
|
||||||
|
$total = max(0, $total - $this->get_discount_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
return apply_filters('yoone_bundle_calculate_price', $total, $this, $selected_items);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取节省金额
|
||||||
|
*/
|
||||||
|
public function get_savings($selected_items = array()) {
|
||||||
|
$original_total = 0;
|
||||||
|
$bundle_total = $this->calculate_price($selected_items);
|
||||||
|
|
||||||
|
$items = empty($selected_items) ? $this->get_items() : $selected_items;
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$product = wc_get_product($item['product_id']);
|
||||||
|
if ($product) {
|
||||||
|
$original_total += $product->get_price() * $item['quantity'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return max(0, $original_total - $bundle_total);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 验证方法
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证数量
|
||||||
|
*/
|
||||||
|
public function validate_quantity($quantity) {
|
||||||
|
$min = $this->get_min_quantity();
|
||||||
|
$max = $this->get_max_quantity();
|
||||||
|
|
||||||
|
if ($quantity < $min) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($max && $quantity > $max) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否可用
|
||||||
|
*/
|
||||||
|
public function is_available() {
|
||||||
|
return $this->get_status() === 'active' && !empty($this->get_items());
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 数据库操作
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从数据库读取
|
||||||
|
*/
|
||||||
|
protected function read() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$data = $wpdb->get_row($wpdb->prepare("
|
||||||
|
SELECT * FROM {$wpdb->prefix}yoone_bundles WHERE id = %d
|
||||||
|
", $this->get_id()));
|
||||||
|
|
||||||
|
if ($data) {
|
||||||
|
$this->set_props(array(
|
||||||
|
'name' => $data->name,
|
||||||
|
'description' => $data->description,
|
||||||
|
'status' => $data->status,
|
||||||
|
'discount_type' => $data->discount_type,
|
||||||
|
'discount_value' => $data->discount_value,
|
||||||
|
'min_quantity' => $data->min_quantity,
|
||||||
|
'max_quantity' => $data->max_quantity,
|
||||||
|
'created_at' => $data->created_at,
|
||||||
|
'updated_at' => $data->updated_at
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建记录
|
||||||
|
*/
|
||||||
|
protected function create() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'name' => $this->get_name('edit'),
|
||||||
|
'description' => $this->get_description('edit'),
|
||||||
|
'status' => $this->get_status('edit'),
|
||||||
|
'discount_type' => $this->get_discount_type('edit'),
|
||||||
|
'discount_value' => $this->get_discount_value('edit'),
|
||||||
|
'min_quantity' => $this->get_min_quantity('edit'),
|
||||||
|
'max_quantity' => $this->get_max_quantity('edit'),
|
||||||
|
'created_at' => current_time('mysql'),
|
||||||
|
'updated_at' => current_time('mysql')
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $wpdb->insert(
|
||||||
|
$wpdb->prefix . 'yoone_bundles',
|
||||||
|
$data,
|
||||||
|
array('%s', '%s', '%s', '%s', '%f', '%d', '%d', '%s', '%s')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
$this->set_id($wpdb->insert_id);
|
||||||
|
$this->save_items();
|
||||||
|
do_action('yoone_bundle_created', $this->get_id(), $this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新记录
|
||||||
|
*/
|
||||||
|
protected function update() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$changes = $this->get_changes();
|
||||||
|
|
||||||
|
if (empty($changes)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$changes['updated_at'] = current_time('mysql');
|
||||||
|
|
||||||
|
$result = $wpdb->update(
|
||||||
|
$wpdb->prefix . 'yoone_bundles',
|
||||||
|
$changes,
|
||||||
|
array('id' => $this->get_id()),
|
||||||
|
null,
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result !== false) {
|
||||||
|
$this->save_items();
|
||||||
|
do_action('yoone_bundle_updated', $this->get_id(), $this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从数据库删除
|
||||||
|
*/
|
||||||
|
protected function delete_from_database($force_delete = false) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// 删除商品
|
||||||
|
$wpdb->delete(
|
||||||
|
$wpdb->prefix . 'yoone_bundle_items',
|
||||||
|
array('bundle_id' => $this->get_id()),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
// 删除混装
|
||||||
|
$result = $wpdb->delete(
|
||||||
|
$wpdb->prefix . 'yoone_bundles',
|
||||||
|
array('id' => $this->get_id()),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
return $result !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置多个属性
|
||||||
|
*/
|
||||||
|
protected function set_props($props) {
|
||||||
|
foreach ($props as $prop => $value) {
|
||||||
|
if (array_key_exists($prop, $this->data)) {
|
||||||
|
$this->data[$prop] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 前端辅助方法
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据产品ID获取套装数据
|
||||||
|
*/
|
||||||
|
public function get_bundle_by_product_id($product_id) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$bundle_data = $wpdb->get_row($wpdb->prepare("
|
||||||
|
SELECT * FROM {$wpdb->prefix}yoone_bundles
|
||||||
|
WHERE product_id = %d AND status = 'active'
|
||||||
|
LIMIT 1
|
||||||
|
", $product_id), ARRAY_A);
|
||||||
|
|
||||||
|
return $bundle_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取套装项目
|
||||||
|
*/
|
||||||
|
public function get_bundle_items($bundle_id = null) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$id = $bundle_id ? $bundle_id : $this->get_id();
|
||||||
|
|
||||||
|
if (!$id) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = $wpdb->get_results($wpdb->prepare("
|
||||||
|
SELECT bi.*, p.post_title as product_name
|
||||||
|
FROM {$wpdb->prefix}yoone_bundle_items bi
|
||||||
|
LEFT JOIN {$wpdb->posts} p ON bi.product_id = p.ID
|
||||||
|
WHERE bi.bundle_id = %d
|
||||||
|
ORDER BY bi.sort_order ASC
|
||||||
|
", $id), ARRAY_A);
|
||||||
|
|
||||||
|
return $items ? $items : array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算套装价格
|
||||||
|
*/
|
||||||
|
public function calculate_bundle_price($selected_items) {
|
||||||
|
if (empty($selected_items)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$total_price = 0;
|
||||||
|
|
||||||
|
// 计算原始总价
|
||||||
|
foreach ($selected_items as $item) {
|
||||||
|
$product = wc_get_product($item['product_id']);
|
||||||
|
if ($product) {
|
||||||
|
$total_price += $product->get_price() * $item['quantity'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用折扣
|
||||||
|
$discount_type = $this->get_discount_type();
|
||||||
|
$discount_value = $this->get_discount_value();
|
||||||
|
|
||||||
|
if ($discount_type === 'percentage' && $discount_value > 0) {
|
||||||
|
$discount_amount = $total_price * ($discount_value / 100);
|
||||||
|
$total_price -= $discount_amount;
|
||||||
|
} elseif ($discount_type === 'fixed' && $discount_value > 0) {
|
||||||
|
$total_price -= $discount_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保价格不为负数
|
||||||
|
return max(0, $total_price);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取关联的产品ID
|
||||||
|
*/
|
||||||
|
public function get_product_id() {
|
||||||
|
return $this->get_prop('product_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置关联的产品ID
|
||||||
|
*/
|
||||||
|
public function set_product_id($product_id) {
|
||||||
|
$this->set_prop('product_id', absint($product_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,870 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 后台管理类
|
||||||
|
*
|
||||||
|
* @package YooneSubscriptions
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yoone_Admin类
|
||||||
|
* 处理后台管理功能
|
||||||
|
*/
|
||||||
|
class Yoone_Admin {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
$this->init_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化钩子
|
||||||
|
*/
|
||||||
|
private function init_hooks() {
|
||||||
|
// 管理菜单
|
||||||
|
add_action('admin_menu', array($this, 'add_admin_menus'));
|
||||||
|
|
||||||
|
// 产品数据面板
|
||||||
|
add_filter('woocommerce_product_data_tabs', array($this, 'add_product_data_tabs'));
|
||||||
|
add_action('woocommerce_product_data_panels', array($this, 'add_product_data_panels'));
|
||||||
|
add_action('woocommerce_process_product_meta', array($this, 'save_product_meta'));
|
||||||
|
|
||||||
|
// 产品类型
|
||||||
|
add_filter('product_type_selector', array($this, 'add_product_types'));
|
||||||
|
|
||||||
|
// 订单管理
|
||||||
|
add_action('add_meta_boxes', array($this, 'add_order_meta_boxes'));
|
||||||
|
add_action('woocommerce_order_status_changed', array($this, 'handle_order_status_change'), 10, 4);
|
||||||
|
|
||||||
|
// 列表表格
|
||||||
|
add_filter('manage_edit-product_columns', array($this, 'add_product_columns'));
|
||||||
|
add_action('manage_product_posts_custom_column', array($this, 'product_column_content'), 10, 2);
|
||||||
|
|
||||||
|
// 设置页面
|
||||||
|
add_filter('woocommerce_settings_tabs_array', array($this, 'add_settings_tab'), 50);
|
||||||
|
add_action('woocommerce_settings_tabs_yoone_subscriptions', array($this, 'settings_tab_content'));
|
||||||
|
add_action('woocommerce_update_options_yoone_subscriptions', array($this, 'update_settings'));
|
||||||
|
|
||||||
|
// 通知
|
||||||
|
add_action('admin_notices', array($this, 'admin_notices'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加管理菜单
|
||||||
|
*/
|
||||||
|
public function add_admin_menus() {
|
||||||
|
// 主菜单
|
||||||
|
add_menu_page(
|
||||||
|
__('Yoone 订阅', 'yoone-subscriptions'),
|
||||||
|
__('Yoone 订阅', 'yoone-subscriptions'),
|
||||||
|
'manage_woocommerce',
|
||||||
|
'yoone-subscriptions',
|
||||||
|
array($this, 'subscriptions_page'),
|
||||||
|
'dashicons-update',
|
||||||
|
56
|
||||||
|
);
|
||||||
|
|
||||||
|
// 订阅管理
|
||||||
|
add_submenu_page(
|
||||||
|
'yoone-subscriptions',
|
||||||
|
__('订阅管理', 'yoone-subscriptions'),
|
||||||
|
__('订阅管理', 'yoone-subscriptions'),
|
||||||
|
'manage_woocommerce',
|
||||||
|
'yoone-subscriptions',
|
||||||
|
array($this, 'subscriptions_page')
|
||||||
|
);
|
||||||
|
|
||||||
|
// 混装产品
|
||||||
|
add_submenu_page(
|
||||||
|
'yoone-subscriptions',
|
||||||
|
__('混装产品', 'yoone-subscriptions'),
|
||||||
|
__('混装产品', 'yoone-subscriptions'),
|
||||||
|
'manage_woocommerce',
|
||||||
|
'yoone-bundles',
|
||||||
|
array($this, 'bundles_page')
|
||||||
|
);
|
||||||
|
|
||||||
|
// 支付设置
|
||||||
|
add_submenu_page(
|
||||||
|
'yoone-subscriptions',
|
||||||
|
__('支付设置', 'yoone-subscriptions'),
|
||||||
|
__('支付设置', 'yoone-subscriptions'),
|
||||||
|
'manage_woocommerce',
|
||||||
|
'yoone-payment-settings',
|
||||||
|
array($this, 'payment_settings_page')
|
||||||
|
);
|
||||||
|
|
||||||
|
// 报告
|
||||||
|
add_submenu_page(
|
||||||
|
'yoone-subscriptions',
|
||||||
|
__('报告', 'yoone-subscriptions'),
|
||||||
|
__('报告', 'yoone-subscriptions'),
|
||||||
|
'manage_woocommerce',
|
||||||
|
'yoone-reports',
|
||||||
|
array($this, 'reports_page')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅管理页面
|
||||||
|
*/
|
||||||
|
public function subscriptions_page() {
|
||||||
|
$action = isset($_GET['action']) ? $_GET['action'] : 'list';
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'view':
|
||||||
|
$this->view_subscription();
|
||||||
|
break;
|
||||||
|
case 'edit':
|
||||||
|
$this->edit_subscription();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$this->list_subscriptions();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅列表
|
||||||
|
*/
|
||||||
|
private function list_subscriptions() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$per_page = 20;
|
||||||
|
$current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
|
||||||
|
$offset = ($current_page - 1) * $per_page;
|
||||||
|
|
||||||
|
$status_filter = isset($_GET['status']) ? $_GET['status'] : '';
|
||||||
|
$search = isset($_GET['s']) ? $_GET['s'] : '';
|
||||||
|
|
||||||
|
$where_conditions = array('1=1');
|
||||||
|
$params = array();
|
||||||
|
|
||||||
|
if ($status_filter) {
|
||||||
|
$where_conditions[] = 'status = %s';
|
||||||
|
$params[] = $status_filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$where_conditions[] = '(id LIKE %s OR customer_id IN (SELECT ID FROM ' . $wpdb->users . ' WHERE display_name LIKE %s OR user_email LIKE %s))';
|
||||||
|
$params[] = '%' . $search . '%';
|
||||||
|
$params[] = '%' . $search . '%';
|
||||||
|
$params[] = '%' . $search . '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
$where_clause = implode(' AND ', $where_conditions);
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
$total_query = "SELECT COUNT(*) FROM {$wpdb->prefix}yoone_subscriptions WHERE {$where_clause}";
|
||||||
|
$total_items = $wpdb->get_var($wpdb->prepare($total_query, $params));
|
||||||
|
|
||||||
|
// 获取订阅列表
|
||||||
|
$params[] = $per_page;
|
||||||
|
$params[] = $offset;
|
||||||
|
$subscriptions_query = "SELECT * FROM {$wpdb->prefix}yoone_subscriptions WHERE {$where_clause} ORDER BY created_at DESC LIMIT %d OFFSET %d";
|
||||||
|
$subscriptions = $wpdb->get_results($wpdb->prepare($subscriptions_query, $params));
|
||||||
|
|
||||||
|
include YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'templates/admin/subscriptions-list.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查看订阅
|
||||||
|
*/
|
||||||
|
private function view_subscription() {
|
||||||
|
$subscription_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
|
||||||
|
|
||||||
|
if (!$subscription_id) {
|
||||||
|
wp_die(__('无效的订阅ID', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$subscription = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_subscriptions WHERE id = %d",
|
||||||
|
$subscription_id
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$subscription) {
|
||||||
|
wp_die(__('订阅不存在', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订阅项目
|
||||||
|
$items = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_subscription_items WHERE subscription_id = %d",
|
||||||
|
$subscription_id
|
||||||
|
));
|
||||||
|
|
||||||
|
// 获取日志
|
||||||
|
$logs = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_subscription_logs WHERE subscription_id = %d ORDER BY created_at DESC LIMIT 50",
|
||||||
|
$subscription_id
|
||||||
|
));
|
||||||
|
|
||||||
|
include YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'templates/admin/subscription-view.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑订阅
|
||||||
|
*/
|
||||||
|
private function edit_subscription() {
|
||||||
|
$subscription_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
|
||||||
|
|
||||||
|
if (!$subscription_id) {
|
||||||
|
wp_die(__('无效的订阅ID', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理表单提交
|
||||||
|
if (isset($_POST['submit'])) {
|
||||||
|
$this->save_subscription($subscription_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$subscription = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_subscriptions WHERE id = %d",
|
||||||
|
$subscription_id
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$subscription) {
|
||||||
|
wp_die(__('订阅不存在', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
include YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'templates/admin/subscription-edit.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存订阅
|
||||||
|
*/
|
||||||
|
private function save_subscription($subscription_id) {
|
||||||
|
if (!wp_verify_nonce($_POST['_wpnonce'], 'edit_subscription_' . $subscription_id)) {
|
||||||
|
wp_die(__('安全验证失败', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$update_data = array(
|
||||||
|
'status' => sanitize_text_field($_POST['status']),
|
||||||
|
'next_payment_date' => sanitize_text_field($_POST['next_payment_date']),
|
||||||
|
'updated_at' => current_time('mysql')
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $wpdb->update(
|
||||||
|
$wpdb->prefix . 'yoone_subscriptions',
|
||||||
|
$update_data,
|
||||||
|
array('id' => $subscription_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result !== false) {
|
||||||
|
add_action('admin_notices', function() {
|
||||||
|
echo '<div class="notice notice-success"><p>' . __('订阅已更新', 'yoone-subscriptions') . '</p></div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 混装产品页面
|
||||||
|
*/
|
||||||
|
public function bundles_page() {
|
||||||
|
$action = isset($_GET['action']) ? $_GET['action'] : 'list';
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'add':
|
||||||
|
$this->add_bundle();
|
||||||
|
break;
|
||||||
|
case 'edit':
|
||||||
|
$this->edit_bundle();
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
$this->delete_bundle();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$this->list_bundles();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加混装产品
|
||||||
|
*/
|
||||||
|
private function add_bundle() {
|
||||||
|
if (isset($_POST['submit'])) {
|
||||||
|
$this->save_bundle();
|
||||||
|
}
|
||||||
|
|
||||||
|
include YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'templates/admin/bundle-add.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑混装产品
|
||||||
|
*/
|
||||||
|
private function edit_bundle() {
|
||||||
|
$bundle_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
|
||||||
|
|
||||||
|
if (!$bundle_id) {
|
||||||
|
wp_die(__('无效的混装产品ID', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_POST['submit'])) {
|
||||||
|
$this->save_bundle($bundle_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$bundle = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_bundles WHERE id = %d",
|
||||||
|
$bundle_id
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$bundle) {
|
||||||
|
wp_die(__('混装产品不存在', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_bundle_items WHERE bundle_id = %d ORDER BY sort_order",
|
||||||
|
$bundle_id
|
||||||
|
));
|
||||||
|
|
||||||
|
include YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'templates/admin/bundle-edit.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除混装产品
|
||||||
|
*/
|
||||||
|
private function delete_bundle() {
|
||||||
|
$bundle_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
|
||||||
|
|
||||||
|
if (!$bundle_id) {
|
||||||
|
wp_die(__('无效的混装产品ID', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wp_verify_nonce($_GET['_wpnonce'], 'delete_bundle_' . $bundle_id)) {
|
||||||
|
wp_die(__('安全验证失败', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// 删除混装项目
|
||||||
|
$wpdb->delete(
|
||||||
|
$wpdb->prefix . 'yoone_bundle_items',
|
||||||
|
array('bundle_id' => $bundle_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 删除混装产品
|
||||||
|
$result = $wpdb->delete(
|
||||||
|
$wpdb->prefix . 'yoone_bundles',
|
||||||
|
array('id' => $bundle_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
wp_redirect(admin_url('admin.php?page=yoone-bundles&deleted=1'));
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
wp_die(__('删除失败', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存混装产品
|
||||||
|
*/
|
||||||
|
private function save_bundle($bundle_id = 0) {
|
||||||
|
$nonce_action = $bundle_id ? 'edit_bundle_' . $bundle_id : 'add_bundle';
|
||||||
|
|
||||||
|
if (!wp_verify_nonce($_POST['_wpnonce'], $nonce_action)) {
|
||||||
|
wp_die(__('安全验证失败', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$bundle_data = array(
|
||||||
|
'name' => sanitize_text_field($_POST['name']),
|
||||||
|
'description' => sanitize_textarea_field($_POST['description']),
|
||||||
|
'discount_type' => sanitize_text_field($_POST['discount_type']),
|
||||||
|
'discount_value' => floatval($_POST['discount_value']),
|
||||||
|
'status' => sanitize_text_field($_POST['status']),
|
||||||
|
'updated_at' => current_time('mysql')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($bundle_id) {
|
||||||
|
// 更新现有混装产品
|
||||||
|
$result = $wpdb->update(
|
||||||
|
$wpdb->prefix . 'yoone_bundles',
|
||||||
|
$bundle_data,
|
||||||
|
array('id' => $bundle_id)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 创建新混装产品
|
||||||
|
$bundle_data['created_at'] = current_time('mysql');
|
||||||
|
$result = $wpdb->insert(
|
||||||
|
$wpdb->prefix . 'yoone_bundles',
|
||||||
|
$bundle_data
|
||||||
|
);
|
||||||
|
$bundle_id = $wpdb->insert_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($result !== false) {
|
||||||
|
// 保存混装项目
|
||||||
|
if (isset($_POST['bundle_items']) && is_array($_POST['bundle_items'])) {
|
||||||
|
// 删除现有项目
|
||||||
|
$wpdb->delete(
|
||||||
|
$wpdb->prefix . 'yoone_bundle_items',
|
||||||
|
array('bundle_id' => $bundle_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 添加新项目
|
||||||
|
foreach ($_POST['bundle_items'] as $sort_order => $item) {
|
||||||
|
$wpdb->insert(
|
||||||
|
$wpdb->prefix . 'yoone_bundle_items',
|
||||||
|
array(
|
||||||
|
'bundle_id' => $bundle_id,
|
||||||
|
'product_id' => intval($item['product_id']),
|
||||||
|
'quantity' => intval($item['quantity']),
|
||||||
|
'sort_order' => intval($sort_order)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_redirect(admin_url('admin.php?page=yoone-bundles&saved=1'));
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
wp_die(__('保存失败', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 混装产品列表
|
||||||
|
*/
|
||||||
|
private function list_bundles() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$bundles = $wpdb->get_results(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_bundles ORDER BY created_at DESC"
|
||||||
|
);
|
||||||
|
|
||||||
|
include YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'templates/admin/bundles-list.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付设置页面
|
||||||
|
*/
|
||||||
|
public function payment_settings_page() {
|
||||||
|
if (isset($_POST['submit'])) {
|
||||||
|
$this->save_payment_settings();
|
||||||
|
}
|
||||||
|
|
||||||
|
include YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'templates/admin/payment-settings.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存支付设置
|
||||||
|
*/
|
||||||
|
private function save_payment_settings() {
|
||||||
|
if (!wp_verify_nonce($_POST['_wpnonce'], 'yoone_payment_settings')) {
|
||||||
|
wp_die(__('安全验证失败', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = array(
|
||||||
|
'yoone_moneris_enabled' => isset($_POST['moneris_enabled']) ? 'yes' : 'no',
|
||||||
|
'yoone_moneris_testmode' => isset($_POST['moneris_testmode']) ? 'yes' : 'no',
|
||||||
|
'yoone_moneris_store_id' => sanitize_text_field($_POST['moneris_store_id']),
|
||||||
|
'yoone_moneris_api_token' => sanitize_text_field($_POST['moneris_api_token']),
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($settings as $key => $value) {
|
||||||
|
update_option($key, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
add_action('admin_notices', function() {
|
||||||
|
echo '<div class="notice notice-success"><p>' . __('设置已保存', 'yoone-subscriptions') . '</p></div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 报告页面
|
||||||
|
*/
|
||||||
|
public function reports_page() {
|
||||||
|
include YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'templates/admin/reports.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加产品数据标签
|
||||||
|
*/
|
||||||
|
public function add_product_data_tabs($tabs) {
|
||||||
|
$tabs['yoone_subscription'] = array(
|
||||||
|
'label' => __('订阅设置', 'yoone-subscriptions'),
|
||||||
|
'target' => 'yoone_subscription_data',
|
||||||
|
'class' => array('show_if_simple', 'show_if_variable'),
|
||||||
|
);
|
||||||
|
|
||||||
|
$tabs['yoone_bundle'] = array(
|
||||||
|
'label' => __('混装设置', 'yoone-subscriptions'),
|
||||||
|
'target' => 'yoone_bundle_data',
|
||||||
|
'class' => array('show_if_yoone_bundle'),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加产品数据面板
|
||||||
|
*/
|
||||||
|
public function add_product_data_panels() {
|
||||||
|
global $post;
|
||||||
|
|
||||||
|
// 订阅设置面板
|
||||||
|
echo '<div id="yoone_subscription_data" class="panel woocommerce_options_panel">';
|
||||||
|
|
||||||
|
woocommerce_wp_checkbox(array(
|
||||||
|
'id' => '_yoone_subscription_enabled',
|
||||||
|
'label' => __('启用订阅', 'yoone-subscriptions'),
|
||||||
|
'description' => __('将此产品设置为订阅产品', 'yoone-subscriptions'),
|
||||||
|
));
|
||||||
|
|
||||||
|
woocommerce_wp_select(array(
|
||||||
|
'id' => '_yoone_subscription_trial_period',
|
||||||
|
'label' => __('试用期', 'yoone-subscriptions'),
|
||||||
|
'options' => array(
|
||||||
|
'' => __('无试用期', 'yoone-subscriptions'),
|
||||||
|
'7' => __('7天', 'yoone-subscriptions'),
|
||||||
|
'14' => __('14天', 'yoone-subscriptions'),
|
||||||
|
'30' => __('30天', 'yoone-subscriptions'),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
echo '<div class="yoone-subscription-periods">';
|
||||||
|
echo '<h4>' . __('订阅周期', 'yoone-subscriptions') . '</h4>';
|
||||||
|
|
||||||
|
$periods = get_post_meta($post->ID, '_yoone_subscription_periods', true);
|
||||||
|
if (!is_array($periods)) {
|
||||||
|
$periods = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<div id="yoone-subscription-periods-container">';
|
||||||
|
foreach ($periods as $key => $period) {
|
||||||
|
$this->render_subscription_period_row($key, $period);
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
echo '<button type="button" class="button" id="add-subscription-period">' . __('添加周期', 'yoone-subscriptions') . '</button>';
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
// 混装设置面板
|
||||||
|
echo '<div id="yoone_bundle_data" class="panel woocommerce_options_panel">';
|
||||||
|
|
||||||
|
woocommerce_wp_text_input(array(
|
||||||
|
'id' => '_yoone_bundle_min_quantity',
|
||||||
|
'label' => __('最小数量', 'yoone-subscriptions'),
|
||||||
|
'type' => 'number',
|
||||||
|
'custom_attributes' => array('min' => '1'),
|
||||||
|
));
|
||||||
|
|
||||||
|
woocommerce_wp_select(array(
|
||||||
|
'id' => '_yoone_bundle_discount_type',
|
||||||
|
'label' => __('折扣类型', 'yoone-subscriptions'),
|
||||||
|
'options' => array(
|
||||||
|
'percentage' => __('百分比', 'yoone-subscriptions'),
|
||||||
|
'fixed' => __('固定金额', 'yoone-subscriptions'),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
woocommerce_wp_text_input(array(
|
||||||
|
'id' => '_yoone_bundle_discount_value',
|
||||||
|
'label' => __('折扣值', 'yoone-subscriptions'),
|
||||||
|
'type' => 'number',
|
||||||
|
'custom_attributes' => array('step' => '0.01', 'min' => '0'),
|
||||||
|
));
|
||||||
|
|
||||||
|
echo '<div class="yoone-bundle-items">';
|
||||||
|
echo '<h4>' . __('混装项目', 'yoone-subscriptions') . '</h4>';
|
||||||
|
echo '<div id="yoone-bundle-items-container"></div>';
|
||||||
|
echo '<button type="button" class="button" id="search-bundle-products">' . __('添加产品', 'yoone-subscriptions') . '</button>';
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染订阅周期行
|
||||||
|
*/
|
||||||
|
private function render_subscription_period_row($key, $period) {
|
||||||
|
echo '<div class="subscription-period-row">';
|
||||||
|
echo '<input type="text" name="_yoone_subscription_periods[' . $key . '][name]" value="' . esc_attr($period['name']) . '" placeholder="' . __('周期名称', 'yoone-subscriptions') . '">';
|
||||||
|
echo '<input type="number" name="_yoone_subscription_periods[' . $key . '][interval]" value="' . esc_attr($period['interval']) . '" placeholder="' . __('间隔', 'yoone-subscriptions') . '" min="1">';
|
||||||
|
echo '<select name="_yoone_subscription_periods[' . $key . '][period]">';
|
||||||
|
echo '<option value="day"' . selected($period['period'], 'day', false) . '>' . __('天', 'yoone-subscriptions') . '</option>';
|
||||||
|
echo '<option value="week"' . selected($period['period'], 'week', false) . '>' . __('周', 'yoone-subscriptions') . '</option>';
|
||||||
|
echo '<option value="month"' . selected($period['period'], 'month', false) . '>' . __('月', 'yoone-subscriptions') . '</option>';
|
||||||
|
echo '<option value="year"' . selected($period['period'], 'year', false) . '>' . __('年', 'yoone-subscriptions') . '</option>';
|
||||||
|
echo '</select>';
|
||||||
|
echo '<input type="number" name="_yoone_subscription_periods[' . $key . '][discount_value]" value="' . esc_attr($period['discount_value']) . '" placeholder="' . __('折扣值', 'yoone-subscriptions') . '" step="0.01" min="0">';
|
||||||
|
echo '<button type="button" class="button remove-period">' . __('删除', 'yoone-subscriptions') . '</button>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存产品元数据
|
||||||
|
*/
|
||||||
|
public function save_product_meta($post_id) {
|
||||||
|
// 保存订阅设置
|
||||||
|
$subscription_enabled = isset($_POST['_yoone_subscription_enabled']) ? 'yes' : 'no';
|
||||||
|
update_post_meta($post_id, '_yoone_subscription_enabled', $subscription_enabled);
|
||||||
|
|
||||||
|
if (isset($_POST['_yoone_subscription_trial_period'])) {
|
||||||
|
update_post_meta($post_id, '_yoone_subscription_trial_period', sanitize_text_field($_POST['_yoone_subscription_trial_period']));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_POST['_yoone_subscription_periods'])) {
|
||||||
|
update_post_meta($post_id, '_yoone_subscription_periods', $_POST['_yoone_subscription_periods']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存混装设置
|
||||||
|
if (isset($_POST['_yoone_bundle_min_quantity'])) {
|
||||||
|
update_post_meta($post_id, '_yoone_bundle_min_quantity', intval($_POST['_yoone_bundle_min_quantity']));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_POST['_yoone_bundle_discount_type'])) {
|
||||||
|
update_post_meta($post_id, '_yoone_bundle_discount_type', sanitize_text_field($_POST['_yoone_bundle_discount_type']));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_POST['_yoone_bundle_discount_value'])) {
|
||||||
|
update_post_meta($post_id, '_yoone_bundle_discount_value', floatval($_POST['_yoone_bundle_discount_value']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加产品类型
|
||||||
|
*/
|
||||||
|
public function add_product_types($types) {
|
||||||
|
$types['yoone_bundle'] = __('混装产品', 'yoone-subscriptions');
|
||||||
|
return $types;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加订单元框
|
||||||
|
*/
|
||||||
|
public function add_order_meta_boxes() {
|
||||||
|
add_meta_box(
|
||||||
|
'yoone-subscription-info',
|
||||||
|
__('订阅信息', 'yoone-subscriptions'),
|
||||||
|
array($this, 'order_subscription_meta_box'),
|
||||||
|
'shop_order',
|
||||||
|
'side',
|
||||||
|
'default'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单订阅信息元框
|
||||||
|
*/
|
||||||
|
public function order_subscription_meta_box($post) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$subscriptions = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_subscriptions WHERE parent_order_id = %d",
|
||||||
|
$post->ID
|
||||||
|
));
|
||||||
|
|
||||||
|
if (empty($subscriptions)) {
|
||||||
|
echo '<p>' . __('此订单没有关联的订阅', 'yoone-subscriptions') . '</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($subscriptions as $subscription) {
|
||||||
|
echo '<div class="subscription-info">';
|
||||||
|
echo '<p><strong>' . __('订阅ID:', 'yoone-subscriptions') . '</strong> ' . $subscription->id . '</p>';
|
||||||
|
echo '<p><strong>' . __('状态:', 'yoone-subscriptions') . '</strong> ' . Yoone_Frontend::get_subscription_status_label($subscription->status) . '</p>';
|
||||||
|
echo '<p><strong>' . __('下次付款:', 'yoone-subscriptions') . '</strong> ' . $subscription->next_payment_date . '</p>';
|
||||||
|
echo '<p><a href="' . admin_url('admin.php?page=yoone-subscriptions&action=view&id=' . $subscription->id) . '" class="button">' . __('查看详情', 'yoone-subscriptions') . '</a></p>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理订单状态变化
|
||||||
|
*/
|
||||||
|
public function handle_order_status_change($order_id, $old_status, $new_status, $order) {
|
||||||
|
if ($new_status === 'completed') {
|
||||||
|
$this->create_subscription_from_order($order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从订单创建订阅
|
||||||
|
*/
|
||||||
|
private function create_subscription_from_order($order) {
|
||||||
|
foreach ($order->get_items() as $item) {
|
||||||
|
$product_id = $item->get_product_id();
|
||||||
|
$subscription_enabled = get_post_meta($product_id, '_yoone_subscription_enabled', true);
|
||||||
|
|
||||||
|
if ($subscription_enabled === 'yes') {
|
||||||
|
$subscription_period = $item->get_meta('_yoone_subscription_period');
|
||||||
|
if ($subscription_period) {
|
||||||
|
$this->create_subscription($order, $item, $subscription_period);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建订阅
|
||||||
|
*/
|
||||||
|
private function create_subscription($order, $item, $period) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$subscription_data = array(
|
||||||
|
'customer_id' => $order->get_customer_id(),
|
||||||
|
'parent_order_id' => $order->get_id(),
|
||||||
|
'status' => 'active',
|
||||||
|
'billing_period' => $period['period'],
|
||||||
|
'billing_interval' => $period['interval'],
|
||||||
|
'total' => $item->get_total(),
|
||||||
|
'currency' => $order->get_currency(),
|
||||||
|
'payment_method' => $order->get_payment_method(),
|
||||||
|
'payment_method_title' => $order->get_payment_method_title(),
|
||||||
|
'next_payment_date' => $this->calculate_next_payment_date($period),
|
||||||
|
'start_date' => current_time('mysql'),
|
||||||
|
'created_at' => current_time('mysql'),
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $wpdb->insert(
|
||||||
|
$wpdb->prefix . 'yoone_subscriptions',
|
||||||
|
$subscription_data
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
$subscription_id = $wpdb->insert_id;
|
||||||
|
|
||||||
|
// 创建订阅项目
|
||||||
|
$wpdb->insert(
|
||||||
|
$wpdb->prefix . 'yoone_subscription_items',
|
||||||
|
array(
|
||||||
|
'subscription_id' => $subscription_id,
|
||||||
|
'product_id' => $item->get_product_id(),
|
||||||
|
'variation_id' => $item->get_variation_id(),
|
||||||
|
'quantity' => $item->get_quantity(),
|
||||||
|
'price' => $item->get_total() / $item->get_quantity(),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 记录日志
|
||||||
|
Yoone_Logger::info("从订单 {$order->get_id()} 创建订阅 {$subscription_id}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算下次付款日期
|
||||||
|
*/
|
||||||
|
private function calculate_next_payment_date($period) {
|
||||||
|
$interval = $period['interval'];
|
||||||
|
$period_type = $period['period'];
|
||||||
|
|
||||||
|
switch ($period_type) {
|
||||||
|
case 'day':
|
||||||
|
return date('Y-m-d H:i:s', strtotime("+{$interval} days"));
|
||||||
|
case 'week':
|
||||||
|
return date('Y-m-d H:i:s', strtotime("+{$interval} weeks"));
|
||||||
|
case 'month':
|
||||||
|
return date('Y-m-d H:i:s', strtotime("+{$interval} months"));
|
||||||
|
case 'year':
|
||||||
|
return date('Y-m-d H:i:s', strtotime("+{$interval} years"));
|
||||||
|
default:
|
||||||
|
return date('Y-m-d H:i:s', strtotime("+1 month"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加产品列
|
||||||
|
*/
|
||||||
|
public function add_product_columns($columns) {
|
||||||
|
$columns['yoone_subscription'] = __('订阅', 'yoone-subscriptions');
|
||||||
|
$columns['yoone_bundle'] = __('混装', 'yoone-subscriptions');
|
||||||
|
return $columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 产品列内容
|
||||||
|
*/
|
||||||
|
public function product_column_content($column, $post_id) {
|
||||||
|
switch ($column) {
|
||||||
|
case 'yoone_subscription':
|
||||||
|
$enabled = get_post_meta($post_id, '_yoone_subscription_enabled', true);
|
||||||
|
echo $enabled === 'yes' ? '<span class="dashicons dashicons-yes-alt" style="color: green;"></span>' : '—';
|
||||||
|
break;
|
||||||
|
case 'yoone_bundle':
|
||||||
|
$product = wc_get_product($post_id);
|
||||||
|
echo $product && $product->get_type() === 'yoone_bundle' ? '<span class="dashicons dashicons-yes-alt" style="color: green;"></span>' : '—';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加设置标签
|
||||||
|
*/
|
||||||
|
public function add_settings_tab($settings_tabs) {
|
||||||
|
$settings_tabs['yoone_subscriptions'] = __('Yoone 订阅', 'yoone-subscriptions');
|
||||||
|
return $settings_tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置标签内容
|
||||||
|
*/
|
||||||
|
public function settings_tab_content() {
|
||||||
|
woocommerce_admin_fields($this->get_settings());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新设置
|
||||||
|
*/
|
||||||
|
public function update_settings() {
|
||||||
|
woocommerce_update_options($this->get_settings());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设置字段
|
||||||
|
*/
|
||||||
|
private function get_settings() {
|
||||||
|
return array(
|
||||||
|
'section_title' => array(
|
||||||
|
'name' => __('Yoone 订阅设置', 'yoone-subscriptions'),
|
||||||
|
'type' => 'title',
|
||||||
|
'desc' => '',
|
||||||
|
'id' => 'yoone_subscriptions_section_title'
|
||||||
|
),
|
||||||
|
'subscription_enabled' => array(
|
||||||
|
'name' => __('启用订阅功能', 'yoone-subscriptions'),
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'desc' => __('启用订阅功能', 'yoone-subscriptions'),
|
||||||
|
'id' => 'yoone_subscriptions_enabled'
|
||||||
|
),
|
||||||
|
'bundle_enabled' => array(
|
||||||
|
'name' => __('启用混装功能', 'yoone-subscriptions'),
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'desc' => __('启用混装产品功能', 'yoone-subscriptions'),
|
||||||
|
'id' => 'yoone_bundles_enabled'
|
||||||
|
),
|
||||||
|
'section_end' => array(
|
||||||
|
'type' => 'sectionend',
|
||||||
|
'id' => 'yoone_subscriptions_section_end'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理通知
|
||||||
|
*/
|
||||||
|
public function admin_notices() {
|
||||||
|
// 检查WooCommerce依赖
|
||||||
|
if (!class_exists('WooCommerce')) {
|
||||||
|
echo '<div class="notice notice-error"><p>';
|
||||||
|
echo __('Yoone Subscriptions 需要 WooCommerce 插件才能正常工作。', 'yoone-subscriptions');
|
||||||
|
echo '</p></div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,384 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AJAX处理类
|
||||||
|
*
|
||||||
|
* @package YooneSubscriptions
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yoone_Ajax类
|
||||||
|
* 处理所有AJAX请求
|
||||||
|
*/
|
||||||
|
class Yoone_Ajax {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
$this->init_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化钩子
|
||||||
|
*/
|
||||||
|
private function init_hooks() {
|
||||||
|
// 前端AJAX钩子
|
||||||
|
add_action('wp_ajax_yoone_calculate_bundle_price', array($this, 'calculate_bundle_price'));
|
||||||
|
add_action('wp_ajax_nopriv_yoone_calculate_bundle_price', array($this, 'calculate_bundle_price'));
|
||||||
|
|
||||||
|
add_action('wp_ajax_yoone_add_bundle_to_cart', array($this, 'add_bundle_to_cart'));
|
||||||
|
add_action('wp_ajax_nopriv_yoone_add_bundle_to_cart', array($this, 'add_bundle_to_cart'));
|
||||||
|
|
||||||
|
add_action('wp_ajax_yoone_update_subscription', array($this, 'update_subscription'));
|
||||||
|
add_action('wp_ajax_yoone_cancel_subscription', array($this, 'cancel_subscription'));
|
||||||
|
add_action('wp_ajax_yoone_pause_subscription', array($this, 'pause_subscription'));
|
||||||
|
add_action('wp_ajax_yoone_resume_subscription', array($this, 'resume_subscription'));
|
||||||
|
|
||||||
|
// 后端AJAX钩子
|
||||||
|
add_action('wp_ajax_yoone_search_products', array($this, 'search_products'));
|
||||||
|
add_action('wp_ajax_yoone_add_bundle_item', array($this, 'add_bundle_item'));
|
||||||
|
add_action('wp_ajax_yoone_remove_bundle_item', array($this, 'remove_bundle_item'));
|
||||||
|
add_action('wp_ajax_yoone_test_moneris_connection', array($this, 'test_moneris_connection'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算混装价格
|
||||||
|
*/
|
||||||
|
public function calculate_bundle_price() {
|
||||||
|
check_ajax_referer('yoone_frontend_nonce', 'nonce');
|
||||||
|
|
||||||
|
$product_id = intval($_POST['product_id']);
|
||||||
|
$quantities = isset($_POST['quantities']) ? array_map('intval', $_POST['quantities']) : array();
|
||||||
|
|
||||||
|
if (!$product_id) {
|
||||||
|
wp_send_json_error('无效的产品ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
$bundle = new Yoone_Bundle();
|
||||||
|
$bundle_data = $bundle->get_bundle_by_product_id($product_id);
|
||||||
|
|
||||||
|
if (!$bundle_data) {
|
||||||
|
wp_send_json_error('未找到混装数据');
|
||||||
|
}
|
||||||
|
|
||||||
|
$total_price = $bundle->calculate_bundle_price($product_id, $quantities);
|
||||||
|
|
||||||
|
wp_send_json_success(array(
|
||||||
|
'total_price' => $total_price,
|
||||||
|
'formatted_price' => wc_price($total_price)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加混装到购物车
|
||||||
|
*/
|
||||||
|
public function add_bundle_to_cart() {
|
||||||
|
check_ajax_referer('yoone_frontend_nonce', 'nonce');
|
||||||
|
|
||||||
|
$product_id = intval($_POST['product_id']);
|
||||||
|
$quantities = isset($_POST['quantities']) ? array_map('intval', $_POST['quantities']) : array();
|
||||||
|
|
||||||
|
if (!$product_id) {
|
||||||
|
wp_send_json_error('无效的产品ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
$bundle = new Yoone_Bundle();
|
||||||
|
$bundle_data = $bundle->get_bundle_by_product_id($product_id);
|
||||||
|
|
||||||
|
if (!$bundle_data) {
|
||||||
|
wp_send_json_error('未找到混装数据');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加混装到购物车
|
||||||
|
$cart_item_key = WC()->cart->add_to_cart($product_id, 1, 0, array(), array(
|
||||||
|
'yoone_bundle' => true,
|
||||||
|
'yoone_bundle_items' => $quantities
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($cart_item_key) {
|
||||||
|
wp_send_json_success(array(
|
||||||
|
'message' => '混装产品已添加到购物车',
|
||||||
|
'cart_count' => WC()->cart->get_cart_contents_count()
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
wp_send_json_error('添加到购物车失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新订阅
|
||||||
|
*/
|
||||||
|
public function update_subscription() {
|
||||||
|
check_ajax_referer('yoone_frontend_nonce', 'nonce');
|
||||||
|
|
||||||
|
if (!is_user_logged_in()) {
|
||||||
|
wp_send_json_error('请先登录');
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscription_id = intval($_POST['subscription_id']);
|
||||||
|
$action = sanitize_text_field($_POST['action_type']);
|
||||||
|
|
||||||
|
if (!$subscription_id) {
|
||||||
|
wp_send_json_error('无效的订阅ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscription = new Yoone_Subscription($subscription_id);
|
||||||
|
|
||||||
|
if (!$subscription->get_id() || $subscription->get_customer_id() !== get_current_user_id()) {
|
||||||
|
wp_send_json_error('订阅不存在或无权限');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = false;
|
||||||
|
$message = '';
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'pause':
|
||||||
|
$result = $subscription->pause();
|
||||||
|
$message = $result ? '订阅已暂停' : '暂停订阅失败';
|
||||||
|
break;
|
||||||
|
case 'resume':
|
||||||
|
$result = $subscription->resume();
|
||||||
|
$message = $result ? '订阅已恢复' : '恢复订阅失败';
|
||||||
|
break;
|
||||||
|
case 'cancel':
|
||||||
|
$result = $subscription->cancel();
|
||||||
|
$message = $result ? '订阅已取消' : '取消订阅失败';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
wp_send_json_error('无效的操作');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
wp_send_json_success(array(
|
||||||
|
'message' => $message,
|
||||||
|
'status' => $subscription->get_status()
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
wp_send_json_error($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消订阅
|
||||||
|
*/
|
||||||
|
public function cancel_subscription() {
|
||||||
|
check_ajax_referer('yoone_frontend_nonce', 'nonce');
|
||||||
|
|
||||||
|
if (!is_user_logged_in()) {
|
||||||
|
wp_send_json_error('请先登录');
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscription_id = intval($_POST['subscription_id']);
|
||||||
|
$reason = sanitize_textarea_field($_POST['reason']);
|
||||||
|
|
||||||
|
if (!$subscription_id) {
|
||||||
|
wp_send_json_error('无效的订阅ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscription = new Yoone_Subscription($subscription_id);
|
||||||
|
|
||||||
|
if (!$subscription->get_id() || $subscription->get_customer_id() !== get_current_user_id()) {
|
||||||
|
wp_send_json_error('订阅不存在或无权限');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $subscription->cancel($reason);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
wp_send_json_success(array(
|
||||||
|
'message' => '订阅已成功取消',
|
||||||
|
'status' => $subscription->get_status()
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
wp_send_json_error('取消订阅失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 暂停订阅
|
||||||
|
*/
|
||||||
|
public function pause_subscription() {
|
||||||
|
check_ajax_referer('yoone_frontend_nonce', 'nonce');
|
||||||
|
|
||||||
|
if (!is_user_logged_in()) {
|
||||||
|
wp_send_json_error('请先登录');
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscription_id = intval($_POST['subscription_id']);
|
||||||
|
|
||||||
|
if (!$subscription_id) {
|
||||||
|
wp_send_json_error('无效的订阅ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscription = new Yoone_Subscription($subscription_id);
|
||||||
|
|
||||||
|
if (!$subscription->get_id() || $subscription->get_customer_id() !== get_current_user_id()) {
|
||||||
|
wp_send_json_error('订阅不存在或无权限');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $subscription->pause();
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
wp_send_json_success(array(
|
||||||
|
'message' => '订阅已暂停',
|
||||||
|
'status' => $subscription->get_status()
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
wp_send_json_error('暂停订阅失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复订阅
|
||||||
|
*/
|
||||||
|
public function resume_subscription() {
|
||||||
|
check_ajax_referer('yoone_frontend_nonce', 'nonce');
|
||||||
|
|
||||||
|
if (!is_user_logged_in()) {
|
||||||
|
wp_send_json_error('请先登录');
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscription_id = intval($_POST['subscription_id']);
|
||||||
|
|
||||||
|
if (!$subscription_id) {
|
||||||
|
wp_send_json_error('无效的订阅ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscription = new Yoone_Subscription($subscription_id);
|
||||||
|
|
||||||
|
if (!$subscription->get_id() || $subscription->get_customer_id() !== get_current_user_id()) {
|
||||||
|
wp_send_json_error('订阅不存在或无权限');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $subscription->resume();
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
wp_send_json_success(array(
|
||||||
|
'message' => '订阅已恢复',
|
||||||
|
'status' => $subscription->get_status()
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
wp_send_json_error('恢复订阅失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索产品(后台使用)
|
||||||
|
*/
|
||||||
|
public function search_products() {
|
||||||
|
check_ajax_referer('yoone_admin_nonce', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_woocommerce')) {
|
||||||
|
wp_send_json_error('权限不足');
|
||||||
|
}
|
||||||
|
|
||||||
|
$term = sanitize_text_field($_POST['term']);
|
||||||
|
|
||||||
|
if (strlen($term) < 2) {
|
||||||
|
wp_send_json_error('搜索词太短');
|
||||||
|
}
|
||||||
|
|
||||||
|
$products = wc_get_products(array(
|
||||||
|
'limit' => 20,
|
||||||
|
'status' => 'publish',
|
||||||
|
's' => $term,
|
||||||
|
'return' => 'objects'
|
||||||
|
));
|
||||||
|
|
||||||
|
$results = array();
|
||||||
|
foreach ($products as $product) {
|
||||||
|
$results[] = array(
|
||||||
|
'id' => $product->get_id(),
|
||||||
|
'text' => $product->get_name() . ' (#' . $product->get_id() . ')',
|
||||||
|
'price' => $product->get_price()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success($results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加混装商品项
|
||||||
|
*/
|
||||||
|
public function add_bundle_item() {
|
||||||
|
check_ajax_referer('yoone_admin_nonce', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_woocommerce')) {
|
||||||
|
wp_send_json_error('权限不足');
|
||||||
|
}
|
||||||
|
|
||||||
|
$bundle_id = intval($_POST['bundle_id']);
|
||||||
|
$product_id = intval($_POST['product_id']);
|
||||||
|
$quantity = intval($_POST['quantity']);
|
||||||
|
|
||||||
|
if (!$bundle_id || !$product_id || !$quantity) {
|
||||||
|
wp_send_json_error('参数无效');
|
||||||
|
}
|
||||||
|
|
||||||
|
$bundle = new Yoone_Bundle($bundle_id);
|
||||||
|
$result = $bundle->add_item($product_id, $quantity);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
wp_send_json_success('商品已添加到混装');
|
||||||
|
} else {
|
||||||
|
wp_send_json_error('添加失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除混装商品项
|
||||||
|
*/
|
||||||
|
public function remove_bundle_item() {
|
||||||
|
check_ajax_referer('yoone_admin_nonce', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_woocommerce')) {
|
||||||
|
wp_send_json_error('权限不足');
|
||||||
|
}
|
||||||
|
|
||||||
|
$item_id = intval($_POST['item_id']);
|
||||||
|
|
||||||
|
if (!$item_id) {
|
||||||
|
wp_send_json_error('参数无效');
|
||||||
|
}
|
||||||
|
|
||||||
|
$bundle = new Yoone_Bundle();
|
||||||
|
$result = $bundle->remove_item($item_id);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
wp_send_json_success('商品已从混装中移除');
|
||||||
|
} else {
|
||||||
|
wp_send_json_error('移除失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试 Moneris 连接
|
||||||
|
*/
|
||||||
|
public function test_moneris_connection() {
|
||||||
|
check_ajax_referer('yoone_admin_nonce', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_woocommerce')) {
|
||||||
|
wp_send_json_error('权限不足');
|
||||||
|
}
|
||||||
|
|
||||||
|
$store_id = sanitize_text_field($_POST['store_id']);
|
||||||
|
$api_token = sanitize_text_field($_POST['api_token']);
|
||||||
|
$testmode = isset($_POST['testmode']) && $_POST['testmode'] === 'yes';
|
||||||
|
|
||||||
|
if (!$store_id || !$api_token) {
|
||||||
|
wp_send_json_error('Store ID 和 API Token 不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$gateway = new Yoone_Moneris_Gateway();
|
||||||
|
$result = $gateway->test_connection($store_id, $api_token, $testmode);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
wp_send_json_success('Moneris 连接测试成功');
|
||||||
|
} else {
|
||||||
|
wp_send_json_error('Moneris 连接测试失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,533 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API类
|
||||||
|
*
|
||||||
|
* @package YooneSubscriptions
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yoone_API类
|
||||||
|
* 处理REST API端点和内部API调用
|
||||||
|
*/
|
||||||
|
class Yoone_API {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API版本
|
||||||
|
*/
|
||||||
|
const API_VERSION = 'v1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API命名空间
|
||||||
|
*/
|
||||||
|
const API_NAMESPACE = 'yoone-subscriptions/v1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
add_action('rest_api_init', array($this, 'register_routes'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册REST API路由
|
||||||
|
*/
|
||||||
|
public function register_routes() {
|
||||||
|
// 订阅相关端点
|
||||||
|
register_rest_route(self::API_NAMESPACE, '/subscriptions', array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => array($this, 'get_subscriptions'),
|
||||||
|
'permission_callback' => array($this, 'check_permissions'),
|
||||||
|
));
|
||||||
|
|
||||||
|
register_rest_route(self::API_NAMESPACE, '/subscriptions/(?P<id>\d+)', array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => array($this, 'get_subscription'),
|
||||||
|
'permission_callback' => array($this, 'check_permissions'),
|
||||||
|
));
|
||||||
|
|
||||||
|
register_rest_route(self::API_NAMESPACE, '/subscriptions/(?P<id>\d+)', array(
|
||||||
|
'methods' => 'PUT',
|
||||||
|
'callback' => array($this, 'update_subscription'),
|
||||||
|
'permission_callback' => array($this, 'check_permissions'),
|
||||||
|
));
|
||||||
|
|
||||||
|
register_rest_route(self::API_NAMESPACE, '/subscriptions/(?P<id>\d+)/pause', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'pause_subscription'),
|
||||||
|
'permission_callback' => array($this, 'check_permissions'),
|
||||||
|
));
|
||||||
|
|
||||||
|
register_rest_route(self::API_NAMESPACE, '/subscriptions/(?P<id>\d+)/resume', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'resume_subscription'),
|
||||||
|
'permission_callback' => array($this, 'check_permissions'),
|
||||||
|
));
|
||||||
|
|
||||||
|
register_rest_route(self::API_NAMESPACE, '/subscriptions/(?P<id>\d+)/cancel', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'cancel_subscription'),
|
||||||
|
'permission_callback' => array($this, 'check_permissions'),
|
||||||
|
));
|
||||||
|
|
||||||
|
// 混装产品相关端点
|
||||||
|
register_rest_route(self::API_NAMESPACE, '/bundles', array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => array($this, 'get_bundles'),
|
||||||
|
'permission_callback' => array($this, 'check_permissions'),
|
||||||
|
));
|
||||||
|
|
||||||
|
register_rest_route(self::API_NAMESPACE, '/bundles/(?P<id>\d+)', array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => array($this, 'get_bundle'),
|
||||||
|
'permission_callback' => array($this, 'check_permissions'),
|
||||||
|
));
|
||||||
|
|
||||||
|
register_rest_route(self::API_NAMESPACE, '/bundles/(?P<id>\d+)/price', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'calculate_bundle_price'),
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
));
|
||||||
|
|
||||||
|
// 支付相关端点
|
||||||
|
register_rest_route(self::API_NAMESPACE, '/payment/tokens', array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => array($this, 'get_payment_tokens'),
|
||||||
|
'permission_callback' => array($this, 'check_permissions'),
|
||||||
|
));
|
||||||
|
|
||||||
|
register_rest_route(self::API_NAMESPACE, '/payment/tokens/(?P<id>\d+)', array(
|
||||||
|
'methods' => 'DELETE',
|
||||||
|
'callback' => array($this, 'delete_payment_token'),
|
||||||
|
'permission_callback' => array($this, 'check_permissions'),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Webhook端点
|
||||||
|
register_rest_route(self::API_NAMESPACE, '/webhook/moneris', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'handle_moneris_webhook'),
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查权限
|
||||||
|
*/
|
||||||
|
public function check_permissions($request) {
|
||||||
|
return current_user_can('manage_woocommerce') || current_user_can('edit_shop_orders');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订阅列表
|
||||||
|
*/
|
||||||
|
public function get_subscriptions($request) {
|
||||||
|
$customer_id = $request->get_param('customer_id');
|
||||||
|
$status = $request->get_param('status');
|
||||||
|
$page = $request->get_param('page') ?: 1;
|
||||||
|
$per_page = $request->get_param('per_page') ?: 10;
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$where_conditions = array('1=1');
|
||||||
|
$params = array();
|
||||||
|
|
||||||
|
if ($customer_id) {
|
||||||
|
$where_conditions[] = 'customer_id = %d';
|
||||||
|
$params[] = $customer_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status) {
|
||||||
|
$where_conditions[] = 'status = %s';
|
||||||
|
$params[] = $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
$where_clause = implode(' AND ', $where_conditions);
|
||||||
|
$offset = ($page - 1) * $per_page;
|
||||||
|
|
||||||
|
$query = $wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_subscriptions
|
||||||
|
WHERE {$where_clause}
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT %d OFFSET %d",
|
||||||
|
array_merge($params, array($per_page, $offset))
|
||||||
|
);
|
||||||
|
|
||||||
|
$subscriptions = $wpdb->get_results($query);
|
||||||
|
|
||||||
|
return rest_ensure_response($subscriptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个订阅
|
||||||
|
*/
|
||||||
|
public function get_subscription($request) {
|
||||||
|
$id = $request->get_param('id');
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$subscription = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_subscriptions WHERE id = %d",
|
||||||
|
$id
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$subscription) {
|
||||||
|
return new WP_Error('subscription_not_found', '订阅不存在', array('status' => 404));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订阅项目
|
||||||
|
$items = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_subscription_items WHERE subscription_id = %d",
|
||||||
|
$id
|
||||||
|
));
|
||||||
|
|
||||||
|
$subscription->items = $items;
|
||||||
|
|
||||||
|
return rest_ensure_response($subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新订阅
|
||||||
|
*/
|
||||||
|
public function update_subscription($request) {
|
||||||
|
$id = $request->get_param('id');
|
||||||
|
$data = $request->get_json_params();
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$allowed_fields = array('status', 'next_payment_date', 'billing_period', 'billing_interval');
|
||||||
|
$update_data = array();
|
||||||
|
|
||||||
|
foreach ($allowed_fields as $field) {
|
||||||
|
if (isset($data[$field])) {
|
||||||
|
$update_data[$field] = $data[$field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($update_data)) {
|
||||||
|
return new WP_Error('no_data', '没有提供更新数据', array('status' => 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
$update_data['updated_at'] = current_time('mysql');
|
||||||
|
|
||||||
|
$result = $wpdb->update(
|
||||||
|
$wpdb->prefix . 'yoone_subscriptions',
|
||||||
|
$update_data,
|
||||||
|
array('id' => $id),
|
||||||
|
array('%s'),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result === false) {
|
||||||
|
return new WP_Error('update_failed', '更新失败', array('status' => 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
return rest_ensure_response(array('success' => true, 'message' => '订阅更新成功'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 暂停订阅
|
||||||
|
*/
|
||||||
|
public function pause_subscription($request) {
|
||||||
|
$id = $request->get_param('id');
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$result = $wpdb->update(
|
||||||
|
$wpdb->prefix . 'yoone_subscriptions',
|
||||||
|
array(
|
||||||
|
'status' => 'paused',
|
||||||
|
'updated_at' => current_time('mysql')
|
||||||
|
),
|
||||||
|
array('id' => $id),
|
||||||
|
array('%s', '%s'),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result === false) {
|
||||||
|
return new WP_Error('pause_failed', '暂停失败', array('status' => 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录日志
|
||||||
|
Yoone_Logger::info("订阅 {$id} 已暂停", array('subscription_id' => $id));
|
||||||
|
|
||||||
|
return rest_ensure_response(array('success' => true, 'message' => '订阅已暂停'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复订阅
|
||||||
|
*/
|
||||||
|
public function resume_subscription($request) {
|
||||||
|
$id = $request->get_param('id');
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$result = $wpdb->update(
|
||||||
|
$wpdb->prefix . 'yoone_subscriptions',
|
||||||
|
array(
|
||||||
|
'status' => 'active',
|
||||||
|
'updated_at' => current_time('mysql')
|
||||||
|
),
|
||||||
|
array('id' => $id),
|
||||||
|
array('%s', '%s'),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result === false) {
|
||||||
|
return new WP_Error('resume_failed', '恢复失败', array('status' => 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录日志
|
||||||
|
Yoone_Logger::info("订阅 {$id} 已恢复", array('subscription_id' => $id));
|
||||||
|
|
||||||
|
return rest_ensure_response(array('success' => true, 'message' => '订阅已恢复'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消订阅
|
||||||
|
*/
|
||||||
|
public function cancel_subscription($request) {
|
||||||
|
$id = $request->get_param('id');
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$result = $wpdb->update(
|
||||||
|
$wpdb->prefix . 'yoone_subscriptions',
|
||||||
|
array(
|
||||||
|
'status' => 'cancelled',
|
||||||
|
'end_date' => current_time('mysql'),
|
||||||
|
'updated_at' => current_time('mysql')
|
||||||
|
),
|
||||||
|
array('id' => $id),
|
||||||
|
array('%s', '%s', '%s'),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result === false) {
|
||||||
|
return new WP_Error('cancel_failed', '取消失败', array('status' => 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录日志
|
||||||
|
Yoone_Logger::info("订阅 {$id} 已取消", array('subscription_id' => $id));
|
||||||
|
|
||||||
|
return rest_ensure_response(array('success' => true, 'message' => '订阅已取消'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取混装产品列表
|
||||||
|
*/
|
||||||
|
public function get_bundles($request) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$bundles = $wpdb->get_results(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_bundles WHERE status = 'active' ORDER BY created_at DESC"
|
||||||
|
);
|
||||||
|
|
||||||
|
return rest_ensure_response($bundles);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个混装产品
|
||||||
|
*/
|
||||||
|
public function get_bundle($request) {
|
||||||
|
$id = $request->get_param('id');
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$bundle = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_bundles WHERE id = %d",
|
||||||
|
$id
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$bundle) {
|
||||||
|
return new WP_Error('bundle_not_found', '混装产品不存在', array('status' => 404));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取混装项目
|
||||||
|
$items = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_bundle_items WHERE bundle_id = %d ORDER BY sort_order",
|
||||||
|
$id
|
||||||
|
));
|
||||||
|
|
||||||
|
$bundle->items = $items;
|
||||||
|
|
||||||
|
return rest_ensure_response($bundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算混装产品价格
|
||||||
|
*/
|
||||||
|
public function calculate_bundle_price($request) {
|
||||||
|
$bundle_id = $request->get_param('id');
|
||||||
|
$quantities = $request->get_param('quantities') ?: array();
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// 获取混装产品信息
|
||||||
|
$bundle = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_bundles WHERE id = %d AND status = 'active'",
|
||||||
|
$bundle_id
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$bundle) {
|
||||||
|
return new WP_Error('bundle_not_found', '混装产品不存在', array('status' => 404));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取混装项目
|
||||||
|
$items = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_bundle_items WHERE bundle_id = %d",
|
||||||
|
$bundle_id
|
||||||
|
));
|
||||||
|
|
||||||
|
$total_price = 0;
|
||||||
|
$item_details = array();
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$product = wc_get_product($item->product_id);
|
||||||
|
if (!$product) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$quantity = isset($quantities[$item->product_id]) ? intval($quantities[$item->product_id]) : $item->quantity;
|
||||||
|
$price = $product->get_price();
|
||||||
|
$subtotal = $price * $quantity;
|
||||||
|
$total_price += $subtotal;
|
||||||
|
|
||||||
|
$item_details[] = array(
|
||||||
|
'product_id' => $item->product_id,
|
||||||
|
'name' => $product->get_name(),
|
||||||
|
'price' => $price,
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'subtotal' => $subtotal
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用折扣
|
||||||
|
$discount_amount = 0;
|
||||||
|
if ($bundle->discount_type === 'percentage') {
|
||||||
|
$discount_amount = $total_price * ($bundle->discount_value / 100);
|
||||||
|
} elseif ($bundle->discount_type === 'fixed') {
|
||||||
|
$discount_amount = $bundle->discount_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$final_price = max(0, $total_price - $discount_amount);
|
||||||
|
|
||||||
|
return rest_ensure_response(array(
|
||||||
|
'bundle_id' => $bundle_id,
|
||||||
|
'original_price' => $total_price,
|
||||||
|
'discount_amount' => $discount_amount,
|
||||||
|
'final_price' => $final_price,
|
||||||
|
'items' => $item_details
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支付令牌
|
||||||
|
*/
|
||||||
|
public function get_payment_tokens($request) {
|
||||||
|
$customer_id = $request->get_param('customer_id') ?: get_current_user_id();
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$tokens = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_payment_tokens
|
||||||
|
WHERE customer_id = %d AND (expires_at IS NULL OR expires_at > NOW())
|
||||||
|
ORDER BY is_default DESC, created_at DESC",
|
||||||
|
$customer_id
|
||||||
|
));
|
||||||
|
|
||||||
|
return rest_ensure_response($tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除支付令牌
|
||||||
|
*/
|
||||||
|
public function delete_payment_token($request) {
|
||||||
|
$id = $request->get_param('id');
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$result = $wpdb->delete(
|
||||||
|
$wpdb->prefix . 'yoone_payment_tokens',
|
||||||
|
array('id' => $id),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result === false) {
|
||||||
|
return new WP_Error('delete_failed', '删除失败', array('status' => 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
return rest_ensure_response(array('success' => true, 'message' => '支付令牌已删除'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理Moneris Webhook
|
||||||
|
*/
|
||||||
|
public function handle_moneris_webhook($request) {
|
||||||
|
$body = $request->get_body();
|
||||||
|
$headers = $request->get_headers();
|
||||||
|
|
||||||
|
// 验证webhook签名
|
||||||
|
if (!$this->verify_webhook_signature($body, $headers)) {
|
||||||
|
return new WP_Error('invalid_signature', '无效的签名', array('status' => 401));
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($body, true);
|
||||||
|
|
||||||
|
if (!$data) {
|
||||||
|
return new WP_Error('invalid_data', '无效的数据', array('status' => 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理不同类型的webhook事件
|
||||||
|
switch ($data['event_type']) {
|
||||||
|
case 'payment_success':
|
||||||
|
$this->handle_payment_success($data);
|
||||||
|
break;
|
||||||
|
case 'payment_failed':
|
||||||
|
$this->handle_payment_failed($data);
|
||||||
|
break;
|
||||||
|
case 'subscription_cancelled':
|
||||||
|
$this->handle_subscription_cancelled($data);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Yoone_Logger::warning('未知的webhook事件类型: ' . $data['event_type']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rest_ensure_response(array('success' => true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证webhook签名
|
||||||
|
*/
|
||||||
|
private function verify_webhook_signature($body, $headers) {
|
||||||
|
// 这里应该实现Moneris的签名验证逻辑
|
||||||
|
// 暂时返回true,实际使用时需要根据Moneris文档实现
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理支付成功
|
||||||
|
*/
|
||||||
|
private function handle_payment_success($data) {
|
||||||
|
// 实现支付成功处理逻辑
|
||||||
|
Yoone_Logger::info('收到支付成功webhook', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理支付失败
|
||||||
|
*/
|
||||||
|
private function handle_payment_failed($data) {
|
||||||
|
// 实现支付失败处理逻辑
|
||||||
|
Yoone_Logger::warning('收到支付失败webhook', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理订阅取消
|
||||||
|
*/
|
||||||
|
private function handle_subscription_cancelled($data) {
|
||||||
|
// 实现订阅取消处理逻辑
|
||||||
|
Yoone_Logger::info('收到订阅取消webhook', $data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,363 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 计划任务类
|
||||||
|
*
|
||||||
|
* @package YooneSubscriptions
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yoone_Cron类
|
||||||
|
* 处理计划任务
|
||||||
|
*/
|
||||||
|
class Yoone_Cron {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
$this->init_hooks();
|
||||||
|
$this->schedule_events();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化钩子
|
||||||
|
*/
|
||||||
|
private function init_hooks() {
|
||||||
|
// 订阅续费处理
|
||||||
|
add_action('yoone_process_subscription_renewals', array($this, 'process_subscription_renewals'));
|
||||||
|
|
||||||
|
// 清理过期令牌
|
||||||
|
add_action('yoone_cleanup_expired_tokens', array($this, 'cleanup_expired_tokens'));
|
||||||
|
|
||||||
|
// 清理过期订阅
|
||||||
|
add_action('yoone_cleanup_expired_subscriptions', array($this, 'cleanup_expired_subscriptions'));
|
||||||
|
|
||||||
|
// 发送订阅提醒邮件
|
||||||
|
add_action('yoone_send_subscription_reminders', array($this, 'send_subscription_reminders'));
|
||||||
|
|
||||||
|
// 清理日志文件
|
||||||
|
add_action('yoone_cleanup_logs', array($this, 'cleanup_logs'));
|
||||||
|
|
||||||
|
// 添加自定义计划间隔
|
||||||
|
add_filter('cron_schedules', array($this, 'add_cron_schedules'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安排计划事件
|
||||||
|
*/
|
||||||
|
private function schedule_events() {
|
||||||
|
// 每小时处理订阅续费
|
||||||
|
if (!wp_next_scheduled('yoone_process_subscription_renewals')) {
|
||||||
|
wp_schedule_event(time(), 'hourly', 'yoone_process_subscription_renewals');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每天清理过期令牌
|
||||||
|
if (!wp_next_scheduled('yoone_cleanup_expired_tokens')) {
|
||||||
|
wp_schedule_event(time(), 'daily', 'yoone_cleanup_expired_tokens');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每天清理过期订阅
|
||||||
|
if (!wp_next_scheduled('yoone_cleanup_expired_subscriptions')) {
|
||||||
|
wp_schedule_event(time(), 'daily', 'yoone_cleanup_expired_subscriptions');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每天发送订阅提醒邮件
|
||||||
|
if (!wp_next_scheduled('yoone_send_subscription_reminders')) {
|
||||||
|
wp_schedule_event(time(), 'daily', 'yoone_send_subscription_reminders');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每周清理日志文件
|
||||||
|
if (!wp_next_scheduled('yoone_cleanup_logs')) {
|
||||||
|
wp_schedule_event(time(), 'weekly', 'yoone_cleanup_logs');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加自定义计划间隔
|
||||||
|
*
|
||||||
|
* @param array $schedules 现有计划
|
||||||
|
* @return array 更新后的计划
|
||||||
|
*/
|
||||||
|
public function add_cron_schedules($schedules) {
|
||||||
|
// 每15分钟
|
||||||
|
$schedules['fifteen_minutes'] = array(
|
||||||
|
'interval' => 15 * 60,
|
||||||
|
'display' => __('每15分钟', 'yoone-subscriptions')
|
||||||
|
);
|
||||||
|
|
||||||
|
// 每30分钟
|
||||||
|
$schedules['thirty_minutes'] = array(
|
||||||
|
'interval' => 30 * 60,
|
||||||
|
'display' => __('每30分钟', 'yoone-subscriptions')
|
||||||
|
);
|
||||||
|
|
||||||
|
return $schedules;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理订阅续费
|
||||||
|
*/
|
||||||
|
public function process_subscription_renewals() {
|
||||||
|
Yoone_Logger::info('开始处理订阅续费');
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// 获取需要续费的订阅
|
||||||
|
$subscriptions = $wpdb->get_results($wpdb->prepare("
|
||||||
|
SELECT * FROM {$wpdb->prefix}yoone_subscriptions
|
||||||
|
WHERE status = 'active'
|
||||||
|
AND next_payment_date <= %s
|
||||||
|
AND next_payment_date IS NOT NULL
|
||||||
|
LIMIT 50
|
||||||
|
", current_time('mysql')));
|
||||||
|
|
||||||
|
$processed = 0;
|
||||||
|
$failed = 0;
|
||||||
|
|
||||||
|
foreach ($subscriptions as $subscription_data) {
|
||||||
|
try {
|
||||||
|
$subscription = new Yoone_Subscription($subscription_data->id);
|
||||||
|
|
||||||
|
if ($subscription->process_renewal()) {
|
||||||
|
$processed++;
|
||||||
|
Yoone_Logger::log_subscription($subscription->get_id(), '订阅续费成功');
|
||||||
|
} else {
|
||||||
|
$failed++;
|
||||||
|
Yoone_Logger::log_subscription($subscription->get_id(), '订阅续费失败', Yoone_Logger::ERROR);
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$failed++;
|
||||||
|
Yoone_Logger::error('订阅续费异常: ' . $e->getMessage(), array(
|
||||||
|
'subscription_id' => $subscription_data->id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Yoone_Logger::info("订阅续费处理完成: 成功 {$processed}, 失败 {$failed}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期令牌
|
||||||
|
*/
|
||||||
|
public function cleanup_expired_tokens() {
|
||||||
|
Yoone_Logger::info('开始清理过期支付令牌');
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$deleted = $wpdb->query($wpdb->prepare("
|
||||||
|
DELETE FROM {$wpdb->prefix}yoone_payment_tokens
|
||||||
|
WHERE expires_at < %s
|
||||||
|
", current_time('mysql')));
|
||||||
|
|
||||||
|
Yoone_Logger::info("清理过期令牌完成: 删除 {$deleted} 个令牌");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期订阅
|
||||||
|
*/
|
||||||
|
public function cleanup_expired_subscriptions() {
|
||||||
|
Yoone_Logger::info('开始清理过期订阅');
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// 将长时间未付款的活跃订阅标记为过期
|
||||||
|
$expired_days = apply_filters('yoone_subscription_expiry_days', 30);
|
||||||
|
|
||||||
|
$updated = $wpdb->query($wpdb->prepare("
|
||||||
|
UPDATE {$wpdb->prefix}yoone_subscriptions
|
||||||
|
SET status = 'expired', updated_at = %s
|
||||||
|
WHERE status = 'active'
|
||||||
|
AND next_payment_date < %s
|
||||||
|
", current_time('mysql'), date('Y-m-d H:i:s', strtotime("-{$expired_days} days"))));
|
||||||
|
|
||||||
|
if ($updated > 0) {
|
||||||
|
Yoone_Logger::info("标记过期订阅完成: 更新 {$updated} 个订阅");
|
||||||
|
|
||||||
|
// 发送过期通知邮件
|
||||||
|
$this->send_expiry_notifications();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送订阅提醒邮件
|
||||||
|
*/
|
||||||
|
public function send_subscription_reminders() {
|
||||||
|
Yoone_Logger::info('开始发送订阅提醒邮件');
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// 获取即将到期的订阅(3天内)
|
||||||
|
$subscriptions = $wpdb->get_results($wpdb->prepare("
|
||||||
|
SELECT s.*, u.user_email, u.display_name
|
||||||
|
FROM {$wpdb->prefix}yoone_subscriptions s
|
||||||
|
LEFT JOIN {$wpdb->users} u ON s.customer_id = u.ID
|
||||||
|
WHERE s.status = 'active'
|
||||||
|
AND s.next_payment_date BETWEEN %s AND %s
|
||||||
|
AND s.reminder_sent = 0
|
||||||
|
", current_time('mysql'), date('Y-m-d H:i:s', strtotime('+3 days'))));
|
||||||
|
|
||||||
|
$sent = 0;
|
||||||
|
|
||||||
|
foreach ($subscriptions as $subscription_data) {
|
||||||
|
try {
|
||||||
|
$this->send_renewal_reminder($subscription_data);
|
||||||
|
|
||||||
|
// 标记提醒已发送
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->prefix . 'yoone_subscriptions',
|
||||||
|
array('reminder_sent' => 1),
|
||||||
|
array('id' => $subscription_data->id),
|
||||||
|
array('%d'),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
$sent++;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Yoone_Logger::error('发送提醒邮件失败: ' . $e->getMessage(), array(
|
||||||
|
'subscription_id' => $subscription_data->id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Yoone_Logger::info("发送订阅提醒邮件完成: 发送 {$sent} 封邮件");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理日志文件
|
||||||
|
*/
|
||||||
|
public function cleanup_logs() {
|
||||||
|
Yoone_Logger::info('开始清理日志文件');
|
||||||
|
|
||||||
|
$days_to_keep = apply_filters('yoone_log_retention_days', 30);
|
||||||
|
Yoone_Logger::cleanup_old_logs($days_to_keep);
|
||||||
|
|
||||||
|
Yoone_Logger::info("清理日志文件完成: 保留 {$days_to_keep} 天内的日志");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送续费提醒邮件
|
||||||
|
*
|
||||||
|
* @param object $subscription_data 订阅数据
|
||||||
|
*/
|
||||||
|
private function send_renewal_reminder($subscription_data) {
|
||||||
|
$subject = sprintf(__('您的订阅即将续费 - %s', 'yoone-subscriptions'), get_bloginfo('name'));
|
||||||
|
|
||||||
|
$message = sprintf(
|
||||||
|
__('亲爱的 %s,
|
||||||
|
|
||||||
|
您的订阅即将在 %s 自动续费。
|
||||||
|
|
||||||
|
订阅详情:
|
||||||
|
- 订阅ID: %d
|
||||||
|
- 金额: %s
|
||||||
|
- 下次付款日期: %s
|
||||||
|
|
||||||
|
如需管理您的订阅,请访问:%s
|
||||||
|
|
||||||
|
谢谢!
|
||||||
|
%s', 'yoone-subscriptions'),
|
||||||
|
$subscription_data->display_name,
|
||||||
|
Yoone_Helper::format_date($subscription_data->next_payment_date, 'Y-m-d'),
|
||||||
|
$subscription_data->id,
|
||||||
|
wc_price($subscription_data->total),
|
||||||
|
Yoone_Helper::format_date($subscription_data->next_payment_date, 'Y-m-d H:i'),
|
||||||
|
wc_get_account_endpoint_url('subscriptions'),
|
||||||
|
get_bloginfo('name')
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_mail($subscription_data->user_email, $subject, $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送过期通知邮件
|
||||||
|
*/
|
||||||
|
private function send_expiry_notifications() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// 获取刚刚标记为过期的订阅
|
||||||
|
$subscriptions = $wpdb->get_results($wpdb->prepare("
|
||||||
|
SELECT s.*, u.user_email, u.display_name
|
||||||
|
FROM {$wpdb->prefix}yoone_subscriptions s
|
||||||
|
LEFT JOIN {$wpdb->users} u ON s.customer_id = u.ID
|
||||||
|
WHERE s.status = 'expired'
|
||||||
|
AND s.updated_at >= %s
|
||||||
|
", date('Y-m-d H:i:s', strtotime('-1 hour'))));
|
||||||
|
|
||||||
|
foreach ($subscriptions as $subscription_data) {
|
||||||
|
$this->send_expiry_notification($subscription_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送过期通知邮件
|
||||||
|
*
|
||||||
|
* @param object $subscription_data 订阅数据
|
||||||
|
*/
|
||||||
|
private function send_expiry_notification($subscription_data) {
|
||||||
|
$subject = sprintf(__('您的订阅已过期 - %s', 'yoone-subscriptions'), get_bloginfo('name'));
|
||||||
|
|
||||||
|
$message = sprintf(
|
||||||
|
__('亲爱的 %s,
|
||||||
|
|
||||||
|
您的订阅已因长时间未付款而过期。
|
||||||
|
|
||||||
|
订阅详情:
|
||||||
|
- 订阅ID: %d
|
||||||
|
- 金额: %s
|
||||||
|
- 过期时间: %s
|
||||||
|
|
||||||
|
如需重新激活订阅,请访问:%s
|
||||||
|
|
||||||
|
谢谢!
|
||||||
|
%s', 'yoone-subscriptions'),
|
||||||
|
$subscription_data->display_name,
|
||||||
|
$subscription_data->id,
|
||||||
|
wc_price($subscription_data->total),
|
||||||
|
Yoone_Helper::format_date($subscription_data->updated_at, 'Y-m-d H:i'),
|
||||||
|
wc_get_account_endpoint_url('subscriptions'),
|
||||||
|
get_bloginfo('name')
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_mail($subscription_data->user_email, $subject, $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消所有计划事件
|
||||||
|
*/
|
||||||
|
public static function unschedule_all_events() {
|
||||||
|
$events = array(
|
||||||
|
'yoone_process_subscription_renewals',
|
||||||
|
'yoone_cleanup_expired_tokens',
|
||||||
|
'yoone_cleanup_expired_subscriptions',
|
||||||
|
'yoone_send_subscription_reminders',
|
||||||
|
'yoone_cleanup_logs'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($events as $event) {
|
||||||
|
$timestamp = wp_next_scheduled($event);
|
||||||
|
if ($timestamp) {
|
||||||
|
wp_unschedule_event($timestamp, $event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动触发订阅续费处理
|
||||||
|
*/
|
||||||
|
public static function manual_process_renewals() {
|
||||||
|
$cron = new self();
|
||||||
|
$cron->process_subscription_renewals();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动清理过期令牌
|
||||||
|
*/
|
||||||
|
public static function manual_cleanup_tokens() {
|
||||||
|
$cron = new self();
|
||||||
|
$cron->cleanup_expired_tokens();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,454 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 前端类
|
||||||
|
*
|
||||||
|
* @package YooneSubscriptions
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yoone_Frontend类
|
||||||
|
* 处理前端功能和显示
|
||||||
|
*/
|
||||||
|
class Yoone_Frontend {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
$this->init_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化钩子
|
||||||
|
*/
|
||||||
|
private function init_hooks() {
|
||||||
|
// 产品页面钩子
|
||||||
|
add_action('woocommerce_single_product_summary', array($this, 'display_bundle_options'), 25);
|
||||||
|
add_action('woocommerce_single_product_summary', array($this, 'display_subscription_options'), 30);
|
||||||
|
|
||||||
|
// 购物车钩子
|
||||||
|
add_filter('woocommerce_add_cart_item_data', array($this, 'add_cart_item_data'), 10, 3);
|
||||||
|
add_filter('woocommerce_get_cart_item_from_session', array($this, 'get_cart_item_from_session'), 10, 3);
|
||||||
|
add_filter('woocommerce_cart_item_price', array($this, 'cart_item_price'), 10, 3);
|
||||||
|
add_filter('woocommerce_cart_item_name', array($this, 'cart_item_name'), 10, 3);
|
||||||
|
|
||||||
|
// 结账钩子
|
||||||
|
add_action('woocommerce_checkout_create_order_line_item', array($this, 'add_order_item_meta'), 10, 4);
|
||||||
|
|
||||||
|
// 我的账户钩子
|
||||||
|
add_filter('woocommerce_account_menu_items', array($this, 'add_account_menu_items'));
|
||||||
|
add_action('woocommerce_account_subscriptions_endpoint', array($this, 'subscriptions_endpoint_content'));
|
||||||
|
|
||||||
|
// 短代码
|
||||||
|
add_shortcode('yoone_subscriptions', array($this, 'subscriptions_shortcode'));
|
||||||
|
add_shortcode('yoone_bundles', array($this, 'bundles_shortcode'));
|
||||||
|
|
||||||
|
// 模板钩子
|
||||||
|
add_filter('woocommerce_locate_template', array($this, 'locate_template'), 10, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示混装产品选项
|
||||||
|
*/
|
||||||
|
public function display_bundle_options() {
|
||||||
|
global $product;
|
||||||
|
|
||||||
|
if (!$product || $product->get_type() !== 'yoone_bundle') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bundle_id = get_post_meta($product->get_id(), '_yoone_bundle_id', true);
|
||||||
|
if (!$bundle_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// 获取混装项目
|
||||||
|
$items = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT bi.*, p.post_title as product_name
|
||||||
|
FROM {$wpdb->prefix}yoone_bundle_items bi
|
||||||
|
LEFT JOIN {$wpdb->posts} p ON bi.product_id = p.ID
|
||||||
|
WHERE bi.bundle_id = %d
|
||||||
|
ORDER BY bi.sort_order",
|
||||||
|
$bundle_id
|
||||||
|
));
|
||||||
|
|
||||||
|
if (empty($items)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载模板
|
||||||
|
wc_get_template(
|
||||||
|
'single-product/bundle-options.php',
|
||||||
|
array(
|
||||||
|
'bundle_id' => $bundle_id,
|
||||||
|
'items' => $items,
|
||||||
|
'product' => $product
|
||||||
|
),
|
||||||
|
'',
|
||||||
|
YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'templates/'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示订阅选项
|
||||||
|
*/
|
||||||
|
public function display_subscription_options() {
|
||||||
|
global $product;
|
||||||
|
|
||||||
|
if (!$product) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$is_subscription = get_post_meta($product->get_id(), '_yoone_subscription_enabled', true);
|
||||||
|
if ($is_subscription !== 'yes') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscription_periods = get_post_meta($product->get_id(), '_yoone_subscription_periods', true);
|
||||||
|
if (empty($subscription_periods)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载模板
|
||||||
|
wc_get_template(
|
||||||
|
'single-product/subscription-options.php',
|
||||||
|
array(
|
||||||
|
'product' => $product,
|
||||||
|
'subscription_periods' => $subscription_periods
|
||||||
|
),
|
||||||
|
'',
|
||||||
|
YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'templates/'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加购物车项目数据
|
||||||
|
*/
|
||||||
|
public function add_cart_item_data($cart_item_data, $product_id, $variation_id) {
|
||||||
|
// 处理混装产品数据
|
||||||
|
if (isset($_POST['yoone_bundle_items'])) {
|
||||||
|
$cart_item_data['yoone_bundle_items'] = $_POST['yoone_bundle_items'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理订阅数据
|
||||||
|
if (isset($_POST['yoone_subscription_period'])) {
|
||||||
|
$cart_item_data['yoone_subscription_period'] = $_POST['yoone_subscription_period'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_POST['yoone_subscription_type'])) {
|
||||||
|
$cart_item_data['yoone_subscription_type'] = $_POST['yoone_subscription_type'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cart_item_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从会话获取购物车项目
|
||||||
|
*/
|
||||||
|
public function get_cart_item_from_session($item, $values, $key) {
|
||||||
|
if (array_key_exists('yoone_bundle_items', $values)) {
|
||||||
|
$item['yoone_bundle_items'] = $values['yoone_bundle_items'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('yoone_subscription_period', $values)) {
|
||||||
|
$item['yoone_subscription_period'] = $values['yoone_subscription_period'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('yoone_subscription_type', $values)) {
|
||||||
|
$item['yoone_subscription_type'] = $values['yoone_subscription_type'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改购物车项目价格
|
||||||
|
*/
|
||||||
|
public function cart_item_price($price, $cart_item, $cart_item_key) {
|
||||||
|
if (isset($cart_item['yoone_bundle_items'])) {
|
||||||
|
// 计算混装产品价格
|
||||||
|
$bundle_price = $this->calculate_bundle_price($cart_item);
|
||||||
|
if ($bundle_price !== false) {
|
||||||
|
return wc_price($bundle_price);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($cart_item['yoone_subscription_period'])) {
|
||||||
|
// 计算订阅产品价格
|
||||||
|
$subscription_price = $this->calculate_subscription_price($cart_item);
|
||||||
|
if ($subscription_price !== false) {
|
||||||
|
return wc_price($subscription_price);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $price;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改购物车项目名称
|
||||||
|
*/
|
||||||
|
public function cart_item_name($name, $cart_item, $cart_item_key) {
|
||||||
|
if (isset($cart_item['yoone_bundle_items'])) {
|
||||||
|
$name .= '<br><small>' . __('混装产品', 'yoone-subscriptions') . '</small>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($cart_item['yoone_subscription_period'])) {
|
||||||
|
$period = $cart_item['yoone_subscription_period'];
|
||||||
|
$name .= '<br><small>' . sprintf(__('订阅周期: %s', 'yoone-subscriptions'), $period) . '</small>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加订单项目元数据
|
||||||
|
*/
|
||||||
|
public function add_order_item_meta($item, $cart_item_key, $values, $order) {
|
||||||
|
if (isset($values['yoone_bundle_items'])) {
|
||||||
|
$item->add_meta_data('_yoone_bundle_items', $values['yoone_bundle_items']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($values['yoone_subscription_period'])) {
|
||||||
|
$item->add_meta_data('_yoone_subscription_period', $values['yoone_subscription_period']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($values['yoone_subscription_type'])) {
|
||||||
|
$item->add_meta_data('_yoone_subscription_type', $values['yoone_subscription_type']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加我的账户菜单项
|
||||||
|
*/
|
||||||
|
public function add_account_menu_items($items) {
|
||||||
|
// 在订单后面插入订阅菜单
|
||||||
|
$new_items = array();
|
||||||
|
foreach ($items as $key => $item) {
|
||||||
|
$new_items[$key] = $item;
|
||||||
|
if ($key === 'orders') {
|
||||||
|
$new_items['subscriptions'] = __('我的订阅', 'yoone-subscriptions');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $new_items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅端点内容
|
||||||
|
*/
|
||||||
|
public function subscriptions_endpoint_content() {
|
||||||
|
$customer_id = get_current_user_id();
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$subscriptions = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_subscriptions
|
||||||
|
WHERE customer_id = %d
|
||||||
|
ORDER BY created_at DESC",
|
||||||
|
$customer_id
|
||||||
|
));
|
||||||
|
|
||||||
|
wc_get_template(
|
||||||
|
'myaccount/subscriptions.php',
|
||||||
|
array('subscriptions' => $subscriptions),
|
||||||
|
'',
|
||||||
|
YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'templates/'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅短代码
|
||||||
|
*/
|
||||||
|
public function subscriptions_shortcode($atts) {
|
||||||
|
$atts = shortcode_atts(array(
|
||||||
|
'customer_id' => get_current_user_id(),
|
||||||
|
'status' => '',
|
||||||
|
'limit' => 10
|
||||||
|
), $atts);
|
||||||
|
|
||||||
|
if (!$atts['customer_id']) {
|
||||||
|
return '<p>' . __('请先登录查看订阅。', 'yoone-subscriptions') . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$where_conditions = array('customer_id = %d');
|
||||||
|
$params = array($atts['customer_id']);
|
||||||
|
|
||||||
|
if ($atts['status']) {
|
||||||
|
$where_conditions[] = 'status = %s';
|
||||||
|
$params[] = $atts['status'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$where_clause = implode(' AND ', $where_conditions);
|
||||||
|
$params[] = intval($atts['limit']);
|
||||||
|
|
||||||
|
$subscriptions = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_subscriptions
|
||||||
|
WHERE {$where_clause}
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT %d",
|
||||||
|
$params
|
||||||
|
));
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
wc_get_template(
|
||||||
|
'shortcodes/subscriptions.php',
|
||||||
|
array('subscriptions' => $subscriptions),
|
||||||
|
'',
|
||||||
|
YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'templates/'
|
||||||
|
);
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 混装产品短代码
|
||||||
|
*/
|
||||||
|
public function bundles_shortcode($atts) {
|
||||||
|
$atts = shortcode_atts(array(
|
||||||
|
'limit' => 12,
|
||||||
|
'columns' => 4,
|
||||||
|
'orderby' => 'date',
|
||||||
|
'order' => 'DESC'
|
||||||
|
), $atts);
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$bundles = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_bundles
|
||||||
|
WHERE status = 'active'
|
||||||
|
ORDER BY created_at %s
|
||||||
|
LIMIT %d",
|
||||||
|
$atts['order'] === 'ASC' ? 'ASC' : 'DESC',
|
||||||
|
intval($atts['limit'])
|
||||||
|
));
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
wc_get_template(
|
||||||
|
'shortcodes/bundles.php',
|
||||||
|
array(
|
||||||
|
'bundles' => $bundles,
|
||||||
|
'columns' => intval($atts['columns'])
|
||||||
|
),
|
||||||
|
'',
|
||||||
|
YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'templates/'
|
||||||
|
);
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定位模板
|
||||||
|
*/
|
||||||
|
public function locate_template($template, $template_name, $template_path) {
|
||||||
|
if (strpos($template_name, 'yoone-') === 0) {
|
||||||
|
$plugin_template = YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'templates/' . $template_name;
|
||||||
|
if (file_exists($plugin_template)) {
|
||||||
|
return $plugin_template;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $template;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算混装产品价格
|
||||||
|
*/
|
||||||
|
private function calculate_bundle_price($cart_item) {
|
||||||
|
if (!isset($cart_item['yoone_bundle_items'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$total_price = 0;
|
||||||
|
$bundle_items = $cart_item['yoone_bundle_items'];
|
||||||
|
|
||||||
|
foreach ($bundle_items as $product_id => $quantity) {
|
||||||
|
$product = wc_get_product($product_id);
|
||||||
|
if ($product) {
|
||||||
|
$total_price += $product->get_price() * intval($quantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用混装折扣
|
||||||
|
$product = wc_get_product($cart_item['product_id']);
|
||||||
|
if ($product) {
|
||||||
|
$discount_type = get_post_meta($product->get_id(), '_yoone_bundle_discount_type', true);
|
||||||
|
$discount_value = get_post_meta($product->get_id(), '_yoone_bundle_discount_value', true);
|
||||||
|
|
||||||
|
if ($discount_type === 'percentage' && $discount_value > 0) {
|
||||||
|
$total_price = $total_price * (1 - $discount_value / 100);
|
||||||
|
} elseif ($discount_type === 'fixed' && $discount_value > 0) {
|
||||||
|
$total_price = max(0, $total_price - $discount_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $total_price;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算订阅产品价格
|
||||||
|
*/
|
||||||
|
private function calculate_subscription_price($cart_item) {
|
||||||
|
if (!isset($cart_item['yoone_subscription_period'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$product = wc_get_product($cart_item['product_id']);
|
||||||
|
if (!$product) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscription_periods = get_post_meta($product->get_id(), '_yoone_subscription_periods', true);
|
||||||
|
$period = $cart_item['yoone_subscription_period'];
|
||||||
|
|
||||||
|
if (isset($subscription_periods[$period])) {
|
||||||
|
$period_data = $subscription_periods[$period];
|
||||||
|
$base_price = $product->get_price();
|
||||||
|
|
||||||
|
if (isset($period_data['discount_type']) && isset($period_data['discount_value'])) {
|
||||||
|
if ($period_data['discount_type'] === 'percentage') {
|
||||||
|
return $base_price * (1 - $period_data['discount_value'] / 100);
|
||||||
|
} elseif ($period_data['discount_type'] === 'fixed') {
|
||||||
|
return max(0, $base_price - $period_data['discount_value']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $product->get_price();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订阅状态标签
|
||||||
|
*/
|
||||||
|
public static function get_subscription_status_label($status) {
|
||||||
|
$statuses = array(
|
||||||
|
'pending' => __('待处理', 'yoone-subscriptions'),
|
||||||
|
'active' => __('活跃', 'yoone-subscriptions'),
|
||||||
|
'paused' => __('已暂停', 'yoone-subscriptions'),
|
||||||
|
'cancelled' => __('已取消', 'yoone-subscriptions'),
|
||||||
|
'expired' => __('已过期', 'yoone-subscriptions'),
|
||||||
|
'failed' => __('失败', 'yoone-subscriptions')
|
||||||
|
);
|
||||||
|
|
||||||
|
return isset($statuses[$status]) ? $statuses[$status] : $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订阅状态颜色
|
||||||
|
*/
|
||||||
|
public static function get_subscription_status_color($status) {
|
||||||
|
$colors = array(
|
||||||
|
'pending' => '#ffba00',
|
||||||
|
'active' => '#7ad03a',
|
||||||
|
'paused' => '#999999',
|
||||||
|
'cancelled' => '#a00',
|
||||||
|
'expired' => '#a00',
|
||||||
|
'failed' => '#a00'
|
||||||
|
);
|
||||||
|
|
||||||
|
return isset($colors[$status]) ? $colors[$status] : '#999999';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,372 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 工具类
|
||||||
|
*
|
||||||
|
* @package YooneSubscriptions
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yoone_Helper类
|
||||||
|
* 提供各种工具方法
|
||||||
|
*/
|
||||||
|
class Yoone_Helper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化价格
|
||||||
|
*
|
||||||
|
* @param float $price 价格
|
||||||
|
* @return string 格式化后的价格
|
||||||
|
*/
|
||||||
|
public static function format_price($price) {
|
||||||
|
return wc_price($price);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期
|
||||||
|
*
|
||||||
|
* @param string $date 日期
|
||||||
|
* @param string $format 格式
|
||||||
|
* @return string 格式化后的日期
|
||||||
|
*/
|
||||||
|
public static function format_date($date, $format = 'Y-m-d H:i:s') {
|
||||||
|
if (empty($date)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$datetime = new DateTime($date);
|
||||||
|
return $datetime->format($format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订阅状态标签
|
||||||
|
*
|
||||||
|
* @param string $status 状态
|
||||||
|
* @return string 状态标签
|
||||||
|
*/
|
||||||
|
public static function get_subscription_status_label($status) {
|
||||||
|
$labels = array(
|
||||||
|
'active' => __('活跃', 'yoone-subscriptions'),
|
||||||
|
'paused' => __('暂停', 'yoone-subscriptions'),
|
||||||
|
'cancelled' => __('已取消', 'yoone-subscriptions'),
|
||||||
|
'expired' => __('已过期', 'yoone-subscriptions'),
|
||||||
|
'pending' => __('待处理', 'yoone-subscriptions'),
|
||||||
|
);
|
||||||
|
|
||||||
|
return isset($labels[$status]) ? $labels[$status] : $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订阅状态颜色
|
||||||
|
*
|
||||||
|
* @param string $status 状态
|
||||||
|
* @return string 颜色类名
|
||||||
|
*/
|
||||||
|
public static function get_subscription_status_color($status) {
|
||||||
|
$colors = array(
|
||||||
|
'active' => 'success',
|
||||||
|
'paused' => 'warning',
|
||||||
|
'cancelled' => 'danger',
|
||||||
|
'expired' => 'secondary',
|
||||||
|
'pending' => 'info',
|
||||||
|
);
|
||||||
|
|
||||||
|
return isset($colors[$status]) ? $colors[$status] : 'secondary';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化计费周期
|
||||||
|
*
|
||||||
|
* @param string $period 周期
|
||||||
|
* @param int $interval 间隔
|
||||||
|
* @return string 格式化后的计费周期
|
||||||
|
*/
|
||||||
|
public static function format_billing_period($period, $interval = 1) {
|
||||||
|
$periods = array(
|
||||||
|
'day' => array(
|
||||||
|
'singular' => __('天', 'yoone-subscriptions'),
|
||||||
|
'plural' => __('天', 'yoone-subscriptions')
|
||||||
|
),
|
||||||
|
'week' => array(
|
||||||
|
'singular' => __('周', 'yoone-subscriptions'),
|
||||||
|
'plural' => __('周', 'yoone-subscriptions')
|
||||||
|
),
|
||||||
|
'month' => array(
|
||||||
|
'singular' => __('月', 'yoone-subscriptions'),
|
||||||
|
'plural' => __('月', 'yoone-subscriptions')
|
||||||
|
),
|
||||||
|
'year' => array(
|
||||||
|
'singular' => __('年', 'yoone-subscriptions'),
|
||||||
|
'plural' => __('年', 'yoone-subscriptions')
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isset($periods[$period])) {
|
||||||
|
return $period;
|
||||||
|
}
|
||||||
|
|
||||||
|
$period_text = $interval == 1 ? $periods[$period]['singular'] : $periods[$period]['plural'];
|
||||||
|
|
||||||
|
if ($interval == 1) {
|
||||||
|
return sprintf(__('每%s', 'yoone-subscriptions'), $period_text);
|
||||||
|
} else {
|
||||||
|
return sprintf(__('每%d%s', 'yoone-subscriptions'), $interval, $period_text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取周期标签
|
||||||
|
*
|
||||||
|
* @param string $period 周期类型
|
||||||
|
* @param bool $plural 是否复数形式
|
||||||
|
* @return string 周期标签
|
||||||
|
*/
|
||||||
|
public static function get_period_label($period, $plural = false) {
|
||||||
|
$periods = array(
|
||||||
|
'day' => array(
|
||||||
|
'singular' => __('天', 'yoone-subscriptions'),
|
||||||
|
'plural' => __('天', 'yoone-subscriptions')
|
||||||
|
),
|
||||||
|
'week' => array(
|
||||||
|
'singular' => __('周', 'yoone-subscriptions'),
|
||||||
|
'plural' => __('周', 'yoone-subscriptions')
|
||||||
|
),
|
||||||
|
'month' => array(
|
||||||
|
'singular' => __('月', 'yoone-subscriptions'),
|
||||||
|
'plural' => __('月', 'yoone-subscriptions')
|
||||||
|
),
|
||||||
|
'year' => array(
|
||||||
|
'singular' => __('年', 'yoone-subscriptions'),
|
||||||
|
'plural' => __('年', 'yoone-subscriptions')
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isset($periods[$period])) {
|
||||||
|
return $period;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $plural ? $periods[$period]['plural'] : $periods[$period]['singular'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算下次付款日期
|
||||||
|
*
|
||||||
|
* @param string $last_payment 上次付款日期
|
||||||
|
* @param string $period 周期
|
||||||
|
* @param int $interval 间隔
|
||||||
|
* @return string 下次付款日期
|
||||||
|
*/
|
||||||
|
public static function calculate_next_payment_date($last_payment, $period, $interval = 1) {
|
||||||
|
$datetime = new DateTime($last_payment);
|
||||||
|
|
||||||
|
switch ($period) {
|
||||||
|
case 'day':
|
||||||
|
$datetime->add(new DateInterval('P' . $interval . 'D'));
|
||||||
|
break;
|
||||||
|
case 'week':
|
||||||
|
$datetime->add(new DateInterval('P' . ($interval * 7) . 'D'));
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
$datetime->add(new DateInterval('P' . $interval . 'M'));
|
||||||
|
break;
|
||||||
|
case 'year':
|
||||||
|
$datetime->add(new DateInterval('P' . $interval . 'Y'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $datetime->format('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证邮箱
|
||||||
|
*
|
||||||
|
* @param string $email 邮箱
|
||||||
|
* @return bool 是否有效
|
||||||
|
*/
|
||||||
|
public static function is_valid_email($email) {
|
||||||
|
return is_email($email);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机字符串
|
||||||
|
*
|
||||||
|
* @param int $length 长度
|
||||||
|
* @return string 随机字符串
|
||||||
|
*/
|
||||||
|
public static function generate_random_string($length = 32) {
|
||||||
|
return wp_generate_password($length, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户的订阅
|
||||||
|
*
|
||||||
|
* @param int $user_id 用户ID
|
||||||
|
* @return array 订阅列表
|
||||||
|
*/
|
||||||
|
public static function get_user_subscriptions($user_id = 0) {
|
||||||
|
if (!$user_id) {
|
||||||
|
$user_id = get_current_user_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$subscriptions = $wpdb->get_results($wpdb->prepare("
|
||||||
|
SELECT * FROM {$wpdb->prefix}yoone_subscriptions
|
||||||
|
WHERE customer_id = %d
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
", $user_id));
|
||||||
|
|
||||||
|
return $subscriptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查产品是否启用订阅
|
||||||
|
*
|
||||||
|
* @param int $product_id 产品ID
|
||||||
|
* @return bool 是否启用订阅
|
||||||
|
*/
|
||||||
|
public static function is_subscription_product($product_id) {
|
||||||
|
return get_post_meta($product_id, '_subscription_enabled', true) === 'yes';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查产品是否是混装产品
|
||||||
|
*
|
||||||
|
* @param int $product_id 产品ID
|
||||||
|
* @return bool 是否是混装产品
|
||||||
|
*/
|
||||||
|
public static function is_bundle_product($product_id) {
|
||||||
|
$product = wc_get_product($product_id);
|
||||||
|
return $product && $product->get_type() === 'yoone_bundle';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订阅产品的折扣价格
|
||||||
|
*
|
||||||
|
* @param int $product_id 产品ID
|
||||||
|
* @param float $original_price 原价
|
||||||
|
* @return float 折扣后价格
|
||||||
|
*/
|
||||||
|
public static function get_subscription_discounted_price($product_id, $original_price) {
|
||||||
|
$discount_type = get_post_meta($product_id, '_subscription_discount_type', true);
|
||||||
|
$discount_value = floatval(get_post_meta($product_id, '_subscription_discount_value', true));
|
||||||
|
|
||||||
|
if (!$discount_value) {
|
||||||
|
return $original_price;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($discount_type === 'percentage') {
|
||||||
|
return $original_price * (1 - $discount_value / 100);
|
||||||
|
} else {
|
||||||
|
return max(0, $original_price - $discount_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录日志
|
||||||
|
*
|
||||||
|
* @param string $message 消息
|
||||||
|
* @param string $level 级别
|
||||||
|
* @param string $source 来源
|
||||||
|
*/
|
||||||
|
public static function log($message, $level = 'info', $source = 'yoone-subscriptions') {
|
||||||
|
if (class_exists('WC_Logger')) {
|
||||||
|
$logger = wc_get_logger();
|
||||||
|
$logger->log($level, $message, array('source' => $source));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送邮件通知
|
||||||
|
*
|
||||||
|
* @param string $to 收件人
|
||||||
|
* @param string $subject 主题
|
||||||
|
* @param string $message 内容
|
||||||
|
* @param array $headers 头部
|
||||||
|
* @return bool 是否发送成功
|
||||||
|
*/
|
||||||
|
public static function send_email($to, $subject, $message, $headers = array()) {
|
||||||
|
if (!self::is_valid_email($to)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return wp_mail($to, $subject, $message, $headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取货币符号
|
||||||
|
*
|
||||||
|
* @return string 货币符号
|
||||||
|
*/
|
||||||
|
public static function get_currency_symbol() {
|
||||||
|
return get_woocommerce_currency_symbol();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理HTML标签
|
||||||
|
*
|
||||||
|
* @param string $content 内容
|
||||||
|
* @return string 清理后的内容
|
||||||
|
*/
|
||||||
|
public static function clean_html($content) {
|
||||||
|
return wp_strip_all_tags($content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转义HTML属性
|
||||||
|
*
|
||||||
|
* @param string $text 文本
|
||||||
|
* @return string 转义后的文本
|
||||||
|
*/
|
||||||
|
public static function esc_attr($text) {
|
||||||
|
return esc_attr($text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转义HTML
|
||||||
|
*
|
||||||
|
* @param string $text 文本
|
||||||
|
* @return string 转义后的文本
|
||||||
|
*/
|
||||||
|
public static function esc_html($text) {
|
||||||
|
return esc_html($text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件版本
|
||||||
|
*
|
||||||
|
* @return string 版本号
|
||||||
|
*/
|
||||||
|
public static function get_plugin_version() {
|
||||||
|
return YOONE_SUBSCRIPTIONS_VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否是管理员
|
||||||
|
*
|
||||||
|
* @return bool 是否是管理员
|
||||||
|
*/
|
||||||
|
public static function is_admin() {
|
||||||
|
return current_user_can('manage_woocommerce');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取页面URL
|
||||||
|
*
|
||||||
|
* @param string $page 页面
|
||||||
|
* @param array $args 参数
|
||||||
|
* @return string URL
|
||||||
|
*/
|
||||||
|
public static function get_admin_url($page, $args = array()) {
|
||||||
|
$url = admin_url('admin.php?page=' . $page);
|
||||||
|
|
||||||
|
if (!empty($args)) {
|
||||||
|
$url = add_query_arg($args, $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,292 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 插件安装类
|
||||||
|
*
|
||||||
|
* 处理插件激活、停用和数据库表创建
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件安装类
|
||||||
|
*/
|
||||||
|
class Yoone_Install {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库版本
|
||||||
|
*/
|
||||||
|
const DB_VERSION = '1.0.0';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件激活
|
||||||
|
*/
|
||||||
|
public static function activate() {
|
||||||
|
// 检查 WooCommerce 是否激活
|
||||||
|
if (!class_exists('WooCommerce')) {
|
||||||
|
deactivate_plugins(plugin_basename(YOONE_SUBSCRIPTIONS_PLUGIN_FILE));
|
||||||
|
wp_die(__('Yoone Subscriptions 需要 WooCommerce 插件才能正常工作。', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建数据库表
|
||||||
|
self::create_tables();
|
||||||
|
|
||||||
|
// 创建默认页面
|
||||||
|
self::create_pages();
|
||||||
|
|
||||||
|
// 设置默认选项
|
||||||
|
self::set_default_options();
|
||||||
|
|
||||||
|
// 创建订阅状态
|
||||||
|
self::create_subscription_statuses();
|
||||||
|
|
||||||
|
// 刷新重写规则
|
||||||
|
flush_rewrite_rules();
|
||||||
|
|
||||||
|
// 记录激活时间
|
||||||
|
update_option('yoone_subscriptions_activated_time', time());
|
||||||
|
|
||||||
|
do_action('yoone_subscriptions_activated');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件停用
|
||||||
|
*/
|
||||||
|
public static function deactivate() {
|
||||||
|
// 清理计划任务
|
||||||
|
wp_clear_scheduled_hook('yoone_subscriptions_process_renewals');
|
||||||
|
wp_clear_scheduled_hook('yoone_subscriptions_cleanup_expired');
|
||||||
|
|
||||||
|
// 刷新重写规则
|
||||||
|
flush_rewrite_rules();
|
||||||
|
|
||||||
|
do_action('yoone_subscriptions_deactivated');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建数据库表
|
||||||
|
*/
|
||||||
|
private static function create_tables() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$charset_collate = $wpdb->get_charset_collate();
|
||||||
|
|
||||||
|
// 订阅表
|
||||||
|
$subscriptions_table = "
|
||||||
|
CREATE TABLE {$wpdb->prefix}yoone_subscriptions (
|
||||||
|
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
customer_id bigint(20) unsigned NOT NULL,
|
||||||
|
parent_order_id bigint(20) unsigned DEFAULT NULL,
|
||||||
|
status varchar(20) NOT NULL DEFAULT 'pending',
|
||||||
|
billing_period varchar(20) NOT NULL DEFAULT 'month',
|
||||||
|
billing_interval int(11) NOT NULL DEFAULT 1,
|
||||||
|
start_date datetime NOT NULL,
|
||||||
|
next_payment_date datetime DEFAULT NULL,
|
||||||
|
end_date datetime DEFAULT NULL,
|
||||||
|
trial_end_date datetime DEFAULT NULL,
|
||||||
|
total decimal(10,2) NOT NULL DEFAULT 0.00,
|
||||||
|
currency varchar(3) NOT NULL DEFAULT 'CAD',
|
||||||
|
payment_method varchar(50) DEFAULT NULL,
|
||||||
|
payment_token varchar(255) DEFAULT NULL,
|
||||||
|
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY customer_id (customer_id),
|
||||||
|
KEY parent_order_id (parent_order_id),
|
||||||
|
KEY status (status),
|
||||||
|
KEY next_payment_date (next_payment_date)
|
||||||
|
) $charset_collate;
|
||||||
|
";
|
||||||
|
|
||||||
|
// 订阅商品表
|
||||||
|
$subscription_items_table = "
|
||||||
|
CREATE TABLE {$wpdb->prefix}yoone_subscription_items (
|
||||||
|
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
subscription_id bigint(20) unsigned NOT NULL,
|
||||||
|
product_id bigint(20) unsigned NOT NULL,
|
||||||
|
variation_id bigint(20) unsigned DEFAULT NULL,
|
||||||
|
quantity int(11) NOT NULL DEFAULT 1,
|
||||||
|
line_total decimal(10,2) NOT NULL DEFAULT 0.00,
|
||||||
|
line_subtotal decimal(10,2) NOT NULL DEFAULT 0.00,
|
||||||
|
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY subscription_id (subscription_id),
|
||||||
|
KEY product_id (product_id)
|
||||||
|
) $charset_collate;
|
||||||
|
";
|
||||||
|
|
||||||
|
// 混装组合表
|
||||||
|
$bundles_table = "
|
||||||
|
CREATE TABLE {$wpdb->prefix}yoone_bundles (
|
||||||
|
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
name varchar(255) NOT NULL,
|
||||||
|
description text,
|
||||||
|
status varchar(20) NOT NULL DEFAULT 'active',
|
||||||
|
discount_type varchar(20) DEFAULT 'percentage',
|
||||||
|
discount_value decimal(10,2) DEFAULT 0.00,
|
||||||
|
min_quantity int(11) DEFAULT 1,
|
||||||
|
max_quantity int(11) DEFAULT NULL,
|
||||||
|
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY status (status)
|
||||||
|
) $charset_collate;
|
||||||
|
";
|
||||||
|
|
||||||
|
// 混装商品表
|
||||||
|
$bundle_items_table = "
|
||||||
|
CREATE TABLE {$wpdb->prefix}yoone_bundle_items (
|
||||||
|
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
bundle_id bigint(20) unsigned NOT NULL,
|
||||||
|
product_id bigint(20) unsigned NOT NULL,
|
||||||
|
quantity int(11) NOT NULL DEFAULT 1,
|
||||||
|
discount_type varchar(20) DEFAULT 'none',
|
||||||
|
discount_value decimal(10,2) DEFAULT 0.00,
|
||||||
|
is_required tinyint(1) NOT NULL DEFAULT 0,
|
||||||
|
sort_order int(11) DEFAULT 0,
|
||||||
|
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY bundle_id (bundle_id),
|
||||||
|
KEY product_id (product_id)
|
||||||
|
) $charset_collate;
|
||||||
|
";
|
||||||
|
|
||||||
|
// 支付令牌表
|
||||||
|
$payment_tokens_table = "
|
||||||
|
CREATE TABLE {$wpdb->prefix}yoone_payment_tokens (
|
||||||
|
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
customer_id bigint(20) unsigned NOT NULL,
|
||||||
|
gateway_id varchar(50) NOT NULL,
|
||||||
|
token varchar(255) NOT NULL,
|
||||||
|
token_type varchar(20) NOT NULL DEFAULT 'credit_card',
|
||||||
|
card_type varchar(20) DEFAULT NULL,
|
||||||
|
last_four varchar(4) DEFAULT NULL,
|
||||||
|
expiry_month varchar(2) DEFAULT NULL,
|
||||||
|
expiry_year varchar(4) DEFAULT NULL,
|
||||||
|
is_default tinyint(1) NOT NULL DEFAULT 0,
|
||||||
|
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY customer_id (customer_id),
|
||||||
|
KEY gateway_id (gateway_id),
|
||||||
|
UNIQUE KEY unique_token (gateway_id, token)
|
||||||
|
) $charset_collate;
|
||||||
|
";
|
||||||
|
|
||||||
|
// 订阅日志表
|
||||||
|
$subscription_logs_table = "
|
||||||
|
CREATE TABLE {$wpdb->prefix}yoone_subscription_logs (
|
||||||
|
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
subscription_id bigint(20) unsigned NOT NULL,
|
||||||
|
type varchar(50) NOT NULL,
|
||||||
|
message text NOT NULL,
|
||||||
|
data longtext DEFAULT NULL,
|
||||||
|
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY subscription_id (subscription_id),
|
||||||
|
KEY type (type),
|
||||||
|
KEY created_at (created_at)
|
||||||
|
) $charset_collate;
|
||||||
|
";
|
||||||
|
|
||||||
|
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||||||
|
|
||||||
|
dbDelta($subscriptions_table);
|
||||||
|
dbDelta($subscription_items_table);
|
||||||
|
dbDelta($bundles_table);
|
||||||
|
dbDelta($bundle_items_table);
|
||||||
|
dbDelta($payment_tokens_table);
|
||||||
|
dbDelta($subscription_logs_table);
|
||||||
|
|
||||||
|
// 更新数据库版本
|
||||||
|
update_option('yoone_subscriptions_db_version', self::DB_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建默认页面
|
||||||
|
*/
|
||||||
|
private static function create_pages() {
|
||||||
|
$pages = array(
|
||||||
|
'subscriptions' => array(
|
||||||
|
'name' => _x('subscriptions', 'Page slug', 'yoone-subscriptions'),
|
||||||
|
'title' => _x('我的订阅', 'Page title', 'yoone-subscriptions'),
|
||||||
|
'content' => '[yoone_my_subscriptions]'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($pages as $key => $page) {
|
||||||
|
$option_key = 'yoone_subscriptions_' . $key . '_page_id';
|
||||||
|
|
||||||
|
if (!get_option($option_key)) {
|
||||||
|
$page_id = wp_insert_post(array(
|
||||||
|
'post_title' => $page['title'],
|
||||||
|
'post_content' => $page['content'],
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'post_type' => 'page',
|
||||||
|
'post_name' => $page['name'],
|
||||||
|
'comment_status' => 'closed'
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($page_id) {
|
||||||
|
update_option($option_key, $page_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置默认选项
|
||||||
|
*/
|
||||||
|
private static function set_default_options() {
|
||||||
|
$default_options = array(
|
||||||
|
'yoone_subscriptions_enable_bundles' => 'yes',
|
||||||
|
'yoone_subscriptions_enable_trials' => 'yes',
|
||||||
|
'yoone_subscriptions_default_period' => 'month',
|
||||||
|
'yoone_subscriptions_default_interval' => 1,
|
||||||
|
'yoone_subscriptions_retry_failed_payments' => 'yes',
|
||||||
|
'yoone_subscriptions_max_retry_attempts' => 3,
|
||||||
|
'yoone_subscriptions_retry_interval' => 1
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($default_options as $option => $value) {
|
||||||
|
if (!get_option($option)) {
|
||||||
|
update_option($option, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建订阅状态
|
||||||
|
*/
|
||||||
|
private static function create_subscription_statuses() {
|
||||||
|
$statuses = array(
|
||||||
|
'yoone-pending' => _x('待处理', 'Subscription status', 'yoone-subscriptions'),
|
||||||
|
'yoone-active' => _x('活跃', 'Subscription status', 'yoone-subscriptions'),
|
||||||
|
'yoone-paused' => _x('暂停', 'Subscription status', 'yoone-subscriptions'),
|
||||||
|
'yoone-cancelled' => _x('已取消', 'Subscription status', 'yoone-subscriptions'),
|
||||||
|
'yoone-expired' => _x('已过期', 'Subscription status', 'yoone-subscriptions'),
|
||||||
|
'yoone-trial' => _x('试用期', 'Subscription status', 'yoone-subscriptions')
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($statuses as $status => $label) {
|
||||||
|
register_post_status($status, array(
|
||||||
|
'label' => $label,
|
||||||
|
'public' => false,
|
||||||
|
'exclude_from_search' => false,
|
||||||
|
'show_in_admin_all_list' => true,
|
||||||
|
'show_in_admin_status_list' => true,
|
||||||
|
'label_count' => _n_noop($label . ' <span class="count">(%s)</span>', $label . ' <span class="count">(%s)</span>', 'yoone-subscriptions')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查数据库版本
|
||||||
|
*/
|
||||||
|
public static function check_version() {
|
||||||
|
if (get_option('yoone_subscriptions_db_version') !== self::DB_VERSION) {
|
||||||
|
self::create_tables();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,488 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 日志分析器类
|
||||||
|
*
|
||||||
|
* 用于分析日志数据,生成统计报告和趋势分析
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志分析器类
|
||||||
|
*/
|
||||||
|
class Yoone_Log_Analyzer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析最近的日志
|
||||||
|
*
|
||||||
|
* @param int $days 分析天数
|
||||||
|
* @return array 分析结果
|
||||||
|
*/
|
||||||
|
public static function analyze_recent_logs($days = 7) {
|
||||||
|
$logs = Yoone_Logger::get_recent_logs(1000);
|
||||||
|
$cutoff_date = date('Y-m-d H:i:s', strtotime("-{$days} days"));
|
||||||
|
|
||||||
|
$analysis = array(
|
||||||
|
'total_logs' => 0,
|
||||||
|
'error_count' => 0,
|
||||||
|
'warning_count' => 0,
|
||||||
|
'info_count' => 0,
|
||||||
|
'debug_count' => 0,
|
||||||
|
'daily_stats' => array(),
|
||||||
|
'hourly_stats' => array(),
|
||||||
|
'top_errors' => array(),
|
||||||
|
'subscription_stats' => array(),
|
||||||
|
'payment_stats' => array(),
|
||||||
|
'performance_issues' => array()
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($logs as $log_line) {
|
||||||
|
$parsed_log = self::parse_log_line($log_line);
|
||||||
|
|
||||||
|
if (!$parsed_log || $parsed_log['timestamp'] < $cutoff_date) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$analysis['total_logs']++;
|
||||||
|
|
||||||
|
// 统计日志级别
|
||||||
|
$level = $parsed_log['level'];
|
||||||
|
if (isset($analysis[$level . '_count'])) {
|
||||||
|
$analysis[$level . '_count']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按日统计
|
||||||
|
$date = date('Y-m-d', strtotime($parsed_log['timestamp']));
|
||||||
|
if (!isset($analysis['daily_stats'][$date])) {
|
||||||
|
$analysis['daily_stats'][$date] = array(
|
||||||
|
'total' => 0,
|
||||||
|
'error' => 0,
|
||||||
|
'warning' => 0,
|
||||||
|
'info' => 0,
|
||||||
|
'debug' => 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$analysis['daily_stats'][$date]['total']++;
|
||||||
|
$analysis['daily_stats'][$date][$level]++;
|
||||||
|
|
||||||
|
// 按小时统计
|
||||||
|
$hour = date('H', strtotime($parsed_log['timestamp']));
|
||||||
|
if (!isset($analysis['hourly_stats'][$hour])) {
|
||||||
|
$analysis['hourly_stats'][$hour] = 0;
|
||||||
|
}
|
||||||
|
$analysis['hourly_stats'][$hour]++;
|
||||||
|
|
||||||
|
// 收集错误信息
|
||||||
|
if ($level === 'error') {
|
||||||
|
$error_key = md5($parsed_log['message']);
|
||||||
|
if (!isset($analysis['top_errors'][$error_key])) {
|
||||||
|
$analysis['top_errors'][$error_key] = array(
|
||||||
|
'message' => $parsed_log['message'],
|
||||||
|
'count' => 0,
|
||||||
|
'first_seen' => $parsed_log['timestamp'],
|
||||||
|
'last_seen' => $parsed_log['timestamp']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$analysis['top_errors'][$error_key]['count']++;
|
||||||
|
$analysis['top_errors'][$error_key]['last_seen'] = $parsed_log['timestamp'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订阅相关统计
|
||||||
|
if (strpos($parsed_log['message'], 'subscription') !== false) {
|
||||||
|
$analysis['subscription_stats']['total'] = ($analysis['subscription_stats']['total'] ?? 0) + 1;
|
||||||
|
|
||||||
|
if (strpos($parsed_log['message'], 'created') !== false) {
|
||||||
|
$analysis['subscription_stats']['created'] = ($analysis['subscription_stats']['created'] ?? 0) + 1;
|
||||||
|
} elseif (strpos($parsed_log['message'], 'cancelled') !== false) {
|
||||||
|
$analysis['subscription_stats']['cancelled'] = ($analysis['subscription_stats']['cancelled'] ?? 0) + 1;
|
||||||
|
} elseif (strpos($parsed_log['message'], 'renewed') !== false) {
|
||||||
|
$analysis['subscription_stats']['renewed'] = ($analysis['subscription_stats']['renewed'] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支付相关统计
|
||||||
|
if (strpos($parsed_log['message'], 'payment') !== false) {
|
||||||
|
$analysis['payment_stats']['total'] = ($analysis['payment_stats']['total'] ?? 0) + 1;
|
||||||
|
|
||||||
|
if (strpos($parsed_log['message'], 'success') !== false) {
|
||||||
|
$analysis['payment_stats']['success'] = ($analysis['payment_stats']['success'] ?? 0) + 1;
|
||||||
|
} elseif (strpos($parsed_log['message'], 'failed') !== false) {
|
||||||
|
$analysis['payment_stats']['failed'] = ($analysis['payment_stats']['failed'] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 性能问题检测
|
||||||
|
if (strpos($parsed_log['message'], 'timeout') !== false ||
|
||||||
|
strpos($parsed_log['message'], 'slow') !== false ||
|
||||||
|
strpos($parsed_log['message'], 'memory') !== false) {
|
||||||
|
$analysis['performance_issues'][] = array(
|
||||||
|
'timestamp' => $parsed_log['timestamp'],
|
||||||
|
'message' => $parsed_log['message'],
|
||||||
|
'level' => $level
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排序错误统计
|
||||||
|
uasort($analysis['top_errors'], function($a, $b) {
|
||||||
|
return $b['count'] - $a['count'];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 只保留前10个错误
|
||||||
|
$analysis['top_errors'] = array_slice($analysis['top_errors'], 0, 10, true);
|
||||||
|
|
||||||
|
return $analysis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成健康报告
|
||||||
|
*
|
||||||
|
* @return array 健康报告
|
||||||
|
*/
|
||||||
|
public static function generate_health_report() {
|
||||||
|
$analysis = self::analyze_recent_logs(7);
|
||||||
|
|
||||||
|
$health_score = 100;
|
||||||
|
$issues = array();
|
||||||
|
$recommendations = array();
|
||||||
|
|
||||||
|
// 错误率检查
|
||||||
|
$error_rate = $analysis['total_logs'] > 0 ? ($analysis['error_count'] / $analysis['total_logs']) * 100 : 0;
|
||||||
|
if ($error_rate > 10) {
|
||||||
|
$health_score -= 30;
|
||||||
|
$issues[] = sprintf(__('错误率过高: %.1f%%', 'yoone-subscriptions'), $error_rate);
|
||||||
|
$recommendations[] = __('检查并修复频繁出现的错误', 'yoone-subscriptions');
|
||||||
|
} elseif ($error_rate > 5) {
|
||||||
|
$health_score -= 15;
|
||||||
|
$issues[] = sprintf(__('错误率较高: %.1f%%', 'yoone-subscriptions'), $error_rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 警告率检查
|
||||||
|
$warning_rate = $analysis['total_logs'] > 0 ? ($analysis['warning_count'] / $analysis['total_logs']) * 100 : 0;
|
||||||
|
if ($warning_rate > 20) {
|
||||||
|
$health_score -= 20;
|
||||||
|
$issues[] = sprintf(__('警告率过高: %.1f%%', 'yoone-subscriptions'), $warning_rate);
|
||||||
|
$recommendations[] = __('关注警告信息,预防潜在问题', 'yoone-subscriptions');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支付失败率检查
|
||||||
|
if (isset($analysis['payment_stats']['total']) && $analysis['payment_stats']['total'] > 0) {
|
||||||
|
$payment_failure_rate = (($analysis['payment_stats']['failed'] ?? 0) / $analysis['payment_stats']['total']) * 100;
|
||||||
|
if ($payment_failure_rate > 15) {
|
||||||
|
$health_score -= 25;
|
||||||
|
$issues[] = sprintf(__('支付失败率过高: %.1f%%', 'yoone-subscriptions'), $payment_failure_rate);
|
||||||
|
$recommendations[] = __('检查支付网关配置和网络连接', 'yoone-subscriptions');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 性能问题检查
|
||||||
|
if (count($analysis['performance_issues']) > 5) {
|
||||||
|
$health_score -= 20;
|
||||||
|
$issues[] = sprintf(__('发现 %d 个性能问题', 'yoone-subscriptions'), count($analysis['performance_issues']));
|
||||||
|
$recommendations[] = __('优化代码性能,检查服务器资源', 'yoone-subscriptions');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 频繁错误检查
|
||||||
|
foreach ($analysis['top_errors'] as $error) {
|
||||||
|
if ($error['count'] > 10) {
|
||||||
|
$health_score -= 10;
|
||||||
|
$issues[] = sprintf(__('频繁错误: %s (出现 %d 次)', 'yoone-subscriptions'),
|
||||||
|
substr($error['message'], 0, 50) . '...', $error['count']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保分数不低于0
|
||||||
|
$health_score = max(0, $health_score);
|
||||||
|
|
||||||
|
// 健康等级
|
||||||
|
if ($health_score >= 90) {
|
||||||
|
$health_level = 'excellent';
|
||||||
|
$health_text = __('优秀', 'yoone-subscriptions');
|
||||||
|
} elseif ($health_score >= 70) {
|
||||||
|
$health_level = 'good';
|
||||||
|
$health_text = __('良好', 'yoone-subscriptions');
|
||||||
|
} elseif ($health_score >= 50) {
|
||||||
|
$health_level = 'fair';
|
||||||
|
$health_text = __('一般', 'yoone-subscriptions');
|
||||||
|
} else {
|
||||||
|
$health_level = 'poor';
|
||||||
|
$health_text = __('较差', 'yoone-subscriptions');
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'score' => $health_score,
|
||||||
|
'level' => $health_level,
|
||||||
|
'text' => $health_text,
|
||||||
|
'issues' => $issues,
|
||||||
|
'recommendations' => $recommendations,
|
||||||
|
'analysis' => $analysis
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取趋势数据
|
||||||
|
*
|
||||||
|
* @param int $days 天数
|
||||||
|
* @return array 趋势数据
|
||||||
|
*/
|
||||||
|
public static function get_trend_data($days = 30) {
|
||||||
|
$analysis = self::analyze_recent_logs($days);
|
||||||
|
|
||||||
|
$trends = array(
|
||||||
|
'daily_errors' => array(),
|
||||||
|
'daily_warnings' => array(),
|
||||||
|
'daily_total' => array(),
|
||||||
|
'subscription_activity' => array(),
|
||||||
|
'payment_activity' => array()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 填充每日数据
|
||||||
|
for ($i = $days - 1; $i >= 0; $i--) {
|
||||||
|
$date = date('Y-m-d', strtotime("-{$i} days"));
|
||||||
|
|
||||||
|
$trends['daily_errors'][$date] = $analysis['daily_stats'][$date]['error'] ?? 0;
|
||||||
|
$trends['daily_warnings'][$date] = $analysis['daily_stats'][$date]['warning'] ?? 0;
|
||||||
|
$trends['daily_total'][$date] = $analysis['daily_stats'][$date]['total'] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $trends;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测异常模式
|
||||||
|
*
|
||||||
|
* @return array 异常模式
|
||||||
|
*/
|
||||||
|
public static function detect_anomalies() {
|
||||||
|
$analysis = self::analyze_recent_logs(7);
|
||||||
|
$anomalies = array();
|
||||||
|
|
||||||
|
// 检测错误激增
|
||||||
|
$daily_errors = array();
|
||||||
|
foreach ($analysis['daily_stats'] as $date => $stats) {
|
||||||
|
$daily_errors[] = $stats['error'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($daily_errors) >= 3) {
|
||||||
|
$avg_errors = array_sum($daily_errors) / count($daily_errors);
|
||||||
|
$latest_errors = end($daily_errors);
|
||||||
|
|
||||||
|
if ($latest_errors > $avg_errors * 2 && $latest_errors > 5) {
|
||||||
|
$anomalies[] = array(
|
||||||
|
'type' => 'error_spike',
|
||||||
|
'severity' => 'high',
|
||||||
|
'message' => sprintf(__('今日错误数量异常增加: %d (平均: %.1f)', 'yoone-subscriptions'),
|
||||||
|
$latest_errors, $avg_errors),
|
||||||
|
'timestamp' => current_time('mysql')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测重复错误
|
||||||
|
foreach ($analysis['top_errors'] as $error) {
|
||||||
|
if ($error['count'] > 20) {
|
||||||
|
$anomalies[] = array(
|
||||||
|
'type' => 'repeated_error',
|
||||||
|
'severity' => 'medium',
|
||||||
|
'message' => sprintf(__('重复错误: %s (出现 %d 次)', 'yoone-subscriptions'),
|
||||||
|
substr($error['message'], 0, 100), $error['count']),
|
||||||
|
'timestamp' => $error['last_seen']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测支付问题
|
||||||
|
if (isset($analysis['payment_stats']['failed']) && $analysis['payment_stats']['failed'] > 10) {
|
||||||
|
$anomalies[] = array(
|
||||||
|
'type' => 'payment_issues',
|
||||||
|
'severity' => 'high',
|
||||||
|
'message' => sprintf(__('支付失败次数过多: %d', 'yoone-subscriptions'),
|
||||||
|
$analysis['payment_stats']['failed']),
|
||||||
|
'timestamp' => current_time('mysql')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $anomalies;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成报告摘要
|
||||||
|
*
|
||||||
|
* @param array $analysis 分析数据
|
||||||
|
* @return string 报告摘要
|
||||||
|
*/
|
||||||
|
public static function generate_summary($analysis) {
|
||||||
|
$summary = array();
|
||||||
|
|
||||||
|
$summary[] = sprintf(__('总计 %d 条日志记录', 'yoone-subscriptions'), $analysis['total_logs']);
|
||||||
|
|
||||||
|
if ($analysis['error_count'] > 0) {
|
||||||
|
$summary[] = sprintf(__('%d 个错误', 'yoone-subscriptions'), $analysis['error_count']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($analysis['warning_count'] > 0) {
|
||||||
|
$summary[] = sprintf(__('%d 个警告', 'yoone-subscriptions'), $analysis['warning_count']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($analysis['subscription_stats']['total'])) {
|
||||||
|
$summary[] = sprintf(__('%d 个订阅相关事件', 'yoone-subscriptions'),
|
||||||
|
$analysis['subscription_stats']['total']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($analysis['payment_stats']['total'])) {
|
||||||
|
$summary[] = sprintf(__('%d 个支付相关事件', 'yoone-subscriptions'),
|
||||||
|
$analysis['payment_stats']['total']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(',', $summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析日志行
|
||||||
|
*
|
||||||
|
* @param string $log_line 日志行
|
||||||
|
* @return array|false 解析结果
|
||||||
|
*/
|
||||||
|
private static function parse_log_line($log_line) {
|
||||||
|
// 匹配WooCommerce日志格式
|
||||||
|
if (preg_match('/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2})\s+(\w+)\s+(.+)$/', $log_line, $matches)) {
|
||||||
|
return array(
|
||||||
|
'timestamp' => $matches[1],
|
||||||
|
'level' => strtolower($matches[2]),
|
||||||
|
'message' => $matches[3]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出分析报告
|
||||||
|
*
|
||||||
|
* @param string $format 格式 (json|csv|html)
|
||||||
|
* @param int $days 分析天数
|
||||||
|
* @return string 报告内容
|
||||||
|
*/
|
||||||
|
public static function export_report($format = 'json', $days = 7) {
|
||||||
|
$health_report = self::generate_health_report();
|
||||||
|
$trends = self::get_trend_data($days);
|
||||||
|
$anomalies = self::detect_anomalies();
|
||||||
|
|
||||||
|
$report_data = array(
|
||||||
|
'generated_at' => current_time('mysql'),
|
||||||
|
'period_days' => $days,
|
||||||
|
'health' => $health_report,
|
||||||
|
'trends' => $trends,
|
||||||
|
'anomalies' => $anomalies
|
||||||
|
);
|
||||||
|
|
||||||
|
switch ($format) {
|
||||||
|
case 'json':
|
||||||
|
return json_encode($report_data, JSON_PRETTY_PRINT);
|
||||||
|
|
||||||
|
case 'csv':
|
||||||
|
return self::convert_to_csv($report_data);
|
||||||
|
|
||||||
|
case 'html':
|
||||||
|
return self::convert_to_html($report_data);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return json_encode($report_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为CSV格式
|
||||||
|
*
|
||||||
|
* @param array $data 数据
|
||||||
|
* @return string CSV内容
|
||||||
|
*/
|
||||||
|
private static function convert_to_csv($data) {
|
||||||
|
$csv = "Yoone Subscriptions Log Analysis Report\n";
|
||||||
|
$csv .= "Generated: " . $data['generated_at'] . "\n";
|
||||||
|
$csv .= "Period: " . $data['period_days'] . " days\n\n";
|
||||||
|
|
||||||
|
$csv .= "Health Score," . $data['health']['score'] . "\n";
|
||||||
|
$csv .= "Health Level," . $data['health']['text'] . "\n\n";
|
||||||
|
|
||||||
|
if (!empty($data['health']['issues'])) {
|
||||||
|
$csv .= "Issues:\n";
|
||||||
|
foreach ($data['health']['issues'] as $issue) {
|
||||||
|
$csv .= "," . $issue . "\n";
|
||||||
|
}
|
||||||
|
$csv .= "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data['anomalies'])) {
|
||||||
|
$csv .= "Anomalies:\n";
|
||||||
|
$csv .= "Type,Severity,Message,Timestamp\n";
|
||||||
|
foreach ($data['anomalies'] as $anomaly) {
|
||||||
|
$csv .= $anomaly['type'] . "," . $anomaly['severity'] . "," .
|
||||||
|
$anomaly['message'] . "," . $anomaly['timestamp'] . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $csv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为HTML格式
|
||||||
|
*
|
||||||
|
* @param array $data 数据
|
||||||
|
* @return string HTML内容
|
||||||
|
*/
|
||||||
|
private static function convert_to_html($data) {
|
||||||
|
$html = '<html><head><title>Yoone Subscriptions Log Analysis Report</title>';
|
||||||
|
$html .= '<style>body{font-family:Arial,sans-serif;margin:20px;}';
|
||||||
|
$html .= '.health-score{font-size:24px;font-weight:bold;margin:20px 0;}';
|
||||||
|
$html .= '.excellent{color:#28a745;}.good{color:#17a2b8;}.fair{color:#ffc107;}.poor{color:#dc3545;}';
|
||||||
|
$html .= 'table{border-collapse:collapse;width:100%;margin:20px 0;}';
|
||||||
|
$html .= 'th,td{border:1px solid #ddd;padding:8px;text-align:left;}';
|
||||||
|
$html .= 'th{background-color:#f2f2f2;}</style></head><body>';
|
||||||
|
|
||||||
|
$html .= '<h1>Yoone Subscriptions 日志分析报告</h1>';
|
||||||
|
$html .= '<p>生成时间: ' . $data['generated_at'] . '</p>';
|
||||||
|
$html .= '<p>分析周期: ' . $data['period_days'] . ' 天</p>';
|
||||||
|
|
||||||
|
$html .= '<div class="health-score ' . $data['health']['level'] . '">';
|
||||||
|
$html .= '健康评分: ' . $data['health']['score'] . '/100 (' . $data['health']['text'] . ')';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
if (!empty($data['health']['issues'])) {
|
||||||
|
$html .= '<h2>发现的问题</h2><ul>';
|
||||||
|
foreach ($data['health']['issues'] as $issue) {
|
||||||
|
$html .= '<li>' . htmlspecialchars($issue) . '</li>';
|
||||||
|
}
|
||||||
|
$html .= '</ul>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data['health']['recommendations'])) {
|
||||||
|
$html .= '<h2>建议</h2><ul>';
|
||||||
|
foreach ($data['health']['recommendations'] as $rec) {
|
||||||
|
$html .= '<li>' . htmlspecialchars($rec) . '</li>';
|
||||||
|
}
|
||||||
|
$html .= '</ul>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data['anomalies'])) {
|
||||||
|
$html .= '<h2>异常检测</h2><table>';
|
||||||
|
$html .= '<tr><th>类型</th><th>严重程度</th><th>描述</th><th>时间</th></tr>';
|
||||||
|
foreach ($data['anomalies'] as $anomaly) {
|
||||||
|
$html .= '<tr>';
|
||||||
|
$html .= '<td>' . htmlspecialchars($anomaly['type']) . '</td>';
|
||||||
|
$html .= '<td>' . htmlspecialchars($anomaly['severity']) . '</td>';
|
||||||
|
$html .= '<td>' . htmlspecialchars($anomaly['message']) . '</td>';
|
||||||
|
$html .= '<td>' . htmlspecialchars($anomaly['timestamp']) . '</td>';
|
||||||
|
$html .= '</tr>';
|
||||||
|
}
|
||||||
|
$html .= '</table>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$html .= '</body></html>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,314 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 日志类
|
||||||
|
*
|
||||||
|
* @package YooneSubscriptions
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yoone_Logger类
|
||||||
|
* 处理日志记录
|
||||||
|
*/
|
||||||
|
class Yoone_Logger {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志级别
|
||||||
|
*/
|
||||||
|
const EMERGENCY = 'emergency';
|
||||||
|
const ALERT = 'alert';
|
||||||
|
const CRITICAL = 'critical';
|
||||||
|
const ERROR = 'error';
|
||||||
|
const WARNING = 'warning';
|
||||||
|
const NOTICE = 'notice';
|
||||||
|
const INFO = 'info';
|
||||||
|
const DEBUG = 'debug';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WooCommerce日志实例
|
||||||
|
*
|
||||||
|
* @var WC_Logger
|
||||||
|
*/
|
||||||
|
private static $logger = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志来源
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private static $source = 'yoone-subscriptions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取日志实例
|
||||||
|
*
|
||||||
|
* @return WC_Logger
|
||||||
|
*/
|
||||||
|
private static function get_logger() {
|
||||||
|
if (is_null(self::$logger)) {
|
||||||
|
self::$logger = wc_get_logger();
|
||||||
|
}
|
||||||
|
return self::$logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录紧急日志
|
||||||
|
*
|
||||||
|
* @param string $message 消息
|
||||||
|
* @param array $context 上下文
|
||||||
|
*/
|
||||||
|
public static function emergency($message, $context = array()) {
|
||||||
|
self::log(self::EMERGENCY, $message, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录警报日志
|
||||||
|
*
|
||||||
|
* @param string $message 消息
|
||||||
|
* @param array $context 上下文
|
||||||
|
*/
|
||||||
|
public static function alert($message, $context = array()) {
|
||||||
|
self::log(self::ALERT, $message, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录严重日志
|
||||||
|
*
|
||||||
|
* @param string $message 消息
|
||||||
|
* @param array $context 上下文
|
||||||
|
*/
|
||||||
|
public static function critical($message, $context = array()) {
|
||||||
|
self::log(self::CRITICAL, $message, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录错误日志
|
||||||
|
*
|
||||||
|
* @param string $message 消息
|
||||||
|
* @param array $context 上下文
|
||||||
|
*/
|
||||||
|
public static function error($message, $context = array()) {
|
||||||
|
self::log(self::ERROR, $message, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录警告日志
|
||||||
|
*
|
||||||
|
* @param string $message 消息
|
||||||
|
* @param array $context 上下文
|
||||||
|
*/
|
||||||
|
public static function warning($message, $context = array()) {
|
||||||
|
self::log(self::WARNING, $message, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录通知日志
|
||||||
|
*
|
||||||
|
* @param string $message 消息
|
||||||
|
* @param array $context 上下文
|
||||||
|
*/
|
||||||
|
public static function notice($message, $context = array()) {
|
||||||
|
self::log(self::NOTICE, $message, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录信息日志
|
||||||
|
*
|
||||||
|
* @param string $message 消息
|
||||||
|
* @param array $context 上下文
|
||||||
|
*/
|
||||||
|
public static function info($message, $context = array()) {
|
||||||
|
self::log(self::INFO, $message, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录调试日志
|
||||||
|
*
|
||||||
|
* @param string $message 消息
|
||||||
|
* @param array $context 上下文
|
||||||
|
*/
|
||||||
|
public static function debug($message, $context = array()) {
|
||||||
|
self::log(self::DEBUG, $message, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录日志
|
||||||
|
*
|
||||||
|
* @param string $level 级别
|
||||||
|
* @param string $message 消息
|
||||||
|
* @param array $context 上下文
|
||||||
|
*/
|
||||||
|
public static function log($level, $message, $context = array()) {
|
||||||
|
if (!class_exists('WC_Logger')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$logger = self::get_logger();
|
||||||
|
|
||||||
|
// 添加上下文信息到消息中
|
||||||
|
if (!empty($context)) {
|
||||||
|
$message .= ' ' . wp_json_encode($context);
|
||||||
|
}
|
||||||
|
|
||||||
|
$logger->log($level, $message, array('source' => self::$source));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录订阅相关日志
|
||||||
|
*
|
||||||
|
* @param int $subscription_id 订阅ID
|
||||||
|
* @param string $message 消息
|
||||||
|
* @param string $level 级别
|
||||||
|
*/
|
||||||
|
public static function log_subscription($subscription_id, $message, $level = self::INFO) {
|
||||||
|
$context = array('subscription_id' => $subscription_id);
|
||||||
|
self::log($level, $message, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录支付相关日志
|
||||||
|
*
|
||||||
|
* @param int $order_id 订单ID
|
||||||
|
* @param string $message 消息
|
||||||
|
* @param string $level 级别
|
||||||
|
*/
|
||||||
|
public static function log_payment($order_id, $message, $level = self::INFO) {
|
||||||
|
$context = array('order_id' => $order_id);
|
||||||
|
self::log($level, $message, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录混装相关日志
|
||||||
|
*
|
||||||
|
* @param int $bundle_id 混装ID
|
||||||
|
* @param string $message 消息
|
||||||
|
* @param string $level 级别
|
||||||
|
*/
|
||||||
|
public static function log_bundle($bundle_id, $message, $level = self::INFO) {
|
||||||
|
$context = array('bundle_id' => $bundle_id);
|
||||||
|
self::log($level, $message, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录API调用日志
|
||||||
|
*
|
||||||
|
* @param string $api API名称
|
||||||
|
* @param array $request 请求数据
|
||||||
|
* @param array $response 响应数据
|
||||||
|
* @param string $level 级别
|
||||||
|
*/
|
||||||
|
public static function log_api_call($api, $request = array(), $response = array(), $level = self::INFO) {
|
||||||
|
$context = array(
|
||||||
|
'api' => $api,
|
||||||
|
'request' => $request,
|
||||||
|
'response' => $response
|
||||||
|
);
|
||||||
|
self::log($level, 'API调用: ' . $api, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录Moneris支付日志
|
||||||
|
*
|
||||||
|
* @param string $transaction_type 交易类型
|
||||||
|
* @param array $request 请求数据
|
||||||
|
* @param array $response 响应数据
|
||||||
|
* @param string $level 级别
|
||||||
|
*/
|
||||||
|
public static function log_moneris($transaction_type, $request = array(), $response = array(), $level = self::INFO) {
|
||||||
|
// 移除敏感信息
|
||||||
|
if (isset($request['api_token'])) {
|
||||||
|
$request['api_token'] = '***';
|
||||||
|
}
|
||||||
|
if (isset($request['pan'])) {
|
||||||
|
$request['pan'] = substr($request['pan'], 0, 4) . '****' . substr($request['pan'], -4);
|
||||||
|
}
|
||||||
|
if (isset($request['cvd'])) {
|
||||||
|
$request['cvd'] = '***';
|
||||||
|
}
|
||||||
|
|
||||||
|
$context = array(
|
||||||
|
'transaction_type' => $transaction_type,
|
||||||
|
'request' => $request,
|
||||||
|
'response' => $response
|
||||||
|
);
|
||||||
|
self::log($level, 'Moneris交易: ' . $transaction_type, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取日志文件路径
|
||||||
|
*
|
||||||
|
* @return string 日志文件路径
|
||||||
|
*/
|
||||||
|
public static function get_log_file_path() {
|
||||||
|
$upload_dir = wp_upload_dir();
|
||||||
|
return $upload_dir['basedir'] . '/wc-logs/' . self::$source . '-' . sanitize_file_name(wp_hash(self::$source)) . '.log';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理旧日志
|
||||||
|
*
|
||||||
|
* @param int $days 保留天数
|
||||||
|
*/
|
||||||
|
public static function cleanup_old_logs($days = 30) {
|
||||||
|
if (!class_exists('WC_Log_Handler_File')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$log_handler = new WC_Log_Handler_File();
|
||||||
|
$logs = $log_handler->get_log_files();
|
||||||
|
|
||||||
|
foreach ($logs as $log_file) {
|
||||||
|
if (strpos($log_file, self::$source) !== false) {
|
||||||
|
$file_path = WC_Log_Handler_File::get_log_file_path($log_file);
|
||||||
|
|
||||||
|
if (file_exists($file_path)) {
|
||||||
|
$file_time = filemtime($file_path);
|
||||||
|
$cutoff_time = time() - ($days * 24 * 60 * 60);
|
||||||
|
|
||||||
|
if ($file_time < $cutoff_time) {
|
||||||
|
unlink($file_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最近的日志条目
|
||||||
|
*
|
||||||
|
* @param int $limit 限制数量
|
||||||
|
* @return array 日志条目
|
||||||
|
*/
|
||||||
|
public static function get_recent_logs($limit = 100) {
|
||||||
|
$log_file = self::get_log_file_path();
|
||||||
|
|
||||||
|
if (!file_exists($log_file)) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = file($log_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
|
$lines = array_reverse($lines);
|
||||||
|
|
||||||
|
return array_slice($lines, 0, $limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置日志来源
|
||||||
|
*
|
||||||
|
* @param string $source 来源
|
||||||
|
*/
|
||||||
|
public static function set_source($source) {
|
||||||
|
self::$source = $source;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取日志来源
|
||||||
|
*
|
||||||
|
* @return string 来源
|
||||||
|
*/
|
||||||
|
public static function get_source() {
|
||||||
|
return self::$source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,524 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 前端功能管理类
|
||||||
|
*
|
||||||
|
* 处理前端页面展示和用户交互
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 前端管理类
|
||||||
|
*/
|
||||||
|
class Yoone_Frontend {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
$this->init_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化钩子
|
||||||
|
*/
|
||||||
|
private function init_hooks() {
|
||||||
|
// 产品页面钩子
|
||||||
|
add_action('woocommerce_single_product_summary', array($this, 'display_bundle_options'), 25);
|
||||||
|
add_action('woocommerce_single_product_summary', array($this, 'display_subscription_options'), 30);
|
||||||
|
|
||||||
|
// 购物车钩子
|
||||||
|
add_filter('woocommerce_cart_item_name', array($this, 'display_cart_bundle_info'), 10, 3);
|
||||||
|
add_filter('woocommerce_cart_item_price', array($this, 'display_cart_subscription_info'), 10, 3);
|
||||||
|
|
||||||
|
// 结账钩子
|
||||||
|
add_action('woocommerce_checkout_order_review', array($this, 'display_checkout_subscription_info'));
|
||||||
|
|
||||||
|
// 我的账户钩子
|
||||||
|
add_filter('woocommerce_account_menu_items', array($this, 'add_subscriptions_menu_item'));
|
||||||
|
add_action('init', array($this, 'add_subscriptions_endpoint'));
|
||||||
|
add_action('woocommerce_account_subscriptions_endpoint', array($this, 'subscriptions_content'));
|
||||||
|
|
||||||
|
// AJAX 钩子
|
||||||
|
add_action('wp_ajax_yoone_calculate_bundle_price', array($this, 'ajax_calculate_bundle_price'));
|
||||||
|
add_action('wp_ajax_nopriv_yoone_calculate_bundle_price', array($this, 'ajax_calculate_bundle_price'));
|
||||||
|
add_action('wp_ajax_yoone_add_bundle_to_cart', array($this, 'ajax_add_bundle_to_cart'));
|
||||||
|
add_action('wp_ajax_nopriv_yoone_add_bundle_to_cart', array($this, 'ajax_add_bundle_to_cart'));
|
||||||
|
|
||||||
|
// 脚本和样式
|
||||||
|
add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts'));
|
||||||
|
|
||||||
|
// 产品类型支持
|
||||||
|
add_filter('woocommerce_product_class', array($this, 'product_class_filter'), 10, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载脚本和样式
|
||||||
|
*/
|
||||||
|
public function enqueue_scripts() {
|
||||||
|
if (is_product() || is_cart() || is_checkout() || is_account_page()) {
|
||||||
|
wp_enqueue_script(
|
||||||
|
'yoone-frontend',
|
||||||
|
YOONE_SUBSCRIPTIONS_PLUGIN_URL . 'assets/js/frontend.js',
|
||||||
|
array('jquery'),
|
||||||
|
YOONE_SUBSCRIPTIONS_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_enqueue_style(
|
||||||
|
'yoone-frontend',
|
||||||
|
YOONE_SUBSCRIPTIONS_PLUGIN_URL . 'assets/css/frontend.css',
|
||||||
|
array(),
|
||||||
|
YOONE_SUBSCRIPTIONS_VERSION
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_localize_script('yoone-frontend', 'yoone_frontend_params', array(
|
||||||
|
'ajax_url' => admin_url('admin-ajax.php'),
|
||||||
|
'nonce' => wp_create_nonce('yoone_frontend_nonce'),
|
||||||
|
'i18n' => array(
|
||||||
|
'loading' => __('加载中...', 'yoone-subscriptions'),
|
||||||
|
'error' => __('发生错误,请重试', 'yoone-subscriptions'),
|
||||||
|
'select_products' => __('请选择产品', 'yoone-subscriptions'),
|
||||||
|
'min_quantity' => __('最小数量: %d', 'yoone-subscriptions'),
|
||||||
|
'max_quantity' => __('最大数量: %d', 'yoone-subscriptions'),
|
||||||
|
'bundle_savings' => __('套装优惠: %s', 'yoone-subscriptions'),
|
||||||
|
'subscription_info' => __('订阅信息', 'yoone-subscriptions'),
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 产品页面功能
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示套装选项
|
||||||
|
*/
|
||||||
|
public function display_bundle_options() {
|
||||||
|
global $product;
|
||||||
|
|
||||||
|
if (!$product || $product->get_type() !== 'yoone_bundle') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bundle = new Yoone_Bundle();
|
||||||
|
$bundle_data = $bundle->get_bundle_by_product_id($product->get_id());
|
||||||
|
|
||||||
|
if (!$bundle_data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bundle_items = $bundle->get_bundle_items($bundle_data['id']);
|
||||||
|
|
||||||
|
if (empty($bundle_items)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wc_get_template(
|
||||||
|
'single-product/bundle-options.php',
|
||||||
|
array(
|
||||||
|
'bundle' => $bundle_data,
|
||||||
|
'bundle_items' => $bundle_items,
|
||||||
|
'product' => $product
|
||||||
|
),
|
||||||
|
'',
|
||||||
|
YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'templates/'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示订阅选项
|
||||||
|
*/
|
||||||
|
public function display_subscription_options() {
|
||||||
|
global $product;
|
||||||
|
|
||||||
|
if (!$product) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscription_enabled = $product->get_meta('_yoone_subscription_enabled');
|
||||||
|
|
||||||
|
if ($subscription_enabled !== 'yes') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscription_data = array(
|
||||||
|
'period' => $product->get_meta('_yoone_subscription_period'),
|
||||||
|
'interval' => $product->get_meta('_yoone_subscription_interval'),
|
||||||
|
'length' => $product->get_meta('_yoone_subscription_length'),
|
||||||
|
'trial' => $product->get_meta('_yoone_subscription_trial_length'),
|
||||||
|
'signup' => $product->get_meta('_yoone_subscription_signup_fee')
|
||||||
|
);
|
||||||
|
|
||||||
|
wc_get_template(
|
||||||
|
'single-product/subscription-options.php',
|
||||||
|
array(
|
||||||
|
'subscription_data' => $subscription_data,
|
||||||
|
'product' => $product
|
||||||
|
),
|
||||||
|
'',
|
||||||
|
YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'templates/'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 购物车功能
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示购物车套装信息
|
||||||
|
*/
|
||||||
|
public function display_cart_bundle_info($name, $cart_item, $cart_item_key) {
|
||||||
|
if (isset($cart_item['yoone_bundle_data'])) {
|
||||||
|
$bundle_data = $cart_item['yoone_bundle_data'];
|
||||||
|
|
||||||
|
$name .= '<div class="yoone-bundle-info">';
|
||||||
|
$name .= '<small>' . __('套装产品', 'yoone-subscriptions') . '</small>';
|
||||||
|
|
||||||
|
if (!empty($bundle_data['items'])) {
|
||||||
|
$name .= '<ul class="yoone-bundle-items">';
|
||||||
|
foreach ($bundle_data['items'] as $item) {
|
||||||
|
$item_product = wc_get_product($item['product_id']);
|
||||||
|
if ($item_product) {
|
||||||
|
$name .= '<li>' . $item_product->get_name() . ' × ' . $item['quantity'] . '</li>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$name .= '</ul>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($bundle_data['savings']) && $bundle_data['savings'] > 0) {
|
||||||
|
$name .= '<small class="yoone-savings">' . sprintf(__('节省: %s', 'yoone-subscriptions'), wc_price($bundle_data['savings'])) . '</small>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$name .= '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示购物车订阅信息
|
||||||
|
*/
|
||||||
|
public function display_cart_subscription_info($price, $cart_item, $cart_item_key) {
|
||||||
|
if (isset($cart_item['yoone_subscription_data'])) {
|
||||||
|
$subscription_data = $cart_item['yoone_subscription_data'];
|
||||||
|
|
||||||
|
$price .= '<div class="yoone-subscription-info">';
|
||||||
|
$price .= '<small>' . $this->format_subscription_string($subscription_data) . '</small>';
|
||||||
|
$price .= '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $price;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 结账功能
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示结账订阅信息
|
||||||
|
*/
|
||||||
|
public function display_checkout_subscription_info() {
|
||||||
|
$has_subscription = false;
|
||||||
|
|
||||||
|
foreach (WC()->cart->get_cart() as $cart_item) {
|
||||||
|
if (isset($cart_item['yoone_subscription_data'])) {
|
||||||
|
$has_subscription = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$has_subscription) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<div class="yoone-checkout-subscription-info">';
|
||||||
|
echo '<h3>' . __('订阅信息', 'yoone-subscriptions') . '</h3>';
|
||||||
|
echo '<p>' . __('您的订单包含订阅产品,将会自动续费。', 'yoone-subscriptions') . '</p>';
|
||||||
|
|
||||||
|
foreach (WC()->cart->get_cart() as $cart_item) {
|
||||||
|
if (isset($cart_item['yoone_subscription_data'])) {
|
||||||
|
$product = $cart_item['data'];
|
||||||
|
$subscription_data = $cart_item['yoone_subscription_data'];
|
||||||
|
|
||||||
|
echo '<div class="subscription-item">';
|
||||||
|
echo '<strong>' . $product->get_name() . '</strong><br>';
|
||||||
|
echo '<small>' . $this->format_subscription_string($subscription_data) . '</small>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 我的账户功能
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加订阅菜单项
|
||||||
|
*/
|
||||||
|
public function add_subscriptions_menu_item($items) {
|
||||||
|
$new_items = array();
|
||||||
|
|
||||||
|
foreach ($items as $key => $item) {
|
||||||
|
$new_items[$key] = $item;
|
||||||
|
|
||||||
|
if ($key === 'orders') {
|
||||||
|
$new_items['subscriptions'] = __('我的订阅', 'yoone-subscriptions');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $new_items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加订阅端点
|
||||||
|
*/
|
||||||
|
public function add_subscriptions_endpoint() {
|
||||||
|
add_rewrite_endpoint('subscriptions', EP_ROOT | EP_PAGES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅页面内容
|
||||||
|
*/
|
||||||
|
public function subscriptions_content() {
|
||||||
|
$customer_id = get_current_user_id();
|
||||||
|
|
||||||
|
if (!$customer_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户订阅
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$subscriptions = $wpdb->get_results($wpdb->prepare("
|
||||||
|
SELECT * FROM {$wpdb->prefix}yoone_subscriptions
|
||||||
|
WHERE customer_id = %d
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
", $customer_id));
|
||||||
|
|
||||||
|
wc_get_template(
|
||||||
|
'myaccount/subscriptions.php',
|
||||||
|
array(
|
||||||
|
'subscriptions' => $subscriptions,
|
||||||
|
'customer_id' => $customer_id
|
||||||
|
),
|
||||||
|
'',
|
||||||
|
YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'templates/'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| AJAX 处理
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX 计算套装价格
|
||||||
|
*/
|
||||||
|
public function ajax_calculate_bundle_price() {
|
||||||
|
check_ajax_referer('yoone_frontend_nonce', 'nonce');
|
||||||
|
|
||||||
|
$bundle_id = intval($_POST['bundle_id']);
|
||||||
|
$selected_items = isset($_POST['selected_items']) ? $_POST['selected_items'] : array();
|
||||||
|
|
||||||
|
if (!$bundle_id || empty($selected_items)) {
|
||||||
|
wp_send_json_error(__('无效的参数', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$bundle = new Yoone_Bundle($bundle_id);
|
||||||
|
|
||||||
|
if (!$bundle->get_id()) {
|
||||||
|
wp_send_json_error(__('套装不存在', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算价格
|
||||||
|
$total_price = 0;
|
||||||
|
$original_price = 0;
|
||||||
|
$valid_items = array();
|
||||||
|
|
||||||
|
foreach ($selected_items as $item) {
|
||||||
|
$product_id = intval($item['product_id']);
|
||||||
|
$quantity = intval($item['quantity']);
|
||||||
|
|
||||||
|
if ($quantity <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$product = wc_get_product($product_id);
|
||||||
|
if (!$product) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$item_price = $product->get_price() * $quantity;
|
||||||
|
$original_price += $item_price;
|
||||||
|
|
||||||
|
$valid_items[] = array(
|
||||||
|
'product_id' => $product_id,
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'price' => $item_price
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($valid_items)) {
|
||||||
|
wp_send_json_error(__('请选择有效的产品', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用套装折扣
|
||||||
|
$bundle_price = $bundle->calculate_bundle_price($valid_items);
|
||||||
|
$savings = $original_price - $bundle_price;
|
||||||
|
|
||||||
|
wp_send_json_success(array(
|
||||||
|
'original_price' => wc_price($original_price),
|
||||||
|
'bundle_price' => wc_price($bundle_price),
|
||||||
|
'savings' => wc_price($savings),
|
||||||
|
'savings_amount' => $savings
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX 添加套装到购物车
|
||||||
|
*/
|
||||||
|
public function ajax_add_bundle_to_cart() {
|
||||||
|
check_ajax_referer('yoone_frontend_nonce', 'nonce');
|
||||||
|
|
||||||
|
$bundle_id = intval($_POST['bundle_id']);
|
||||||
|
$selected_items = isset($_POST['selected_items']) ? $_POST['selected_items'] : array();
|
||||||
|
|
||||||
|
if (!$bundle_id || empty($selected_items)) {
|
||||||
|
wp_send_json_error(__('无效的参数', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$bundle = new Yoone_Bundle($bundle_id);
|
||||||
|
|
||||||
|
if (!$bundle->get_id()) {
|
||||||
|
wp_send_json_error(__('套装不存在', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证数量限制
|
||||||
|
$total_quantity = array_sum(array_column($selected_items, 'quantity'));
|
||||||
|
|
||||||
|
if ($bundle->get_min_quantity() > 0 && $total_quantity < $bundle->get_min_quantity()) {
|
||||||
|
wp_send_json_error(sprintf(__('最小数量要求: %d', 'yoone-subscriptions'), $bundle->get_min_quantity()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($bundle->get_max_quantity() > 0 && $total_quantity > $bundle->get_max_quantity()) {
|
||||||
|
wp_send_json_error(sprintf(__('最大数量限制: %d', 'yoone-subscriptions'), $bundle->get_max_quantity()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建套装产品并添加到购物车
|
||||||
|
$bundle_product_id = $bundle->get_product_id();
|
||||||
|
|
||||||
|
if (!$bundle_product_id) {
|
||||||
|
wp_send_json_error(__('套装产品不存在', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算套装价格
|
||||||
|
$bundle_price = $bundle->calculate_bundle_price($selected_items);
|
||||||
|
$original_price = 0;
|
||||||
|
|
||||||
|
foreach ($selected_items as $item) {
|
||||||
|
$product = wc_get_product($item['product_id']);
|
||||||
|
if ($product) {
|
||||||
|
$original_price += $product->get_price() * $item['quantity'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$savings = $original_price - $bundle_price;
|
||||||
|
|
||||||
|
// 添加到购物车
|
||||||
|
$cart_item_data = array(
|
||||||
|
'yoone_bundle_data' => array(
|
||||||
|
'bundle_id' => $bundle_id,
|
||||||
|
'items' => $selected_items,
|
||||||
|
'original_price' => $original_price,
|
||||||
|
'bundle_price' => $bundle_price,
|
||||||
|
'savings' => $savings
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$cart_item_key = WC()->cart->add_to_cart($bundle_product_id, 1, 0, array(), $cart_item_data);
|
||||||
|
|
||||||
|
if ($cart_item_key) {
|
||||||
|
wp_send_json_success(array(
|
||||||
|
'message' => __('套装已添加到购物车', 'yoone-subscriptions'),
|
||||||
|
'cart_url' => wc_get_cart_url()
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
wp_send_json_error(__('添加到购物车失败', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 辅助方法
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化订阅字符串
|
||||||
|
*/
|
||||||
|
private function format_subscription_string($subscription_data) {
|
||||||
|
$period_strings = array(
|
||||||
|
'day' => __('天', 'yoone-subscriptions'),
|
||||||
|
'week' => __('周', 'yoone-subscriptions'),
|
||||||
|
'month' => __('月', 'yoone-subscriptions'),
|
||||||
|
'year' => __('年', 'yoone-subscriptions')
|
||||||
|
);
|
||||||
|
|
||||||
|
$period = isset($subscription_data['period']) ? $subscription_data['period'] : 'month';
|
||||||
|
$interval = isset($subscription_data['interval']) ? intval($subscription_data['interval']) : 1;
|
||||||
|
|
||||||
|
$period_string = isset($period_strings[$period]) ? $period_strings[$period] : $period;
|
||||||
|
|
||||||
|
if ($interval > 1) {
|
||||||
|
return sprintf(__('每 %d %s', 'yoone-subscriptions'), $interval, $period_string);
|
||||||
|
} else {
|
||||||
|
return sprintf(__('每%s', 'yoone-subscriptions'), $period_string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 产品类型过滤器
|
||||||
|
*/
|
||||||
|
public function product_class_filter($classname, $product_type) {
|
||||||
|
if ($product_type === 'yoone_bundle') {
|
||||||
|
return 'Yoone_Bundle_Product';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $classname;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户订阅统计
|
||||||
|
*/
|
||||||
|
public function get_customer_subscription_stats($customer_id) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$stats = $wpdb->get_row($wpdb->prepare("
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_subscriptions,
|
||||||
|
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active_subscriptions,
|
||||||
|
SUM(CASE WHEN status = 'paused' THEN 1 ELSE 0 END) as paused_subscriptions,
|
||||||
|
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled_subscriptions,
|
||||||
|
SUM(CASE WHEN status = 'active' THEN total ELSE 0 END) as monthly_total
|
||||||
|
FROM {$wpdb->prefix}yoone_subscriptions
|
||||||
|
WHERE customer_id = %d
|
||||||
|
", $customer_id), ARRAY_A);
|
||||||
|
|
||||||
|
return $stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 支付网关接口
|
||||||
|
*
|
||||||
|
* 定义支付网关必须实现的方法
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付网关接口
|
||||||
|
*/
|
||||||
|
interface Interface_Yoone_Payment_Gateway {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理支付
|
||||||
|
*/
|
||||||
|
public function process_payment($order_id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理订阅支付
|
||||||
|
*/
|
||||||
|
public function process_subscription_payment($subscription_id, $amount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建支付令牌
|
||||||
|
*/
|
||||||
|
public function create_payment_token($payment_data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除支付令牌
|
||||||
|
*/
|
||||||
|
public function delete_payment_token($token_id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证支付令牌
|
||||||
|
*/
|
||||||
|
public function validate_payment_token($token_id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理退款
|
||||||
|
*/
|
||||||
|
public function process_refund($order_id, $amount = null, $reason = '');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预授权
|
||||||
|
*/
|
||||||
|
public function process_preauth($order_id, $amount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成预授权
|
||||||
|
*/
|
||||||
|
public function complete_preauth($transaction_id, $amount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支付方式标题
|
||||||
|
*/
|
||||||
|
public function get_title();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支付方式描述
|
||||||
|
*/
|
||||||
|
public function get_description();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否支持功能
|
||||||
|
*/
|
||||||
|
public function supports($feature);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否可用
|
||||||
|
*/
|
||||||
|
public function is_available();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支付字段
|
||||||
|
*/
|
||||||
|
public function payment_fields();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证支付字段
|
||||||
|
*/
|
||||||
|
public function validate_fields();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理管理选项
|
||||||
|
*/
|
||||||
|
public function process_admin_options();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取网关设置
|
||||||
|
*/
|
||||||
|
public function get_option($key, $empty_value = null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置网关选项
|
||||||
|
*/
|
||||||
|
public function update_option($key, $value);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 订阅接口
|
||||||
|
*
|
||||||
|
* 定义订阅对象必须实现的方法
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅接口
|
||||||
|
*/
|
||||||
|
interface Interface_Yoone_Subscription {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订阅状态
|
||||||
|
*/
|
||||||
|
public function get_status($context = 'view');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置订阅状态
|
||||||
|
*/
|
||||||
|
public function set_status($status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取客户ID
|
||||||
|
*/
|
||||||
|
public function get_customer_id($context = 'view');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置客户ID
|
||||||
|
*/
|
||||||
|
public function set_customer_id($customer_id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订阅周期
|
||||||
|
*/
|
||||||
|
public function get_billing_period($context = 'view');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置订阅周期
|
||||||
|
*/
|
||||||
|
public function set_billing_period($period);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订阅间隔
|
||||||
|
*/
|
||||||
|
public function get_billing_interval($context = 'view');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置订阅间隔
|
||||||
|
*/
|
||||||
|
public function set_billing_interval($interval);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下次付款日期
|
||||||
|
*/
|
||||||
|
public function get_next_payment_date($context = 'view');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置下次付款日期
|
||||||
|
*/
|
||||||
|
public function set_next_payment_date($date);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订阅金额
|
||||||
|
*/
|
||||||
|
public function get_total($context = 'view');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置订阅金额
|
||||||
|
*/
|
||||||
|
public function set_total($total);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支付方式
|
||||||
|
*/
|
||||||
|
public function get_payment_method($context = 'view');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置支付方式
|
||||||
|
*/
|
||||||
|
public function set_payment_method($method);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支付令牌
|
||||||
|
*/
|
||||||
|
public function get_payment_token($context = 'view');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置支付令牌
|
||||||
|
*/
|
||||||
|
public function set_payment_token($token);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订阅商品
|
||||||
|
*/
|
||||||
|
public function get_items();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加订阅商品
|
||||||
|
*/
|
||||||
|
public function add_item($item);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除订阅商品
|
||||||
|
*/
|
||||||
|
public function remove_item($item_id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活订阅
|
||||||
|
*/
|
||||||
|
public function activate();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 暂停订阅
|
||||||
|
*/
|
||||||
|
public function pause();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复订阅
|
||||||
|
*/
|
||||||
|
public function resume();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消订阅
|
||||||
|
*/
|
||||||
|
public function cancel();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理续费
|
||||||
|
*/
|
||||||
|
public function process_renewal();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否可以续费
|
||||||
|
*/
|
||||||
|
public function can_be_renewed();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否可以暂停
|
||||||
|
*/
|
||||||
|
public function can_be_paused();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否可以取消
|
||||||
|
*/
|
||||||
|
public function can_be_cancelled();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,842 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Moneris 支付网关类
|
||||||
|
*
|
||||||
|
* 处理 Moneris 支付集成和订阅支付
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moneris 支付网关类
|
||||||
|
*/
|
||||||
|
class Yoone_Moneris_Gateway extends WC_Payment_Gateway implements Interface_Yoone_Payment_Gateway {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网关ID
|
||||||
|
*/
|
||||||
|
public $id = 'yoone_moneris';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持的功能
|
||||||
|
*/
|
||||||
|
public $supports = array(
|
||||||
|
'products',
|
||||||
|
'subscriptions',
|
||||||
|
'subscription_cancellation',
|
||||||
|
'subscription_suspension',
|
||||||
|
'subscription_reactivation',
|
||||||
|
'subscription_amount_changes',
|
||||||
|
'subscription_date_changes',
|
||||||
|
'multiple_subscriptions',
|
||||||
|
'refunds',
|
||||||
|
'pre-orders'
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试模式
|
||||||
|
*/
|
||||||
|
protected $testmode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 凭据
|
||||||
|
*/
|
||||||
|
protected $store_id;
|
||||||
|
protected $api_token;
|
||||||
|
protected $processing_country;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
$this->method_title = __('Yoone Moneris', 'yoone-subscriptions');
|
||||||
|
$this->method_description = __('通过 Moneris 处理信用卡支付和订阅', 'yoone-subscriptions');
|
||||||
|
$this->has_fields = true;
|
||||||
|
|
||||||
|
// 加载设置
|
||||||
|
$this->init_form_fields();
|
||||||
|
$this->init_settings();
|
||||||
|
|
||||||
|
// 获取设置值
|
||||||
|
$this->title = $this->get_option('title');
|
||||||
|
$this->description = $this->get_option('description');
|
||||||
|
$this->testmode = 'yes' === $this->get_option('testmode');
|
||||||
|
$this->store_id = $this->get_option('store_id');
|
||||||
|
$this->api_token = $this->get_option('api_token');
|
||||||
|
$this->processing_country = $this->get_option('processing_country', 'CA');
|
||||||
|
|
||||||
|
// 钩子
|
||||||
|
add_action('woocommerce_update_options_payment_gateways_' . $this->id, array($this, 'process_admin_options'));
|
||||||
|
add_action('wp_enqueue_scripts', array($this, 'payment_scripts'));
|
||||||
|
|
||||||
|
// 订阅钩子
|
||||||
|
add_action('woocommerce_scheduled_subscription_payment_' . $this->id, array($this, 'scheduled_subscription_payment'), 10, 2);
|
||||||
|
add_action('wcs_resubscribe_order_created', array($this, 'delete_resubscribe_meta'), 10);
|
||||||
|
|
||||||
|
// 支持令牌化
|
||||||
|
$this->supports[] = 'tokenization';
|
||||||
|
$this->supports[] = 'add_payment_method';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化表单字段
|
||||||
|
*/
|
||||||
|
public function init_form_fields() {
|
||||||
|
$this->form_fields = array(
|
||||||
|
'enabled' => array(
|
||||||
|
'title' => __('启用/禁用', 'yoone-subscriptions'),
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'label' => __('启用 Moneris 支付', 'yoone-subscriptions'),
|
||||||
|
'default' => 'no'
|
||||||
|
),
|
||||||
|
'title' => array(
|
||||||
|
'title' => __('标题', 'yoone-subscriptions'),
|
||||||
|
'type' => 'text',
|
||||||
|
'description' => __('用户在结账时看到的支付方式标题', 'yoone-subscriptions'),
|
||||||
|
'default' => __('信用卡', 'yoone-subscriptions'),
|
||||||
|
'desc_tip' => true,
|
||||||
|
),
|
||||||
|
'description' => array(
|
||||||
|
'title' => __('描述', 'yoone-subscriptions'),
|
||||||
|
'type' => 'textarea',
|
||||||
|
'description' => __('用户在结账时看到的支付方式描述', 'yoone-subscriptions'),
|
||||||
|
'default' => __('使用信用卡安全支付', 'yoone-subscriptions'),
|
||||||
|
'desc_tip' => true,
|
||||||
|
),
|
||||||
|
'testmode' => array(
|
||||||
|
'title' => __('测试模式', 'yoone-subscriptions'),
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'label' => __('启用测试模式', 'yoone-subscriptions'),
|
||||||
|
'default' => 'yes',
|
||||||
|
'description' => __('在测试模式下,您可以使用测试卡号进行测试', 'yoone-subscriptions'),
|
||||||
|
),
|
||||||
|
'store_id' => array(
|
||||||
|
'title' => __('Store ID', 'yoone-subscriptions'),
|
||||||
|
'type' => 'text',
|
||||||
|
'description' => __('从 Moneris 获取的 Store ID', 'yoone-subscriptions'),
|
||||||
|
'default' => '',
|
||||||
|
'desc_tip' => true,
|
||||||
|
),
|
||||||
|
'api_token' => array(
|
||||||
|
'title' => __('API Token', 'yoone-subscriptions'),
|
||||||
|
'type' => 'password',
|
||||||
|
'description' => __('从 Moneris 获取的 API Token', 'yoone-subscriptions'),
|
||||||
|
'default' => '',
|
||||||
|
'desc_tip' => true,
|
||||||
|
),
|
||||||
|
'processing_country' => array(
|
||||||
|
'title' => __('处理国家', 'yoone-subscriptions'),
|
||||||
|
'type' => 'select',
|
||||||
|
'description' => __('选择处理支付的国家', 'yoone-subscriptions'),
|
||||||
|
'default' => 'CA',
|
||||||
|
'desc_tip' => true,
|
||||||
|
'options' => array(
|
||||||
|
'CA' => __('加拿大', 'yoone-subscriptions'),
|
||||||
|
'US' => __('美国', 'yoone-subscriptions'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载支付脚本
|
||||||
|
*/
|
||||||
|
public function payment_scripts() {
|
||||||
|
if (!is_cart() && !is_checkout() && !isset($_GET['pay_for_order'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('no' === $this->enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($this->store_id) || empty($this->api_token)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_enqueue_script('yoone-moneris-js', YOONE_SUBSCRIPTIONS_PLUGIN_URL . 'assets/js/moneris-payment.js', array('jquery'), YOONE_SUBSCRIPTIONS_VERSION, true);
|
||||||
|
|
||||||
|
wp_localize_script('yoone-moneris-js', 'yoone_moneris_params', array(
|
||||||
|
'ajax_url' => admin_url('admin-ajax.php'),
|
||||||
|
'nonce' => wp_create_nonce('yoone_moneris_nonce'),
|
||||||
|
'testmode' => $this->testmode,
|
||||||
|
'store_id' => $this->store_id,
|
||||||
|
'i18n' => array(
|
||||||
|
'invalid_card' => __('请输入有效的信用卡号', 'yoone-subscriptions'),
|
||||||
|
'invalid_cvv' => __('请输入有效的CVV', 'yoone-subscriptions'),
|
||||||
|
'invalid_date' => __('请输入有效的到期日期', 'yoone-subscriptions'),
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付字段
|
||||||
|
*/
|
||||||
|
public function payment_fields() {
|
||||||
|
if ($this->description) {
|
||||||
|
echo wpautop(wptexturize($this->description));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->testmode) {
|
||||||
|
echo '<p class="testmode-info">' . __('测试模式已启用。您可以使用测试卡号:4242424242424242', 'yoone-subscriptions') . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<fieldset id="wc-' . esc_attr($this->id) . '-cc-form" class="wc-credit-card-form wc-payment-form" style="background:transparent;">';
|
||||||
|
|
||||||
|
// 如果支持令牌化,显示保存的支付方式
|
||||||
|
if ($this->supports('tokenization') && is_checkout()) {
|
||||||
|
$this->tokenization_script();
|
||||||
|
$this->saved_payment_methods();
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<div class="wc-credit-card-form-card-number">
|
||||||
|
<label for="' . esc_attr($this->id) . '-card-number">' . __('卡号', 'yoone-subscriptions') . ' <span class="required">*</span></label>
|
||||||
|
<input id="' . esc_attr($this->id) . '-card-number" class="input-text wc-credit-card-form-card-number" type="text" maxlength="20" autocomplete="cc-number" placeholder="•••• •••• •••• ••••" name="' . esc_attr($this->id) . '-card-number" />
|
||||||
|
</div>';
|
||||||
|
|
||||||
|
echo '<div class="wc-credit-card-form-card-expiry">
|
||||||
|
<label for="' . esc_attr($this->id) . '-card-expiry">' . __('到期日期 (MM/YY)', 'yoone-subscriptions') . ' <span class="required">*</span></label>
|
||||||
|
<input id="' . esc_attr($this->id) . '-card-expiry" class="input-text wc-credit-card-form-card-expiry" type="text" autocomplete="cc-exp" placeholder="MM / YY" name="' . esc_attr($this->id) . '-card-expiry" />
|
||||||
|
</div>';
|
||||||
|
|
||||||
|
echo '<div class="wc-credit-card-form-card-cvc">
|
||||||
|
<label for="' . esc_attr($this->id) . '-card-cvc">' . __('CVV', 'yoone-subscriptions') . ' <span class="required">*</span></label>
|
||||||
|
<input id="' . esc_attr($this->id) . '-card-cvc" class="input-text wc-credit-card-form-card-cvc" type="text" autocomplete="cc-csc" placeholder="CVV" name="' . esc_attr($this->id) . '-card-cvc" />
|
||||||
|
</div>';
|
||||||
|
|
||||||
|
// 保存支付方式选项
|
||||||
|
if ($this->supports('tokenization') && is_checkout() && !is_add_payment_method_page()) {
|
||||||
|
echo '<div class="wc-credit-card-form-save-payment-method">
|
||||||
|
<input id="wc-' . esc_attr($this->id) . '-new-payment-method" name="wc-' . esc_attr($this->id) . '-new-payment-method" type="checkbox" value="true" style="width:auto;" />
|
||||||
|
<label for="wc-' . esc_attr($this->id) . '-new-payment-method" style="display:inline;">' . __('保存支付方式以便将来使用', 'yoone-subscriptions') . '</label>
|
||||||
|
</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<div class="clear"></div></fieldset>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证字段
|
||||||
|
*/
|
||||||
|
public function validate_fields() {
|
||||||
|
if (empty($_POST[$this->id . '-card-number'])) {
|
||||||
|
wc_add_notice(__('请输入信用卡号', 'yoone-subscriptions'), 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($_POST[$this->id . '-card-expiry'])) {
|
||||||
|
wc_add_notice(__('请输入到期日期', 'yoone-subscriptions'), 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($_POST[$this->id . '-card-cvc'])) {
|
||||||
|
wc_add_notice(__('请输入CVV', 'yoone-subscriptions'), 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理支付
|
||||||
|
*/
|
||||||
|
public function process_payment($order_id) {
|
||||||
|
$order = wc_get_order($order_id);
|
||||||
|
|
||||||
|
if (!$order) {
|
||||||
|
return array(
|
||||||
|
'result' => 'failure',
|
||||||
|
'messages' => __('订单不存在', 'yoone-subscriptions')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否包含订阅
|
||||||
|
$has_subscription = $this->order_contains_subscription($order);
|
||||||
|
|
||||||
|
if ($has_subscription) {
|
||||||
|
return $this->process_subscription_payment($order_id, $order->get_total());
|
||||||
|
} else {
|
||||||
|
return $this->process_regular_payment($order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理常规支付
|
||||||
|
*/
|
||||||
|
protected function process_regular_payment($order) {
|
||||||
|
$card_data = $this->get_card_data();
|
||||||
|
|
||||||
|
if (!$card_data) {
|
||||||
|
return array(
|
||||||
|
'result' => 'failure',
|
||||||
|
'messages' => __('信用卡信息无效', 'yoone-subscriptions')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用 Moneris Purchase API
|
||||||
|
$response = $this->moneris_purchase($order, $card_data);
|
||||||
|
|
||||||
|
if ($response && $response['success']) {
|
||||||
|
// 支付成功
|
||||||
|
$order->payment_complete($response['transaction_id']);
|
||||||
|
$order->add_order_note(sprintf(__('Moneris 支付完成。交易ID: %s', 'yoone-subscriptions'), $response['transaction_id']));
|
||||||
|
|
||||||
|
// 清空购物车
|
||||||
|
WC()->cart->empty_cart();
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'result' => 'success',
|
||||||
|
'redirect' => $this->get_return_url($order)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$error_message = isset($response['message']) ? $response['message'] : __('支付处理失败', 'yoone-subscriptions');
|
||||||
|
wc_add_notice($error_message, 'error');
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'result' => 'failure',
|
||||||
|
'messages' => $error_message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理订阅支付
|
||||||
|
*/
|
||||||
|
public function process_subscription_payment($subscription_id, $amount) {
|
||||||
|
// 获取订阅对象
|
||||||
|
if (is_numeric($subscription_id)) {
|
||||||
|
$subscription = new Yoone_Subscription($subscription_id);
|
||||||
|
} else {
|
||||||
|
$order = wc_get_order($subscription_id);
|
||||||
|
$subscription = $this->get_subscription_from_order($order);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$subscription || !$subscription->get_id()) {
|
||||||
|
return array(
|
||||||
|
'result' => 'failure',
|
||||||
|
'messages' => __('订阅不存在', 'yoone-subscriptions')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首次支付需要创建令牌
|
||||||
|
if (!$subscription->get_payment_token()) {
|
||||||
|
return $this->process_initial_subscription_payment($subscription, $amount);
|
||||||
|
} else {
|
||||||
|
return $this->process_recurring_subscription_payment($subscription, $amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理首次订阅支付
|
||||||
|
*/
|
||||||
|
protected function process_initial_subscription_payment($subscription, $amount) {
|
||||||
|
$card_data = $this->get_card_data();
|
||||||
|
|
||||||
|
if (!$card_data) {
|
||||||
|
return array(
|
||||||
|
'result' => 'failure',
|
||||||
|
'messages' => __('信用卡信息无效', 'yoone-subscriptions')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 添加信用卡到 Vault
|
||||||
|
$vault_response = $this->moneris_res_add_cc($card_data);
|
||||||
|
|
||||||
|
if (!$vault_response || !$vault_response['success']) {
|
||||||
|
return array(
|
||||||
|
'result' => 'failure',
|
||||||
|
'messages' => __('创建支付令牌失败', 'yoone-subscriptions')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data_key = $vault_response['data_key'];
|
||||||
|
|
||||||
|
// 2. 使用令牌进行首次支付
|
||||||
|
$payment_response = $this->moneris_res_purchase_cc($subscription, $data_key, $amount);
|
||||||
|
|
||||||
|
if ($payment_response && $payment_response['success']) {
|
||||||
|
// 保存支付令牌到订阅
|
||||||
|
$subscription->set_payment_token($data_key);
|
||||||
|
$subscription->save();
|
||||||
|
|
||||||
|
// 保存支付令牌到客户
|
||||||
|
$this->save_payment_token($subscription->get_customer_id(), $data_key, $card_data);
|
||||||
|
|
||||||
|
// 激活订阅
|
||||||
|
$subscription->activate();
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'result' => 'success',
|
||||||
|
'redirect' => wc_get_checkout_url() . '/order-received/' . $subscription->get_parent_order_id() . '/?key=' . wc_get_order($subscription->get_parent_order_id())->get_order_key()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 删除创建的令牌
|
||||||
|
$this->moneris_res_delete($data_key);
|
||||||
|
|
||||||
|
$error_message = isset($payment_response['message']) ? $payment_response['message'] : __('订阅支付失败', 'yoone-subscriptions');
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'result' => 'failure',
|
||||||
|
'messages' => $error_message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理续费支付
|
||||||
|
*/
|
||||||
|
protected function process_recurring_subscription_payment($subscription, $amount) {
|
||||||
|
$data_key = $subscription->get_payment_token();
|
||||||
|
|
||||||
|
if (!$data_key) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用保存的令牌进行支付
|
||||||
|
$response = $this->moneris_res_purchase_cc($subscription, $data_key, $amount);
|
||||||
|
|
||||||
|
if ($response && $response['success']) {
|
||||||
|
$subscription->add_log('payment_success', sprintf(__('续费支付成功。交易ID: %s', 'yoone-subscriptions'), $response['transaction_id']));
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
$error_message = isset($response['message']) ? $response['message'] : __('续费支付失败', 'yoone-subscriptions');
|
||||||
|
$subscription->add_log('payment_failed', $error_message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建支付令牌
|
||||||
|
*/
|
||||||
|
public function create_payment_token($payment_data) {
|
||||||
|
return $this->moneris_res_add_cc($payment_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除支付令牌
|
||||||
|
*/
|
||||||
|
public function delete_payment_token($token_id) {
|
||||||
|
return $this->moneris_res_delete($token_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证支付令牌
|
||||||
|
*/
|
||||||
|
public function validate_payment_token($token_id) {
|
||||||
|
$response = $this->moneris_res_lookup_full($token_id);
|
||||||
|
return $response && $response['success'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理退款
|
||||||
|
*/
|
||||||
|
public function process_refund($order_id, $amount = null, $reason = '') {
|
||||||
|
$order = wc_get_order($order_id);
|
||||||
|
|
||||||
|
if (!$order) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$transaction_id = $order->get_transaction_id();
|
||||||
|
|
||||||
|
if (!$transaction_id) {
|
||||||
|
return new WP_Error('error', __('没有找到交易ID', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->moneris_refund($transaction_id, $amount, $reason);
|
||||||
|
|
||||||
|
if ($response && $response['success']) {
|
||||||
|
$order->add_order_note(sprintf(__('退款成功。金额: %s, 原因: %s', 'yoone-subscriptions'), wc_price($amount), $reason));
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
$error_message = isset($response['message']) ? $response['message'] : __('退款失败', 'yoone-subscriptions');
|
||||||
|
return new WP_Error('error', $error_message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预授权
|
||||||
|
*/
|
||||||
|
public function process_preauth($order_id, $amount) {
|
||||||
|
$order = wc_get_order($order_id);
|
||||||
|
$card_data = $this->get_card_data();
|
||||||
|
|
||||||
|
return $this->moneris_preauth($order, $card_data, $amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成预授权
|
||||||
|
*/
|
||||||
|
public function complete_preauth($transaction_id, $amount) {
|
||||||
|
return $this->moneris_completion($transaction_id, $amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Moneris API 调用
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moneris Purchase API
|
||||||
|
*/
|
||||||
|
protected function moneris_purchase($order, $card_data) {
|
||||||
|
$data = array(
|
||||||
|
'store_id' => $this->store_id,
|
||||||
|
'api_token' => $this->api_token,
|
||||||
|
'order_id' => $order->get_id() . '-' . time(),
|
||||||
|
'amount' => number_format($order->get_total(), 2, '.', ''),
|
||||||
|
'pan' => $card_data['number'],
|
||||||
|
'expdate' => $card_data['exp_month'] . $card_data['exp_year'],
|
||||||
|
'cvd' => $card_data['cvc'],
|
||||||
|
'crypt_type' => '7'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->make_api_request('purchase', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moneris Res Add CC API
|
||||||
|
*/
|
||||||
|
protected function moneris_res_add_cc($card_data) {
|
||||||
|
$data = array(
|
||||||
|
'store_id' => $this->store_id,
|
||||||
|
'api_token' => $this->api_token,
|
||||||
|
'pan' => $card_data['number'],
|
||||||
|
'expdate' => $card_data['exp_month'] . $card_data['exp_year'],
|
||||||
|
'crypt_type' => '7'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->make_api_request('res_add_cc', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moneris Res Purchase CC API
|
||||||
|
*/
|
||||||
|
protected function moneris_res_purchase_cc($subscription, $data_key, $amount) {
|
||||||
|
$data = array(
|
||||||
|
'store_id' => $this->store_id,
|
||||||
|
'api_token' => $this->api_token,
|
||||||
|
'order_id' => 'sub-' . $subscription->get_id() . '-' . time(),
|
||||||
|
'amount' => number_format($amount, 2, '.', ''),
|
||||||
|
'data_key' => $data_key,
|
||||||
|
'crypt_type' => '1'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->make_api_request('res_purchase_cc', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moneris Res Delete API
|
||||||
|
*/
|
||||||
|
protected function moneris_res_delete($data_key) {
|
||||||
|
$data = array(
|
||||||
|
'store_id' => $this->store_id,
|
||||||
|
'api_token' => $this->api_token,
|
||||||
|
'data_key' => $data_key
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->make_api_request('res_delete', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moneris Res Lookup Full API
|
||||||
|
*/
|
||||||
|
protected function moneris_res_lookup_full($data_key) {
|
||||||
|
$data = array(
|
||||||
|
'store_id' => $this->store_id,
|
||||||
|
'api_token' => $this->api_token,
|
||||||
|
'data_key' => $data_key
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->make_api_request('res_lookup_full', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moneris Refund API
|
||||||
|
*/
|
||||||
|
protected function moneris_refund($transaction_id, $amount, $reason = '') {
|
||||||
|
$data = array(
|
||||||
|
'store_id' => $this->store_id,
|
||||||
|
'api_token' => $this->api_token,
|
||||||
|
'order_id' => 'refund-' . $transaction_id . '-' . time(),
|
||||||
|
'amount' => number_format($amount, 2, '.', ''),
|
||||||
|
'txn_number' => $transaction_id,
|
||||||
|
'crypt_type' => '7'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->make_api_request('refund', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moneris Preauth API
|
||||||
|
*/
|
||||||
|
protected function moneris_preauth($order, $card_data, $amount) {
|
||||||
|
$data = array(
|
||||||
|
'store_id' => $this->store_id,
|
||||||
|
'api_token' => $this->api_token,
|
||||||
|
'order_id' => 'preauth-' . $order->get_id() . '-' . time(),
|
||||||
|
'amount' => number_format($amount, 2, '.', ''),
|
||||||
|
'pan' => $card_data['number'],
|
||||||
|
'expdate' => $card_data['exp_month'] . $card_data['exp_year'],
|
||||||
|
'cvd' => $card_data['cvc'],
|
||||||
|
'crypt_type' => '7'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->make_api_request('preauth', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moneris Completion API
|
||||||
|
*/
|
||||||
|
protected function moneris_completion($transaction_id, $amount) {
|
||||||
|
$data = array(
|
||||||
|
'store_id' => $this->store_id,
|
||||||
|
'api_token' => $this->api_token,
|
||||||
|
'order_id' => 'completion-' . $transaction_id . '-' . time(),
|
||||||
|
'comp_amount' => number_format($amount, 2, '.', ''),
|
||||||
|
'txn_number' => $transaction_id,
|
||||||
|
'crypt_type' => '7'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->make_api_request('completion', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发起 API 请求
|
||||||
|
*/
|
||||||
|
protected function make_api_request($endpoint, $data) {
|
||||||
|
$url = $this->get_api_url();
|
||||||
|
|
||||||
|
$xml_data = $this->build_xml_request($endpoint, $data);
|
||||||
|
|
||||||
|
$response = wp_remote_post($url, array(
|
||||||
|
'body' => $xml_data,
|
||||||
|
'headers' => array(
|
||||||
|
'Content-Type' => 'application/xml',
|
||||||
|
'User-Agent' => 'Yoone Subscriptions/' . YOONE_SUBSCRIPTIONS_VERSION
|
||||||
|
),
|
||||||
|
'timeout' => 30,
|
||||||
|
'sslverify' => !$this->testmode
|
||||||
|
));
|
||||||
|
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
return array(
|
||||||
|
'success' => false,
|
||||||
|
'message' => $response->get_error_message()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->parse_xml_response(wp_remote_retrieve_body($response));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 API URL
|
||||||
|
*/
|
||||||
|
protected function get_api_url() {
|
||||||
|
if ($this->testmode) {
|
||||||
|
return $this->processing_country === 'US'
|
||||||
|
? 'https://esqa.moneris.com/gateway2/servlet/MpgRequest'
|
||||||
|
: 'https://esqa.moneris.com/gateway2/servlet/MpgRequest';
|
||||||
|
} else {
|
||||||
|
return $this->processing_country === 'US'
|
||||||
|
? 'https://esp.moneris.com/gateway2/servlet/MpgRequest'
|
||||||
|
: 'https://www3.moneris.com/gateway2/servlet/MpgRequest';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 XML 请求
|
||||||
|
*/
|
||||||
|
protected function build_xml_request($endpoint, $data) {
|
||||||
|
$xml = '<?xml version="1.0"?>';
|
||||||
|
$xml .= '<request>';
|
||||||
|
$xml .= '<store_id>' . htmlspecialchars($data['store_id']) . '</store_id>';
|
||||||
|
$xml .= '<api_token>' . htmlspecialchars($data['api_token']) . '</api_token>';
|
||||||
|
$xml .= '<' . $endpoint . '>';
|
||||||
|
|
||||||
|
foreach ($data as $key => $value) {
|
||||||
|
if ($key !== 'store_id' && $key !== 'api_token') {
|
||||||
|
$xml .= '<' . $key . '>' . htmlspecialchars($value) . '</' . $key . '>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$xml .= '</' . $endpoint . '>';
|
||||||
|
$xml .= '</request>';
|
||||||
|
|
||||||
|
return $xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 XML 响应
|
||||||
|
*/
|
||||||
|
protected function parse_xml_response($xml_string) {
|
||||||
|
$xml = simplexml_load_string($xml_string);
|
||||||
|
|
||||||
|
if (!$xml) {
|
||||||
|
return array(
|
||||||
|
'success' => false,
|
||||||
|
'message' => __('无效的响应格式', 'yoone-subscriptions')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response_code = (string) $xml->receipt->ResponseCode;
|
||||||
|
$success = $response_code !== null && intval($response_code) < 50;
|
||||||
|
|
||||||
|
$result = array(
|
||||||
|
'success' => $success,
|
||||||
|
'response_code' => $response_code,
|
||||||
|
'message' => (string) $xml->receipt->Message,
|
||||||
|
'transaction_id' => (string) $xml->receipt->TransID,
|
||||||
|
'reference_num' => (string) $xml->receipt->ReferenceNum,
|
||||||
|
'data_key' => (string) $xml->receipt->DataKey
|
||||||
|
);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 辅助方法
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取信用卡数据
|
||||||
|
*/
|
||||||
|
protected function get_card_data() {
|
||||||
|
if (empty($_POST[$this->id . '-card-number'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$card_number = str_replace(' ', '', $_POST[$this->id . '-card-number']);
|
||||||
|
$card_expiry = $_POST[$this->id . '-card-expiry'];
|
||||||
|
$card_cvc = $_POST[$this->id . '-card-cvc'];
|
||||||
|
|
||||||
|
// 解析到期日期
|
||||||
|
$expiry_parts = explode('/', str_replace(' ', '', $card_expiry));
|
||||||
|
if (count($expiry_parts) !== 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$exp_month = str_pad($expiry_parts[0], 2, '0', STR_PAD_LEFT);
|
||||||
|
$exp_year = strlen($expiry_parts[1]) === 2 ? '20' . $expiry_parts[1] : $expiry_parts[1];
|
||||||
|
$exp_year = substr($exp_year, -2); // Moneris 需要 2 位年份
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'number' => $card_number,
|
||||||
|
'exp_month' => $exp_month,
|
||||||
|
'exp_year' => $exp_year,
|
||||||
|
'cvc' => $card_cvc
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存支付令牌
|
||||||
|
*/
|
||||||
|
protected function save_payment_token($customer_id, $data_key, $card_data) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// 获取卡片信息
|
||||||
|
$lookup_response = $this->moneris_res_lookup_full($data_key);
|
||||||
|
|
||||||
|
$card_type = '';
|
||||||
|
$last_four = '';
|
||||||
|
|
||||||
|
if ($lookup_response && $lookup_response['success']) {
|
||||||
|
// 从响应中提取卡片信息
|
||||||
|
$card_type = $this->get_card_type($card_data['number']);
|
||||||
|
$last_four = substr($card_data['number'], -4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $wpdb->insert(
|
||||||
|
$wpdb->prefix . 'yoone_payment_tokens',
|
||||||
|
array(
|
||||||
|
'customer_id' => $customer_id,
|
||||||
|
'gateway_id' => $this->id,
|
||||||
|
'token' => $data_key,
|
||||||
|
'token_type' => 'credit_card',
|
||||||
|
'card_type' => $card_type,
|
||||||
|
'last_four' => $last_four,
|
||||||
|
'expiry_month' => $card_data['exp_month'],
|
||||||
|
'expiry_year' => '20' . $card_data['exp_year'],
|
||||||
|
'is_default' => 0,
|
||||||
|
'created_at' => current_time('mysql'),
|
||||||
|
'updated_at' => current_time('mysql')
|
||||||
|
),
|
||||||
|
array('%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取卡片类型
|
||||||
|
*/
|
||||||
|
protected function get_card_type($card_number) {
|
||||||
|
$card_number = str_replace(' ', '', $card_number);
|
||||||
|
|
||||||
|
if (preg_match('/^4/', $card_number)) {
|
||||||
|
return 'visa';
|
||||||
|
} elseif (preg_match('/^5[1-5]/', $card_number)) {
|
||||||
|
return 'mastercard';
|
||||||
|
} elseif (preg_match('/^3[47]/', $card_number)) {
|
||||||
|
return 'amex';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查订单是否包含订阅
|
||||||
|
*/
|
||||||
|
protected function order_contains_subscription($order) {
|
||||||
|
foreach ($order->get_items() as $item) {
|
||||||
|
$product = $item->get_product();
|
||||||
|
if ($product && $product->get_meta('_yoone_subscription_enabled') === 'yes') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从订单获取订阅
|
||||||
|
*/
|
||||||
|
protected function get_subscription_from_order($order) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$subscription_id = $wpdb->get_var($wpdb->prepare("
|
||||||
|
SELECT id FROM {$wpdb->prefix}yoone_subscriptions
|
||||||
|
WHERE parent_order_id = %d
|
||||||
|
LIMIT 1
|
||||||
|
", $order->get_id()));
|
||||||
|
|
||||||
|
return $subscription_id ? new Yoone_Subscription($subscription_id) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计划订阅支付
|
||||||
|
*/
|
||||||
|
public function scheduled_subscription_payment($amount_to_charge, $renewal_order) {
|
||||||
|
$subscription = $this->get_subscription_from_order($renewal_order);
|
||||||
|
|
||||||
|
if ($subscription) {
|
||||||
|
$result = $this->process_recurring_subscription_payment($subscription, $amount_to_charge);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
$renewal_order->payment_complete();
|
||||||
|
} else {
|
||||||
|
$renewal_order->update_status('failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否可用
|
||||||
|
*/
|
||||||
|
public function is_available() {
|
||||||
|
return parent::is_available() && !empty($this->store_id) && !empty($this->api_token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,534 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 支付令牌类
|
||||||
|
*
|
||||||
|
* 管理客户的支付令牌,用于订阅续费和重复支付
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付令牌类
|
||||||
|
*/
|
||||||
|
class Yoone_Payment_Token extends Abstract_Yoone_Data {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象类型
|
||||||
|
*/
|
||||||
|
protected $object_type = 'yoone_payment_token';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据结构
|
||||||
|
*/
|
||||||
|
protected $data = array(
|
||||||
|
'customer_id' => 0,
|
||||||
|
'gateway_id' => '',
|
||||||
|
'token' => '',
|
||||||
|
'token_type' => 'credit_card',
|
||||||
|
'card_type' => '',
|
||||||
|
'last_four' => '',
|
||||||
|
'expiry_month' => '',
|
||||||
|
'expiry_year' => '',
|
||||||
|
'is_default' => false,
|
||||||
|
'expires_at' => null,
|
||||||
|
'created_at' => null,
|
||||||
|
'updated_at' => null
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
public function __construct($id = 0) {
|
||||||
|
parent::__construct($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Getters
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取客户ID
|
||||||
|
*/
|
||||||
|
public function get_customer_id($context = 'view') {
|
||||||
|
return $this->get_prop('customer_id', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取网关ID
|
||||||
|
*/
|
||||||
|
public function get_gateway_id($context = 'view') {
|
||||||
|
return $this->get_prop('gateway_id', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取令牌
|
||||||
|
*/
|
||||||
|
public function get_token($context = 'view') {
|
||||||
|
return $this->get_prop('token', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取令牌类型
|
||||||
|
*/
|
||||||
|
public function get_token_type($context = 'view') {
|
||||||
|
return $this->get_prop('token_type', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取卡片类型
|
||||||
|
*/
|
||||||
|
public function get_card_type($context = 'view') {
|
||||||
|
return $this->get_prop('card_type', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取卡片后四位
|
||||||
|
*/
|
||||||
|
public function get_last_four($context = 'view') {
|
||||||
|
return $this->get_prop('last_four', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取过期月份
|
||||||
|
*/
|
||||||
|
public function get_expiry_month($context = 'view') {
|
||||||
|
return $this->get_prop('expiry_month', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取过期年份
|
||||||
|
*/
|
||||||
|
public function get_expiry_year($context = 'view') {
|
||||||
|
return $this->get_prop('expiry_year', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为默认令牌
|
||||||
|
*/
|
||||||
|
public function is_default($context = 'view') {
|
||||||
|
return $this->get_prop('is_default', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取过期时间
|
||||||
|
*/
|
||||||
|
public function get_expires_at($context = 'view') {
|
||||||
|
return $this->get_prop('expires_at', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取创建时间
|
||||||
|
*/
|
||||||
|
public function get_created_at($context = 'view') {
|
||||||
|
return $this->get_prop('created_at', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取更新时间
|
||||||
|
*/
|
||||||
|
public function get_updated_at($context = 'view') {
|
||||||
|
return $this->get_prop('updated_at', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Setters
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置客户ID
|
||||||
|
*/
|
||||||
|
public function set_customer_id($customer_id) {
|
||||||
|
$this->set_prop('customer_id', absint($customer_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置网关ID
|
||||||
|
*/
|
||||||
|
public function set_gateway_id($gateway_id) {
|
||||||
|
$this->set_prop('gateway_id', sanitize_text_field($gateway_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置令牌
|
||||||
|
*/
|
||||||
|
public function set_token($token) {
|
||||||
|
$this->set_prop('token', sanitize_text_field($token));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置令牌类型
|
||||||
|
*/
|
||||||
|
public function set_token_type($type) {
|
||||||
|
$valid_types = array('credit_card', 'debit_card', 'bank_account');
|
||||||
|
if (in_array($type, $valid_types)) {
|
||||||
|
$this->set_prop('token_type', $type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置卡片类型
|
||||||
|
*/
|
||||||
|
public function set_card_type($type) {
|
||||||
|
$this->set_prop('card_type', sanitize_text_field($type));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置卡片后四位
|
||||||
|
*/
|
||||||
|
public function set_last_four($last_four) {
|
||||||
|
$this->set_prop('last_four', sanitize_text_field($last_four));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置过期月份
|
||||||
|
*/
|
||||||
|
public function set_expiry_month($month) {
|
||||||
|
$this->set_prop('expiry_month', str_pad(absint($month), 2, '0', STR_PAD_LEFT));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置过期年份
|
||||||
|
*/
|
||||||
|
public function set_expiry_year($year) {
|
||||||
|
$this->set_prop('expiry_year', absint($year));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置为默认令牌
|
||||||
|
*/
|
||||||
|
public function set_default($is_default) {
|
||||||
|
$this->set_prop('is_default', (bool) $is_default);
|
||||||
|
|
||||||
|
// 如果设置为默认,需要取消其他令牌的默认状态
|
||||||
|
if ($is_default && $this->get_customer_id()) {
|
||||||
|
$this->unset_other_defaults();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置过期时间
|
||||||
|
*/
|
||||||
|
public function set_expires_at($expires_at) {
|
||||||
|
$this->set_prop('expires_at', $this->format_date($expires_at));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 验证方法
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查令牌是否有效
|
||||||
|
*/
|
||||||
|
public function is_valid() {
|
||||||
|
// 检查基本信息
|
||||||
|
if (empty($this->get_token()) || empty($this->get_gateway_id()) || !$this->get_customer_id()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
if ($this->is_expired()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查令牌是否过期
|
||||||
|
*/
|
||||||
|
public function is_expired() {
|
||||||
|
$expires_at = $this->get_expires_at();
|
||||||
|
|
||||||
|
if (!$expires_at) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return strtotime($expires_at) < time();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查卡片是否过期
|
||||||
|
*/
|
||||||
|
public function is_card_expired() {
|
||||||
|
$month = $this->get_expiry_month();
|
||||||
|
$year = $this->get_expiry_year();
|
||||||
|
|
||||||
|
if (!$month || !$year) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$expiry_date = mktime(23, 59, 59, $month, date('t', mktime(0, 0, 0, $month, 1, $year)), $year);
|
||||||
|
|
||||||
|
return $expiry_date < time();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 辅助方法
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取显示名称
|
||||||
|
*/
|
||||||
|
public function get_display_name() {
|
||||||
|
$card_type = $this->get_card_type();
|
||||||
|
$last_four = $this->get_last_four();
|
||||||
|
|
||||||
|
if ($card_type && $last_four) {
|
||||||
|
return sprintf('%s ending in %s', ucfirst($card_type), $last_four);
|
||||||
|
}
|
||||||
|
|
||||||
|
return __('支付方式', 'yoone-subscriptions');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消其他令牌的默认状态
|
||||||
|
*/
|
||||||
|
protected function unset_other_defaults() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->prefix . 'yoone_payment_tokens',
|
||||||
|
array('is_default' => 0),
|
||||||
|
array(
|
||||||
|
'customer_id' => $this->get_customer_id(),
|
||||||
|
'gateway_id' => $this->get_gateway_id()
|
||||||
|
),
|
||||||
|
array('%d'),
|
||||||
|
array('%d', '%s')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期
|
||||||
|
*/
|
||||||
|
protected function format_date($date) {
|
||||||
|
if (empty($date)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_numeric($date)) {
|
||||||
|
return date('Y-m-d H:i:s', $date);
|
||||||
|
}
|
||||||
|
|
||||||
|
return date('Y-m-d H:i:s', strtotime($date));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 数据库操作
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从数据库读取
|
||||||
|
*/
|
||||||
|
protected function read() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$data = $wpdb->get_row($wpdb->prepare("
|
||||||
|
SELECT * FROM {$wpdb->prefix}yoone_payment_tokens WHERE id = %d
|
||||||
|
", $this->get_id()));
|
||||||
|
|
||||||
|
if ($data) {
|
||||||
|
$this->set_props(array(
|
||||||
|
'customer_id' => $data->customer_id,
|
||||||
|
'gateway_id' => $data->gateway_id,
|
||||||
|
'token' => $data->token,
|
||||||
|
'token_type' => $data->token_type,
|
||||||
|
'card_type' => $data->card_type,
|
||||||
|
'last_four' => $data->last_four,
|
||||||
|
'expiry_month' => $data->expiry_month,
|
||||||
|
'expiry_year' => $data->expiry_year,
|
||||||
|
'is_default' => (bool) $data->is_default,
|
||||||
|
'expires_at' => $data->expires_at,
|
||||||
|
'created_at' => $data->created_at,
|
||||||
|
'updated_at' => $data->updated_at
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建记录
|
||||||
|
*/
|
||||||
|
protected function create() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'customer_id' => $this->get_customer_id('edit'),
|
||||||
|
'gateway_id' => $this->get_gateway_id('edit'),
|
||||||
|
'token' => $this->get_token('edit'),
|
||||||
|
'token_type' => $this->get_token_type('edit'),
|
||||||
|
'card_type' => $this->get_card_type('edit'),
|
||||||
|
'last_four' => $this->get_last_four('edit'),
|
||||||
|
'expiry_month' => $this->get_expiry_month('edit'),
|
||||||
|
'expiry_year' => $this->get_expiry_year('edit'),
|
||||||
|
'is_default' => $this->is_default('edit') ? 1 : 0,
|
||||||
|
'expires_at' => $this->get_expires_at('edit'),
|
||||||
|
'created_at' => current_time('mysql'),
|
||||||
|
'updated_at' => current_time('mysql')
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $wpdb->insert(
|
||||||
|
$wpdb->prefix . 'yoone_payment_tokens',
|
||||||
|
$data,
|
||||||
|
array('%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
$this->set_id($wpdb->insert_id);
|
||||||
|
do_action('yoone_payment_token_created', $this->get_id(), $this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新记录
|
||||||
|
*/
|
||||||
|
protected function update() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$changes = $this->get_changes();
|
||||||
|
|
||||||
|
if (empty($changes)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$changes['updated_at'] = current_time('mysql');
|
||||||
|
|
||||||
|
$result = $wpdb->update(
|
||||||
|
$wpdb->prefix . 'yoone_payment_tokens',
|
||||||
|
$changes,
|
||||||
|
array('id' => $this->get_id()),
|
||||||
|
null,
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result !== false) {
|
||||||
|
do_action('yoone_payment_token_updated', $this->get_id(), $this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从数据库删除
|
||||||
|
*/
|
||||||
|
protected function delete_from_database($force_delete = false) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$result = $wpdb->delete(
|
||||||
|
$wpdb->prefix . 'yoone_payment_tokens',
|
||||||
|
array('id' => $this->get_id()),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
do_action('yoone_payment_token_deleted', $this->get_id());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置多个属性
|
||||||
|
*/
|
||||||
|
protected function set_props($props) {
|
||||||
|
foreach ($props as $prop => $value) {
|
||||||
|
if (array_key_exists($prop, $this->data)) {
|
||||||
|
$this->data[$prop] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 静态方法
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据客户ID获取令牌
|
||||||
|
*/
|
||||||
|
public static function get_customer_tokens($customer_id, $gateway_id = '') {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$where = $wpdb->prepare("customer_id = %d", $customer_id);
|
||||||
|
|
||||||
|
if ($gateway_id) {
|
||||||
|
$where .= $wpdb->prepare(" AND gateway_id = %s", $gateway_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$where .= " AND (expires_at IS NULL OR expires_at > NOW())";
|
||||||
|
|
||||||
|
$results = $wpdb->get_results("
|
||||||
|
SELECT id FROM {$wpdb->prefix}yoone_payment_tokens
|
||||||
|
WHERE {$where}
|
||||||
|
ORDER BY is_default DESC, created_at DESC
|
||||||
|
");
|
||||||
|
|
||||||
|
$tokens = array();
|
||||||
|
foreach ($results as $result) {
|
||||||
|
$tokens[] = new self($result->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据令牌字符串获取令牌对象
|
||||||
|
*/
|
||||||
|
public static function get_by_token($token, $gateway_id) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$id = $wpdb->get_var($wpdb->prepare("
|
||||||
|
SELECT id FROM {$wpdb->prefix}yoone_payment_tokens
|
||||||
|
WHERE token = %s AND gateway_id = %s
|
||||||
|
", $token, $gateway_id));
|
||||||
|
|
||||||
|
return $id ? new self($id) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取客户的默认令牌
|
||||||
|
*/
|
||||||
|
public static function get_default_token($customer_id, $gateway_id) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$id = $wpdb->get_var($wpdb->prepare("
|
||||||
|
SELECT id FROM {$wpdb->prefix}yoone_payment_tokens
|
||||||
|
WHERE customer_id = %d AND gateway_id = %s AND is_default = 1
|
||||||
|
AND (expires_at IS NULL OR expires_at > NOW())
|
||||||
|
", $customer_id, $gateway_id));
|
||||||
|
|
||||||
|
return $id ? new self($id) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期令牌
|
||||||
|
*/
|
||||||
|
public static function cleanup_expired_tokens() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$count = $wpdb->query("
|
||||||
|
DELETE FROM {$wpdb->prefix}yoone_payment_tokens
|
||||||
|
WHERE expires_at IS NOT NULL AND expires_at < NOW()
|
||||||
|
");
|
||||||
|
|
||||||
|
if ($count > 0) {
|
||||||
|
Yoone_Logger::info("清理了 {$count} 个过期的支付令牌");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,871 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 订阅类
|
||||||
|
*
|
||||||
|
* 处理订阅的创建、管理和续费
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅类
|
||||||
|
*/
|
||||||
|
class Yoone_Subscription extends Abstract_Yoone_Data implements Interface_Yoone_Subscription {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象类型
|
||||||
|
*/
|
||||||
|
protected $object_type = 'yoone_subscription';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据结构
|
||||||
|
*/
|
||||||
|
protected $data = array(
|
||||||
|
'customer_id' => 0,
|
||||||
|
'parent_order_id' => 0,
|
||||||
|
'status' => 'pending',
|
||||||
|
'billing_period' => 'month',
|
||||||
|
'billing_interval' => 1,
|
||||||
|
'start_date' => null,
|
||||||
|
'next_payment_date' => null,
|
||||||
|
'end_date' => null,
|
||||||
|
'trial_end_date' => null,
|
||||||
|
'total' => 0.00,
|
||||||
|
'currency' => 'CAD',
|
||||||
|
'payment_method' => '',
|
||||||
|
'payment_token' => '',
|
||||||
|
'created_at' => null,
|
||||||
|
'updated_at' => null
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅商品
|
||||||
|
*/
|
||||||
|
protected $items = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
public function __construct($id = 0) {
|
||||||
|
parent::__construct($id);
|
||||||
|
|
||||||
|
if ($this->get_id() > 0) {
|
||||||
|
$this->load_items();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Getters
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取客户ID
|
||||||
|
*/
|
||||||
|
public function get_customer_id($context = 'view') {
|
||||||
|
return $this->get_prop('customer_id', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取父订单ID
|
||||||
|
*/
|
||||||
|
public function get_parent_order_id($context = 'view') {
|
||||||
|
return $this->get_prop('parent_order_id', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订阅状态
|
||||||
|
*/
|
||||||
|
public function get_status($context = 'view') {
|
||||||
|
return $this->get_prop('status', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订阅周期
|
||||||
|
*/
|
||||||
|
public function get_billing_period($context = 'view') {
|
||||||
|
return $this->get_prop('billing_period', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订阅间隔
|
||||||
|
*/
|
||||||
|
public function get_billing_interval($context = 'view') {
|
||||||
|
return $this->get_prop('billing_interval', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取开始日期
|
||||||
|
*/
|
||||||
|
public function get_start_date($context = 'view') {
|
||||||
|
return $this->get_prop('start_date', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下次付款日期
|
||||||
|
*/
|
||||||
|
public function get_next_payment_date($context = 'view') {
|
||||||
|
return $this->get_prop('next_payment_date', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取结束日期
|
||||||
|
*/
|
||||||
|
public function get_end_date($context = 'view') {
|
||||||
|
return $this->get_prop('end_date', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取试用结束日期
|
||||||
|
*/
|
||||||
|
public function get_trial_end_date($context = 'view') {
|
||||||
|
return $this->get_prop('trial_end_date', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订阅金额
|
||||||
|
*/
|
||||||
|
public function get_total($context = 'view') {
|
||||||
|
return $this->get_prop('total', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取货币
|
||||||
|
*/
|
||||||
|
public function get_currency($context = 'view') {
|
||||||
|
return $this->get_prop('currency', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支付方式
|
||||||
|
*/
|
||||||
|
public function get_payment_method($context = 'view') {
|
||||||
|
return $this->get_prop('payment_method', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支付令牌
|
||||||
|
*/
|
||||||
|
public function get_payment_token($context = 'view') {
|
||||||
|
return $this->get_prop('payment_token', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取创建时间
|
||||||
|
*/
|
||||||
|
public function get_created_at($context = 'view') {
|
||||||
|
return $this->get_prop('created_at', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取更新时间
|
||||||
|
*/
|
||||||
|
public function get_updated_at($context = 'view') {
|
||||||
|
return $this->get_prop('updated_at', $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Setters
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置客户ID
|
||||||
|
*/
|
||||||
|
public function set_customer_id($customer_id) {
|
||||||
|
$this->set_prop('customer_id', absint($customer_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置父订单ID
|
||||||
|
*/
|
||||||
|
public function set_parent_order_id($order_id) {
|
||||||
|
$this->set_prop('parent_order_id', absint($order_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置订阅状态
|
||||||
|
*/
|
||||||
|
public function set_status($status) {
|
||||||
|
$valid_statuses = array('pending', 'active', 'paused', 'cancelled', 'expired', 'trial');
|
||||||
|
if (in_array($status, $valid_statuses)) {
|
||||||
|
$this->set_prop('status', $status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置订阅周期
|
||||||
|
*/
|
||||||
|
public function set_billing_period($period) {
|
||||||
|
$valid_periods = array('day', 'week', 'month', 'year');
|
||||||
|
if (in_array($period, $valid_periods)) {
|
||||||
|
$this->set_prop('billing_period', $period);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置订阅间隔
|
||||||
|
*/
|
||||||
|
public function set_billing_interval($interval) {
|
||||||
|
$this->set_prop('billing_interval', max(1, absint($interval)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置开始日期
|
||||||
|
*/
|
||||||
|
public function set_start_date($date) {
|
||||||
|
$this->set_prop('start_date', $this->format_date($date));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置下次付款日期
|
||||||
|
*/
|
||||||
|
public function set_next_payment_date($date) {
|
||||||
|
$this->set_prop('next_payment_date', $this->format_date($date));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置结束日期
|
||||||
|
*/
|
||||||
|
public function set_end_date($date) {
|
||||||
|
$this->set_prop('end_date', $this->format_date($date));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置试用结束日期
|
||||||
|
*/
|
||||||
|
public function set_trial_end_date($date) {
|
||||||
|
$this->set_prop('trial_end_date', $this->format_date($date));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置订阅金额
|
||||||
|
*/
|
||||||
|
public function set_total($total) {
|
||||||
|
$this->set_prop('total', floatval($total));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置货币
|
||||||
|
*/
|
||||||
|
public function set_currency($currency) {
|
||||||
|
$this->set_prop('currency', strtoupper($currency));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置支付方式
|
||||||
|
*/
|
||||||
|
public function set_payment_method($method) {
|
||||||
|
$this->set_prop('payment_method', sanitize_text_field($method));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置支付令牌
|
||||||
|
*/
|
||||||
|
public function set_payment_token($token) {
|
||||||
|
$this->set_prop('payment_token', sanitize_text_field($token));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 商品管理
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订阅商品
|
||||||
|
*/
|
||||||
|
public function get_items() {
|
||||||
|
return $this->items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加订阅商品
|
||||||
|
*/
|
||||||
|
public function add_item($item) {
|
||||||
|
if (is_array($item)) {
|
||||||
|
$defaults = array(
|
||||||
|
'product_id' => 0,
|
||||||
|
'variation_id' => 0,
|
||||||
|
'quantity' => 1,
|
||||||
|
'line_total' => 0.00,
|
||||||
|
'line_subtotal' => 0.00
|
||||||
|
);
|
||||||
|
|
||||||
|
$item = wp_parse_args($item, $defaults);
|
||||||
|
$this->items[] = $item;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除订阅商品
|
||||||
|
*/
|
||||||
|
public function remove_item($item_id) {
|
||||||
|
foreach ($this->items as $key => $item) {
|
||||||
|
if (isset($item['id']) && $item['id'] == $item_id) {
|
||||||
|
unset($this->items[$key]);
|
||||||
|
$this->items = array_values($this->items);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空商品
|
||||||
|
*/
|
||||||
|
public function clear_items() {
|
||||||
|
$this->items = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载商品
|
||||||
|
*/
|
||||||
|
protected function load_items() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
if (!$this->get_id()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = $wpdb->get_results($wpdb->prepare("
|
||||||
|
SELECT * FROM {$wpdb->prefix}yoone_subscription_items
|
||||||
|
WHERE subscription_id = %d
|
||||||
|
ORDER BY id ASC
|
||||||
|
", $this->get_id()), ARRAY_A);
|
||||||
|
|
||||||
|
$this->items = $items ?: array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存商品
|
||||||
|
*/
|
||||||
|
protected function save_items() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
if (!$this->get_id()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除现有商品
|
||||||
|
$wpdb->delete(
|
||||||
|
$wpdb->prefix . 'yoone_subscription_items',
|
||||||
|
array('subscription_id' => $this->get_id()),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
// 插入新商品
|
||||||
|
foreach ($this->items as $item) {
|
||||||
|
$wpdb->insert(
|
||||||
|
$wpdb->prefix . 'yoone_subscription_items',
|
||||||
|
array(
|
||||||
|
'subscription_id' => $this->get_id(),
|
||||||
|
'product_id' => $item['product_id'],
|
||||||
|
'variation_id' => $item['variation_id'],
|
||||||
|
'quantity' => $item['quantity'],
|
||||||
|
'line_total' => $item['line_total'],
|
||||||
|
'line_subtotal' => $item['line_subtotal']
|
||||||
|
),
|
||||||
|
array('%d', '%d', '%d', '%d', '%f', '%f')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 订阅状态管理
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活订阅
|
||||||
|
*/
|
||||||
|
public function activate() {
|
||||||
|
if ($this->can_be_activated()) {
|
||||||
|
$this->set_status('active');
|
||||||
|
|
||||||
|
if (!$this->get_start_date()) {
|
||||||
|
$this->set_start_date(current_time('mysql'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->calculate_next_payment_date();
|
||||||
|
$this->save();
|
||||||
|
|
||||||
|
$this->add_log('activated', __('订阅已激活', 'yoone-subscriptions'));
|
||||||
|
|
||||||
|
do_action('yoone_subscription_activated', $this);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 暂停订阅
|
||||||
|
*/
|
||||||
|
public function pause() {
|
||||||
|
if ($this->can_be_paused()) {
|
||||||
|
$this->set_status('paused');
|
||||||
|
$this->save();
|
||||||
|
|
||||||
|
$this->add_log('paused', __('订阅已暂停', 'yoone-subscriptions'));
|
||||||
|
|
||||||
|
do_action('yoone_subscription_paused', $this);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复订阅
|
||||||
|
*/
|
||||||
|
public function resume() {
|
||||||
|
if ($this->get_status() === 'paused') {
|
||||||
|
$this->set_status('active');
|
||||||
|
$this->calculate_next_payment_date();
|
||||||
|
$this->save();
|
||||||
|
|
||||||
|
$this->add_log('resumed', __('订阅已恢复', 'yoone-subscriptions'));
|
||||||
|
|
||||||
|
do_action('yoone_subscription_resumed', $this);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消订阅
|
||||||
|
*/
|
||||||
|
public function cancel() {
|
||||||
|
if ($this->can_be_cancelled()) {
|
||||||
|
$this->set_status('cancelled');
|
||||||
|
$this->set_end_date(current_time('mysql'));
|
||||||
|
$this->save();
|
||||||
|
|
||||||
|
$this->add_log('cancelled', __('订阅已取消', 'yoone-subscriptions'));
|
||||||
|
|
||||||
|
do_action('yoone_subscription_cancelled', $this);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 续费处理
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理续费
|
||||||
|
*/
|
||||||
|
public function process_renewal() {
|
||||||
|
if (!$this->can_be_renewed()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->add_log('renewal_started', __('开始处理续费', 'yoone-subscriptions'));
|
||||||
|
|
||||||
|
// 创建续费订单
|
||||||
|
$renewal_order = $this->create_renewal_order();
|
||||||
|
|
||||||
|
if (!$renewal_order) {
|
||||||
|
$this->add_log('renewal_failed', __('创建续费订单失败', 'yoone-subscriptions'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理支付
|
||||||
|
$payment_result = $this->process_renewal_payment($renewal_order);
|
||||||
|
|
||||||
|
if ($payment_result) {
|
||||||
|
$this->calculate_next_payment_date();
|
||||||
|
$this->save();
|
||||||
|
|
||||||
|
$this->add_log('renewal_success', sprintf(__('续费成功,订单号:%s', 'yoone-subscriptions'), $renewal_order->get_id()));
|
||||||
|
|
||||||
|
do_action('yoone_subscription_renewed', $this, $renewal_order);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
$this->add_log('renewal_failed', sprintf(__('续费支付失败,订单号:%s', 'yoone-subscriptions'), $renewal_order->get_id()));
|
||||||
|
|
||||||
|
do_action('yoone_subscription_renewal_failed', $this, $renewal_order);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建续费订单
|
||||||
|
*/
|
||||||
|
protected function create_renewal_order() {
|
||||||
|
$order = wc_create_order(array(
|
||||||
|
'customer_id' => $this->get_customer_id(),
|
||||||
|
'status' => 'pending'
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$order) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加订阅商品到订单
|
||||||
|
foreach ($this->get_items() as $item) {
|
||||||
|
$product = wc_get_product($item['product_id']);
|
||||||
|
if ($product) {
|
||||||
|
$order->add_product($product, $item['quantity']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置订单总额
|
||||||
|
$order->set_total($this->get_total());
|
||||||
|
|
||||||
|
// 设置支付方式
|
||||||
|
$order->set_payment_method($this->get_payment_method());
|
||||||
|
|
||||||
|
// 添加订阅关联
|
||||||
|
$order->update_meta_data('_yoone_subscription_id', $this->get_id());
|
||||||
|
$order->update_meta_data('_yoone_subscription_renewal', 'yes');
|
||||||
|
|
||||||
|
$order->save();
|
||||||
|
|
||||||
|
return $order;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理续费支付
|
||||||
|
*/
|
||||||
|
protected function process_renewal_payment($order) {
|
||||||
|
$payment_gateways = WC()->payment_gateways()->payment_gateways();
|
||||||
|
$gateway = isset($payment_gateways[$this->get_payment_method()]) ? $payment_gateways[$this->get_payment_method()] : null;
|
||||||
|
|
||||||
|
if (!$gateway || !$gateway->supports('subscriptions')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用支付令牌处理支付
|
||||||
|
return $gateway->process_subscription_payment($this->get_id(), $this->get_total());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算下次付款日期
|
||||||
|
*/
|
||||||
|
public function calculate_next_payment_date() {
|
||||||
|
$current_date = $this->get_next_payment_date() ?: $this->get_start_date();
|
||||||
|
|
||||||
|
if (!$current_date) {
|
||||||
|
$current_date = current_time('mysql');
|
||||||
|
}
|
||||||
|
|
||||||
|
$next_date = $this->add_billing_period($current_date);
|
||||||
|
$this->set_next_payment_date($next_date);
|
||||||
|
|
||||||
|
return $next_date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加计费周期
|
||||||
|
*/
|
||||||
|
protected function add_billing_period($date) {
|
||||||
|
$timestamp = strtotime($date);
|
||||||
|
$period = $this->get_billing_period();
|
||||||
|
$interval = $this->get_billing_interval();
|
||||||
|
|
||||||
|
switch ($period) {
|
||||||
|
case 'day':
|
||||||
|
$timestamp = strtotime("+{$interval} days", $timestamp);
|
||||||
|
break;
|
||||||
|
case 'week':
|
||||||
|
$timestamp = strtotime("+{$interval} weeks", $timestamp);
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
$timestamp = strtotime("+{$interval} months", $timestamp);
|
||||||
|
break;
|
||||||
|
case 'year':
|
||||||
|
$timestamp = strtotime("+{$interval} years", $timestamp);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return date('Y-m-d H:i:s', $timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 验证方法
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否可以激活
|
||||||
|
*/
|
||||||
|
public function can_be_activated() {
|
||||||
|
return in_array($this->get_status(), array('pending', 'trial'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否可以续费
|
||||||
|
*/
|
||||||
|
public function can_be_renewed() {
|
||||||
|
return $this->get_status() === 'active' && $this->get_payment_token();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否可以暂停
|
||||||
|
*/
|
||||||
|
public function can_be_paused() {
|
||||||
|
return $this->get_status() === 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否可以取消
|
||||||
|
*/
|
||||||
|
public function can_be_cancelled() {
|
||||||
|
return in_array($this->get_status(), array('active', 'paused', 'trial'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否在试用期
|
||||||
|
*/
|
||||||
|
public function is_trial() {
|
||||||
|
$trial_end = $this->get_trial_end_date();
|
||||||
|
return $trial_end && strtotime($trial_end) > time();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 日志管理
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加日志
|
||||||
|
*/
|
||||||
|
public function add_log($type, $message, $data = null) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
if (!$this->get_id()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $wpdb->insert(
|
||||||
|
$wpdb->prefix . 'yoone_subscription_logs',
|
||||||
|
array(
|
||||||
|
'subscription_id' => $this->get_id(),
|
||||||
|
'type' => $type,
|
||||||
|
'message' => $message,
|
||||||
|
'data' => $data ? wp_json_encode($data) : null,
|
||||||
|
'created_at' => current_time('mysql')
|
||||||
|
),
|
||||||
|
array('%d', '%s', '%s', '%s', '%s')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取日志
|
||||||
|
*/
|
||||||
|
public function get_logs($type = '', $limit = 50) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
if (!$this->get_id()) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$where = $wpdb->prepare("subscription_id = %d", $this->get_id());
|
||||||
|
|
||||||
|
if ($type) {
|
||||||
|
$where .= $wpdb->prepare(" AND type = %s", $type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $wpdb->get_results("
|
||||||
|
SELECT * FROM {$wpdb->prefix}yoone_subscription_logs
|
||||||
|
WHERE {$where}
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT {$limit}
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 辅助方法
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期
|
||||||
|
*/
|
||||||
|
protected function format_date($date) {
|
||||||
|
if (empty($date)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_numeric($date)) {
|
||||||
|
return date('Y-m-d H:i:s', $date);
|
||||||
|
}
|
||||||
|
|
||||||
|
return date('Y-m-d H:i:s', strtotime($date));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 数据库操作
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从数据库读取
|
||||||
|
*/
|
||||||
|
protected function read() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$data = $wpdb->get_row($wpdb->prepare("
|
||||||
|
SELECT * FROM {$wpdb->prefix}yoone_subscriptions WHERE id = %d
|
||||||
|
", $this->get_id()));
|
||||||
|
|
||||||
|
if ($data) {
|
||||||
|
$this->set_props(array(
|
||||||
|
'customer_id' => $data->customer_id,
|
||||||
|
'parent_order_id' => $data->parent_order_id,
|
||||||
|
'status' => $data->status,
|
||||||
|
'billing_period' => $data->billing_period,
|
||||||
|
'billing_interval' => $data->billing_interval,
|
||||||
|
'start_date' => $data->start_date,
|
||||||
|
'next_payment_date' => $data->next_payment_date,
|
||||||
|
'end_date' => $data->end_date,
|
||||||
|
'trial_end_date' => $data->trial_end_date,
|
||||||
|
'total' => $data->total,
|
||||||
|
'currency' => $data->currency,
|
||||||
|
'payment_method' => $data->payment_method,
|
||||||
|
'payment_token' => $data->payment_token,
|
||||||
|
'created_at' => $data->created_at,
|
||||||
|
'updated_at' => $data->updated_at
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建记录
|
||||||
|
*/
|
||||||
|
protected function create() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'customer_id' => $this->get_customer_id('edit'),
|
||||||
|
'parent_order_id' => $this->get_parent_order_id('edit'),
|
||||||
|
'status' => $this->get_status('edit'),
|
||||||
|
'billing_period' => $this->get_billing_period('edit'),
|
||||||
|
'billing_interval' => $this->get_billing_interval('edit'),
|
||||||
|
'start_date' => $this->get_start_date('edit'),
|
||||||
|
'next_payment_date' => $this->get_next_payment_date('edit'),
|
||||||
|
'end_date' => $this->get_end_date('edit'),
|
||||||
|
'trial_end_date' => $this->get_trial_end_date('edit'),
|
||||||
|
'total' => $this->get_total('edit'),
|
||||||
|
'currency' => $this->get_currency('edit'),
|
||||||
|
'payment_method' => $this->get_payment_method('edit'),
|
||||||
|
'payment_token' => $this->get_payment_token('edit'),
|
||||||
|
'created_at' => current_time('mysql'),
|
||||||
|
'updated_at' => current_time('mysql')
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $wpdb->insert(
|
||||||
|
$wpdb->prefix . 'yoone_subscriptions',
|
||||||
|
$data,
|
||||||
|
array('%d', '%d', '%s', '%s', '%d', '%s', '%s', '%s', '%s', '%f', '%s', '%s', '%s', '%s', '%s')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
$this->set_id($wpdb->insert_id);
|
||||||
|
$this->save_items();
|
||||||
|
do_action('yoone_subscription_created', $this->get_id(), $this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新记录
|
||||||
|
*/
|
||||||
|
protected function update() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$changes = $this->get_changes();
|
||||||
|
|
||||||
|
if (empty($changes)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$changes['updated_at'] = current_time('mysql');
|
||||||
|
|
||||||
|
$result = $wpdb->update(
|
||||||
|
$wpdb->prefix . 'yoone_subscriptions',
|
||||||
|
$changes,
|
||||||
|
array('id' => $this->get_id()),
|
||||||
|
null,
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result !== false) {
|
||||||
|
$this->save_items();
|
||||||
|
do_action('yoone_subscription_updated', $this->get_id(), $this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从数据库删除
|
||||||
|
*/
|
||||||
|
protected function delete_from_database($force_delete = false) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// 删除商品
|
||||||
|
$wpdb->delete(
|
||||||
|
$wpdb->prefix . 'yoone_subscription_items',
|
||||||
|
array('subscription_id' => $this->get_id()),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
// 删除日志
|
||||||
|
$wpdb->delete(
|
||||||
|
$wpdb->prefix . 'yoone_subscription_logs',
|
||||||
|
array('subscription_id' => $this->get_id()),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
// 删除订阅
|
||||||
|
$result = $wpdb->delete(
|
||||||
|
$wpdb->prefix . 'yoone_subscriptions',
|
||||||
|
array('id' => $this->get_id()),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
return $result !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置多个属性
|
||||||
|
*/
|
||||||
|
protected function set_props($props) {
|
||||||
|
foreach ($props as $prop => $value) {
|
||||||
|
if (array_key_exists($prop, $this->data)) {
|
||||||
|
$this->data[$prop] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,276 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 测试运行脚本
|
||||||
|
*
|
||||||
|
* 用于在命令行或浏览器中运行插件测试
|
||||||
|
*
|
||||||
|
* @package Yoone_Subscriptions
|
||||||
|
* @subpackage Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 设置WordPress环境
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
// 尝试找到WordPress根目录
|
||||||
|
$wp_root = dirname(dirname(dirname(dirname(__FILE__))));
|
||||||
|
if (file_exists($wp_root . '/wp-config.php')) {
|
||||||
|
require_once $wp_root . '/wp-config.php';
|
||||||
|
} else {
|
||||||
|
die('无法找到WordPress配置文件');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保插件已加载
|
||||||
|
if (!class_exists('Yoone_Subscriptions')) {
|
||||||
|
die('Yoone Subscriptions插件未激活');
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Yoone Subscriptions 测试运行器</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 3px solid #0073aa;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.test-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
.test-result {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.success {
|
||||||
|
background-color: #d4edda;
|
||||||
|
border-color: #c3e6cb;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
border-color: #f5c6cb;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
border-color: #ffeaa7;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
background-color: #d1ecf1;
|
||||||
|
border-color: #bee5eb;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 20px;
|
||||||
|
margin: 5px;
|
||||||
|
background-color: #0073aa;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #005a87;
|
||||||
|
}
|
||||||
|
.environment-info {
|
||||||
|
background: #e8f4f8;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.environment-info table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.environment-info th,
|
||||||
|
.environment-info td {
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
.environment-info th {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🧪 Yoone Subscriptions 测试运行器</h1>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// 显示环境信息
|
||||||
|
echo '<div class="environment-info">';
|
||||||
|
echo '<h3>📋 测试环境信息</h3>';
|
||||||
|
echo '<table>';
|
||||||
|
|
||||||
|
if (class_exists('Yoone_Test_Config')) {
|
||||||
|
$env_info = Yoone_Test_Config::get_environment_info();
|
||||||
|
foreach ($env_info as $key => $value) {
|
||||||
|
$label = ucwords(str_replace('_', ' ', $key));
|
||||||
|
echo "<tr><th>{$label}</th><td>{$value}</td></tr>";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo '<tr><td colspan="2">无法获取环境信息 - Yoone_Test_Config类不存在</td></tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</table>';
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
// 运行测试的按钮
|
||||||
|
echo '<div class="test-section">';
|
||||||
|
echo '<h3>🚀 运行测试</h3>';
|
||||||
|
echo '<p>选择要运行的测试类型:</p>';
|
||||||
|
|
||||||
|
$test_types = array(
|
||||||
|
'subscription' => '订阅功能测试',
|
||||||
|
'payment' => '支付集成测试',
|
||||||
|
'bundle' => '捆绑产品测试',
|
||||||
|
'cron' => '定时任务测试',
|
||||||
|
'all' => '运行所有测试'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($test_types as $type => $label) {
|
||||||
|
echo "<a href='?run_test={$type}' class='btn'>{$label}</a>";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
// 处理测试运行
|
||||||
|
if (isset($_GET['run_test'])) {
|
||||||
|
$test_type = sanitize_text_field($_GET['run_test']);
|
||||||
|
|
||||||
|
echo '<div class="test-section">';
|
||||||
|
echo "<h3>🔍 运行 {$test_types[$test_type]} 结果</h3>";
|
||||||
|
|
||||||
|
if (class_exists('Yoone_Test_Suite')) {
|
||||||
|
$test_suite = new Yoone_Test_Suite();
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch ($test_type) {
|
||||||
|
case 'subscription':
|
||||||
|
echo '<h4>订阅功能测试</h4>';
|
||||||
|
$results = $test_suite->run_test_suite('subscription');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'payment':
|
||||||
|
echo '<h4>支付集成测试</h4>';
|
||||||
|
$results = $test_suite->run_test_suite('payment');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'bundle':
|
||||||
|
echo '<h4>捆绑产品测试</h4>';
|
||||||
|
$results = $test_suite->run_test_suite('bundle');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'cron':
|
||||||
|
echo '<h4>定时任务测试</h4>';
|
||||||
|
$results = $test_suite->run_test_suite('cron');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'all':
|
||||||
|
echo '<h4>运行所有测试</h4>';
|
||||||
|
$results = $test_suite->run_test_suite('all');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示测试结果
|
||||||
|
if (empty($results['tests'])) {
|
||||||
|
echo '<div class="test-result info">没有测试结果</div>';
|
||||||
|
} else {
|
||||||
|
// 显示摘要
|
||||||
|
$summary = $results['summary'];
|
||||||
|
echo "<div class='test-result info'>";
|
||||||
|
echo "<strong>测试摘要:</strong><br>";
|
||||||
|
echo "总计: {$summary['total']} | 通过: {$summary['passed']} | 失败: {$summary['failed']} | 跳过: {$summary['skipped']}";
|
||||||
|
echo "</div>";
|
||||||
|
|
||||||
|
// 显示详细结果
|
||||||
|
foreach ($results['tests'] as $result) {
|
||||||
|
$class = $result['status'] === 'passed' ? 'success' :
|
||||||
|
($result['status'] === 'failed' ? 'error' : 'warning');
|
||||||
|
|
||||||
|
echo "<div class='test-result {$class}'>";
|
||||||
|
echo "<strong>{$result['name']}</strong><br>";
|
||||||
|
echo "状态: " . ($result['status'] === 'passed' ? '✅ 通过' :
|
||||||
|
($result['status'] === 'failed' ? '❌ 失败' : '⚠️ 警告')) . "<br>";
|
||||||
|
echo "描述: {$result['description']}";
|
||||||
|
|
||||||
|
if (!empty($result['error'])) {
|
||||||
|
echo "<br><strong>错误:</strong> {$result['error']}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($result['result']) && $result['result'] !== true) {
|
||||||
|
echo "<br><strong>结果:</strong> {$result['result']}";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "</div>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "<div class='test-result error'>";
|
||||||
|
echo "<strong>测试运行失败</strong><br>";
|
||||||
|
echo "错误: " . $e->getMessage();
|
||||||
|
echo "</div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
echo '<div class="test-result error">Yoone_Test_Suite类不存在,请确保插件正确安装</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h3>📚 测试说明</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>订阅功能测试</strong>: 测试订阅的创建、激活、暂停、恢复、续费和取消流程</li>
|
||||||
|
<li><strong>支付集成测试</strong>: 测试Moneris支付网关集成和支付令牌管理</li>
|
||||||
|
<li><strong>捆绑产品测试</strong>: 测试产品捆绑功能和价格计算</li>
|
||||||
|
<li><strong>定时任务测试</strong>: 测试自动续费、过期处理等定时任务</li>
|
||||||
|
<li><strong>运行所有测试</strong>: 执行完整的测试套件</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>⚠️ 注意事项</h4>
|
||||||
|
<ul>
|
||||||
|
<li>测试会创建临时数据,测试完成后会自动清理</li>
|
||||||
|
<li>建议在开发环境中运行测试,避免影响生产数据</li>
|
||||||
|
<li>某些测试可能需要有效的支付配置才能完全通过</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h3>🔧 快速操作</h3>
|
||||||
|
<a href="?" class="btn">刷新页面</a>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=yoone-test-suite'); ?>" class="btn">管理后台测试</a>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=yoone-logs'); ?>" class="btn">查看日志</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,260 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 添加混装产品页面模板
|
||||||
|
*
|
||||||
|
* @package Yoone_Subscriptions
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit; // 防止直接访问
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有产品用于选择
|
||||||
|
$products = wc_get_products(array(
|
||||||
|
'status' => 'publish',
|
||||||
|
'limit' => -1,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC'
|
||||||
|
));
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php _e('添加混装产品', 'yoone-subscriptions'); ?></h1>
|
||||||
|
|
||||||
|
<form method="post" action="" id="bundle-form">
|
||||||
|
<?php wp_nonce_field('add_bundle'); ?>
|
||||||
|
|
||||||
|
<table class="form-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="name"><?php _e('混装产品名称', 'yoone-subscriptions'); ?> <span class="required">*</span></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="name" id="name" value="" class="regular-text" required />
|
||||||
|
<p class="description"><?php _e('混装产品的显示名称', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="description"><?php _e('描述', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<textarea name="description" id="description" rows="4" class="large-text"></textarea>
|
||||||
|
<p class="description"><?php _e('混装产品的详细描述', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="discount_type"><?php _e('折扣类型', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<select name="discount_type" id="discount_type" class="regular-text">
|
||||||
|
<option value="none"><?php _e('无折扣', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="percentage"><?php _e('百分比折扣', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="fixed"><?php _e('固定金额折扣', 'yoone-subscriptions'); ?></option>
|
||||||
|
</select>
|
||||||
|
<p class="description"><?php _e('选择折扣的计算方式', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="discount_value"><?php _e('折扣值', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="discount_value" id="discount_value" value="0" min="0" step="0.01" class="regular-text" />
|
||||||
|
<p class="description" id="discount-description"><?php _e('折扣的具体数值', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="status"><?php _e('状态', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<select name="status" id="status" class="regular-text">
|
||||||
|
<option value="active"><?php _e('启用', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="inactive"><?php _e('禁用', 'yoone-subscriptions'); ?></option>
|
||||||
|
</select>
|
||||||
|
<p class="description"><?php _e('混装产品的当前状态', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2><?php _e('混装产品项目', 'yoone-subscriptions'); ?></h2>
|
||||||
|
<p><?php _e('选择要包含在此混装产品中的产品', 'yoone-subscriptions'); ?></p>
|
||||||
|
|
||||||
|
<div id="bundle-items">
|
||||||
|
<div class="bundle-item-template" style="display: none;">
|
||||||
|
<div class="bundle-item" data-index="0">
|
||||||
|
<table class="form-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" style="width: 150px;">
|
||||||
|
<label><?php _e('产品', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td style="width: 300px;">
|
||||||
|
<select name="bundle_items[0][product_id]" class="product-select" style="width: 100%;">
|
||||||
|
<option value=""><?php _e('选择产品', 'yoone-subscriptions'); ?></option>
|
||||||
|
<?php foreach ($products as $product): ?>
|
||||||
|
<option value="<?php echo $product->get_id(); ?>">
|
||||||
|
<?php echo esc_html($product->get_name() . ' (#' . $product->get_id() . ')'); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<th scope="row" style="width: 100px;">
|
||||||
|
<label><?php _e('数量', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td style="width: 100px;">
|
||||||
|
<input type="number" name="bundle_items[0][quantity]" value="1" min="1" class="small-text" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="button remove-item"><?php _e('移除', 'yoone-subscriptions'); ?></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="bundle-items-container">
|
||||||
|
<!-- 动态添加的混装项目将显示在这里 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<button type="button" id="add-bundle-item" class="button button-secondary">
|
||||||
|
<?php _e('添加产品', 'yoone-subscriptions'); ?>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php submit_button(__('保存混装产品', 'yoone-subscriptions')); ?>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=yoone-bundles'); ?>" class="button">
|
||||||
|
<?php _e('返回混装产品列表', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bundle-item {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .form-table {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .form-table th,
|
||||||
|
.bundle-item .form-table td {
|
||||||
|
padding: 5px 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: #d63638;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bundle-items-container:empty::after {
|
||||||
|
content: "<?php _e('暂无产品项目,请点击"添加产品"按钮添加', 'yoone-subscriptions'); ?>";
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
display: block;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
border: 2px dashed #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
var itemIndex = 0;
|
||||||
|
|
||||||
|
// 添加混装项目
|
||||||
|
$('#add-bundle-item').on('click', function() {
|
||||||
|
var template = $('.bundle-item-template').html();
|
||||||
|
template = template.replace(/\[0\]/g, '[' + itemIndex + ']');
|
||||||
|
template = template.replace(/data-index="0"/g, 'data-index="' + itemIndex + '"');
|
||||||
|
|
||||||
|
$('#bundle-items-container').append(template);
|
||||||
|
itemIndex++;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移除混装项目
|
||||||
|
$(document).on('click', '.remove-item', function() {
|
||||||
|
$(this).closest('.bundle-item').remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 折扣类型变化时更新描述
|
||||||
|
$('#discount_type').on('change', function() {
|
||||||
|
var type = $(this).val();
|
||||||
|
var description = $('#discount-description');
|
||||||
|
var valueField = $('#discount_value');
|
||||||
|
|
||||||
|
switch(type) {
|
||||||
|
case 'percentage':
|
||||||
|
description.text('<?php _e('输入百分比数值(例如:10 表示 10% 折扣)', 'yoone-subscriptions'); ?>');
|
||||||
|
valueField.attr('max', '100');
|
||||||
|
break;
|
||||||
|
case 'fixed':
|
||||||
|
description.text('<?php _e('输入固定折扣金额', 'yoone-subscriptions'); ?>');
|
||||||
|
valueField.removeAttr('max');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
description.text('<?php _e('无折扣', 'yoone-subscriptions'); ?>');
|
||||||
|
valueField.val('0');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表单验证
|
||||||
|
$('#bundle-form').on('submit', function(e) {
|
||||||
|
var name = $('#name').val().trim();
|
||||||
|
var items = $('.bundle-item').length;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
alert('<?php _e('请输入混装产品名称', 'yoone-subscriptions'); ?>');
|
||||||
|
$('#name').focus();
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items === 0) {
|
||||||
|
alert('<?php _e('请至少添加一个产品项目', 'yoone-subscriptions'); ?>');
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否所有产品都已选择
|
||||||
|
var hasEmptyProduct = false;
|
||||||
|
$('.product-select').each(function() {
|
||||||
|
if (!$(this).val()) {
|
||||||
|
hasEmptyProduct = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasEmptyProduct) {
|
||||||
|
alert('<?php _e('请为所有项目选择产品', 'yoone-subscriptions'); ?>');
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化时添加一个空项目
|
||||||
|
$('#add-bundle-item').trigger('click');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,327 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 编辑混装产品页面模板
|
||||||
|
*
|
||||||
|
* @package Yoone_Subscriptions
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit; // 防止直接访问
|
||||||
|
}
|
||||||
|
|
||||||
|
// $bundle 和 $items 变量由调用文件传入
|
||||||
|
|
||||||
|
// 获取所有产品用于选择
|
||||||
|
$products = wc_get_products(array(
|
||||||
|
'status' => 'publish',
|
||||||
|
'limit' => -1,
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC'
|
||||||
|
));
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php printf(__('编辑混装产品 #%d', 'yoone-subscriptions'), $bundle->id); ?></h1>
|
||||||
|
|
||||||
|
<form method="post" action="" id="bundle-form">
|
||||||
|
<?php wp_nonce_field('edit_bundle_' . $bundle->id); ?>
|
||||||
|
|
||||||
|
<table class="form-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="bundle_id"><?php _e('混装产品ID', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="bundle_id" value="<?php echo esc_attr($bundle->id); ?>" readonly class="regular-text" />
|
||||||
|
<p class="description"><?php _e('混装产品的唯一标识符', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="name"><?php _e('混装产品名称', 'yoone-subscriptions'); ?> <span class="required">*</span></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="name" id="name" value="<?php echo esc_attr($bundle->name); ?>" class="regular-text" required />
|
||||||
|
<p class="description"><?php _e('混装产品的显示名称', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="description"><?php _e('描述', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<textarea name="description" id="description" rows="4" class="large-text"><?php echo esc_textarea($bundle->description); ?></textarea>
|
||||||
|
<p class="description"><?php _e('混装产品的详细描述', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="discount_type"><?php _e('折扣类型', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<select name="discount_type" id="discount_type" class="regular-text">
|
||||||
|
<option value="none" <?php selected($bundle->discount_type, 'none'); ?>><?php _e('无折扣', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="percentage" <?php selected($bundle->discount_type, 'percentage'); ?>><?php _e('百分比折扣', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="fixed" <?php selected($bundle->discount_type, 'fixed'); ?>><?php _e('固定金额折扣', 'yoone-subscriptions'); ?></option>
|
||||||
|
</select>
|
||||||
|
<p class="description"><?php _e('选择折扣的计算方式', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="discount_value"><?php _e('折扣值', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="discount_value" id="discount_value" value="<?php echo esc_attr($bundle->discount_value); ?>" min="0" step="0.01" class="regular-text" />
|
||||||
|
<p class="description" id="discount-description"><?php _e('折扣的具体数值', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="status"><?php _e('状态', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<select name="status" id="status" class="regular-text">
|
||||||
|
<option value="active" <?php selected($bundle->status, 'active'); ?>><?php _e('启用', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="inactive" <?php selected($bundle->status, 'inactive'); ?>><?php _e('禁用', 'yoone-subscriptions'); ?></option>
|
||||||
|
</select>
|
||||||
|
<p class="description"><?php _e('混装产品的当前状态', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="created_at"><?php _e('创建时间', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" value="<?php echo date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($bundle->created_at)); ?>" readonly class="regular-text" />
|
||||||
|
<p class="description"><?php _e('混装产品创建的时间(只读)', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="updated_at"><?php _e('更新时间', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" value="<?php echo date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($bundle->updated_at)); ?>" readonly class="regular-text" />
|
||||||
|
<p class="description"><?php _e('混装产品最后更新的时间(只读)', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2><?php _e('混装产品项目', 'yoone-subscriptions'); ?></h2>
|
||||||
|
<p><?php _e('选择要包含在此混装产品中的产品', 'yoone-subscriptions'); ?></p>
|
||||||
|
|
||||||
|
<div id="bundle-items">
|
||||||
|
<div class="bundle-item-template" style="display: none;">
|
||||||
|
<div class="bundle-item" data-index="0">
|
||||||
|
<table class="form-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" style="width: 150px;">
|
||||||
|
<label><?php _e('产品', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td style="width: 300px;">
|
||||||
|
<select name="bundle_items[0][product_id]" class="product-select" style="width: 100%;">
|
||||||
|
<option value=""><?php _e('选择产品', 'yoone-subscriptions'); ?></option>
|
||||||
|
<?php foreach ($products as $product): ?>
|
||||||
|
<option value="<?php echo $product->get_id(); ?>">
|
||||||
|
<?php echo esc_html($product->get_name() . ' (#' . $product->get_id() . ')'); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<th scope="row" style="width: 100px;">
|
||||||
|
<label><?php _e('数量', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td style="width: 100px;">
|
||||||
|
<input type="number" name="bundle_items[0][quantity]" value="1" min="1" class="small-text" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="button remove-item"><?php _e('移除', 'yoone-subscriptions'); ?></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="bundle-items-container">
|
||||||
|
<?php if (!empty($items)): ?>
|
||||||
|
<?php foreach ($items as $index => $item): ?>
|
||||||
|
<?php $product = wc_get_product($item->product_id); ?>
|
||||||
|
<div class="bundle-item" data-index="<?php echo $index; ?>">
|
||||||
|
<table class="form-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" style="width: 150px;">
|
||||||
|
<label><?php _e('产品', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td style="width: 300px;">
|
||||||
|
<select name="bundle_items[<?php echo $index; ?>][product_id]" class="product-select" style="width: 100%;">
|
||||||
|
<option value=""><?php _e('选择产品', 'yoone-subscriptions'); ?></option>
|
||||||
|
<?php foreach ($products as $product_option): ?>
|
||||||
|
<option value="<?php echo $product_option->get_id(); ?>" <?php selected($item->product_id, $product_option->get_id()); ?>>
|
||||||
|
<?php echo esc_html($product_option->get_name() . ' (#' . $product_option->get_id() . ')'); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<th scope="row" style="width: 100px;">
|
||||||
|
<label><?php _e('数量', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td style="width: 100px;">
|
||||||
|
<input type="number" name="bundle_items[<?php echo $index; ?>][quantity]" value="<?php echo esc_attr($item->quantity); ?>" min="1" class="small-text" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="button remove-item"><?php _e('移除', 'yoone-subscriptions'); ?></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<button type="button" id="add-bundle-item" class="button button-secondary">
|
||||||
|
<?php _e('添加产品', 'yoone-subscriptions'); ?>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php submit_button(__('更新混装产品', 'yoone-subscriptions')); ?>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=yoone-bundles'); ?>" class="button">
|
||||||
|
<?php _e('返回混装产品列表', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bundle-item {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .form-table {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .form-table th,
|
||||||
|
.bundle-item .form-table td {
|
||||||
|
padding: 5px 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: #d63638;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bundle-items-container:empty::after {
|
||||||
|
content: "<?php _e('暂无产品项目,请点击"添加产品"按钮添加', 'yoone-subscriptions'); ?>";
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
display: block;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
border: 2px dashed #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
var itemIndex = <?php echo count($items); ?>;
|
||||||
|
|
||||||
|
// 添加混装项目
|
||||||
|
$('#add-bundle-item').on('click', function() {
|
||||||
|
var template = $('.bundle-item-template').html();
|
||||||
|
template = template.replace(/\[0\]/g, '[' + itemIndex + ']');
|
||||||
|
template = template.replace(/data-index="0"/g, 'data-index="' + itemIndex + '"');
|
||||||
|
|
||||||
|
$('#bundle-items-container').append(template);
|
||||||
|
itemIndex++;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移除混装项目
|
||||||
|
$(document).on('click', '.remove-item', function() {
|
||||||
|
$(this).closest('.bundle-item').remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 折扣类型变化时更新描述
|
||||||
|
function updateDiscountDescription() {
|
||||||
|
var type = $('#discount_type').val();
|
||||||
|
var description = $('#discount-description');
|
||||||
|
var valueField = $('#discount_value');
|
||||||
|
|
||||||
|
switch(type) {
|
||||||
|
case 'percentage':
|
||||||
|
description.text('<?php _e('输入百分比数值(例如:10 表示 10% 折扣)', 'yoone-subscriptions'); ?>');
|
||||||
|
valueField.attr('max', '100');
|
||||||
|
break;
|
||||||
|
case 'fixed':
|
||||||
|
description.text('<?php _e('输入固定折扣金额', 'yoone-subscriptions'); ?>');
|
||||||
|
valueField.removeAttr('max');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
description.text('<?php _e('无折扣', 'yoone-subscriptions'); ?>');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#discount_type').on('change', updateDiscountDescription);
|
||||||
|
|
||||||
|
// 初始化折扣描述
|
||||||
|
updateDiscountDescription();
|
||||||
|
|
||||||
|
// 表单验证
|
||||||
|
$('#bundle-form').on('submit', function(e) {
|
||||||
|
var name = $('#name').val().trim();
|
||||||
|
var items = $('.bundle-item').length;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
alert('<?php _e('请输入混装产品名称', 'yoone-subscriptions'); ?>');
|
||||||
|
$('#name').focus();
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items === 0) {
|
||||||
|
alert('<?php _e('请至少添加一个产品项目', 'yoone-subscriptions'); ?>');
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否所有产品都已选择
|
||||||
|
var hasEmptyProduct = false;
|
||||||
|
$('.product-select').each(function() {
|
||||||
|
if (!$(this).val()) {
|
||||||
|
hasEmptyProduct = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasEmptyProduct) {
|
||||||
|
alert('<?php _e('请为所有项目选择产品', 'yoone-subscriptions'); ?>');
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,298 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 混装产品列表管理页面模板
|
||||||
|
*
|
||||||
|
* @package Yoone_Subscriptions
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit; // 防止直接访问
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// 处理批量操作
|
||||||
|
if (isset($_POST['action']) && $_POST['action'] === 'delete' && isset($_POST['bundle_ids'])) {
|
||||||
|
if (wp_verify_nonce($_POST['_wpnonce'], 'bulk_delete_bundles')) {
|
||||||
|
$bundle_ids = array_map('intval', $_POST['bundle_ids']);
|
||||||
|
foreach ($bundle_ids as $bundle_id) {
|
||||||
|
// 删除混装项目
|
||||||
|
$wpdb->delete(
|
||||||
|
$wpdb->prefix . 'yoone_bundle_items',
|
||||||
|
array('bundle_id' => $bundle_id)
|
||||||
|
);
|
||||||
|
// 删除混装产品
|
||||||
|
$wpdb->delete(
|
||||||
|
$wpdb->prefix . 'yoone_bundles',
|
||||||
|
array('id' => $bundle_id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
echo '<div class="notice notice-success"><p>' . sprintf(__('已删除 %d 个混装产品', 'yoone-subscriptions'), count($bundle_ids)) . '</p></div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取混装产品列表
|
||||||
|
$per_page = 20;
|
||||||
|
$current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
|
||||||
|
$offset = ($current_page - 1) * $per_page;
|
||||||
|
|
||||||
|
$search = isset($_GET['s']) ? sanitize_text_field($_GET['s']) : '';
|
||||||
|
$status_filter = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : '';
|
||||||
|
|
||||||
|
$where_conditions = array('1=1');
|
||||||
|
$where_values = array();
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$where_conditions[] = "(name LIKE %s OR description LIKE %s)";
|
||||||
|
$where_values[] = '%' . $wpdb->esc_like($search) . '%';
|
||||||
|
$where_values[] = '%' . $wpdb->esc_like($search) . '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status_filter) {
|
||||||
|
$where_conditions[] = "status = %s";
|
||||||
|
$where_values[] = $status_filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
$where_clause = implode(' AND ', $where_conditions);
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
$total_query = "SELECT COUNT(*) FROM {$wpdb->prefix}yoone_bundles WHERE {$where_clause}";
|
||||||
|
|
||||||
|
if (!empty($where_values)) {
|
||||||
|
$total = $wpdb->get_var($wpdb->prepare($total_query, $where_values));
|
||||||
|
} else {
|
||||||
|
$total = $wpdb->get_var($total_query);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取混装产品数据
|
||||||
|
$query = "SELECT * FROM {$wpdb->prefix}yoone_bundles
|
||||||
|
WHERE {$where_clause}
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT %d OFFSET %d";
|
||||||
|
|
||||||
|
$query_values = array_merge($where_values, array($per_page, $offset));
|
||||||
|
$bundles = $wpdb->get_results($wpdb->prepare($query, $query_values));
|
||||||
|
|
||||||
|
$total_pages = ceil($total / $per_page);
|
||||||
|
|
||||||
|
// 获取状态统计
|
||||||
|
$status_counts = $wpdb->get_results(
|
||||||
|
"SELECT status, COUNT(*) as count FROM {$wpdb->prefix}yoone_bundles GROUP BY status"
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<h1 class="wp-heading-inline"><?php _e('混装产品管理', 'yoone-subscriptions'); ?></h1>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=yoone-bundles&action=add'); ?>" class="page-title-action">
|
||||||
|
<?php _e('添加新混装产品', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<?php if (isset($_GET['saved'])): ?>
|
||||||
|
<div class="notice notice-success is-dismissible">
|
||||||
|
<p><?php _e('混装产品已保存', 'yoone-subscriptions'); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (isset($_GET['deleted'])): ?>
|
||||||
|
<div class="notice notice-success is-dismissible">
|
||||||
|
<p><?php _e('混装产品已删除', 'yoone-subscriptions'); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- 搜索和筛选 -->
|
||||||
|
<form method="get" class="search-form">
|
||||||
|
<input type="hidden" name="page" value="yoone-bundles">
|
||||||
|
|
||||||
|
<p class="search-box">
|
||||||
|
<label class="screen-reader-text" for="bundle-search-input"><?php _e('搜索混装产品', 'yoone-subscriptions'); ?>:</label>
|
||||||
|
<input type="search" id="bundle-search-input" name="s" value="<?php echo esc_attr($search); ?>" placeholder="<?php _e('搜索混装产品名称或描述', 'yoone-subscriptions'); ?>">
|
||||||
|
|
||||||
|
<select name="status">
|
||||||
|
<option value=""><?php _e('所有状态', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="active" <?php selected($status_filter, 'active'); ?>><?php _e('启用', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="inactive" <?php selected($status_filter, 'inactive'); ?>><?php _e('禁用', 'yoone-subscriptions'); ?></option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<?php submit_button(__('搜索', 'yoone-subscriptions'), '', '', false, array('id' => 'search-submit')); ?>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- 状态统计 -->
|
||||||
|
<ul class="subsubsub">
|
||||||
|
<li class="all">
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=yoone-bundles'); ?>" <?php echo empty($status_filter) ? 'class="current"' : ''; ?>>
|
||||||
|
<?php _e('全部', 'yoone-subscriptions'); ?> <span class="count">(<?php echo $total; ?>)</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php foreach ($status_counts as $status_count): ?>
|
||||||
|
<li class="<?php echo esc_attr($status_count->status); ?>">
|
||||||
|
| <a href="<?php echo admin_url('admin.php?page=yoone-bundles&status=' . $status_count->status); ?>" <?php echo $status_filter === $status_count->status ? 'class="current"' : ''; ?>>
|
||||||
|
<?php echo $status_count->status === 'active' ? __('启用', 'yoone-subscriptions') : __('禁用', 'yoone-subscriptions'); ?> <span class="count">(<?php echo $status_count->count; ?>)</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- 混装产品列表表格 -->
|
||||||
|
<form method="post" id="bundles-filter">
|
||||||
|
<?php wp_nonce_field('bulk_delete_bundles'); ?>
|
||||||
|
|
||||||
|
<div class="tablenav top">
|
||||||
|
<div class="alignleft actions bulkactions">
|
||||||
|
<select name="action" id="bulk-action-selector-top">
|
||||||
|
<option value="-1"><?php _e('批量操作', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="delete"><?php _e('删除', 'yoone-subscriptions'); ?></option>
|
||||||
|
</select>
|
||||||
|
<input type="submit" id="doaction" class="button action" value="<?php _e('应用', 'yoone-subscriptions'); ?>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="wp-list-table widefat fixed striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td id="cb" class="manage-column column-cb check-column">
|
||||||
|
<label class="screen-reader-text" for="cb-select-all-1"><?php _e('全选', 'yoone-subscriptions'); ?></label>
|
||||||
|
<input id="cb-select-all-1" type="checkbox">
|
||||||
|
</td>
|
||||||
|
<th scope="col" class="manage-column column-id"><?php _e('ID', 'yoone-subscriptions'); ?></th>
|
||||||
|
<th scope="col" class="manage-column column-name"><?php _e('名称', 'yoone-subscriptions'); ?></th>
|
||||||
|
<th scope="col" class="manage-column column-description"><?php _e('描述', 'yoone-subscriptions'); ?></th>
|
||||||
|
<th scope="col" class="manage-column column-discount"><?php _e('折扣', 'yoone-subscriptions'); ?></th>
|
||||||
|
<th scope="col" class="manage-column column-items"><?php _e('产品数量', 'yoone-subscriptions'); ?></th>
|
||||||
|
<th scope="col" class="manage-column column-status"><?php _e('状态', 'yoone-subscriptions'); ?></th>
|
||||||
|
<th scope="col" class="manage-column column-created"><?php _e('创建时间', 'yoone-subscriptions'); ?></th>
|
||||||
|
<th scope="col" class="manage-column column-actions"><?php _e('操作', 'yoone-subscriptions'); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($bundles)): ?>
|
||||||
|
<tr class="no-items">
|
||||||
|
<td class="colspanchange" colspan="9"><?php _e('未找到混装产品', 'yoone-subscriptions'); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($bundles as $bundle): ?>
|
||||||
|
<?php
|
||||||
|
// 获取混装产品项目数量
|
||||||
|
$item_count = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM {$wpdb->prefix}yoone_bundle_items WHERE bundle_id = %d",
|
||||||
|
$bundle->id
|
||||||
|
));
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="check-column">
|
||||||
|
<input type="checkbox" name="bundle_ids[]" value="<?php echo $bundle->id; ?>">
|
||||||
|
</th>
|
||||||
|
<td class="column-id">
|
||||||
|
<strong>#<?php echo $bundle->id; ?></strong>
|
||||||
|
</td>
|
||||||
|
<td class="column-name">
|
||||||
|
<strong>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=yoone-bundles&action=edit&id=' . $bundle->id); ?>">
|
||||||
|
<?php echo esc_html($bundle->name); ?>
|
||||||
|
</a>
|
||||||
|
</strong>
|
||||||
|
<div class="row-actions">
|
||||||
|
<span class="edit">
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=yoone-bundles&action=edit&id=' . $bundle->id); ?>">
|
||||||
|
<?php _e('编辑', 'yoone-subscriptions'); ?>
|
||||||
|
</a> |
|
||||||
|
</span>
|
||||||
|
<span class="delete">
|
||||||
|
<a href="<?php echo wp_nonce_url(admin_url('admin.php?page=yoone-bundles&action=delete&id=' . $bundle->id), 'delete_bundle_' . $bundle->id); ?>"
|
||||||
|
onclick="return confirm('<?php _e('确定要删除这个混装产品吗?', 'yoone-subscriptions'); ?>')">
|
||||||
|
<?php _e('删除', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="column-description">
|
||||||
|
<?php echo esc_html(wp_trim_words($bundle->description, 10)); ?>
|
||||||
|
</td>
|
||||||
|
<td class="column-discount">
|
||||||
|
<?php if ($bundle->discount_type === 'percentage'): ?>
|
||||||
|
<?php echo $bundle->discount_value; ?>%
|
||||||
|
<?php elseif ($bundle->discount_type === 'fixed'): ?>
|
||||||
|
<?php echo Yoone_Helper::format_price($bundle->discount_value); ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php _e('无', 'yoone-subscriptions'); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td class="column-items">
|
||||||
|
<?php echo $item_count; ?> <?php _e('个产品', 'yoone-subscriptions'); ?>
|
||||||
|
</td>
|
||||||
|
<td class="column-status">
|
||||||
|
<span class="bundle-status status-<?php echo esc_attr($bundle->status); ?>">
|
||||||
|
<?php echo $bundle->status === 'active' ? __('启用', 'yoone-subscriptions') : __('禁用', 'yoone-subscriptions'); ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="column-created">
|
||||||
|
<?php echo date_i18n(get_option('date_format'), strtotime($bundle->created_at)); ?>
|
||||||
|
</td>
|
||||||
|
<td class="column-actions">
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=yoone-bundles&action=edit&id=' . $bundle->id); ?>" class="button button-small">
|
||||||
|
<?php _e('编辑', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<?php if ($total_pages > 1): ?>
|
||||||
|
<div class="tablenav bottom">
|
||||||
|
<div class="tablenav-pages">
|
||||||
|
<?php
|
||||||
|
$pagination_args = array(
|
||||||
|
'base' => add_query_arg('paged', '%#%'),
|
||||||
|
'format' => '',
|
||||||
|
'prev_text' => __('«'),
|
||||||
|
'next_text' => __('»'),
|
||||||
|
'total' => $total_pages,
|
||||||
|
'current' => $current_page
|
||||||
|
);
|
||||||
|
echo paginate_links($pagination_args);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bundle-status {
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.status-active { background: #46b450; color: white; }
|
||||||
|
.status-inactive { background: #dc3232; color: white; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
// 全选/取消全选
|
||||||
|
$('#cb-select-all-1').on('click', function() {
|
||||||
|
$('input[name="bundle_ids[]"]').prop('checked', this.checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 批量操作确认
|
||||||
|
$('#bundles-filter').on('submit', function(e) {
|
||||||
|
var action = $('#bulk-action-selector-top').val();
|
||||||
|
var checked = $('input[name="bundle_ids[]"]:checked').length;
|
||||||
|
|
||||||
|
if (action === 'delete' && checked > 0) {
|
||||||
|
if (!confirm('<?php _e('确定要删除选中的混装产品吗?', 'yoone-subscriptions'); ?>')) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
} else if (action !== '-1' && checked === 0) {
|
||||||
|
alert('<?php _e('请选择要操作的混装产品', 'yoone-subscriptions'); ?>');
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,229 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 订阅编辑页面模板
|
||||||
|
*
|
||||||
|
* @package Yoone_Subscriptions
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit; // 防止直接访问
|
||||||
|
}
|
||||||
|
|
||||||
|
// $subscription 变量由调用文件传入
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php printf(__('编辑订阅 #%d', 'yoone-subscriptions'), $subscription->id); ?></h1>
|
||||||
|
|
||||||
|
<form method="post" action="">
|
||||||
|
<?php wp_nonce_field('edit_subscription_' . $subscription->id); ?>
|
||||||
|
|
||||||
|
<table class="form-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="subscription_id"><?php _e('订阅ID', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="subscription_id" value="<?php echo esc_attr($subscription->id); ?>" readonly class="regular-text" />
|
||||||
|
<p class="description"><?php _e('订阅的唯一标识符', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="user_id"><?php _e('用户', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<?php
|
||||||
|
$user = get_user_by('id', $subscription->user_id);
|
||||||
|
if ($user) {
|
||||||
|
echo '<a href="' . admin_url('user-edit.php?user_id=' . $user->ID) . '">';
|
||||||
|
echo esc_html($user->display_name . ' (' . $user->user_email . ')');
|
||||||
|
echo '</a>';
|
||||||
|
} else {
|
||||||
|
_e('用户不存在', 'yoone-subscriptions');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="product_id"><?php _e('产品', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<?php
|
||||||
|
$product = wc_get_product($subscription->product_id);
|
||||||
|
if ($product) {
|
||||||
|
echo '<a href="' . admin_url('post.php?post=' . $product->get_id() . '&action=edit') . '">';
|
||||||
|
echo esc_html($product->get_name());
|
||||||
|
echo '</a>';
|
||||||
|
} else {
|
||||||
|
_e('产品不存在', 'yoone-subscriptions');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="status"><?php _e('状态', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<select name="status" id="status" class="regular-text">
|
||||||
|
<option value="active" <?php selected($subscription->status, 'active'); ?>><?php _e('活跃', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="paused" <?php selected($subscription->status, 'paused'); ?>><?php _e('暂停', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="cancelled" <?php selected($subscription->status, 'cancelled'); ?>><?php _e('已取消', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="expired" <?php selected($subscription->status, 'expired'); ?>><?php _e('已过期', 'yoone-subscriptions'); ?></option>
|
||||||
|
</select>
|
||||||
|
<p class="description"><?php _e('订阅的当前状态', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="billing_period"><?php _e('计费周期', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" value="<?php echo Yoone_Helper::format_billing_period($subscription->billing_period, $subscription->billing_interval); ?>" readonly class="regular-text" />
|
||||||
|
<p class="description"><?php _e('订阅的计费周期(只读)', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="subscription_price"><?php _e('订阅价格', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" value="<?php echo Yoone_Helper::format_price($subscription->subscription_price); ?>" readonly class="regular-text" />
|
||||||
|
<p class="description"><?php _e('每个计费周期的价格(只读)', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="start_date"><?php _e('开始日期', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" value="<?php echo date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($subscription->start_date)); ?>" readonly class="regular-text" />
|
||||||
|
<p class="description"><?php _e('订阅开始的日期(只读)', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="next_payment_date"><?php _e('下次付款日期', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="date" name="next_payment_date" id="next_payment_date"
|
||||||
|
value="<?php echo $subscription->next_payment_date ? date('Y-m-d', strtotime($subscription->next_payment_date)) : ''; ?>"
|
||||||
|
class="regular-text" />
|
||||||
|
<p class="description"><?php _e('下次自动付款的日期', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="end_date"><?php _e('结束日期', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" value="<?php echo $subscription->end_date ? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($subscription->end_date)) : __('无', 'yoone-subscriptions'); ?>" readonly class="regular-text" />
|
||||||
|
<p class="description"><?php _e('订阅结束的日期(只读)', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="trial_end_date"><?php _e('试用结束日期', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" value="<?php echo $subscription->trial_end_date ? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($subscription->trial_end_date)) : __('无试用期', 'yoone-subscriptions'); ?>" readonly class="regular-text" />
|
||||||
|
<p class="description"><?php _e('免费试用期结束的日期(只读)', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="created_at"><?php _e('创建时间', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" value="<?php echo date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($subscription->created_at)); ?>" readonly class="regular-text" />
|
||||||
|
<p class="description"><?php _e('订阅创建的时间(只读)', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="updated_at"><?php _e('更新时间', 'yoone-subscriptions'); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" value="<?php echo date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($subscription->updated_at)); ?>" readonly class="regular-text" />
|
||||||
|
<p class="description"><?php _e('订阅最后更新的时间(只读)', 'yoone-subscriptions'); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<?php submit_button(__('更新订阅', 'yoone-subscriptions')); ?>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- 订阅历史记录 -->
|
||||||
|
<h2><?php _e('订阅历史', 'yoone-subscriptions'); ?></h2>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// 获取相关订单
|
||||||
|
$orders = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT p.ID, p.post_date, pm.meta_value as order_total, pm2.meta_value as order_status
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_order_total'
|
||||||
|
LEFT JOIN {$wpdb->postmeta} pm2 ON p.ID = pm2.post_id AND pm2.meta_key = '_order_status'
|
||||||
|
LEFT JOIN {$wpdb->postmeta} pm3 ON p.ID = pm3.post_id AND pm3.meta_key = '_yoone_subscription_id'
|
||||||
|
WHERE p.post_type = 'shop_order' AND pm3.meta_value = %d
|
||||||
|
ORDER BY p.post_date DESC",
|
||||||
|
$subscription->id
|
||||||
|
));
|
||||||
|
?>
|
||||||
|
|
||||||
|
<table class="wp-list-table widefat fixed striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('订单ID', 'yoone-subscriptions'); ?></th>
|
||||||
|
<th><?php _e('日期', 'yoone-subscriptions'); ?></th>
|
||||||
|
<th><?php _e('金额', 'yoone-subscriptions'); ?></th>
|
||||||
|
<th><?php _e('状态', 'yoone-subscriptions'); ?></th>
|
||||||
|
<th><?php _e('操作', 'yoone-subscriptions'); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($orders)): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="5"><?php _e('暂无相关订单', 'yoone-subscriptions'); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($orders as $order): ?>
|
||||||
|
<tr>
|
||||||
|
<td>#<?php echo $order->ID; ?></td>
|
||||||
|
<td><?php echo date_i18n(get_option('date_format'), strtotime($order->post_date)); ?></td>
|
||||||
|
<td><?php echo Yoone_Helper::format_price($order->order_total); ?></td>
|
||||||
|
<td><?php echo esc_html($order->order_status); ?></td>
|
||||||
|
<td>
|
||||||
|
<a href="<?php echo admin_url('post.php?post=' . $order->ID . '&action=edit'); ?>" class="button button-small">
|
||||||
|
<?php _e('查看', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=yoone-subscriptions'); ?>" class="button">
|
||||||
|
<?php _e('返回订阅列表', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 订阅列表管理页面模板
|
||||||
|
*
|
||||||
|
* @package Yoone_Subscriptions
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit; // 防止直接访问
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// 获取订阅列表
|
||||||
|
$per_page = 20;
|
||||||
|
$current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
|
||||||
|
$offset = ($current_page - 1) * $per_page;
|
||||||
|
|
||||||
|
$search = isset($_GET['s']) ? sanitize_text_field($_GET['s']) : '';
|
||||||
|
$status_filter = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : '';
|
||||||
|
|
||||||
|
$where_conditions = array('1=1');
|
||||||
|
$where_values = array();
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$where_conditions[] = "(s.id LIKE %s OR u.user_email LIKE %s OR u.display_name LIKE %s)";
|
||||||
|
$where_values[] = '%' . $wpdb->esc_like($search) . '%';
|
||||||
|
$where_values[] = '%' . $wpdb->esc_like($search) . '%';
|
||||||
|
$where_values[] = '%' . $wpdb->esc_like($search) . '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status_filter) {
|
||||||
|
$where_conditions[] = "s.status = %s";
|
||||||
|
$where_values[] = $status_filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
$where_clause = implode(' AND ', $where_conditions);
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
$total_query = "SELECT COUNT(*) FROM {$wpdb->prefix}yoone_subscriptions s
|
||||||
|
LEFT JOIN {$wpdb->users} u ON s.user_id = u.ID
|
||||||
|
WHERE {$where_clause}";
|
||||||
|
|
||||||
|
if (!empty($where_values)) {
|
||||||
|
$total = $wpdb->get_var($wpdb->prepare($total_query, $where_values));
|
||||||
|
} else {
|
||||||
|
$total = $wpdb->get_var($total_query);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订阅数据
|
||||||
|
$query = "SELECT s.*, u.user_email, u.display_name, p.post_title as product_name
|
||||||
|
FROM {$wpdb->prefix}yoone_subscriptions s
|
||||||
|
LEFT JOIN {$wpdb->users} u ON s.user_id = u.ID
|
||||||
|
LEFT JOIN {$wpdb->posts} p ON s.product_id = p.ID
|
||||||
|
WHERE {$where_clause}
|
||||||
|
ORDER BY s.created_at DESC
|
||||||
|
LIMIT %d OFFSET %d";
|
||||||
|
|
||||||
|
$query_values = array_merge($where_values, array($per_page, $offset));
|
||||||
|
$subscriptions = $wpdb->get_results($wpdb->prepare($query, $query_values));
|
||||||
|
|
||||||
|
$total_pages = ceil($total / $per_page);
|
||||||
|
|
||||||
|
// 获取状态统计
|
||||||
|
$status_counts = $wpdb->get_results(
|
||||||
|
"SELECT status, COUNT(*) as count FROM {$wpdb->prefix}yoone_subscriptions GROUP BY status"
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<h1 class="wp-heading-inline"><?php _e('订阅管理', 'yoone-subscriptions'); ?></h1>
|
||||||
|
|
||||||
|
<!-- 搜索和筛选 -->
|
||||||
|
<form method="get" class="search-form">
|
||||||
|
<input type="hidden" name="page" value="yoone-subscriptions">
|
||||||
|
|
||||||
|
<p class="search-box">
|
||||||
|
<label class="screen-reader-text" for="subscription-search-input"><?php _e('搜索订阅', 'yoone-subscriptions'); ?>:</label>
|
||||||
|
<input type="search" id="subscription-search-input" name="s" value="<?php echo esc_attr($search); ?>" placeholder="<?php _e('搜索订阅ID、用户邮箱或姓名', 'yoone-subscriptions'); ?>">
|
||||||
|
|
||||||
|
<select name="status">
|
||||||
|
<option value=""><?php _e('所有状态', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="active" <?php selected($status_filter, 'active'); ?>><?php _e('活跃', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="paused" <?php selected($status_filter, 'paused'); ?>><?php _e('暂停', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="cancelled" <?php selected($status_filter, 'cancelled'); ?>><?php _e('已取消', 'yoone-subscriptions'); ?></option>
|
||||||
|
<option value="expired" <?php selected($status_filter, 'expired'); ?>><?php _e('已过期', 'yoone-subscriptions'); ?></option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<?php submit_button(__('搜索', 'yoone-subscriptions'), '', '', false, array('id' => 'search-submit')); ?>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- 状态统计 -->
|
||||||
|
<ul class="subsubsub">
|
||||||
|
<li class="all">
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=yoone-subscriptions'); ?>" <?php echo empty($status_filter) ? 'class="current"' : ''; ?>>
|
||||||
|
<?php _e('全部', 'yoone-subscriptions'); ?> <span class="count">(<?php echo $total; ?>)</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php foreach ($status_counts as $status_count): ?>
|
||||||
|
<li class="<?php echo esc_attr($status_count->status); ?>">
|
||||||
|
| <a href="<?php echo admin_url('admin.php?page=yoone-subscriptions&status=' . $status_count->status); ?>" <?php echo $status_filter === $status_count->status ? 'class="current"' : ''; ?>>
|
||||||
|
<?php echo Yoone_Frontend::get_subscription_status_label($status_count->status); ?> <span class="count">(<?php echo $status_count->count; ?>)</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- 订阅列表表格 -->
|
||||||
|
<table class="wp-list-table widefat fixed striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="manage-column column-id"><?php _e('ID', 'yoone-subscriptions'); ?></th>
|
||||||
|
<th scope="col" class="manage-column column-user"><?php _e('用户', 'yoone-subscriptions'); ?></th>
|
||||||
|
<th scope="col" class="manage-column column-product"><?php _e('产品', 'yoone-subscriptions'); ?></th>
|
||||||
|
<th scope="col" class="manage-column column-status"><?php _e('状态', 'yoone-subscriptions'); ?></th>
|
||||||
|
<th scope="col" class="manage-column column-billing"><?php _e('计费周期', 'yoone-subscriptions'); ?></th>
|
||||||
|
<th scope="col" class="manage-column column-next-payment"><?php _e('下次付款', 'yoone-subscriptions'); ?></th>
|
||||||
|
<th scope="col" class="manage-column column-created"><?php _e('创建时间', 'yoone-subscriptions'); ?></th>
|
||||||
|
<th scope="col" class="manage-column column-actions"><?php _e('操作', 'yoone-subscriptions'); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($subscriptions)): ?>
|
||||||
|
<tr class="no-items">
|
||||||
|
<td class="colspanchange" colspan="8"><?php _e('未找到订阅', 'yoone-subscriptions'); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($subscriptions as $subscription): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="column-id">
|
||||||
|
<strong>#<?php echo $subscription->id; ?></strong>
|
||||||
|
</td>
|
||||||
|
<td class="column-user">
|
||||||
|
<?php if ($subscription->user_id): ?>
|
||||||
|
<a href="<?php echo admin_url('user-edit.php?user_id=' . $subscription->user_id); ?>">
|
||||||
|
<?php echo esc_html($subscription->display_name ?: $subscription->user_email); ?>
|
||||||
|
</a>
|
||||||
|
<br><small><?php echo esc_html($subscription->user_email); ?></small>
|
||||||
|
<?php else: ?>
|
||||||
|
<em><?php _e('访客用户', 'yoone-subscriptions'); ?></em>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td class="column-product">
|
||||||
|
<?php if ($subscription->product_id): ?>
|
||||||
|
<a href="<?php echo admin_url('post.php?post=' . $subscription->product_id . '&action=edit'); ?>">
|
||||||
|
<?php echo esc_html($subscription->product_name ?: __('未知产品', 'yoone-subscriptions')); ?>
|
||||||
|
</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<em><?php _e('产品已删除', 'yoone-subscriptions'); ?></em>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td class="column-status">
|
||||||
|
<span class="subscription-status status-<?php echo esc_attr($subscription->status); ?>">
|
||||||
|
<?php echo Yoone_Frontend::get_subscription_status_label($subscription->status); ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="column-billing">
|
||||||
|
<?php echo Yoone_Helper::format_billing_period($subscription->billing_period, $subscription->billing_interval); ?>
|
||||||
|
</td>
|
||||||
|
<td class="column-next-payment">
|
||||||
|
<?php if ($subscription->next_payment_date && $subscription->status === 'active'): ?>
|
||||||
|
<?php echo date_i18n(get_option('date_format'), strtotime($subscription->next_payment_date)); ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<em><?php _e('无', 'yoone-subscriptions'); ?></em>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td class="column-created">
|
||||||
|
<?php echo date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($subscription->created_at)); ?>
|
||||||
|
</td>
|
||||||
|
<td class="column-actions">
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=yoone-subscriptions&action=edit&id=' . $subscription->id); ?>" class="button button-small">
|
||||||
|
<?php _e('编辑', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<?php if ($total_pages > 1): ?>
|
||||||
|
<div class="tablenav bottom">
|
||||||
|
<div class="tablenav-pages">
|
||||||
|
<?php
|
||||||
|
$pagination_args = array(
|
||||||
|
'base' => add_query_arg('paged', '%#%'),
|
||||||
|
'format' => '',
|
||||||
|
'prev_text' => __('«'),
|
||||||
|
'next_text' => __('»'),
|
||||||
|
'total' => $total_pages,
|
||||||
|
'current' => $current_page
|
||||||
|
);
|
||||||
|
echo paginate_links($pagination_args);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.subscription-status {
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.status-active { background: #46b450; color: white; }
|
||||||
|
.status-paused { background: #ffb900; color: white; }
|
||||||
|
.status-cancelled { background: #dc3232; color: white; }
|
||||||
|
.status-expired { background: #666; color: white; }
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,267 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 混装产品选项模板
|
||||||
|
*
|
||||||
|
* 在产品页面显示混装选项
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $product;
|
||||||
|
|
||||||
|
// 获取混装数据
|
||||||
|
$bundle_helper = new Yoone_Bundle();
|
||||||
|
$bundle = $bundle_helper->get_bundle_by_product_id($product->get_id());
|
||||||
|
|
||||||
|
if (!$bundle || $bundle->get_status() !== 'active') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bundle_items = $bundle->get_bundle_items();
|
||||||
|
|
||||||
|
if (empty($bundle_items)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="yoone-bundle-options" data-product-id="<?php echo esc_attr($product->get_id()); ?>">
|
||||||
|
<h3><?php _e('混装选项', 'yoone-subscriptions'); ?></h3>
|
||||||
|
|
||||||
|
<div class="yoone-bundle-info">
|
||||||
|
<p class="bundle-description"><?php echo esc_html($bundle->get_description()); ?></p>
|
||||||
|
|
||||||
|
<?php if ($bundle->get_discount_value() > 0): ?>
|
||||||
|
<div class="bundle-discount">
|
||||||
|
<?php if ($bundle->get_discount_type() === 'percentage'): ?>
|
||||||
|
<span class="discount-badge"><?php printf(__('混装优惠 %s%%', 'yoone-subscriptions'), $bundle->get_discount_value()); ?></span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="discount-badge"><?php printf(__('混装优惠 %s', 'yoone-subscriptions'), wc_price($bundle->get_discount_value())); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="yoone-bundle-items">
|
||||||
|
<?php foreach ($bundle_items as $item): ?>
|
||||||
|
<?php
|
||||||
|
$item_product = wc_get_product($item['product_id']);
|
||||||
|
if (!$item_product) continue;
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="bundle-item" data-product-id="<?php echo esc_attr($item['product_id']); ?>">
|
||||||
|
<div class="item-image">
|
||||||
|
<?php echo $item_product->get_image('thumbnail'); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-details">
|
||||||
|
<h4 class="item-name"><?php echo esc_html($item_product->get_name()); ?></h4>
|
||||||
|
<p class="item-price"><?php echo $item_product->get_price_html(); ?></p>
|
||||||
|
|
||||||
|
<?php if (!empty($item['description'])): ?>
|
||||||
|
<p class="item-description"><?php echo esc_html($item['description']); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-quantity">
|
||||||
|
<label for="bundle_quantity_<?php echo esc_attr($item['product_id']); ?>">
|
||||||
|
<?php _e('数量', 'yoone-subscriptions'); ?>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="bundle_quantity_<?php echo esc_attr($item['product_id']); ?>"
|
||||||
|
name="bundle_quantities[<?php echo esc_attr($item['product_id']); ?>]"
|
||||||
|
value="<?php echo esc_attr($item['default_quantity']); ?>"
|
||||||
|
min="<?php echo esc_attr($item['min_quantity']); ?>"
|
||||||
|
max="<?php echo esc_attr($item['max_quantity']); ?>"
|
||||||
|
step="1"
|
||||||
|
class="bundle-quantity-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-subtotal">
|
||||||
|
<span class="subtotal-label"><?php _e('小计:', 'yoone-subscriptions'); ?></span>
|
||||||
|
<span class="subtotal-amount" data-base-price="<?php echo esc_attr($item_product->get_price()); ?>">
|
||||||
|
<?php echo wc_price($item_product->get_price() * $item['default_quantity']); ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="yoone-bundle-summary">
|
||||||
|
<div class="bundle-total">
|
||||||
|
<div class="total-row original-total">
|
||||||
|
<span class="total-label"><?php _e('原价:', 'yoone-subscriptions'); ?></span>
|
||||||
|
<span class="total-amount" id="bundle-original-total">-</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($bundle->get_discount_value() > 0): ?>
|
||||||
|
<div class="total-row discount-total">
|
||||||
|
<span class="total-label"><?php _e('优惠:', 'yoone-subscriptions'); ?></span>
|
||||||
|
<span class="total-amount discount" id="bundle-discount-amount">-</span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="total-row final-total">
|
||||||
|
<span class="total-label"><?php _e('混装价格:', 'yoone-subscriptions'); ?></span>
|
||||||
|
<span class="total-amount" id="bundle-final-total">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bundle-actions">
|
||||||
|
<button type="button" class="button yoone-calculate-bundle" id="calculate-bundle-price">
|
||||||
|
<?php _e('计算价格', 'yoone-subscriptions'); ?>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" class="button alt yoone-add-bundle-to-cart" id="add-bundle-to-cart" disabled>
|
||||||
|
<?php _e('添加混装到购物车', 'yoone-subscriptions'); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($bundle->get_min_quantity() || $bundle->get_max_quantity()): ?>
|
||||||
|
<div class="bundle-quantity-info">
|
||||||
|
<?php if ($bundle->get_min_quantity()): ?>
|
||||||
|
<p class="min-quantity-info">
|
||||||
|
<?php printf(__('最小购买数量: %d', 'yoone-subscriptions'), $bundle->get_min_quantity()); ?>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($bundle->get_max_quantity()): ?>
|
||||||
|
<p class="max-quantity-info">
|
||||||
|
<?php printf(__('最大购买数量: %d', 'yoone-subscriptions'), $bundle->get_max_quantity()); ?>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.yoone-bundle-options {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-options h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-discount .discount-badge {
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-items {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .item-image {
|
||||||
|
flex: 0 0 80px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .item-details {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .item-name {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .item-price {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .item-quantity {
|
||||||
|
flex: 0 0 100px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .bundle-quantity-input {
|
||||||
|
width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .item-subtotal {
|
||||||
|
flex: 0 0 120px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-summary {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-total .total-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-total .final-total {
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding-top: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-total .discount {
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-actions .button {
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-quantity-info {
|
||||||
|
margin-top: 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.bundle-item {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item .item-image,
|
||||||
|
.bundle-item .item-details,
|
||||||
|
.bundle-item .item-quantity,
|
||||||
|
.bundle-item .item-subtotal {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,326 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 订阅取消邮件模板
|
||||||
|
*
|
||||||
|
* @package Yoone_Subscriptions
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit; // 防止直接访问
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取传递的变量
|
||||||
|
$subscription = isset($subscription) ? $subscription : null;
|
||||||
|
$user = isset($user) ? $user : null;
|
||||||
|
$reason = isset($reason) ? $reason : '';
|
||||||
|
|
||||||
|
if (!$subscription || !$user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取产品信息
|
||||||
|
$product = wc_get_product($subscription->product_id);
|
||||||
|
$product_name = $product ? $product->get_name() : __('未知产品', 'yoone-subscriptions');
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html <?php language_attributes(); ?>>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=<?php bloginfo('charset'); ?>" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><?php _e('订阅已取消', 'yoone-subscriptions'); ?></title>
|
||||||
|
<style type="text/css">
|
||||||
|
/* 邮件样式 */
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333333;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-header {
|
||||||
|
background-color: #dc3232;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 30px 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-body {
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.greeting {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info h3 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-table th,
|
||||||
|
.info-table td {
|
||||||
|
padding: 8px 0;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-table th {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #555;
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-table td {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-table tr:last-child th,
|
||||||
|
.info-table tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cancelled {
|
||||||
|
color: #dc3232;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancellation-notice {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancellation-notice h4 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-section {
|
||||||
|
background-color: #e7f3ff;
|
||||||
|
border: 1px solid #b8daff;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-section h4 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #004085;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
text-align: center;
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background-color: #46b450;
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background-color: #3a9b3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.secondary {
|
||||||
|
background-color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.secondary:hover {
|
||||||
|
background-color: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-footer {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-footer p {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-footer a {
|
||||||
|
color: #46b450;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
.email-container {
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-header,
|
||||||
|
.email-body,
|
||||||
|
.email-footer {
|
||||||
|
padding: 20px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-table th,
|
||||||
|
.info-table td {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-table th {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons .button {
|
||||||
|
display: block;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="email-container">
|
||||||
|
<!-- 邮件头部 -->
|
||||||
|
<div class="email-header">
|
||||||
|
<h1><?php _e('订阅已取消', 'yoone-subscriptions'); ?></h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 邮件正文 -->
|
||||||
|
<div class="email-body">
|
||||||
|
<div class="greeting">
|
||||||
|
<?php printf(__('亲爱的 %s,', 'yoone-subscriptions'), esc_html($user->display_name)); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><?php _e('我们确认您的订阅已成功取消。以下是取消的订阅详细信息:', 'yoone-subscriptions'); ?></p>
|
||||||
|
|
||||||
|
<!-- 订阅信息 -->
|
||||||
|
<div class="subscription-info">
|
||||||
|
<h3><?php _e('已取消的订阅', 'yoone-subscriptions'); ?></h3>
|
||||||
|
|
||||||
|
<table class="info-table">
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('订阅编号:', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td>#<?php echo esc_html($subscription->id); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('产品名称:', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td><?php echo esc_html($product_name); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('订阅状态:', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td class="status-cancelled"><?php echo Yoone_Helper::get_subscription_status_label($subscription->status); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('开始日期:', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td><?php echo Yoone_Helper::format_date($subscription->start_date, get_option('date_format')); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('取消日期:', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td><?php echo Yoone_Helper::format_date($subscription->end_date, get_option('date_format') . ' ' . get_option('time_format')); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('计费周期:', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td><?php echo Yoone_Helper::format_billing_period($subscription->billing_period, $subscription->billing_interval); ?></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 取消通知 -->
|
||||||
|
<div class="cancellation-notice">
|
||||||
|
<h4><?php _e('重要提醒', 'yoone-subscriptions'); ?></h4>
|
||||||
|
<ul style="margin: 0; padding-left: 20px;">
|
||||||
|
<li><?php _e('您将不会再收到此订阅的自动续费扣款', 'yoone-subscriptions'); ?></li>
|
||||||
|
<li><?php _e('您仍可以访问已付费期间的服务内容', 'yoone-subscriptions'); ?></li>
|
||||||
|
<li><?php _e('如需重新订阅,您可以随时在我们的网站上重新购买', 'yoone-subscriptions'); ?></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 取消原因 -->
|
||||||
|
<?php if ($reason): ?>
|
||||||
|
<div class="feedback-section">
|
||||||
|
<h4><?php _e('取消原因', 'yoone-subscriptions'); ?></h4>
|
||||||
|
<p><?php echo esc_html($reason); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- 反馈邀请 -->
|
||||||
|
<div class="feedback-section">
|
||||||
|
<h4><?php _e('我们重视您的反馈', 'yoone-subscriptions'); ?></h4>
|
||||||
|
<p><?php _e('很遗憾看到您取消了订阅。如果您愿意分享取消的原因,这将帮助我们改进服务质量。', 'yoone-subscriptions'); ?></p>
|
||||||
|
<p><?php _e('如果您在使用过程中遇到了任何问题,我们的客服团队随时准备为您提供帮助。', 'yoone-subscriptions'); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><?php _e('感谢您曾经选择我们的服务,希望未来有机会再次为您服务!', 'yoone-subscriptions'); ?></p>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="action-buttons">
|
||||||
|
<a href="<?php echo esc_url(get_permalink($subscription->product_id)); ?>" class="button">
|
||||||
|
<?php _e('重新订阅', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="<?php echo esc_url(wc_get_account_endpoint_url('yoone-subscriptions')); ?>" class="button secondary">
|
||||||
|
<?php _e('查看所有订阅', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 邮件底部 -->
|
||||||
|
<div class="email-footer">
|
||||||
|
<p><?php printf(__('此邮件由 %s 自动发送', 'yoone-subscriptions'), get_bloginfo('name')); ?></p>
|
||||||
|
<p>
|
||||||
|
<a href="<?php echo esc_url(home_url()); ?>"><?php echo esc_html(get_bloginfo('name')); ?></a> |
|
||||||
|
<a href="<?php echo esc_url(wc_get_page_permalink('contact')); ?>"><?php _e('联系我们', 'yoone-subscriptions'); ?></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin-top: 15px; font-size: 11px; color: #999;">
|
||||||
|
<?php _e('如果您改变主意,随时欢迎您重新订阅我们的服务。', 'yoone-subscriptions'); ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,304 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 订阅续费邮件模板
|
||||||
|
*
|
||||||
|
* @package Yoone_Subscriptions
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit; // 防止直接访问
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取传递的变量
|
||||||
|
$subscription = isset($subscription) ? $subscription : null;
|
||||||
|
$order = isset($order) ? $order : null;
|
||||||
|
$user = isset($user) ? $user : null;
|
||||||
|
|
||||||
|
if (!$subscription || !$order || !$user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取产品信息
|
||||||
|
$product = wc_get_product($subscription->product_id);
|
||||||
|
$product_name = $product ? $product->get_name() : __('未知产品', 'yoone-subscriptions');
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html <?php language_attributes(); ?>>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=<?php bloginfo('charset'); ?>" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><?php _e('订阅续费成功', 'yoone-subscriptions'); ?></title>
|
||||||
|
<style type="text/css">
|
||||||
|
/* 邮件样式 */
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333333;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-header {
|
||||||
|
background-color: #46b450;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 30px 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-body {
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.greeting {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info h3 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-table th,
|
||||||
|
.info-table td {
|
||||||
|
padding: 8px 0;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-table th {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #555;
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-table td {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-table tr:last-child th,
|
||||||
|
.info-table tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #46b450;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-payment {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-payment h4 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
text-align: center;
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background-color: #46b450;
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background-color: #3a9b3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.secondary {
|
||||||
|
background-color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.secondary:hover {
|
||||||
|
background-color: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-footer {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-footer p {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-footer a {
|
||||||
|
color: #46b450;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
.email-container {
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-header,
|
||||||
|
.email-body,
|
||||||
|
.email-footer {
|
||||||
|
padding: 20px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-table th,
|
||||||
|
.info-table td {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-table th {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons .button {
|
||||||
|
display: block;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="email-container">
|
||||||
|
<!-- 邮件头部 -->
|
||||||
|
<div class="email-header">
|
||||||
|
<h1><?php _e('订阅续费成功', 'yoone-subscriptions'); ?></h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 邮件正文 -->
|
||||||
|
<div class="email-body">
|
||||||
|
<div class="greeting">
|
||||||
|
<?php printf(__('亲爱的 %s,', 'yoone-subscriptions'), esc_html($user->display_name)); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><?php _e('您的订阅已成功续费!以下是本次续费的详细信息:', 'yoone-subscriptions'); ?></p>
|
||||||
|
|
||||||
|
<!-- 订阅信息 -->
|
||||||
|
<div class="subscription-info">
|
||||||
|
<h3><?php _e('订阅信息', 'yoone-subscriptions'); ?></h3>
|
||||||
|
|
||||||
|
<table class="info-table">
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('订阅编号:', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td>#<?php echo esc_html($subscription->id); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('产品名称:', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td><?php echo esc_html($product_name); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('续费金额:', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td class="amount"><?php echo Yoone_Helper::format_price($subscription->subscription_price); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('续费日期:', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td><?php echo Yoone_Helper::format_date($order->post_date, get_option('date_format') . ' ' . get_option('time_format')); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('订单编号:', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td>#<?php echo esc_html($order->ID); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('计费周期:', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td><?php echo Yoone_Helper::format_billing_period($subscription->billing_period, $subscription->billing_interval); ?></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 下次付款信息 -->
|
||||||
|
<?php if ($subscription->next_payment_date && $subscription->status === 'active'): ?>
|
||||||
|
<div class="next-payment">
|
||||||
|
<h4><?php _e('下次付款信息', 'yoone-subscriptions'); ?></h4>
|
||||||
|
<p>
|
||||||
|
<?php printf(
|
||||||
|
__('您的下次付款将在 %s 自动处理,金额为 %s。', 'yoone-subscriptions'),
|
||||||
|
'<strong>' . Yoone_Helper::format_date($subscription->next_payment_date, get_option('date_format')) . '</strong>',
|
||||||
|
'<strong>' . Yoone_Helper::format_price($subscription->subscription_price) . '</strong>'
|
||||||
|
); ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<p><?php _e('感谢您继续使用我们的服务!如果您有任何问题或需要帮助,请随时联系我们。', 'yoone-subscriptions'); ?></p>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="action-buttons">
|
||||||
|
<a href="<?php echo esc_url(wc_get_account_endpoint_url('yoone-subscriptions') . '/' . $subscription->id); ?>" class="button">
|
||||||
|
<?php _e('查看订阅详情', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="<?php echo esc_url(wc_get_account_endpoint_url('view-order', $order->ID)); ?>" class="button secondary">
|
||||||
|
<?php _e('查看订单详情', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 邮件底部 -->
|
||||||
|
<div class="email-footer">
|
||||||
|
<p><?php printf(__('此邮件由 %s 自动发送', 'yoone-subscriptions'), get_bloginfo('name')); ?></p>
|
||||||
|
<p>
|
||||||
|
<a href="<?php echo esc_url(home_url()); ?>"><?php echo esc_html(get_bloginfo('name')); ?></a> |
|
||||||
|
<a href="<?php echo esc_url(wc_get_account_endpoint_url('yoone-subscriptions')); ?>"><?php _e('管理订阅', 'yoone-subscriptions'); ?></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<?php if ($subscription->status === 'active'): ?>
|
||||||
|
<p style="margin-top: 15px; font-size: 11px; color: #999;">
|
||||||
|
<?php _e('如果您不希望继续接收此订阅,可以随时在账户页面中取消订阅。', 'yoone-subscriptions'); ?>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,472 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 混装产品前端展示模板
|
||||||
|
*
|
||||||
|
* @package Yoone_Subscriptions
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit; // 防止直接访问
|
||||||
|
}
|
||||||
|
|
||||||
|
global $product;
|
||||||
|
|
||||||
|
// 获取混装产品数据
|
||||||
|
$bundle_id = $product->get_meta('_yoone_bundle_id');
|
||||||
|
if (!$bundle_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// 获取混装详情
|
||||||
|
$bundle = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_bundles WHERE id = %d AND status = 'active'",
|
||||||
|
$bundle_id
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$bundle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取混装商品
|
||||||
|
$bundle_items = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT bi.*, p.post_title as product_name, p.post_excerpt as product_excerpt
|
||||||
|
FROM {$wpdb->prefix}yoone_bundle_items bi
|
||||||
|
LEFT JOIN {$wpdb->posts} p ON bi.product_id = p.ID
|
||||||
|
WHERE bi.bundle_id = %d AND p.post_status = 'publish'
|
||||||
|
ORDER BY bi.sort_order ASC",
|
||||||
|
$bundle->id
|
||||||
|
));
|
||||||
|
|
||||||
|
if (empty($bundle_items)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算总价和折扣
|
||||||
|
$original_total = 0;
|
||||||
|
$bundle_total = 0;
|
||||||
|
|
||||||
|
foreach ($bundle_items as $item) {
|
||||||
|
$item_product = wc_get_product($item->product_id);
|
||||||
|
if ($item_product) {
|
||||||
|
$item_price = $item_product->get_price();
|
||||||
|
$original_total += $item_price * $item->quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算折扣后价格
|
||||||
|
if ($bundle->discount_type === 'percentage') {
|
||||||
|
$bundle_total = $original_total * (1 - $bundle->discount_value / 100);
|
||||||
|
} else {
|
||||||
|
$bundle_total = max(0, $original_total - $bundle->discount_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
$savings = $original_total - $bundle_total;
|
||||||
|
$savings_percentage = $original_total > 0 ? ($savings / $original_total) * 100 : 0;
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="yoone-bundle-product" data-bundle-id="<?php echo esc_attr($bundle->id); ?>">
|
||||||
|
|
||||||
|
<!-- 混装标识 -->
|
||||||
|
<div class="bundle-badge">
|
||||||
|
<span class="bundle-label"><?php _e('混装优惠', 'yoone-subscriptions'); ?></span>
|
||||||
|
<?php if ($savings_percentage > 0): ?>
|
||||||
|
<span class="bundle-savings"><?php printf(__('省 %.0f%%', 'yoone-subscriptions'), $savings_percentage); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 混装描述 -->
|
||||||
|
<?php if ($bundle->description): ?>
|
||||||
|
<div class="bundle-description">
|
||||||
|
<h4><?php _e('套装说明', 'yoone-subscriptions'); ?></h4>
|
||||||
|
<p><?php echo wp_kses_post($bundle->description); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- 混装商品列表 -->
|
||||||
|
<div class="bundle-items">
|
||||||
|
<h4><?php _e('套装包含', 'yoone-subscriptions'); ?></h4>
|
||||||
|
|
||||||
|
<div class="bundle-items-list">
|
||||||
|
<?php foreach ($bundle_items as $item): ?>
|
||||||
|
<?php
|
||||||
|
$item_product = wc_get_product($item->product_id);
|
||||||
|
if (!$item_product) continue;
|
||||||
|
|
||||||
|
$item_price = $item_product->get_price();
|
||||||
|
$item_total = $item_price * $item->quantity;
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="bundle-item" data-product-id="<?php echo esc_attr($item->product_id); ?>">
|
||||||
|
<div class="bundle-item-image">
|
||||||
|
<?php echo $item_product->get_image('thumbnail'); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bundle-item-details">
|
||||||
|
<h5 class="bundle-item-title">
|
||||||
|
<a href="<?php echo get_permalink($item->product_id); ?>">
|
||||||
|
<?php echo esc_html($item->product_name); ?>
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<?php if ($item->product_excerpt): ?>
|
||||||
|
<p class="bundle-item-excerpt"><?php echo esc_html($item->product_excerpt); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="bundle-item-meta">
|
||||||
|
<span class="bundle-item-quantity">
|
||||||
|
<?php printf(__('数量: %d', 'yoone-subscriptions'), $item->quantity); ?>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="bundle-item-price">
|
||||||
|
<?php echo Yoone_Helper::format_price($item_price); ?>
|
||||||
|
<?php if ($item->quantity > 1): ?>
|
||||||
|
<small>(<?php echo Yoone_Helper::format_price($item_total); ?>)</small>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bundle-item-actions">
|
||||||
|
<a href="<?php echo get_permalink($item->product_id); ?>" class="view-product" target="_blank">
|
||||||
|
<?php _e('查看详情', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 价格对比 -->
|
||||||
|
<div class="bundle-pricing">
|
||||||
|
<div class="pricing-comparison">
|
||||||
|
<div class="original-price">
|
||||||
|
<span class="label"><?php _e('单独购买:', 'yoone-subscriptions'); ?></span>
|
||||||
|
<span class="price"><?php echo Yoone_Helper::format_price($original_total); ?></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bundle-price">
|
||||||
|
<span class="label"><?php _e('套装价格:', 'yoone-subscriptions'); ?></span>
|
||||||
|
<span class="price"><?php echo Yoone_Helper::format_price($bundle_total); ?></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($savings > 0): ?>
|
||||||
|
<div class="savings-amount">
|
||||||
|
<span class="label"><?php _e('您节省:', 'yoone-subscriptions'); ?></span>
|
||||||
|
<span class="savings"><?php echo Yoone_Helper::format_price($savings); ?></span>
|
||||||
|
<span class="percentage">(<?php printf('%.0f%%', $savings_percentage); ?>)</span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 混装选项 -->
|
||||||
|
<div class="bundle-options">
|
||||||
|
<div class="bundle-option-field">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="yoone_bundle_purchase" value="1" checked>
|
||||||
|
<?php _e('购买整个套装', 'yoone-subscriptions'); ?>
|
||||||
|
<span class="bundle-option-price"><?php echo Yoone_Helper::format_price($bundle_total); ?></span>
|
||||||
|
</label>
|
||||||
|
<p class="bundle-option-description">
|
||||||
|
<?php _e('选择此选项将以优惠价格购买整个套装', 'yoone-subscriptions'); ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.yoone-bundle-product {
|
||||||
|
border: 2px solid #e1e1e1;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-label {
|
||||||
|
background: #46b450;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-savings {
|
||||||
|
background: #dc3232;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-description {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-description h4 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-items h4 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 2px solid #46b450;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-items-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #e1e1e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-item-image {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-item-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-item-details {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-item-title {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-item-title a {
|
||||||
|
color: #333;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-item-title a:hover {
|
||||||
|
color: #46b450;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-item-excerpt {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-item-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-item-quantity {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-item-price {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #46b450;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-item-actions {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .view-product {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 15px;
|
||||||
|
background: #46b450;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .view-product:hover {
|
||||||
|
background: #3a9b3f;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-pricing {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 2px solid #46b450;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .pricing-comparison {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .pricing-comparison > div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .original-price {
|
||||||
|
border-bottom: 1px solid #e1e1e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .original-price .price {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-price .price {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #46b450;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .savings-amount {
|
||||||
|
background: #f0f8f0;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .savings-amount .savings {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #dc3232;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .savings-amount .percentage {
|
||||||
|
color: #dc3232;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-options {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-option-field label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-option-price {
|
||||||
|
color: #46b450;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-option-description {
|
||||||
|
margin: 10px 0 0 30px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.yoone-bundle-product .bundle-item {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-item-meta {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .pricing-comparison > div {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-option-field label {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-bundle-product .bundle-option-description {
|
||||||
|
margin-left: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
// 混装选项切换
|
||||||
|
$('.yoone-bundle-product input[name="yoone_bundle_purchase"]').on('change', function() {
|
||||||
|
var $bundleProduct = $(this).closest('.yoone-bundle-product');
|
||||||
|
var bundleId = $bundleProduct.data('bundle-id');
|
||||||
|
|
||||||
|
if ($(this).is(':checked')) {
|
||||||
|
// 选择混装购买
|
||||||
|
$bundleProduct.addClass('bundle-selected');
|
||||||
|
|
||||||
|
// 更新产品价格显示
|
||||||
|
var bundlePrice = '<?php echo $bundle_total; ?>';
|
||||||
|
$('.price .woocommerce-Price-amount').text('<?php echo Yoone_Helper::format_price($bundle_total); ?>');
|
||||||
|
|
||||||
|
// 添加隐藏字段
|
||||||
|
if ($('input[name="yoone_bundle_id"]').length === 0) {
|
||||||
|
$('<input>').attr({
|
||||||
|
type: 'hidden',
|
||||||
|
name: 'yoone_bundle_id',
|
||||||
|
value: bundleId
|
||||||
|
}).appendTo('.cart');
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// 取消混装购买
|
||||||
|
$bundleProduct.removeClass('bundle-selected');
|
||||||
|
|
||||||
|
// 恢复原价格显示
|
||||||
|
var originalPrice = '<?php echo $product->get_price(); ?>';
|
||||||
|
$('.price .woocommerce-Price-amount').text('<?php echo Yoone_Helper::format_price($product->get_price()); ?>');
|
||||||
|
|
||||||
|
// 移除隐藏字段
|
||||||
|
$('input[name="yoone_bundle_id"]').remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化状态
|
||||||
|
if ($('.yoone-bundle-product input[name="yoone_bundle_purchase"]').is(':checked')) {
|
||||||
|
$('.yoone-bundle-product input[name="yoone_bundle_purchase"]').trigger('change');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,250 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 我的订阅前端页面模板
|
||||||
|
*
|
||||||
|
* @package Yoone_Subscriptions
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit; // 防止直接访问
|
||||||
|
}
|
||||||
|
|
||||||
|
$user_id = get_current_user_id();
|
||||||
|
|
||||||
|
if (!$user_id) {
|
||||||
|
wc_print_notice(__('请先登录查看您的订阅', 'yoone-subscriptions'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// 获取用户的订阅
|
||||||
|
$subscriptions = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT s.*, p.post_title as product_name
|
||||||
|
FROM {$wpdb->prefix}yoone_subscriptions s
|
||||||
|
LEFT JOIN {$wpdb->posts} p ON s.product_id = p.ID
|
||||||
|
WHERE s.user_id = %d
|
||||||
|
ORDER BY s.created_at DESC",
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="yoone-my-subscriptions">
|
||||||
|
<h2><?php _e('我的订阅', 'yoone-subscriptions'); ?></h2>
|
||||||
|
|
||||||
|
<?php if (empty($subscriptions)): ?>
|
||||||
|
|
||||||
|
<div class="woocommerce-message woocommerce-message--info woocommerce-Message woocommerce-Message--info woocommerce-info">
|
||||||
|
<p><?php _e('您还没有任何订阅。', 'yoone-subscriptions'); ?></p>
|
||||||
|
<p>
|
||||||
|
<a href="<?php echo wc_get_page_permalink('shop'); ?>" class="button">
|
||||||
|
<?php _e('浏览产品', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php else: ?>
|
||||||
|
|
||||||
|
<table class="woocommerce-orders-table woocommerce-MyAccount-orders shop_table shop_table_responsive my_account_orders account-orders-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="woocommerce-orders-table__header woocommerce-orders-table__header-subscription-id">
|
||||||
|
<span class="nobr"><?php _e('订阅', 'yoone-subscriptions'); ?></span>
|
||||||
|
</th>
|
||||||
|
<th class="woocommerce-orders-table__header woocommerce-orders-table__header-subscription-status">
|
||||||
|
<span class="nobr"><?php _e('状态', 'yoone-subscriptions'); ?></span>
|
||||||
|
</th>
|
||||||
|
<th class="woocommerce-orders-table__header woocommerce-orders-table__header-subscription-next-payment">
|
||||||
|
<span class="nobr"><?php _e('下次付款', 'yoone-subscriptions'); ?></span>
|
||||||
|
</th>
|
||||||
|
<th class="woocommerce-orders-table__header woocommerce-orders-table__header-subscription-total">
|
||||||
|
<span class="nobr"><?php _e('总计', 'yoone-subscriptions'); ?></span>
|
||||||
|
</th>
|
||||||
|
<th class="woocommerce-orders-table__header woocommerce-orders-table__header-subscription-actions">
|
||||||
|
<span class="nobr"><?php _e('操作', 'yoone-subscriptions'); ?></span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($subscriptions as $subscription): ?>
|
||||||
|
<tr class="woocommerce-orders-table__row woocommerce-orders-table__row--status-<?php echo esc_attr($subscription->status); ?> order">
|
||||||
|
<td class="woocommerce-orders-table__cell woocommerce-orders-table__cell-subscription-id" data-title="<?php _e('订阅', 'yoone-subscriptions'); ?>">
|
||||||
|
<a href="<?php echo esc_url(wc_get_account_endpoint_url('yoone-subscriptions') . '/' . $subscription->id); ?>">
|
||||||
|
#<?php echo $subscription->id; ?>
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<small><?php echo esc_html($subscription->product_name ?: __('未知产品', 'yoone-subscriptions')); ?></small>
|
||||||
|
</td>
|
||||||
|
<td class="woocommerce-orders-table__cell woocommerce-orders-table__cell-subscription-status" data-title="<?php _e('状态', 'yoone-subscriptions'); ?>">
|
||||||
|
<span class="subscription-status status-<?php echo esc_attr($subscription->status); ?>">
|
||||||
|
<?php echo Yoone_Frontend::get_subscription_status_label($subscription->status); ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="woocommerce-orders-table__cell woocommerce-orders-table__cell-subscription-next-payment" data-title="<?php _e('下次付款', 'yoone-subscriptions'); ?>">
|
||||||
|
<?php if ($subscription->next_payment_date && $subscription->status === 'active'): ?>
|
||||||
|
<time datetime="<?php echo esc_attr($subscription->next_payment_date); ?>">
|
||||||
|
<?php echo date_i18n(wc_date_format(), strtotime($subscription->next_payment_date)); ?>
|
||||||
|
</time>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="na">–</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td class="woocommerce-orders-table__cell woocommerce-orders-table__cell-subscription-total" data-title="<?php _e('总计', 'yoone-subscriptions'); ?>">
|
||||||
|
<span class="woocommerce-Price-amount amount">
|
||||||
|
<?php echo Yoone_Helper::format_price($subscription->subscription_price); ?>
|
||||||
|
</span>
|
||||||
|
<small class="woocommerce-Price-currencySymbol">
|
||||||
|
/ <?php echo Yoone_Helper::format_billing_period($subscription->billing_period, $subscription->billing_interval); ?>
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
<td class="woocommerce-orders-table__cell woocommerce-orders-table__cell-subscription-actions" data-title="<?php _e('操作', 'yoone-subscriptions'); ?>">
|
||||||
|
<a href="<?php echo esc_url(wc_get_account_endpoint_url('yoone-subscriptions') . '/' . $subscription->id); ?>" class="woocommerce-button button view">
|
||||||
|
<?php _e('查看', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<?php if ($subscription->status === 'active'): ?>
|
||||||
|
<a href="<?php echo esc_url(wp_nonce_url(add_query_arg(array('action' => 'pause', 'subscription_id' => $subscription->id)), 'pause_subscription_' . $subscription->id)); ?>"
|
||||||
|
class="woocommerce-button button pause"
|
||||||
|
onclick="return confirm('<?php _e('确定要暂停这个订阅吗?', 'yoone-subscriptions'); ?>')">
|
||||||
|
<?php _e('暂停', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
<?php elseif ($subscription->status === 'paused'): ?>
|
||||||
|
<a href="<?php echo esc_url(wp_nonce_url(add_query_arg(array('action' => 'resume', 'subscription_id' => $subscription->id)), 'resume_subscription_' . $subscription->id)); ?>"
|
||||||
|
class="woocommerce-button button resume">
|
||||||
|
<?php _e('恢复', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (in_array($subscription->status, array('active', 'paused'))): ?>
|
||||||
|
<a href="<?php echo esc_url(wp_nonce_url(add_query_arg(array('action' => 'cancel', 'subscription_id' => $subscription->id)), 'cancel_subscription_' . $subscription->id)); ?>"
|
||||||
|
class="woocommerce-button button cancel"
|
||||||
|
onclick="return confirm('<?php _e('确定要取消这个订阅吗?此操作不可撤销。', 'yoone-subscriptions'); ?>')">
|
||||||
|
<?php _e('取消', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.yoone-my-subscriptions .subscription-status {
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-my-subscriptions .status-active { background: #46b450; }
|
||||||
|
.yoone-my-subscriptions .status-paused { background: #ffb900; }
|
||||||
|
.yoone-my-subscriptions .status-cancelled { background: #dc3232; }
|
||||||
|
.yoone-my-subscriptions .status-expired { background: #666; }
|
||||||
|
|
||||||
|
.yoone-my-subscriptions .woocommerce-button {
|
||||||
|
margin-right: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-my-subscriptions .woocommerce-button.pause {
|
||||||
|
background-color: #ffb900;
|
||||||
|
border-color: #ffb900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-my-subscriptions .woocommerce-button.resume {
|
||||||
|
background-color: #46b450;
|
||||||
|
border-color: #46b450;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-my-subscriptions .woocommerce-button.cancel {
|
||||||
|
background-color: #dc3232;
|
||||||
|
border-color: #dc3232;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.yoone-my-subscriptions .woocommerce-orders-table__cell {
|
||||||
|
display: block;
|
||||||
|
text-align: right;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #e1e1e1;
|
||||||
|
padding: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-my-subscriptions .woocommerce-orders-table__cell:before {
|
||||||
|
content: attr(data-title) ": ";
|
||||||
|
font-weight: bold;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-my-subscriptions .woocommerce-orders-table__cell-subscription-actions {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-my-subscriptions .woocommerce-orders-table__cell-subscription-actions:before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// 处理订阅操作
|
||||||
|
if (isset($_GET['action']) && isset($_GET['subscription_id'])) {
|
||||||
|
$action = sanitize_text_field($_GET['action']);
|
||||||
|
$subscription_id = intval($_GET['subscription_id']);
|
||||||
|
|
||||||
|
// 验证订阅属于当前用户
|
||||||
|
$subscription = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}yoone_subscriptions WHERE id = %d AND user_id = %d",
|
||||||
|
$subscription_id,
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($subscription) {
|
||||||
|
switch ($action) {
|
||||||
|
case 'pause':
|
||||||
|
if (wp_verify_nonce($_GET['_wpnonce'], 'pause_subscription_' . $subscription_id)) {
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->prefix . 'yoone_subscriptions',
|
||||||
|
array('status' => 'paused', 'updated_at' => current_time('mysql')),
|
||||||
|
array('id' => $subscription_id)
|
||||||
|
);
|
||||||
|
wc_add_notice(__('订阅已暂停', 'yoone-subscriptions'), 'success');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'resume':
|
||||||
|
if (wp_verify_nonce($_GET['_wpnonce'], 'resume_subscription_' . $subscription_id)) {
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->prefix . 'yoone_subscriptions',
|
||||||
|
array('status' => 'active', 'updated_at' => current_time('mysql')),
|
||||||
|
array('id' => $subscription_id)
|
||||||
|
);
|
||||||
|
wc_add_notice(__('订阅已恢复', 'yoone-subscriptions'), 'success');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'cancel':
|
||||||
|
if (wp_verify_nonce($_GET['_wpnonce'], 'cancel_subscription_' . $subscription_id)) {
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->prefix . 'yoone_subscriptions',
|
||||||
|
array('status' => 'cancelled', 'end_date' => current_time('mysql'), 'updated_at' => current_time('mysql')),
|
||||||
|
array('id' => $subscription_id)
|
||||||
|
);
|
||||||
|
wc_add_notice(__('订阅已取消', 'yoone-subscriptions'), 'success');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重定向以避免重复提交
|
||||||
|
wp_redirect(wc_get_account_endpoint_url('yoone-subscriptions'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
@ -0,0 +1,355 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 混装产品选项模板
|
||||||
|
*
|
||||||
|
* 在单个产品页面显示混装选项
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="yoone-bundle-options" data-bundle-id="<?php echo esc_attr($bundle->get_id()); ?>">
|
||||||
|
<h3 class="bundle-title"><?php echo esc_html($bundle->get_name()); ?></h3>
|
||||||
|
|
||||||
|
<?php if ($bundle->get_description()): ?>
|
||||||
|
<div class="bundle-description">
|
||||||
|
<?php echo wp_kses_post($bundle->get_description()); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="bundle-items">
|
||||||
|
<h4><?php _e('选择商品', 'yoone-subscriptions'); ?></h4>
|
||||||
|
|
||||||
|
<?php foreach ($items as $index => $item): ?>
|
||||||
|
<?php
|
||||||
|
$item_product = wc_get_product($item['product_id']);
|
||||||
|
if (!$item_product) continue;
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="bundle-item" data-product-id="<?php echo esc_attr($item['product_id']); ?>">
|
||||||
|
<div class="item-header">
|
||||||
|
<label class="item-checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="bundle_items[<?php echo esc_attr($item['product_id']); ?>]"
|
||||||
|
value="<?php echo esc_attr($item['quantity']); ?>"
|
||||||
|
<?php echo $item['is_required'] ? 'checked disabled' : ''; ?>
|
||||||
|
class="bundle-item-checkbox"
|
||||||
|
/>
|
||||||
|
<span class="item-name">
|
||||||
|
<?php echo esc_html($item_product->get_name()); ?>
|
||||||
|
<?php if ($item['is_required']): ?>
|
||||||
|
<span class="required-badge"><?php _e('必选', 'yoone-subscriptions'); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="item-price">
|
||||||
|
<?php if ($item['discount_type'] !== 'none' && $item['discount_value'] > 0): ?>
|
||||||
|
<span class="original-price">
|
||||||
|
<?php echo wc_price($item_product->get_price() * $item['quantity']); ?>
|
||||||
|
</span>
|
||||||
|
<span class="discounted-price">
|
||||||
|
<?php
|
||||||
|
$discounted_price = $item_product->get_price() * $item['quantity'];
|
||||||
|
if ($item['discount_type'] === 'percentage') {
|
||||||
|
$discounted_price *= (1 - $item['discount_value'] / 100);
|
||||||
|
} else {
|
||||||
|
$discounted_price -= $item['discount_value'];
|
||||||
|
}
|
||||||
|
echo wc_price(max(0, $discounted_price));
|
||||||
|
?>
|
||||||
|
</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php echo wc_price($item_product->get_price() * $item['quantity']); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-details">
|
||||||
|
<div class="item-image">
|
||||||
|
<?php echo $item_product->get_image('thumbnail'); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-info">
|
||||||
|
<div class="item-description">
|
||||||
|
<?php echo wp_trim_words($item_product->get_short_description(), 20); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-quantity">
|
||||||
|
<?php _e('数量:', 'yoone-subscriptions'); ?>
|
||||||
|
<span class="quantity-value"><?php echo esc_html($item['quantity']); ?></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($item['discount_type'] !== 'none' && $item['discount_value'] > 0): ?>
|
||||||
|
<div class="item-discount">
|
||||||
|
<?php if ($item['discount_type'] === 'percentage'): ?>
|
||||||
|
<span class="discount-badge">
|
||||||
|
<?php printf(__('-%s%%', 'yoone-subscriptions'), $item['discount_value']); ?>
|
||||||
|
</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="discount-badge">
|
||||||
|
<?php printf(__('-%s', 'yoone-subscriptions'), wc_price($item['discount_value'])); ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bundle-summary">
|
||||||
|
<div class="price-calculation">
|
||||||
|
<div class="original-total">
|
||||||
|
<span class="label"><?php _e('原价总计:', 'yoone-subscriptions'); ?></span>
|
||||||
|
<span class="amount" id="bundle-original-total">-</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($bundle->get_discount_type() !== 'none' && $bundle->get_discount_value() > 0): ?>
|
||||||
|
<div class="bundle-discount">
|
||||||
|
<span class="label"><?php _e('混装折扣:', 'yoone-subscriptions'); ?></span>
|
||||||
|
<span class="amount">
|
||||||
|
<?php if ($bundle->get_discount_type() === 'percentage'): ?>
|
||||||
|
<?php printf(__('-%s%%', 'yoone-subscriptions'), $bundle->get_discount_value()); ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php printf(__('-%s', 'yoone-subscriptions'), wc_price($bundle->get_discount_value())); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="final-total">
|
||||||
|
<span class="label"><?php _e('混装价格:', 'yoone-subscriptions'); ?></span>
|
||||||
|
<span class="amount" id="bundle-final-total">-</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="savings" id="bundle-savings" style="display: none;">
|
||||||
|
<span class="label"><?php _e('您节省了:', 'yoone-subscriptions'); ?></span>
|
||||||
|
<span class="amount savings-amount">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="quantity-controls">
|
||||||
|
<label for="bundle-quantity"><?php _e('混装数量:', 'yoone-subscriptions'); ?></label>
|
||||||
|
<div class="quantity-input">
|
||||||
|
<button type="button" class="qty-btn minus">-</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="bundle-quantity"
|
||||||
|
name="quantity"
|
||||||
|
value="<?php echo esc_attr($bundle->get_min_quantity()); ?>"
|
||||||
|
min="<?php echo esc_attr($bundle->get_min_quantity()); ?>"
|
||||||
|
<?php if ($bundle->get_max_quantity()): ?>
|
||||||
|
max="<?php echo esc_attr($bundle->get_max_quantity()); ?>"
|
||||||
|
<?php endif; ?>
|
||||||
|
step="1"
|
||||||
|
/>
|
||||||
|
<button type="button" class="qty-btn plus">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bundle-messages">
|
||||||
|
<div class="validation-messages" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.yoone-bundle-options {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-title {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-description {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-items h4 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #fff;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-item.selected {
|
||||||
|
border-color: #007cba;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-checkbox input {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required-badge {
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-price {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-price {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: #999;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discounted-price {
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-details {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-image {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-image img {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-description {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-quantity {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discount-badge {
|
||||||
|
background: #27ae60;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
margin-top: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle-summary {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-calculation {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-calculation > div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-total {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1em;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.savings {
|
||||||
|
color: #27ae60;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-input {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty-btn {
|
||||||
|
background: #f0f0f0;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty-btn:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bundle-quantity {
|
||||||
|
border: none;
|
||||||
|
padding: 8px 12px;
|
||||||
|
width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-messages {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,355 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 订阅详情前端页面模板
|
||||||
|
*
|
||||||
|
* @package Yoone_Subscriptions
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit; // 防止直接访问
|
||||||
|
}
|
||||||
|
|
||||||
|
$user_id = get_current_user_id();
|
||||||
|
|
||||||
|
if (!$user_id) {
|
||||||
|
wc_print_notice(__('请先登录查看订阅详情', 'yoone-subscriptions'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订阅ID
|
||||||
|
$subscription_id = get_query_var('yoone-subscription-id');
|
||||||
|
if (!$subscription_id) {
|
||||||
|
$subscription_id = isset($_GET['subscription_id']) ? intval($_GET['subscription_id']) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$subscription_id) {
|
||||||
|
wc_print_notice(__('无效的订阅ID', 'yoone-subscriptions'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// 获取订阅详情
|
||||||
|
$subscription = $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT s.*, p.post_title as product_name
|
||||||
|
FROM {$wpdb->prefix}yoone_subscriptions s
|
||||||
|
LEFT JOIN {$wpdb->posts} p ON s.product_id = p.ID
|
||||||
|
WHERE s.id = %d AND s.user_id = %d",
|
||||||
|
$subscription_id,
|
||||||
|
$user_id
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$subscription) {
|
||||||
|
wc_print_notice(__('订阅不存在或您没有权限查看', 'yoone-subscriptions'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取相关订单
|
||||||
|
$orders = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT p.ID, p.post_date, pm.meta_value as order_total, pm2.meta_value as order_status
|
||||||
|
FROM {$wpdb->posts} p
|
||||||
|
LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_order_total'
|
||||||
|
LEFT JOIN {$wpdb->postmeta} pm2 ON p.ID = pm2.post_id AND pm2.meta_key = '_order_status'
|
||||||
|
LEFT JOIN {$wpdb->postmeta} pm3 ON p.ID = pm3.post_id AND pm3.meta_key = '_yoone_subscription_id'
|
||||||
|
WHERE p.post_type = 'shop_order' AND pm3.meta_value = %d
|
||||||
|
ORDER BY p.post_date DESC",
|
||||||
|
$subscription->id
|
||||||
|
));
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="yoone-subscription-details">
|
||||||
|
<h2><?php printf(__('订阅 #%d', 'yoone-subscriptions'), $subscription->id); ?></h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="<?php echo esc_url(wc_get_account_endpoint_url('yoone-subscriptions')); ?>" class="woocommerce-button button">
|
||||||
|
← <?php _e('返回我的订阅', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- 订阅基本信息 -->
|
||||||
|
<div class="subscription-overview">
|
||||||
|
<h3><?php _e('订阅概览', 'yoone-subscriptions'); ?></h3>
|
||||||
|
|
||||||
|
<table class="woocommerce-table woocommerce-table--order-details shop_table order_details">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('订阅状态', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td>
|
||||||
|
<span class="subscription-status status-<?php echo esc_attr($subscription->status); ?>">
|
||||||
|
<?php echo Yoone_Frontend::get_subscription_status_label($subscription->status); ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('产品', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td>
|
||||||
|
<?php if ($subscription->product_id): ?>
|
||||||
|
<a href="<?php echo get_permalink($subscription->product_id); ?>">
|
||||||
|
<?php echo esc_html($subscription->product_name ?: __('未知产品', 'yoone-subscriptions')); ?>
|
||||||
|
</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<em><?php _e('产品已删除', 'yoone-subscriptions'); ?></em>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('订阅价格', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td>
|
||||||
|
<span class="woocommerce-Price-amount amount">
|
||||||
|
<?php echo Yoone_Helper::format_price($subscription->subscription_price); ?>
|
||||||
|
</span>
|
||||||
|
<small>
|
||||||
|
/ <?php echo Yoone_Helper::format_billing_period($subscription->billing_period, $subscription->billing_interval); ?>
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('开始日期', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td>
|
||||||
|
<time datetime="<?php echo esc_attr($subscription->start_date); ?>">
|
||||||
|
<?php echo date_i18n(wc_date_format() . ' ' . wc_time_format(), strtotime($subscription->start_date)); ?>
|
||||||
|
</time>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php if ($subscription->trial_end_date): ?>
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('试用期结束', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td>
|
||||||
|
<time datetime="<?php echo esc_attr($subscription->trial_end_date); ?>">
|
||||||
|
<?php echo date_i18n(wc_date_format(), strtotime($subscription->trial_end_date)); ?>
|
||||||
|
</time>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($subscription->next_payment_date && $subscription->status === 'active'): ?>
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('下次付款日期', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td>
|
||||||
|
<time datetime="<?php echo esc_attr($subscription->next_payment_date); ?>">
|
||||||
|
<?php echo date_i18n(wc_date_format(), strtotime($subscription->next_payment_date)); ?>
|
||||||
|
</time>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($subscription->end_date): ?>
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('结束日期', 'yoone-subscriptions'); ?></th>
|
||||||
|
<td>
|
||||||
|
<time datetime="<?php echo esc_attr($subscription->end_date); ?>">
|
||||||
|
<?php echo date_i18n(wc_date_format() . ' ' . wc_time_format(), strtotime($subscription->end_date)); ?>
|
||||||
|
</time>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 订阅操作 -->
|
||||||
|
<div class="subscription-actions">
|
||||||
|
<h3><?php _e('订阅操作', 'yoone-subscriptions'); ?></h3>
|
||||||
|
|
||||||
|
<div class="subscription-action-buttons">
|
||||||
|
<?php if ($subscription->status === 'active'): ?>
|
||||||
|
<a href="<?php echo esc_url(wp_nonce_url(add_query_arg(array('action' => 'pause', 'subscription_id' => $subscription->id)), 'pause_subscription_' . $subscription->id)); ?>"
|
||||||
|
class="woocommerce-button button pause"
|
||||||
|
onclick="return confirm('<?php _e('确定要暂停这个订阅吗?', 'yoone-subscriptions'); ?>')">
|
||||||
|
<?php _e('暂停订阅', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
<?php elseif ($subscription->status === 'paused'): ?>
|
||||||
|
<a href="<?php echo esc_url(wp_nonce_url(add_query_arg(array('action' => 'resume', 'subscription_id' => $subscription->id)), 'resume_subscription_' . $subscription->id)); ?>"
|
||||||
|
class="woocommerce-button button resume">
|
||||||
|
<?php _e('恢复订阅', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (in_array($subscription->status, array('active', 'paused'))): ?>
|
||||||
|
<a href="<?php echo esc_url(wp_nonce_url(add_query_arg(array('action' => 'cancel', 'subscription_id' => $subscription->id)), 'cancel_subscription_' . $subscription->id)); ?>"
|
||||||
|
class="woocommerce-button button cancel"
|
||||||
|
onclick="return confirm('<?php _e('确定要取消这个订阅吗?此操作不可撤销。', 'yoone-subscriptions'); ?>')">
|
||||||
|
<?php _e('取消订阅', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 相关订单 -->
|
||||||
|
<div class="subscription-orders">
|
||||||
|
<h3><?php _e('相关订单', 'yoone-subscriptions'); ?></h3>
|
||||||
|
|
||||||
|
<?php if (empty($orders)): ?>
|
||||||
|
<p><?php _e('暂无相关订单', 'yoone-subscriptions'); ?></p>
|
||||||
|
<?php else: ?>
|
||||||
|
<table class="woocommerce-orders-table woocommerce-MyAccount-orders shop_table shop_table_responsive my_account_orders account-orders-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="woocommerce-orders-table__header woocommerce-orders-table__header-order-number">
|
||||||
|
<span class="nobr"><?php _e('订单', 'yoone-subscriptions'); ?></span>
|
||||||
|
</th>
|
||||||
|
<th class="woocommerce-orders-table__header woocommerce-orders-table__header-order-date">
|
||||||
|
<span class="nobr"><?php _e('日期', 'yoone-subscriptions'); ?></span>
|
||||||
|
</th>
|
||||||
|
<th class="woocommerce-orders-table__header woocommerce-orders-table__header-order-status">
|
||||||
|
<span class="nobr"><?php _e('状态', 'yoone-subscriptions'); ?></span>
|
||||||
|
</th>
|
||||||
|
<th class="woocommerce-orders-table__header woocommerce-orders-table__header-order-total">
|
||||||
|
<span class="nobr"><?php _e('总计', 'yoone-subscriptions'); ?></span>
|
||||||
|
</th>
|
||||||
|
<th class="woocommerce-orders-table__header woocommerce-orders-table__header-order-actions">
|
||||||
|
<span class="nobr"><?php _e('操作', 'yoone-subscriptions'); ?></span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($orders as $order): ?>
|
||||||
|
<tr class="woocommerce-orders-table__row order">
|
||||||
|
<td class="woocommerce-orders-table__cell woocommerce-orders-table__cell-order-number" data-title="<?php _e('订单', 'yoone-subscriptions'); ?>">
|
||||||
|
<a href="<?php echo esc_url(wc_get_account_endpoint_url('view-order', $order->ID)); ?>">
|
||||||
|
#<?php echo $order->ID; ?>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="woocommerce-orders-table__cell woocommerce-orders-table__cell-order-date" data-title="<?php _e('日期', 'yoone-subscriptions'); ?>">
|
||||||
|
<time datetime="<?php echo esc_attr($order->post_date); ?>">
|
||||||
|
<?php echo date_i18n(wc_date_format(), strtotime($order->post_date)); ?>
|
||||||
|
</time>
|
||||||
|
</td>
|
||||||
|
<td class="woocommerce-orders-table__cell woocommerce-orders-table__cell-order-status" data-title="<?php _e('状态', 'yoone-subscriptions'); ?>">
|
||||||
|
<?php echo esc_html(wc_get_order_status_name($order->order_status)); ?>
|
||||||
|
</td>
|
||||||
|
<td class="woocommerce-orders-table__cell woocommerce-orders-table__cell-order-total" data-title="<?php _e('总计', 'yoone-subscriptions'); ?>">
|
||||||
|
<?php echo Yoone_Helper::format_price($order->order_total); ?>
|
||||||
|
</td>
|
||||||
|
<td class="woocommerce-orders-table__cell woocommerce-orders-table__cell-order-actions" data-title="<?php _e('操作', 'yoone-subscriptions'); ?>">
|
||||||
|
<a href="<?php echo esc_url(wc_get_account_endpoint_url('view-order', $order->ID)); ?>" class="woocommerce-button button view">
|
||||||
|
<?php _e('查看', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.yoone-subscription-details .subscription-status {
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-details .status-active { background: #46b450; }
|
||||||
|
.yoone-subscription-details .status-paused { background: #ffb900; }
|
||||||
|
.yoone-subscription-details .status-cancelled { background: #dc3232; }
|
||||||
|
.yoone-subscription-details .status-expired { background: #666; }
|
||||||
|
|
||||||
|
.yoone-subscription-details .subscription-overview,
|
||||||
|
.yoone-subscription-details .subscription-actions,
|
||||||
|
.yoone-subscription-details .subscription-orders {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-details .subscription-action-buttons {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-details .subscription-action-buttons .woocommerce-button {
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-details .woocommerce-button.pause {
|
||||||
|
background-color: #ffb900;
|
||||||
|
border-color: #ffb900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-details .woocommerce-button.resume {
|
||||||
|
background-color: #46b450;
|
||||||
|
border-color: #46b450;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-details .woocommerce-button.cancel {
|
||||||
|
background-color: #dc3232;
|
||||||
|
border-color: #dc3232;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.yoone-subscription-details .woocommerce-orders-table__cell {
|
||||||
|
display: block;
|
||||||
|
text-align: right;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #e1e1e1;
|
||||||
|
padding: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-details .woocommerce-orders-table__cell:before {
|
||||||
|
content: attr(data-title) ": ";
|
||||||
|
font-weight: bold;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-details .woocommerce-orders-table__cell-order-actions {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-details .woocommerce-orders-table__cell-order-actions:before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// 处理订阅操作
|
||||||
|
if (isset($_GET['action']) && isset($_GET['subscription_id'])) {
|
||||||
|
$action = sanitize_text_field($_GET['action']);
|
||||||
|
$action_subscription_id = intval($_GET['subscription_id']);
|
||||||
|
|
||||||
|
// 验证订阅ID匹配
|
||||||
|
if ($action_subscription_id === $subscription->id) {
|
||||||
|
switch ($action) {
|
||||||
|
case 'pause':
|
||||||
|
if (wp_verify_nonce($_GET['_wpnonce'], 'pause_subscription_' . $subscription->id)) {
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->prefix . 'yoone_subscriptions',
|
||||||
|
array('status' => 'paused', 'updated_at' => current_time('mysql')),
|
||||||
|
array('id' => $subscription->id)
|
||||||
|
);
|
||||||
|
wc_add_notice(__('订阅已暂停', 'yoone-subscriptions'), 'success');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'resume':
|
||||||
|
if (wp_verify_nonce($_GET['_wpnonce'], 'resume_subscription_' . $subscription->id)) {
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->prefix . 'yoone_subscriptions',
|
||||||
|
array('status' => 'active', 'updated_at' => current_time('mysql')),
|
||||||
|
array('id' => $subscription->id)
|
||||||
|
);
|
||||||
|
wc_add_notice(__('订阅已恢复', 'yoone-subscriptions'), 'success');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'cancel':
|
||||||
|
if (wp_verify_nonce($_GET['_wpnonce'], 'cancel_subscription_' . $subscription->id)) {
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->prefix . 'yoone_subscriptions',
|
||||||
|
array('status' => 'cancelled', 'end_date' => current_time('mysql'), 'updated_at' => current_time('mysql')),
|
||||||
|
array('id' => $subscription->id)
|
||||||
|
);
|
||||||
|
wc_add_notice(__('订阅已取消', 'yoone-subscriptions'), 'success');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重定向以避免重复提交
|
||||||
|
wp_redirect(wc_get_account_endpoint_url('yoone-subscriptions') . '/' . $subscription->id);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
@ -0,0 +1,514 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 订阅选项前端模板
|
||||||
|
*
|
||||||
|
* @package Yoone_Subscriptions
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit; // 防止直接访问
|
||||||
|
}
|
||||||
|
|
||||||
|
global $product;
|
||||||
|
|
||||||
|
// 检查产品是否支持订阅
|
||||||
|
$is_subscription = $product->get_meta('_yoone_subscription_enabled');
|
||||||
|
if (!$is_subscription) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订阅设置
|
||||||
|
$subscription_price = $product->get_meta('_yoone_subscription_price');
|
||||||
|
$billing_period = $product->get_meta('_yoone_billing_period') ?: 'month';
|
||||||
|
$billing_interval = $product->get_meta('_yoone_billing_interval') ?: 1;
|
||||||
|
$trial_period = $product->get_meta('_yoone_trial_period');
|
||||||
|
$trial_length = $product->get_meta('_yoone_trial_length');
|
||||||
|
$subscription_length = $product->get_meta('_yoone_subscription_length');
|
||||||
|
$signup_fee = $product->get_meta('_yoone_signup_fee');
|
||||||
|
|
||||||
|
// 计算价格
|
||||||
|
$regular_price = $product->get_regular_price();
|
||||||
|
$subscription_price = $subscription_price ?: $regular_price;
|
||||||
|
$trial_price = 0; // 试用期免费
|
||||||
|
|
||||||
|
// 计算节省金额(如果订阅价格更低)
|
||||||
|
$monthly_regular = $regular_price;
|
||||||
|
$monthly_subscription = $subscription_price;
|
||||||
|
|
||||||
|
// 转换为月度价格进行比较
|
||||||
|
switch ($billing_period) {
|
||||||
|
case 'week':
|
||||||
|
$monthly_subscription = $subscription_price * 4.33; // 平均每月周数
|
||||||
|
break;
|
||||||
|
case 'year':
|
||||||
|
$monthly_subscription = $subscription_price / 12;
|
||||||
|
break;
|
||||||
|
case 'day':
|
||||||
|
$monthly_subscription = $subscription_price * 30;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$monthly_savings = max(0, $monthly_regular - $monthly_subscription);
|
||||||
|
$savings_percentage = $monthly_regular > 0 ? ($monthly_savings / $monthly_regular) * 100 : 0;
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="yoone-subscription-options" data-product-id="<?php echo esc_attr($product->get_id()); ?>">
|
||||||
|
|
||||||
|
<!-- 购买方式选择 -->
|
||||||
|
<div class="purchase-options">
|
||||||
|
<h4><?php _e('购买方式', 'yoone-subscriptions'); ?></h4>
|
||||||
|
|
||||||
|
<div class="purchase-option-list">
|
||||||
|
<!-- 一次性购买 -->
|
||||||
|
<div class="purchase-option">
|
||||||
|
<label class="purchase-option-label">
|
||||||
|
<input type="radio" name="yoone_purchase_type" value="one_time" checked>
|
||||||
|
<span class="option-content">
|
||||||
|
<span class="option-title"><?php _e('一次性购买', 'yoone-subscriptions'); ?></span>
|
||||||
|
<span class="option-price"><?php echo Yoone_Helper::format_price($regular_price); ?></span>
|
||||||
|
<span class="option-description"><?php _e('立即付款,拥有产品', 'yoone-subscriptions'); ?></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 订阅购买 -->
|
||||||
|
<div class="purchase-option subscription-option">
|
||||||
|
<label class="purchase-option-label">
|
||||||
|
<input type="radio" name="yoone_purchase_type" value="subscription">
|
||||||
|
<span class="option-content">
|
||||||
|
<span class="option-title">
|
||||||
|
<?php _e('订阅购买', 'yoone-subscriptions'); ?>
|
||||||
|
<?php if ($savings_percentage > 5): ?>
|
||||||
|
<span class="savings-badge"><?php printf(__('省 %.0f%%', 'yoone-subscriptions'), $savings_percentage); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
<span class="option-price">
|
||||||
|
<?php echo Yoone_Helper::format_price($subscription_price); ?>
|
||||||
|
<small>/ <?php echo Yoone_Helper::format_billing_period($billing_period, $billing_interval); ?></small>
|
||||||
|
</span>
|
||||||
|
<span class="option-description">
|
||||||
|
<?php
|
||||||
|
if ($trial_period && $trial_length > 0) {
|
||||||
|
printf(__('免费试用 %d %s,然后每%s收费', 'yoone-subscriptions'),
|
||||||
|
$trial_length,
|
||||||
|
Yoone_Helper::get_period_label($trial_period),
|
||||||
|
Yoone_Helper::format_billing_period($billing_period, $billing_interval)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
printf(__('每%s自动续费', 'yoone-subscriptions'),
|
||||||
|
Yoone_Helper::format_billing_period($billing_period, $billing_interval)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 订阅详情(仅在选择订阅时显示) -->
|
||||||
|
<div class="subscription-details" style="display: none;">
|
||||||
|
|
||||||
|
<!-- 价格明细 -->
|
||||||
|
<div class="subscription-pricing">
|
||||||
|
<h5><?php _e('价格明细', 'yoone-subscriptions'); ?></h5>
|
||||||
|
|
||||||
|
<div class="pricing-breakdown">
|
||||||
|
<?php if ($signup_fee > 0): ?>
|
||||||
|
<div class="pricing-item">
|
||||||
|
<span class="label"><?php _e('注册费用:', 'yoone-subscriptions'); ?></span>
|
||||||
|
<span class="value"><?php echo Yoone_Helper::format_price($signup_fee); ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($trial_period && $trial_length > 0): ?>
|
||||||
|
<div class="pricing-item trial">
|
||||||
|
<span class="label">
|
||||||
|
<?php printf(__('试用期 (%d %s):', 'yoone-subscriptions'),
|
||||||
|
$trial_length,
|
||||||
|
Yoone_Helper::get_period_label($trial_period)
|
||||||
|
); ?>
|
||||||
|
</span>
|
||||||
|
<span class="value free"><?php _e('免费', 'yoone-subscriptions'); ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="pricing-item recurring">
|
||||||
|
<span class="label">
|
||||||
|
<?php printf(__('续费价格 (每%s):', 'yoone-subscriptions'),
|
||||||
|
Yoone_Helper::format_billing_period($billing_period, $billing_interval)
|
||||||
|
); ?>
|
||||||
|
</span>
|
||||||
|
<span class="value"><?php echo Yoone_Helper::format_price($subscription_price); ?></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($subscription_length > 0): ?>
|
||||||
|
<div class="pricing-item">
|
||||||
|
<span class="label"><?php _e('订阅期限:', 'yoone-subscriptions'); ?></span>
|
||||||
|
<span class="value">
|
||||||
|
<?php printf(__('%d 次付款', 'yoone-subscriptions'), $subscription_length); ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 订阅优势 -->
|
||||||
|
<?php if ($monthly_savings > 0): ?>
|
||||||
|
<div class="subscription-benefits">
|
||||||
|
<h5><?php _e('订阅优势', 'yoone-subscriptions'); ?></h5>
|
||||||
|
<ul class="benefits-list">
|
||||||
|
<li>
|
||||||
|
<span class="benefit-icon">💰</span>
|
||||||
|
<?php printf(__('每月节省 %s (%.0f%%)', 'yoone-subscriptions'),
|
||||||
|
Yoone_Helper::format_price($monthly_savings),
|
||||||
|
$savings_percentage
|
||||||
|
); ?>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="benefit-icon">🔄</span>
|
||||||
|
<?php _e('自动续费,无需手动购买', 'yoone-subscriptions'); ?>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="benefit-icon">⏸️</span>
|
||||||
|
<?php _e('随时暂停或取消订阅', 'yoone-subscriptions'); ?>
|
||||||
|
</li>
|
||||||
|
<?php if ($trial_period && $trial_length > 0): ?>
|
||||||
|
<li>
|
||||||
|
<span class="benefit-icon">🎁</span>
|
||||||
|
<?php printf(__('免费试用 %d %s', 'yoone-subscriptions'),
|
||||||
|
$trial_length,
|
||||||
|
Yoone_Helper::get_period_label($trial_period)
|
||||||
|
); ?>
|
||||||
|
</li>
|
||||||
|
<?php endif; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- 订阅条款 -->
|
||||||
|
<div class="subscription-terms">
|
||||||
|
<h5><?php _e('订阅条款', 'yoone-subscriptions'); ?></h5>
|
||||||
|
<div class="terms-content">
|
||||||
|
<p>
|
||||||
|
<?php
|
||||||
|
if ($trial_period && $trial_length > 0) {
|
||||||
|
printf(__('您将获得 %d %s 的免费试用期。试用期结束后,将按 %s 的价格每%s自动收费。', 'yoone-subscriptions'),
|
||||||
|
$trial_length,
|
||||||
|
Yoone_Helper::get_period_label($trial_period),
|
||||||
|
Yoone_Helper::format_price($subscription_price),
|
||||||
|
Yoone_Helper::format_billing_period($billing_period, $billing_interval)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
printf(__('您将按 %s 的价格每%s自动收费。', 'yoone-subscriptions'),
|
||||||
|
Yoone_Helper::format_price($subscription_price),
|
||||||
|
Yoone_Helper::format_billing_period($billing_period, $billing_interval)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p><?php _e('您可以随时在账户页面中管理您的订阅,包括暂停、恢复或取消订阅。', 'yoone-subscriptions'); ?></p>
|
||||||
|
|
||||||
|
<?php if ($subscription_length > 0): ?>
|
||||||
|
<p>
|
||||||
|
<?php printf(__('此订阅将在 %d 次付款后自动结束。', 'yoone-subscriptions'), $subscription_length); ?>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 隐藏字段 -->
|
||||||
|
<input type="hidden" name="yoone_subscription_data" value="">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.yoone-subscription-options {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #e1e1e1;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options h4 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 2px solid #46b450;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .purchase-option-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .purchase-option {
|
||||||
|
background: white;
|
||||||
|
border: 2px solid #e1e1e1;
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .purchase-option:hover {
|
||||||
|
border-color: #46b450;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .purchase-option.selected {
|
||||||
|
border-color: #46b450;
|
||||||
|
background: #f0f8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .purchase-option-label {
|
||||||
|
display: block;
|
||||||
|
padding: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .purchase-option-label input[type="radio"] {
|
||||||
|
margin: 0 10px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .option-content {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .option-title {
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .savings-badge {
|
||||||
|
background: #dc3232;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .option-price {
|
||||||
|
display: block;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #46b450;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .option-price small {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .option-description {
|
||||||
|
display: block;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .subscription-details {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #e1e1e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .subscription-details h5 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .pricing-breakdown {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .pricing-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .pricing-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .pricing-item.trial .value.free {
|
||||||
|
color: #46b450;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .pricing-item.recurring {
|
||||||
|
background: #f0f8f0;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: none;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .pricing-item.recurring .value {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #46b450;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .benefits-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .benefits-list li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .benefit-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .subscription-terms {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f8f8f8;
|
||||||
|
border-radius: 3px;
|
||||||
|
border-left: 4px solid #46b450;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .terms-content p {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .terms-content p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.yoone-subscription-options {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .purchase-option-label {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .option-title {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .option-price {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .pricing-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options .benefits-list li {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
var $subscriptionOptions = $('.yoone-subscription-options');
|
||||||
|
var $purchaseOptions = $subscriptionOptions.find('input[name="yoone_purchase_type"]');
|
||||||
|
var $subscriptionDetails = $subscriptionOptions.find('.subscription-details');
|
||||||
|
var $hiddenField = $subscriptionOptions.find('input[name="yoone_subscription_data"]');
|
||||||
|
|
||||||
|
// 购买方式切换
|
||||||
|
$purchaseOptions.on('change', function() {
|
||||||
|
var purchaseType = $(this).val();
|
||||||
|
var $option = $(this).closest('.purchase-option');
|
||||||
|
|
||||||
|
// 更新选中状态
|
||||||
|
$('.purchase-option').removeClass('selected');
|
||||||
|
$option.addClass('selected');
|
||||||
|
|
||||||
|
if (purchaseType === 'subscription') {
|
||||||
|
// 显示订阅详情
|
||||||
|
$subscriptionDetails.slideDown();
|
||||||
|
|
||||||
|
// 更新产品价格显示
|
||||||
|
updateProductPrice('subscription');
|
||||||
|
|
||||||
|
// 设置订阅数据
|
||||||
|
var subscriptionData = {
|
||||||
|
type: 'subscription',
|
||||||
|
price: '<?php echo $subscription_price; ?>',
|
||||||
|
billing_period: '<?php echo $billing_period; ?>',
|
||||||
|
billing_interval: '<?php echo $billing_interval; ?>',
|
||||||
|
trial_period: '<?php echo $trial_period; ?>',
|
||||||
|
trial_length: '<?php echo $trial_length; ?>',
|
||||||
|
signup_fee: '<?php echo $signup_fee; ?>'
|
||||||
|
};
|
||||||
|
$hiddenField.val(JSON.stringify(subscriptionData));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// 隐藏订阅详情
|
||||||
|
$subscriptionDetails.slideUp();
|
||||||
|
|
||||||
|
// 恢复原价格显示
|
||||||
|
updateProductPrice('one_time');
|
||||||
|
|
||||||
|
// 清除订阅数据
|
||||||
|
$hiddenField.val('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新产品价格显示
|
||||||
|
function updateProductPrice(type) {
|
||||||
|
var $priceElement = $('.price .woocommerce-Price-amount');
|
||||||
|
|
||||||
|
if (type === 'subscription') {
|
||||||
|
var subscriptionPrice = '<?php echo Yoone_Helper::format_price($subscription_price); ?>';
|
||||||
|
var billingPeriod = '<?php echo Yoone_Helper::format_billing_period($billing_period, $billing_interval); ?>';
|
||||||
|
|
||||||
|
$priceElement.html(subscriptionPrice + ' <small>/ ' + billingPeriod + '</small>');
|
||||||
|
|
||||||
|
// 添加试用期信息
|
||||||
|
<?php if ($trial_period && $trial_length > 0): ?>
|
||||||
|
var trialInfo = '<?php printf(__("免费试用 %d %s", "yoone-subscriptions"), $trial_length, Yoone_Helper::get_period_label($trial_period)); ?>';
|
||||||
|
if ($('.trial-info').length === 0) {
|
||||||
|
$('<div class="trial-info" style="color: #46b450; font-size: 14px; margin-top: 5px;">' + trialInfo + '</div>').insertAfter($priceElement);
|
||||||
|
}
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
} else {
|
||||||
|
var regularPrice = '<?php echo Yoone_Helper::format_price($regular_price); ?>';
|
||||||
|
$priceElement.html(regularPrice);
|
||||||
|
$('.trial-info').remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化状态
|
||||||
|
$purchaseOptions.filter(':checked').trigger('change');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,374 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 我的账户 - 订阅管理模板
|
||||||
|
*
|
||||||
|
* 显示用户的所有订阅
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$customer_id = get_current_user_id();
|
||||||
|
|
||||||
|
if (!$customer_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户订阅
|
||||||
|
global $wpdb;
|
||||||
|
$subscriptions = $wpdb->get_results($wpdb->prepare("
|
||||||
|
SELECT * FROM {$wpdb->prefix}yoone_subscriptions
|
||||||
|
WHERE customer_id = %d
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
", $customer_id));
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="yoone-my-subscriptions">
|
||||||
|
<h2><?php _e('我的订阅', 'yoone-subscriptions'); ?></h2>
|
||||||
|
|
||||||
|
<?php if (empty($subscriptions)): ?>
|
||||||
|
<div class="no-subscriptions">
|
||||||
|
<p><?php _e('您还没有任何订阅。', 'yoone-subscriptions'); ?></p>
|
||||||
|
<a href="<?php echo esc_url(wc_get_page_permalink('shop')); ?>" class="button">
|
||||||
|
<?php _e('开始购物', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="subscriptions-list">
|
||||||
|
<?php foreach ($subscriptions as $subscription): ?>
|
||||||
|
<?php
|
||||||
|
$subscription_obj = new Yoone_Subscription($subscription->id);
|
||||||
|
$subscription_items = $subscription_obj->get_items();
|
||||||
|
$next_payment = $subscription_obj->get_next_payment_date();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="subscription-item" data-subscription-id="<?php echo esc_attr($subscription->id); ?>">
|
||||||
|
<div class="subscription-header">
|
||||||
|
<div class="subscription-id">
|
||||||
|
<h3><?php printf(__('订阅 #%d', 'yoone-subscriptions'), $subscription->id); ?></h3>
|
||||||
|
<span class="subscription-status status-<?php echo esc_attr($subscription->status); ?>">
|
||||||
|
<?php echo esc_html($this->get_status_label($subscription->status)); ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="subscription-actions">
|
||||||
|
<?php if ($subscription->status === 'active'): ?>
|
||||||
|
<a href="<?php echo esc_url($this->get_subscription_action_url($subscription->id, 'pause')); ?>"
|
||||||
|
class="button subscription-pause">
|
||||||
|
<?php _e('暂停', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
<?php elseif ($subscription->status === 'paused'): ?>
|
||||||
|
<a href="<?php echo esc_url($this->get_subscription_action_url($subscription->id, 'resume')); ?>"
|
||||||
|
class="button subscription-resume">
|
||||||
|
<?php _e('恢复', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (in_array($subscription->status, array('active', 'paused'))): ?>
|
||||||
|
<a href="<?php echo esc_url($this->get_subscription_action_url($subscription->id, 'cancel')); ?>"
|
||||||
|
class="button subscription-cancel"
|
||||||
|
onclick="return confirm('<?php _e('确定要取消这个订阅吗?', 'yoone-subscriptions'); ?>')">
|
||||||
|
<?php _e('取消', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<a href="<?php echo esc_url($this->get_subscription_details_url($subscription->id)); ?>"
|
||||||
|
class="button subscription-details">
|
||||||
|
<?php _e('查看详情', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="subscription-info">
|
||||||
|
<div class="subscription-products">
|
||||||
|
<h4><?php _e('订阅产品', 'yoone-subscriptions'); ?></h4>
|
||||||
|
<?php if (!empty($subscription_items)): ?>
|
||||||
|
<ul class="subscription-items">
|
||||||
|
<?php foreach ($subscription_items as $item): ?>
|
||||||
|
<?php
|
||||||
|
$product = wc_get_product($item['product_id']);
|
||||||
|
if (!$product) continue;
|
||||||
|
?>
|
||||||
|
<li class="subscription-item-row">
|
||||||
|
<div class="item-image">
|
||||||
|
<?php echo $product->get_image('thumbnail'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="item-details">
|
||||||
|
<span class="item-name"><?php echo esc_html($product->get_name()); ?></span>
|
||||||
|
<span class="item-quantity">× <?php echo esc_html($item['quantity']); ?></span>
|
||||||
|
<span class="item-price"><?php echo wc_price($item['price']); ?></span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="subscription-details-info">
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label"><?php _e('订阅金额:', 'yoone-subscriptions'); ?></span>
|
||||||
|
<span class="detail-value"><?php echo wc_price($subscription->total); ?></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label"><?php _e('计费周期:', 'yoone-subscriptions'); ?></span>
|
||||||
|
<span class="detail-value">
|
||||||
|
<?php echo esc_html($this->format_billing_period($subscription->billing_period, $subscription->billing_interval)); ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($subscription->status === 'active' && $next_payment): ?>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label"><?php _e('下次付款:', 'yoone-subscriptions'); ?></span>
|
||||||
|
<span class="detail-value">
|
||||||
|
<?php echo date_i18n(get_option('date_format'), strtotime($next_payment)); ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label"><?php _e('创建日期:', 'yoone-subscriptions'); ?></span>
|
||||||
|
<span class="detail-value">
|
||||||
|
<?php echo date_i18n(get_option('date_format'), strtotime($subscription->created_at)); ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($subscription->payment_method): ?>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label"><?php _e('支付方式:', 'yoone-subscriptions'); ?></span>
|
||||||
|
<span class="detail-value">
|
||||||
|
<?php echo esc_html($this->get_payment_method_title($subscription->payment_method)); ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($subscription->status === 'active'): ?>
|
||||||
|
<div class="subscription-management">
|
||||||
|
<h4><?php _e('订阅管理', 'yoone-subscriptions'); ?></h4>
|
||||||
|
|
||||||
|
<div class="management-actions">
|
||||||
|
<a href="<?php echo esc_url($this->get_subscription_action_url($subscription->id, 'change_payment')); ?>"
|
||||||
|
class="button change-payment">
|
||||||
|
<?php _e('更改支付方式', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="<?php echo esc_url($this->get_subscription_action_url($subscription->id, 'change_address')); ?>"
|
||||||
|
class="button change-address">
|
||||||
|
<?php _e('更改配送地址', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="<?php echo esc_url($this->get_subscription_action_url($subscription->id, 'skip_next')); ?>"
|
||||||
|
class="button skip-delivery">
|
||||||
|
<?php _e('跳过下次配送', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="subscriptions-pagination">
|
||||||
|
<?php
|
||||||
|
// 这里可以添加分页逻辑
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.yoone-my-subscriptions {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-subscriptions {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-item {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-id h3 {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-status {
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-status.status-active {
|
||||||
|
background: #4caf50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-status.status-paused {
|
||||||
|
background: #ff9800;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-status.status-cancelled {
|
||||||
|
background: #f44336;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-status.status-expired {
|
||||||
|
background: #9e9e9e;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-actions .button {
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-products h4,
|
||||||
|
.subscription-details-info h4 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-items {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-item-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-item-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-item-row .item-image {
|
||||||
|
flex: 0 0 50px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-item-row .item-details {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-item-row .item-name {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-item-row .item-quantity,
|
||||||
|
.subscription-item-row .item-price {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-management {
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-management h4 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.management-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.management-actions .button {
|
||||||
|
padding: 8px 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.subscription-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.management-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.management-actions .button {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,345 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 订阅选项模板
|
||||||
|
*
|
||||||
|
* 在产品页面显示订阅选项
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $product;
|
||||||
|
|
||||||
|
// 检查产品是否支持订阅
|
||||||
|
$subscription_enabled = get_post_meta($product->get_id(), '_yoone_subscription_enabled', true);
|
||||||
|
|
||||||
|
if (!$subscription_enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订阅设置
|
||||||
|
$subscription_periods = get_post_meta($product->get_id(), '_yoone_subscription_periods', true);
|
||||||
|
$subscription_discount = get_post_meta($product->get_id(), '_yoone_subscription_discount', true);
|
||||||
|
$subscription_discount_type = get_post_meta($product->get_id(), '_yoone_subscription_discount_type', true);
|
||||||
|
|
||||||
|
if (empty($subscription_periods)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="yoone-subscription-options" data-product-id="<?php echo esc_attr($product->get_id()); ?>">
|
||||||
|
<h3><?php _e('订阅选项', 'yoone-subscriptions'); ?></h3>
|
||||||
|
|
||||||
|
<div class="subscription-purchase-type">
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="purchase_type" value="one_time" checked>
|
||||||
|
<?php _e('单次购买', 'yoone-subscriptions'); ?>
|
||||||
|
<span class="price"><?php echo $product->get_price_html(); ?></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="purchase_type" value="subscription">
|
||||||
|
<?php _e('订阅购买', 'yoone-subscriptions'); ?>
|
||||||
|
<?php if ($subscription_discount > 0): ?>
|
||||||
|
<span class="subscription-discount">
|
||||||
|
<?php if ($subscription_discount_type === 'percentage'): ?>
|
||||||
|
<?php printf(__('(优惠 %s%%)', 'yoone-subscriptions'), $subscription_discount); ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php printf(__('(优惠 %s)', 'yoone-subscriptions'), wc_price($subscription_discount)); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="subscription-periods" style="display: none;">
|
||||||
|
<label for="subscription_period"><?php _e('选择订阅周期:', 'yoone-subscriptions'); ?></label>
|
||||||
|
<select name="subscription_period" id="subscription_period">
|
||||||
|
<?php foreach ($subscription_periods as $period): ?>
|
||||||
|
<?php
|
||||||
|
$period_label = $this->format_subscription_period($period['period'], $period['interval']);
|
||||||
|
$discounted_price = $this->calculate_subscription_price($product->get_price(), $subscription_discount, $subscription_discount_type);
|
||||||
|
?>
|
||||||
|
<option value="<?php echo esc_attr($period['period'] . '_' . $period['interval']); ?>"
|
||||||
|
data-period="<?php echo esc_attr($period['period']); ?>"
|
||||||
|
data-interval="<?php echo esc_attr($period['interval']); ?>"
|
||||||
|
data-price="<?php echo esc_attr($discounted_price); ?>">
|
||||||
|
<?php echo esc_html($period_label); ?> - <?php echo wc_price($discounted_price); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="subscription-info" style="display: none;">
|
||||||
|
<div class="subscription-benefits">
|
||||||
|
<h4><?php _e('订阅优势', 'yoone-subscriptions'); ?></h4>
|
||||||
|
<ul>
|
||||||
|
<?php if ($subscription_discount > 0): ?>
|
||||||
|
<li><?php _e('享受订阅优惠价格', 'yoone-subscriptions'); ?></li>
|
||||||
|
<?php endif; ?>
|
||||||
|
<li><?php _e('自动续订,无需重复下单', 'yoone-subscriptions'); ?></li>
|
||||||
|
<li><?php _e('随时可以暂停或取消', 'yoone-subscriptions'); ?></li>
|
||||||
|
<li><?php _e('优先享受新品和促销', 'yoone-subscriptions'); ?></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="subscription-details">
|
||||||
|
<p class="next-delivery">
|
||||||
|
<strong><?php _e('下次配送:', 'yoone-subscriptions'); ?></strong>
|
||||||
|
<span id="next-delivery-date">-</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="subscription-total">
|
||||||
|
<strong><?php _e('订阅价格:', 'yoone-subscriptions'); ?></strong>
|
||||||
|
<span id="subscription-price"><?php echo wc_price($product->get_price()); ?></span>
|
||||||
|
<span class="billing-cycle" id="billing-cycle"></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="subscription-management">
|
||||||
|
<p class="management-info">
|
||||||
|
<?php _e('订阅后,您可以在', 'yoone-subscriptions'); ?>
|
||||||
|
<a href="<?php echo esc_url(wc_get_account_endpoint_url('subscriptions')); ?>">
|
||||||
|
<?php _e('我的账户 > 订阅管理', 'yoone-subscriptions'); ?>
|
||||||
|
</a>
|
||||||
|
<?php _e('中管理您的订阅。', 'yoone-subscriptions'); ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="subscription-trial" style="display: none;">
|
||||||
|
<?php
|
||||||
|
$trial_period = get_post_meta($product->get_id(), '_yoone_subscription_trial_period', true);
|
||||||
|
$trial_length = get_post_meta($product->get_id(), '_yoone_subscription_trial_length', true);
|
||||||
|
|
||||||
|
if ($trial_length > 0 && $trial_period):
|
||||||
|
?>
|
||||||
|
<div class="trial-info">
|
||||||
|
<h4><?php _e('免费试用', 'yoone-subscriptions'); ?></h4>
|
||||||
|
<p>
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
__('享受 %d %s 免费试用期', 'yoone-subscriptions'),
|
||||||
|
$trial_length,
|
||||||
|
$this->get_period_label($trial_period)
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
var $purchaseType = $('input[name="purchase_type"]');
|
||||||
|
var $subscriptionPeriods = $('.subscription-periods');
|
||||||
|
var $subscriptionInfo = $('.subscription-info');
|
||||||
|
var $subscriptionTrial = $('.subscription-trial');
|
||||||
|
var $subscriptionPeriodSelect = $('#subscription_period');
|
||||||
|
|
||||||
|
// 切换购买类型
|
||||||
|
$purchaseType.on('change', function() {
|
||||||
|
var purchaseType = $(this).val();
|
||||||
|
|
||||||
|
if (purchaseType === 'subscription') {
|
||||||
|
$subscriptionPeriods.show();
|
||||||
|
$subscriptionInfo.show();
|
||||||
|
$subscriptionTrial.show();
|
||||||
|
updateSubscriptionDetails();
|
||||||
|
} else {
|
||||||
|
$subscriptionPeriods.hide();
|
||||||
|
$subscriptionInfo.hide();
|
||||||
|
$subscriptionTrial.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 切换订阅周期
|
||||||
|
$subscriptionPeriodSelect.on('change', function() {
|
||||||
|
updateSubscriptionDetails();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新订阅详情
|
||||||
|
function updateSubscriptionDetails() {
|
||||||
|
var $selectedOption = $subscriptionPeriodSelect.find(':selected');
|
||||||
|
var period = $selectedOption.data('period');
|
||||||
|
var interval = $selectedOption.data('interval');
|
||||||
|
var price = $selectedOption.data('price');
|
||||||
|
|
||||||
|
// 更新价格显示
|
||||||
|
$('#subscription-price').html(formatPrice(price));
|
||||||
|
|
||||||
|
// 更新计费周期
|
||||||
|
var billingCycle = formatBillingCycle(period, interval);
|
||||||
|
$('#billing-cycle').text(billingCycle);
|
||||||
|
|
||||||
|
// 计算下次配送日期
|
||||||
|
var nextDate = calculateNextDeliveryDate(period, interval);
|
||||||
|
$('#next-delivery-date').text(nextDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化价格
|
||||||
|
function formatPrice(price) {
|
||||||
|
return '<?php echo get_woocommerce_currency_symbol(); ?>' + parseFloat(price).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化计费周期
|
||||||
|
function formatBillingCycle(period, interval) {
|
||||||
|
var periods = {
|
||||||
|
'day': '<?php _e('天', 'yoone-subscriptions'); ?>',
|
||||||
|
'week': '<?php _e('周', 'yoone-subscriptions'); ?>',
|
||||||
|
'month': '<?php _e('月', 'yoone-subscriptions'); ?>',
|
||||||
|
'year': '<?php _e('年', 'yoone-subscriptions'); ?>'
|
||||||
|
};
|
||||||
|
|
||||||
|
var periodName = periods[period] || period;
|
||||||
|
|
||||||
|
if (interval > 1) {
|
||||||
|
return '/ <?php _e('每', 'yoone-subscriptions'); ?> ' + interval + ' ' + periodName;
|
||||||
|
} else {
|
||||||
|
return '/ <?php _e('每', 'yoone-subscriptions'); ?>' + periodName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算下次配送日期
|
||||||
|
function calculateNextDeliveryDate(period, interval) {
|
||||||
|
var now = new Date();
|
||||||
|
var nextDate = new Date(now);
|
||||||
|
|
||||||
|
switch (period) {
|
||||||
|
case 'day':
|
||||||
|
nextDate.setDate(now.getDate() + interval);
|
||||||
|
break;
|
||||||
|
case 'week':
|
||||||
|
nextDate.setDate(now.getDate() + (interval * 7));
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
nextDate.setMonth(now.getMonth() + interval);
|
||||||
|
break;
|
||||||
|
case 'year':
|
||||||
|
nextDate.setFullYear(now.getFullYear() + interval);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextDate.toLocaleDateString('zh-CN');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.yoone-subscription-options {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yoone-subscription-options h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-purchase-type label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-purchase-type label:hover {
|
||||||
|
border-color: #007cba;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-purchase-type input[type="radio"]:checked + label,
|
||||||
|
.subscription-purchase-type label:has(input[type="radio"]:checked) {
|
||||||
|
border-color: #007cba;
|
||||||
|
background: #f0f8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-discount {
|
||||||
|
color: #e74c3c;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-periods {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-periods label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-periods select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-info {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-benefits ul {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-benefits li {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-details {
|
||||||
|
margin: 15px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-details p {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-management {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.management-info {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-trial {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #e8f5e8;
|
||||||
|
border: 1px solid #4caf50;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-info h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-info p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: #2e7d32;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,760 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Yoone订阅插件测试套件
|
||||||
|
*
|
||||||
|
* 用于测试插件的核心功能和集成
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试套件类
|
||||||
|
*/
|
||||||
|
class Yoone_Test_Suite {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试结果
|
||||||
|
*/
|
||||||
|
private $test_results = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
add_action('admin_menu', array($this, 'add_test_menu'));
|
||||||
|
add_action('wp_ajax_yoone_run_tests', array($this, 'ajax_run_tests'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加测试菜单
|
||||||
|
*/
|
||||||
|
public function add_test_menu() {
|
||||||
|
add_submenu_page(
|
||||||
|
'yoone-subscriptions',
|
||||||
|
__('功能测试', 'yoone-subscriptions'),
|
||||||
|
__('测试', 'yoone-subscriptions'),
|
||||||
|
'manage_options',
|
||||||
|
'yoone-tests',
|
||||||
|
array($this, 'display_test_page')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示测试页面
|
||||||
|
*/
|
||||||
|
public function display_test_page() {
|
||||||
|
?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php _e('Yoone订阅插件功能测试', 'yoone-subscriptions'); ?></h1>
|
||||||
|
|
||||||
|
<div class="test-controls">
|
||||||
|
<button type="button" class="button button-primary" id="run-all-tests">
|
||||||
|
<?php _e('运行所有测试', 'yoone-subscriptions'); ?>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="button" id="run-subscription-tests">
|
||||||
|
<?php _e('订阅测试', 'yoone-subscriptions'); ?>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="button" id="run-payment-tests">
|
||||||
|
<?php _e('支付测试', 'yoone-subscriptions'); ?>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="button" id="run-bundle-tests">
|
||||||
|
<?php _e('捆绑测试', 'yoone-subscriptions'); ?>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="button" id="run-cron-tests">
|
||||||
|
<?php _e('定时任务测试', 'yoone-subscriptions'); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="test-progress" style="display: none;">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<p class="progress-text">准备测试...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="test-results">
|
||||||
|
<h2><?php _e('测试结果', 'yoone-subscriptions'); ?></h2>
|
||||||
|
<div class="test-summary">
|
||||||
|
<span class="test-count">总计: <strong>0</strong></span>
|
||||||
|
<span class="test-passed">通过: <strong>0</strong></span>
|
||||||
|
<span class="test-failed">失败: <strong>0</strong></span>
|
||||||
|
<span class="test-skipped">跳过: <strong>0</strong></span>
|
||||||
|
</div>
|
||||||
|
<div class="test-details"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.test-controls {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-controls .button {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 20px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: #007cba;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
text-align: center;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-summary {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-summary span {
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-count { background: #e9ecef; }
|
||||||
|
.test-passed { background: #d4edda; color: #155724; }
|
||||||
|
.test-failed { background: #f8d7da; color: #721c24; }
|
||||||
|
.test-skipped { background: #fff3cd; color: #856404; }
|
||||||
|
|
||||||
|
.test-item {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-item.passed {
|
||||||
|
border-left: 4px solid #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-item.failed {
|
||||||
|
border-left: 4px solid #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-item.skipped {
|
||||||
|
border-left: 4px solid #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-name {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-description {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-result {
|
||||||
|
font-family: monospace;
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-error {
|
||||||
|
color: #dc3545;
|
||||||
|
background: #f8d7da;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
var testSuite = {
|
||||||
|
init: function() {
|
||||||
|
this.bindEvents();
|
||||||
|
},
|
||||||
|
|
||||||
|
bindEvents: function() {
|
||||||
|
$('#run-all-tests').on('click', function() {
|
||||||
|
testSuite.runTests('all');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#run-subscription-tests').on('click', function() {
|
||||||
|
testSuite.runTests('subscription');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#run-payment-tests').on('click', function() {
|
||||||
|
testSuite.runTests('payment');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#run-bundle-tests').on('click', function() {
|
||||||
|
testSuite.runTests('bundle');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#run-cron-tests').on('click', function() {
|
||||||
|
testSuite.runTests('cron');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
runTests: function(suite) {
|
||||||
|
$('#test-progress').show();
|
||||||
|
$('.test-controls .button').prop('disabled', true);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: ajaxurl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'yoone_run_tests',
|
||||||
|
suite: suite,
|
||||||
|
nonce: '<?php echo wp_create_nonce('yoone_test_nonce'); ?>'
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
testSuite.displayResults(response.data);
|
||||||
|
} else {
|
||||||
|
alert('测试运行失败: ' + response.data.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
alert('测试请求失败');
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
$('#test-progress').hide();
|
||||||
|
$('.test-controls .button').prop('disabled', false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
displayResults: function(results) {
|
||||||
|
var summary = results.summary;
|
||||||
|
var tests = results.tests;
|
||||||
|
|
||||||
|
// 更新摘要
|
||||||
|
$('.test-count strong').text(summary.total);
|
||||||
|
$('.test-passed strong').text(summary.passed);
|
||||||
|
$('.test-failed strong').text(summary.failed);
|
||||||
|
$('.test-skipped strong').text(summary.skipped);
|
||||||
|
|
||||||
|
// 显示测试详情
|
||||||
|
var detailsHtml = '';
|
||||||
|
tests.forEach(function(test) {
|
||||||
|
detailsHtml += '<div class="test-item ' + test.status + '">';
|
||||||
|
detailsHtml += '<div class="test-name">' + test.name + '</div>';
|
||||||
|
detailsHtml += '<div class="test-description">' + test.description + '</div>';
|
||||||
|
|
||||||
|
if (test.result) {
|
||||||
|
detailsHtml += '<div class="test-result">' + test.result + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (test.error) {
|
||||||
|
detailsHtml += '<div class="test-error">' + test.error + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
detailsHtml += '</div>';
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.test-details').html(detailsHtml);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
testSuite.init();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX运行测试
|
||||||
|
*/
|
||||||
|
public function ajax_run_tests() {
|
||||||
|
check_ajax_referer('yoone_test_nonce', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_send_json_error(array('message' => __('权限不足', 'yoone-subscriptions')));
|
||||||
|
}
|
||||||
|
|
||||||
|
$suite = sanitize_text_field($_POST['suite']);
|
||||||
|
|
||||||
|
$results = $this->run_test_suite($suite);
|
||||||
|
|
||||||
|
wp_send_json_success($results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行测试套件
|
||||||
|
*
|
||||||
|
* @param string $suite 测试套件名称
|
||||||
|
* @return array 测试结果
|
||||||
|
*/
|
||||||
|
public function run_test_suite($suite = 'all') {
|
||||||
|
$this->test_results = array();
|
||||||
|
|
||||||
|
switch ($suite) {
|
||||||
|
case 'subscription':
|
||||||
|
$this->run_subscription_tests();
|
||||||
|
break;
|
||||||
|
case 'payment':
|
||||||
|
$this->run_payment_tests();
|
||||||
|
break;
|
||||||
|
case 'bundle':
|
||||||
|
$this->run_bundle_tests();
|
||||||
|
break;
|
||||||
|
case 'cron':
|
||||||
|
$this->run_cron_tests();
|
||||||
|
break;
|
||||||
|
case 'all':
|
||||||
|
default:
|
||||||
|
$this->run_subscription_tests();
|
||||||
|
$this->run_payment_tests();
|
||||||
|
$this->run_bundle_tests();
|
||||||
|
$this->run_cron_tests();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->compile_results();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行订阅测试
|
||||||
|
*/
|
||||||
|
private function run_subscription_tests() {
|
||||||
|
// 运行完整的订阅流程测试
|
||||||
|
$this->add_test_result(
|
||||||
|
'subscription_flow_test',
|
||||||
|
'订阅完整流程测试',
|
||||||
|
'测试订阅的完整生命周期流程',
|
||||||
|
function() {
|
||||||
|
if (!class_exists('Yoone_Subscription_Flow_Test')) {
|
||||||
|
throw new Exception('Yoone_Subscription_Flow_Test类不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$flow_test = new Yoone_Subscription_Flow_Test();
|
||||||
|
$flow_results = $flow_test->run_full_test();
|
||||||
|
|
||||||
|
// 分析流程测试结果
|
||||||
|
$error_count = 0;
|
||||||
|
$warning_count = 0;
|
||||||
|
|
||||||
|
foreach ($flow_results as $result) {
|
||||||
|
if ($result['type'] === 'error') {
|
||||||
|
$error_count++;
|
||||||
|
} elseif ($result['type'] === 'warning') {
|
||||||
|
$warning_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error_count > 0) {
|
||||||
|
throw new Exception("流程测试发现 {$error_count} 个错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $warning_count === 0 ? true : "通过但有 {$warning_count} 个警告";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测试订阅类是否存在
|
||||||
|
$this->add_test_result(
|
||||||
|
'subscription_class_exists',
|
||||||
|
'订阅类存在性测试',
|
||||||
|
'检查Yoone_Subscription类是否正确加载',
|
||||||
|
function() {
|
||||||
|
return class_exists('Yoone_Subscription');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测试订阅创建
|
||||||
|
$this->add_test_result(
|
||||||
|
'subscription_creation',
|
||||||
|
'订阅创建测试',
|
||||||
|
'测试创建新订阅的功能',
|
||||||
|
function() {
|
||||||
|
if (!class_exists('Yoone_Subscription')) {
|
||||||
|
throw new Exception('Yoone_Subscription类不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscription = new Yoone_Subscription();
|
||||||
|
$subscription->set_customer_id(1);
|
||||||
|
$subscription->set_status('active');
|
||||||
|
$subscription->set_billing_period('month');
|
||||||
|
$subscription->set_billing_interval(1);
|
||||||
|
$subscription->set_start_date(current_time('mysql'));
|
||||||
|
|
||||||
|
// 添加订阅商品
|
||||||
|
$subscription->add_item(array(
|
||||||
|
'product_id' => 100,
|
||||||
|
'quantity' => 1,
|
||||||
|
'line_total' => 29.99
|
||||||
|
));
|
||||||
|
|
||||||
|
// 验证属性设置
|
||||||
|
return $subscription->get_customer_id() === 1 &&
|
||||||
|
$subscription->get_status() === 'active' &&
|
||||||
|
count($subscription->get_items()) === 1;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测试订阅状态管理
|
||||||
|
$this->add_test_result(
|
||||||
|
'subscription_status_management',
|
||||||
|
'订阅状态管理测试',
|
||||||
|
'测试订阅状态的变更功能',
|
||||||
|
function() {
|
||||||
|
if (!class_exists('Yoone_Subscription')) {
|
||||||
|
throw new Exception('Yoone_Subscription类不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscription = new Yoone_Subscription();
|
||||||
|
$subscription->set_status('active');
|
||||||
|
|
||||||
|
// 测试暂停
|
||||||
|
$subscription->pause();
|
||||||
|
if ($subscription->get_status() !== 'paused') {
|
||||||
|
throw new Exception('暂停功能失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试恢复
|
||||||
|
$subscription->resume();
|
||||||
|
if ($subscription->get_status() !== 'active') {
|
||||||
|
throw new Exception('恢复功能失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试取消
|
||||||
|
$subscription->cancel();
|
||||||
|
if ($subscription->get_status() !== 'cancelled') {
|
||||||
|
throw new Exception('取消功能失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测试下次付款日期计算
|
||||||
|
$this->add_test_result(
|
||||||
|
'next_payment_calculation',
|
||||||
|
'下次付款日期计算测试',
|
||||||
|
'测试下次付款日期的计算逻辑',
|
||||||
|
function() {
|
||||||
|
if (!class_exists('Yoone_Subscription')) {
|
||||||
|
throw new Exception('Yoone_Subscription类不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscription = new Yoone_Subscription();
|
||||||
|
$subscription->set_billing_period('month');
|
||||||
|
$subscription->set_billing_interval(1);
|
||||||
|
$subscription->set_start_date('2024-01-01 00:00:00');
|
||||||
|
|
||||||
|
$next_payment = $subscription->calculate_next_payment_date();
|
||||||
|
$expected = '2024-02-01 00:00:00';
|
||||||
|
|
||||||
|
return $next_payment === $expected;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行支付测试
|
||||||
|
*/
|
||||||
|
private function run_payment_tests() {
|
||||||
|
// 测试支付网关接口
|
||||||
|
$this->add_test_result(
|
||||||
|
'payment_gateway_interface',
|
||||||
|
'支付网关接口测试',
|
||||||
|
'检查支付网关接口是否正确定义',
|
||||||
|
function() {
|
||||||
|
return interface_exists('Yoone_Payment_Gateway_Interface');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测试Moneris网关类
|
||||||
|
$this->add_test_result(
|
||||||
|
'moneris_gateway_class',
|
||||||
|
'Moneris网关类测试',
|
||||||
|
'检查Moneris支付网关类是否存在',
|
||||||
|
function() {
|
||||||
|
return class_exists('Yoone_Moneris_Gateway');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测试支付令牌管理
|
||||||
|
$this->add_test_result(
|
||||||
|
'payment_token_management',
|
||||||
|
'支付令牌管理测试',
|
||||||
|
'测试支付令牌的创建和管理',
|
||||||
|
function() {
|
||||||
|
if (!class_exists('Yoone_Payment_Token')) {
|
||||||
|
throw new Exception('Yoone_Payment_Token类不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = new Yoone_Payment_Token();
|
||||||
|
$token->set_customer_id(1);
|
||||||
|
$token->set_gateway_id('moneris');
|
||||||
|
$token->set_token('test_token_123');
|
||||||
|
$token->set_card_type('visa');
|
||||||
|
$token->set_last_four('1234');
|
||||||
|
|
||||||
|
return $token->get_customer_id() === 1 &&
|
||||||
|
$token->get_token() === 'test_token_123';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测试支付处理流程
|
||||||
|
$this->add_test_result(
|
||||||
|
'payment_processing_flow',
|
||||||
|
'支付处理流程测试',
|
||||||
|
'测试支付处理的基本流程',
|
||||||
|
function() {
|
||||||
|
// 这里只测试类和方法的存在性,不进行实际支付
|
||||||
|
if (!class_exists('Yoone_Moneris_Gateway')) {
|
||||||
|
throw new Exception('Moneris网关类不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$gateway = new Yoone_Moneris_Gateway();
|
||||||
|
|
||||||
|
// 检查必要方法是否存在
|
||||||
|
$required_methods = array(
|
||||||
|
'process_payment',
|
||||||
|
'process_subscription_payment',
|
||||||
|
'create_payment_token',
|
||||||
|
'delete_payment_token'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($required_methods as $method) {
|
||||||
|
if (!method_exists($gateway, $method)) {
|
||||||
|
throw new Exception("方法 {$method} 不存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行捆绑测试
|
||||||
|
*/
|
||||||
|
private function run_bundle_tests() {
|
||||||
|
// 测试捆绑类
|
||||||
|
$this->add_test_result(
|
||||||
|
'bundle_class_exists',
|
||||||
|
'捆绑类存在性测试',
|
||||||
|
'检查Yoone_Bundle类是否正确加载',
|
||||||
|
function() {
|
||||||
|
return class_exists('Yoone_Bundle');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测试价格计算
|
||||||
|
$this->add_test_result(
|
||||||
|
'bundle_price_calculation',
|
||||||
|
'捆绑价格计算测试',
|
||||||
|
'测试捆绑产品的价格计算功能',
|
||||||
|
function() {
|
||||||
|
if (!class_exists('Yoone_Bundle')) {
|
||||||
|
throw new Exception('Yoone_Bundle类不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$bundle = new Yoone_Bundle();
|
||||||
|
$bundle->set_discount_type('percentage');
|
||||||
|
$bundle->set_discount_value(10); // 10%折扣
|
||||||
|
|
||||||
|
// 添加测试商品
|
||||||
|
$bundle->add_item(array(
|
||||||
|
'product_id' => 1,
|
||||||
|
'quantity' => 2,
|
||||||
|
'price' => 100
|
||||||
|
));
|
||||||
|
|
||||||
|
$bundle->add_item(array(
|
||||||
|
'product_id' => 2,
|
||||||
|
'quantity' => 1,
|
||||||
|
'price' => 50
|
||||||
|
));
|
||||||
|
|
||||||
|
// 原价应该是 (100 * 2) + (50 * 1) = 250
|
||||||
|
// 折扣后应该是 250 * 0.9 = 225
|
||||||
|
$calculated_price = $bundle->calculate_price();
|
||||||
|
|
||||||
|
return abs($calculated_price - 225) < 0.01; // 允许小数点误差
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测试数量验证
|
||||||
|
$this->add_test_result(
|
||||||
|
'bundle_quantity_validation',
|
||||||
|
'捆绑数量验证测试',
|
||||||
|
'测试捆绑产品的数量验证功能',
|
||||||
|
function() {
|
||||||
|
if (!class_exists('Yoone_Bundle')) {
|
||||||
|
throw new Exception('Yoone_Bundle类不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$bundle = new Yoone_Bundle();
|
||||||
|
$bundle->set_min_quantity(2);
|
||||||
|
$bundle->set_max_quantity(10);
|
||||||
|
|
||||||
|
// 测试有效数量
|
||||||
|
if (!$bundle->validate_quantity(5)) {
|
||||||
|
throw new Exception('有效数量验证失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试无效数量(太少)
|
||||||
|
if ($bundle->validate_quantity(1)) {
|
||||||
|
throw new Exception('最小数量验证失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试无效数量(太多)
|
||||||
|
if ($bundle->validate_quantity(15)) {
|
||||||
|
throw new Exception('最大数量验证失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行定时任务测试
|
||||||
|
*/
|
||||||
|
private function run_cron_tests() {
|
||||||
|
// 测试定时任务类
|
||||||
|
$this->add_test_result(
|
||||||
|
'cron_class_exists',
|
||||||
|
'定时任务类存在性测试',
|
||||||
|
'检查Yoone_Cron类是否正确加载',
|
||||||
|
function() {
|
||||||
|
return class_exists('Yoone_Cron');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测试定时任务调度
|
||||||
|
$this->add_test_result(
|
||||||
|
'cron_scheduling',
|
||||||
|
'定时任务调度测试',
|
||||||
|
'测试定时任务的调度功能',
|
||||||
|
function() {
|
||||||
|
if (!class_exists('Yoone_Cron')) {
|
||||||
|
throw new Exception('Yoone_Cron类不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查WordPress定时任务是否已注册
|
||||||
|
$scheduled_events = wp_get_scheduled_event('yoone_process_subscription_renewals');
|
||||||
|
|
||||||
|
return $scheduled_events !== false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测试日志清理功能
|
||||||
|
$this->add_test_result(
|
||||||
|
'log_cleanup_function',
|
||||||
|
'日志清理功能测试',
|
||||||
|
'测试日志清理功能是否正常',
|
||||||
|
function() {
|
||||||
|
if (!class_exists('Yoone_Logger')) {
|
||||||
|
throw new Exception('Yoone_Logger类不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查清理方法是否存在
|
||||||
|
return method_exists('Yoone_Logger', 'cleanup_old_logs');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加测试结果
|
||||||
|
*
|
||||||
|
* @param string $id 测试ID
|
||||||
|
* @param string $name 测试名称
|
||||||
|
* @param string $description 测试描述
|
||||||
|
* @param callable $test_function 测试函数
|
||||||
|
*/
|
||||||
|
private function add_test_result($id, $name, $description, $test_function) {
|
||||||
|
$result = array(
|
||||||
|
'id' => $id,
|
||||||
|
'name' => $name,
|
||||||
|
'description' => $description,
|
||||||
|
'status' => 'passed',
|
||||||
|
'result' => '',
|
||||||
|
'error' => '',
|
||||||
|
'execution_time' => 0
|
||||||
|
);
|
||||||
|
|
||||||
|
$start_time = microtime(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$test_result = call_user_func($test_function);
|
||||||
|
|
||||||
|
if ($test_result === true) {
|
||||||
|
$result['result'] = '测试通过';
|
||||||
|
} elseif ($test_result === false) {
|
||||||
|
$result['status'] = 'failed';
|
||||||
|
$result['result'] = '测试失败';
|
||||||
|
} else {
|
||||||
|
$result['result'] = '测试通过: ' . $test_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$result['status'] = 'failed';
|
||||||
|
$result['error'] = $e->getMessage();
|
||||||
|
} catch (Error $e) {
|
||||||
|
$result['status'] = 'failed';
|
||||||
|
$result['error'] = $e->getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
$result['execution_time'] = round((microtime(true) - $start_time) * 1000, 2);
|
||||||
|
|
||||||
|
$this->test_results[] = $result;
|
||||||
|
|
||||||
|
// 记录测试日志
|
||||||
|
Yoone_Logger::info("测试执行: {$name}", array(
|
||||||
|
'test_id' => $id,
|
||||||
|
'status' => $result['status'],
|
||||||
|
'execution_time' => $result['execution_time']
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编译测试结果
|
||||||
|
*
|
||||||
|
* @return array 编译后的结果
|
||||||
|
*/
|
||||||
|
private function compile_results() {
|
||||||
|
$summary = array(
|
||||||
|
'total' => count($this->test_results),
|
||||||
|
'passed' => 0,
|
||||||
|
'failed' => 0,
|
||||||
|
'skipped' => 0
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($this->test_results as $result) {
|
||||||
|
$summary[$result['status']]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'summary' => $summary,
|
||||||
|
'tests' => $this->test_results
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化测试套件
|
||||||
|
if (is_admin()) {
|
||||||
|
new Yoone_Test_Suite();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,360 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 捆绑产品测试脚本
|
||||||
|
*
|
||||||
|
* @package Yoone_Subscriptions
|
||||||
|
* @subpackage Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 捆绑产品测试类
|
||||||
|
*/
|
||||||
|
class Yoone_Bundle_Products_Test {
|
||||||
|
|
||||||
|
private $test_results = array();
|
||||||
|
private $bundle_product_id;
|
||||||
|
private $child_product_ids = array();
|
||||||
|
private $customer_id;
|
||||||
|
private $subscription_id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行完整的捆绑产品测试
|
||||||
|
*/
|
||||||
|
public function run_full_test() {
|
||||||
|
$this->log_test('开始捆绑产品测试');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 环境检查
|
||||||
|
$this->test_environment();
|
||||||
|
|
||||||
|
// 2. 创建测试数据
|
||||||
|
$this->setup_test_data();
|
||||||
|
|
||||||
|
// 3. 测试捆绑产品创建
|
||||||
|
$this->test_bundle_product_creation();
|
||||||
|
|
||||||
|
// 4. 测试价格计算
|
||||||
|
$this->test_price_calculation();
|
||||||
|
|
||||||
|
// 5. 测试捆绑产品订阅
|
||||||
|
$this->test_bundle_subscription();
|
||||||
|
|
||||||
|
// 6. 测试子产品管理
|
||||||
|
$this->test_child_product_management();
|
||||||
|
|
||||||
|
// 7. 测试库存管理
|
||||||
|
$this->test_inventory_management();
|
||||||
|
|
||||||
|
// 8. 清理测试数据
|
||||||
|
$this->cleanup_test_data();
|
||||||
|
|
||||||
|
$this->log_test('捆绑产品测试完成');
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->log_test('测试失败: ' . $e->getMessage(), 'error');
|
||||||
|
$this->cleanup_test_data();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->test_results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 环境检查
|
||||||
|
*/
|
||||||
|
private function test_environment() {
|
||||||
|
$this->log_test('检查捆绑产品环境');
|
||||||
|
|
||||||
|
// 检查必要的类是否存在
|
||||||
|
$required_classes = array(
|
||||||
|
'Yoone_Bundle_Product',
|
||||||
|
'Yoone_Subscription'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($required_classes as $class) {
|
||||||
|
if (!class_exists($class)) {
|
||||||
|
throw new Exception("必需的类 {$class} 不存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查WooCommerce是否激活
|
||||||
|
if (!class_exists('WooCommerce')) {
|
||||||
|
throw new Exception('WooCommerce未激活');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test('捆绑产品环境检查通过', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建测试数据
|
||||||
|
*/
|
||||||
|
private function setup_test_data() {
|
||||||
|
$this->log_test('创建捆绑产品测试数据');
|
||||||
|
|
||||||
|
// 创建测试客户
|
||||||
|
$this->customer_id = Yoone_Test_Config::create_test_customer();
|
||||||
|
if (!$this->customer_id) {
|
||||||
|
throw new Exception('创建测试客户失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建子产品
|
||||||
|
$child_products = Yoone_Test_Config::get_bundle_products();
|
||||||
|
foreach ($child_products as $product_data) {
|
||||||
|
$product_id = Yoone_Test_Config::create_test_product($product_data);
|
||||||
|
if ($product_id) {
|
||||||
|
$this->child_product_ids[] = $product_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($this->child_product_ids)) {
|
||||||
|
throw new Exception('创建子产品失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test("捆绑产品测试数据创建成功 - 客户ID: {$this->customer_id}, 子产品数量: " . count($this->child_product_ids), 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试捆绑产品创建
|
||||||
|
*/
|
||||||
|
private function test_bundle_product_creation() {
|
||||||
|
$this->log_test('测试捆绑产品创建');
|
||||||
|
|
||||||
|
// 创建捆绑产品
|
||||||
|
$bundle_data = array(
|
||||||
|
'name' => '测试捆绑产品',
|
||||||
|
'type' => 'yoone_bundle',
|
||||||
|
'regular_price' => 99.99,
|
||||||
|
'subscription_price' => 89.99,
|
||||||
|
'subscription_period' => 'month',
|
||||||
|
'subscription_period_interval' => 1,
|
||||||
|
'status' => 'publish'
|
||||||
|
);
|
||||||
|
|
||||||
|
$product = new WC_Product();
|
||||||
|
$product->set_name($bundle_data['name']);
|
||||||
|
$product->set_regular_price($bundle_data['regular_price']);
|
||||||
|
$product->set_status($bundle_data['status']);
|
||||||
|
|
||||||
|
$this->bundle_product_id = $product->save();
|
||||||
|
|
||||||
|
if (!$this->bundle_product_id) {
|
||||||
|
throw new Exception('捆绑产品创建失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置产品类型为捆绑产品
|
||||||
|
wp_set_object_terms($this->bundle_product_id, $bundle_data['type'], 'product_type');
|
||||||
|
|
||||||
|
// 设置捆绑产品属性
|
||||||
|
update_post_meta($this->bundle_product_id, '_yoone_subscription_price', $bundle_data['subscription_price']);
|
||||||
|
update_post_meta($this->bundle_product_id, '_yoone_subscription_period', $bundle_data['subscription_period']);
|
||||||
|
update_post_meta($this->bundle_product_id, '_yoone_subscription_period_interval', $bundle_data['subscription_period_interval']);
|
||||||
|
|
||||||
|
// 添加子产品
|
||||||
|
$bundle_items = array();
|
||||||
|
foreach ($this->child_product_ids as $index => $child_id) {
|
||||||
|
$bundle_items[] = array(
|
||||||
|
'product_id' => $child_id,
|
||||||
|
'quantity' => 1,
|
||||||
|
'discount' => 10 // 10% 折扣
|
||||||
|
);
|
||||||
|
}
|
||||||
|
update_post_meta($this->bundle_product_id, '_yoone_bundle_items', $bundle_items);
|
||||||
|
|
||||||
|
$this->log_test("捆绑产品创建成功 - ID: {$this->bundle_product_id}", 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试价格计算
|
||||||
|
*/
|
||||||
|
private function test_price_calculation() {
|
||||||
|
$this->log_test('测试捆绑产品价格计算');
|
||||||
|
|
||||||
|
$bundle_product = wc_get_product($this->bundle_product_id);
|
||||||
|
if (!$bundle_product) {
|
||||||
|
throw new Exception('获取捆绑产品失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算子产品总价
|
||||||
|
$child_total = 0;
|
||||||
|
foreach ($this->child_product_ids as $child_id) {
|
||||||
|
$child_product = wc_get_product($child_id);
|
||||||
|
if ($child_product) {
|
||||||
|
$child_total += $child_product->get_regular_price();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证价格计算
|
||||||
|
$bundle_price = $bundle_product->get_regular_price();
|
||||||
|
if ($bundle_price <= 0) {
|
||||||
|
throw new Exception('捆绑产品价格无效');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查折扣是否正确应用
|
||||||
|
$expected_discount = $child_total * 0.1; // 10% 折扣
|
||||||
|
$actual_savings = $child_total - $bundle_price;
|
||||||
|
|
||||||
|
if (abs($actual_savings - $expected_discount) > 0.01) {
|
||||||
|
$this->log_test("价格计算警告 - 预期节省: {$expected_discount}, 实际节省: {$actual_savings}", 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test("价格计算测试通过 - 捆绑价格: {$bundle_price}, 子产品总价: {$child_total}", 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试捆绑产品订阅
|
||||||
|
*/
|
||||||
|
private function test_bundle_subscription() {
|
||||||
|
$this->log_test('测试捆绑产品订阅');
|
||||||
|
|
||||||
|
// 创建订阅
|
||||||
|
$subscription = new Yoone_Subscription();
|
||||||
|
$subscription->set_customer_id($this->customer_id);
|
||||||
|
$subscription->set_billing_period('month');
|
||||||
|
$subscription->set_billing_interval(1);
|
||||||
|
$subscription->set_status('pending');
|
||||||
|
|
||||||
|
// 添加捆绑产品到订阅
|
||||||
|
$subscription->add_item(array(
|
||||||
|
'product_id' => $this->bundle_product_id,
|
||||||
|
'quantity' => 1,
|
||||||
|
'price' => 89.99
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->subscription_id = $subscription->save();
|
||||||
|
|
||||||
|
if (!$this->subscription_id) {
|
||||||
|
throw new Exception('创建捆绑产品订阅失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证订阅项目
|
||||||
|
$items = $subscription->get_items();
|
||||||
|
if (empty($items)) {
|
||||||
|
throw new Exception('订阅项目为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$bundle_item = reset($items);
|
||||||
|
if ($bundle_item['product_id'] != $this->bundle_product_id) {
|
||||||
|
throw new Exception('订阅项目产品ID不匹配');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test("捆绑产品订阅创建成功 - 订阅ID: {$this->subscription_id}", 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试子产品管理
|
||||||
|
*/
|
||||||
|
private function test_child_product_management() {
|
||||||
|
$this->log_test('测试子产品管理');
|
||||||
|
|
||||||
|
// 获取捆绑产品的子产品
|
||||||
|
$bundle_items = get_post_meta($this->bundle_product_id, '_yoone_bundle_items', true);
|
||||||
|
|
||||||
|
if (empty($bundle_items)) {
|
||||||
|
throw new Exception('获取捆绑产品子项目失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证每个子产品
|
||||||
|
foreach ($bundle_items as $item) {
|
||||||
|
$child_product = wc_get_product($item['product_id']);
|
||||||
|
if (!$child_product) {
|
||||||
|
throw new Exception("子产品 {$item['product_id']} 不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$child_product->is_purchasable()) {
|
||||||
|
$this->log_test("子产品 {$item['product_id']} 不可购买", 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试子产品数量验证
|
||||||
|
if (count($bundle_items) !== count($this->child_product_ids)) {
|
||||||
|
throw new Exception('捆绑产品子项目数量不匹配');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test('子产品管理测试通过', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试库存管理
|
||||||
|
*/
|
||||||
|
private function test_inventory_management() {
|
||||||
|
$this->log_test('测试捆绑产品库存管理');
|
||||||
|
|
||||||
|
$bundle_product = wc_get_product($this->bundle_product_id);
|
||||||
|
|
||||||
|
// 检查库存状态
|
||||||
|
if ($bundle_product->managing_stock()) {
|
||||||
|
$stock_quantity = $bundle_product->get_stock_quantity();
|
||||||
|
if ($stock_quantity <= 0) {
|
||||||
|
$this->log_test('捆绑产品库存不足', 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查子产品库存
|
||||||
|
foreach ($this->child_product_ids as $child_id) {
|
||||||
|
$child_product = wc_get_product($child_id);
|
||||||
|
if ($child_product && $child_product->managing_stock()) {
|
||||||
|
$child_stock = $child_product->get_stock_quantity();
|
||||||
|
if ($child_stock <= 0) {
|
||||||
|
$this->log_test("子产品 {$child_id} 库存不足", 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test('库存管理测试完成', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理测试数据
|
||||||
|
*/
|
||||||
|
private function cleanup_test_data() {
|
||||||
|
$this->log_test('清理捆绑产品测试数据');
|
||||||
|
|
||||||
|
// 删除测试订阅
|
||||||
|
if ($this->subscription_id) {
|
||||||
|
$subscription = new Yoone_Subscription($this->subscription_id);
|
||||||
|
$subscription->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除捆绑产品
|
||||||
|
if ($this->bundle_product_id) {
|
||||||
|
wp_delete_post($this->bundle_product_id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除子产品
|
||||||
|
foreach ($this->child_product_ids as $child_id) {
|
||||||
|
wp_delete_post($child_id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除测试客户
|
||||||
|
if ($this->customer_id) {
|
||||||
|
wp_delete_user($this->customer_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test('捆绑产品测试数据清理完成', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录测试结果
|
||||||
|
*/
|
||||||
|
private function log_test($message, $type = 'info') {
|
||||||
|
$this->test_results[] = array(
|
||||||
|
'timestamp' => current_time('mysql'),
|
||||||
|
'message' => $message,
|
||||||
|
'type' => $type
|
||||||
|
);
|
||||||
|
|
||||||
|
// 同时记录到日志系统
|
||||||
|
Yoone_Logger::info('捆绑产品测试: ' . $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取测试结果
|
||||||
|
*/
|
||||||
|
public function get_test_results() {
|
||||||
|
return $this->test_results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,223 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 测试配置文件
|
||||||
|
*
|
||||||
|
* @package Yoone_Subscriptions
|
||||||
|
* @subpackage Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试配置类
|
||||||
|
*/
|
||||||
|
class Yoone_Test_Config {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试数据配置
|
||||||
|
*/
|
||||||
|
public static function get_test_data() {
|
||||||
|
return array(
|
||||||
|
'customer' => array(
|
||||||
|
'email' => 'test@example.com',
|
||||||
|
'first_name' => '测试',
|
||||||
|
'last_name' => '用户',
|
||||||
|
'billing' => array(
|
||||||
|
'first_name' => '测试',
|
||||||
|
'last_name' => '用户',
|
||||||
|
'company' => '',
|
||||||
|
'address_1' => '测试地址1',
|
||||||
|
'address_2' => '',
|
||||||
|
'city' => '测试城市',
|
||||||
|
'state' => 'ON',
|
||||||
|
'postcode' => 'M5V 3A8',
|
||||||
|
'country' => 'CA',
|
||||||
|
'email' => 'test@example.com',
|
||||||
|
'phone' => '416-555-0123'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'products' => array(
|
||||||
|
'subscription' => array(
|
||||||
|
'name' => '测试订阅产品',
|
||||||
|
'price' => 29.99,
|
||||||
|
'billing_period' => 'month',
|
||||||
|
'billing_interval' => 1,
|
||||||
|
'trial_period' => 7
|
||||||
|
),
|
||||||
|
'bundle' => array(
|
||||||
|
'name' => '测试捆绑产品',
|
||||||
|
'items' => array(
|
||||||
|
array('product_id' => 100, 'quantity' => 1, 'discount' => 10),
|
||||||
|
array('product_id' => 101, 'quantity' => 2, 'discount' => 15)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'payment' => array(
|
||||||
|
'card_number' => '4242424242424242',
|
||||||
|
'expiry_month' => '12',
|
||||||
|
'expiry_year' => '2025',
|
||||||
|
'cvv' => '123',
|
||||||
|
'cardholder_name' => '测试用户'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建测试客户
|
||||||
|
*/
|
||||||
|
public static function create_test_customer() {
|
||||||
|
$test_data = self::get_test_data();
|
||||||
|
$customer_data = $test_data['customer'];
|
||||||
|
|
||||||
|
// 检查客户是否已存在
|
||||||
|
$existing_customer = get_user_by('email', $customer_data['email']);
|
||||||
|
if ($existing_customer) {
|
||||||
|
return $existing_customer->ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新客户
|
||||||
|
$customer_id = wp_create_user(
|
||||||
|
$customer_data['email'],
|
||||||
|
wp_generate_password(),
|
||||||
|
$customer_data['email']
|
||||||
|
);
|
||||||
|
|
||||||
|
if (is_wp_error($customer_id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新客户信息
|
||||||
|
wp_update_user(array(
|
||||||
|
'ID' => $customer_id,
|
||||||
|
'first_name' => $customer_data['first_name'],
|
||||||
|
'last_name' => $customer_data['last_name']
|
||||||
|
));
|
||||||
|
|
||||||
|
// 添加账单地址
|
||||||
|
foreach ($customer_data['billing'] as $key => $value) {
|
||||||
|
update_user_meta($customer_id, 'billing_' . $key, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $customer_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取捆绑产品测试数据
|
||||||
|
*/
|
||||||
|
public static function get_bundle_products() {
|
||||||
|
return array(
|
||||||
|
array(
|
||||||
|
'name' => '测试产品A',
|
||||||
|
'price' => 19.99,
|
||||||
|
'type' => 'simple'
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'name' => '测试产品B',
|
||||||
|
'price' => 29.99,
|
||||||
|
'type' => 'simple'
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'name' => '测试产品C',
|
||||||
|
'price' => 39.99,
|
||||||
|
'type' => 'simple'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建测试产品
|
||||||
|
*/
|
||||||
|
public static function create_test_product($product_data = null) {
|
||||||
|
if (!$product_data) {
|
||||||
|
$test_data = self::get_test_data();
|
||||||
|
$product_data = $test_data['products']['subscription'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建产品
|
||||||
|
$product = new WC_Product_Simple();
|
||||||
|
$product->set_name($product_data['name']);
|
||||||
|
$product->set_status('publish');
|
||||||
|
$product->set_catalog_visibility('visible');
|
||||||
|
$product->set_price($product_data['price']);
|
||||||
|
$product->set_regular_price($product_data['price']);
|
||||||
|
$product->set_manage_stock(false);
|
||||||
|
$product->set_stock_status('instock');
|
||||||
|
|
||||||
|
// 保存产品
|
||||||
|
$product_id = $product->save();
|
||||||
|
|
||||||
|
// 如果是订阅产品,添加订阅元数据
|
||||||
|
if (isset($product_data['billing_period'])) {
|
||||||
|
update_post_meta($product_id, '_yoone_subscription_enabled', 'yes');
|
||||||
|
update_post_meta($product_id, '_yoone_billing_period', $product_data['billing_period']);
|
||||||
|
update_post_meta($product_id, '_yoone_billing_interval', $product_data['billing_interval']);
|
||||||
|
if (isset($product_data['trial_period'])) {
|
||||||
|
update_post_meta($product_id, '_yoone_trial_period', $product_data['trial_period']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $product_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理测试数据
|
||||||
|
*/
|
||||||
|
public static function cleanup_test_data() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// 删除测试客户
|
||||||
|
$test_email = self::get_test_data()['customer']['email'];
|
||||||
|
$customer = get_user_by('email', $test_email);
|
||||||
|
if ($customer) {
|
||||||
|
wp_delete_user($customer->ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除测试订阅
|
||||||
|
$wpdb->delete(
|
||||||
|
$wpdb->prefix . 'yoone_subscriptions',
|
||||||
|
array('customer_id' => 0), // 使用0作为测试标识
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
// 删除测试支付令牌
|
||||||
|
$wpdb->delete(
|
||||||
|
$wpdb->prefix . 'yoone_payment_tokens',
|
||||||
|
array('customer_id' => 0),
|
||||||
|
array('%d')
|
||||||
|
);
|
||||||
|
|
||||||
|
// 删除测试产品
|
||||||
|
$products = get_posts(array(
|
||||||
|
'post_type' => 'product',
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => '_test_product',
|
||||||
|
'value' => 'yes'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'posts_per_page' => -1
|
||||||
|
));
|
||||||
|
|
||||||
|
foreach ($products as $product) {
|
||||||
|
wp_delete_post($product->ID, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取测试环境信息
|
||||||
|
*/
|
||||||
|
public static function get_environment_info() {
|
||||||
|
return array(
|
||||||
|
'php_version' => PHP_VERSION,
|
||||||
|
'wordpress_version' => get_bloginfo('version'),
|
||||||
|
'woocommerce_version' => class_exists('WooCommerce') ? WC()->version : 'Not installed',
|
||||||
|
'plugin_version' => YOONE_SUBSCRIPTIONS_VERSION,
|
||||||
|
'memory_limit' => ini_get('memory_limit'),
|
||||||
|
'max_execution_time' => ini_get('max_execution_time'),
|
||||||
|
'database_version' => $GLOBALS['wpdb']->db_version()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,418 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 定时任务测试脚本
|
||||||
|
*
|
||||||
|
* @package Yoone_Subscriptions
|
||||||
|
* @subpackage Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定时任务测试类
|
||||||
|
*/
|
||||||
|
class Yoone_Cron_Jobs_Test {
|
||||||
|
|
||||||
|
private $test_results = array();
|
||||||
|
private $test_subscription_ids = array();
|
||||||
|
private $customer_id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行完整的定时任务测试
|
||||||
|
*/
|
||||||
|
public function run_full_test() {
|
||||||
|
$this->log_test('开始定时任务测试');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 环境检查
|
||||||
|
$this->test_environment();
|
||||||
|
|
||||||
|
// 2. 创建测试数据
|
||||||
|
$this->setup_test_data();
|
||||||
|
|
||||||
|
// 3. 测试续费定时任务
|
||||||
|
$this->test_renewal_cron();
|
||||||
|
|
||||||
|
// 4. 测试过期处理定时任务
|
||||||
|
$this->test_expiry_cron();
|
||||||
|
|
||||||
|
// 5. 测试清理定时任务
|
||||||
|
$this->test_cleanup_cron();
|
||||||
|
|
||||||
|
// 6. 测试定时任务调度
|
||||||
|
$this->test_cron_scheduling();
|
||||||
|
|
||||||
|
// 7. 清理测试数据
|
||||||
|
$this->cleanup_test_data();
|
||||||
|
|
||||||
|
$this->log_test('定时任务测试完成');
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->log_test('测试失败: ' . $e->getMessage(), 'error');
|
||||||
|
$this->cleanup_test_data();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->test_results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 环境检查
|
||||||
|
*/
|
||||||
|
private function test_environment() {
|
||||||
|
$this->log_test('检查定时任务环境');
|
||||||
|
|
||||||
|
// 检查必要的类是否存在
|
||||||
|
$required_classes = array(
|
||||||
|
'Yoone_Cron',
|
||||||
|
'Yoone_Subscription'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($required_classes as $class) {
|
||||||
|
if (!class_exists($class)) {
|
||||||
|
throw new Exception("必需的类 {$class} 不存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查WordPress定时任务功能
|
||||||
|
if (!function_exists('wp_schedule_event')) {
|
||||||
|
throw new Exception('WordPress定时任务功能不可用');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test('定时任务环境检查通过', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建测试数据
|
||||||
|
*/
|
||||||
|
private function setup_test_data() {
|
||||||
|
$this->log_test('创建定时任务测试数据');
|
||||||
|
|
||||||
|
// 创建测试客户
|
||||||
|
$this->customer_id = Yoone_Test_Config::create_test_customer();
|
||||||
|
if (!$this->customer_id) {
|
||||||
|
throw new Exception('创建测试客户失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建不同状态的测试订阅
|
||||||
|
$subscription_scenarios = array(
|
||||||
|
array(
|
||||||
|
'status' => 'active',
|
||||||
|
'next_payment' => date('Y-m-d H:i:s', strtotime('-1 day')), // 需要续费
|
||||||
|
'description' => '需要续费的订阅'
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'status' => 'active',
|
||||||
|
'next_payment' => date('Y-m-d H:i:s', strtotime('+7 days')), // 正常订阅
|
||||||
|
'description' => '正常活跃订阅'
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'status' => 'pending-cancel',
|
||||||
|
'next_payment' => date('Y-m-d H:i:s', strtotime('-2 days')), // 待取消
|
||||||
|
'description' => '待取消订阅'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($subscription_scenarios as $scenario) {
|
||||||
|
$subscription = new Yoone_Subscription();
|
||||||
|
$subscription->set_customer_id($this->customer_id);
|
||||||
|
$subscription->set_status($scenario['status']);
|
||||||
|
$subscription->set_billing_period('month');
|
||||||
|
$subscription->set_billing_interval(1);
|
||||||
|
$subscription->set_next_payment_date($scenario['next_payment']);
|
||||||
|
|
||||||
|
// 添加订阅项目
|
||||||
|
$subscription->add_item(array(
|
||||||
|
'product_id' => Yoone_Test_Config::create_test_product(),
|
||||||
|
'quantity' => 1,
|
||||||
|
'price' => 29.99
|
||||||
|
));
|
||||||
|
|
||||||
|
$subscription_id = $subscription->save();
|
||||||
|
if ($subscription_id) {
|
||||||
|
$this->test_subscription_ids[] = $subscription_id;
|
||||||
|
$this->log_test("创建测试订阅: {$scenario['description']} - ID: {$subscription_id}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($this->test_subscription_ids)) {
|
||||||
|
throw new Exception('创建测试订阅失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test("定时任务测试数据创建成功 - 订阅数量: " . count($this->test_subscription_ids), 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试续费定时任务
|
||||||
|
*/
|
||||||
|
private function test_renewal_cron() {
|
||||||
|
$this->log_test('测试续费定时任务');
|
||||||
|
|
||||||
|
// 检查续费定时任务是否已注册
|
||||||
|
$cron_hooks = array(
|
||||||
|
'yoone_process_subscription_renewals',
|
||||||
|
'yoone_check_renewal_payments'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($cron_hooks as $hook) {
|
||||||
|
if (!wp_next_scheduled($hook)) {
|
||||||
|
$this->log_test("续费定时任务 {$hook} 未调度", 'warning');
|
||||||
|
} else {
|
||||||
|
$next_run = wp_next_scheduled($hook);
|
||||||
|
$this->log_test("续费定时任务 {$hook} 下次运行: " . date('Y-m-d H:i:s', $next_run));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟执行续费检查
|
||||||
|
if (class_exists('Yoone_Cron')) {
|
||||||
|
$cron = new Yoone_Cron();
|
||||||
|
|
||||||
|
// 获取需要续费的订阅
|
||||||
|
$due_subscriptions = $this->get_due_subscriptions();
|
||||||
|
$this->log_test("找到 " . count($due_subscriptions) . " 个需要续费的订阅");
|
||||||
|
|
||||||
|
// 模拟处理续费
|
||||||
|
foreach ($due_subscriptions as $subscription_id) {
|
||||||
|
$subscription = new Yoone_Subscription($subscription_id);
|
||||||
|
if ($subscription->can_be_renewed()) {
|
||||||
|
$this->log_test("订阅 {$subscription_id} 可以续费");
|
||||||
|
} else {
|
||||||
|
$this->log_test("订阅 {$subscription_id} 不能续费", 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test('续费定时任务测试完成', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试过期处理定时任务
|
||||||
|
*/
|
||||||
|
private function test_expiry_cron() {
|
||||||
|
$this->log_test('测试过期处理定时任务');
|
||||||
|
|
||||||
|
// 检查过期处理定时任务
|
||||||
|
$expiry_hooks = array(
|
||||||
|
'yoone_process_expired_subscriptions',
|
||||||
|
'yoone_cleanup_expired_data'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($expiry_hooks as $hook) {
|
||||||
|
if (!wp_next_scheduled($hook)) {
|
||||||
|
$this->log_test("过期处理定时任务 {$hook} 未调度", 'warning');
|
||||||
|
} else {
|
||||||
|
$next_run = wp_next_scheduled($hook);
|
||||||
|
$this->log_test("过期处理定时任务 {$hook} 下次运行: " . date('Y-m-d H:i:s', $next_run));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟处理过期订阅
|
||||||
|
$expired_subscriptions = $this->get_expired_subscriptions();
|
||||||
|
$this->log_test("找到 " . count($expired_subscriptions) . " 个过期订阅");
|
||||||
|
|
||||||
|
foreach ($expired_subscriptions as $subscription_id) {
|
||||||
|
$subscription = new Yoone_Subscription($subscription_id);
|
||||||
|
$status = $subscription->get_status();
|
||||||
|
$this->log_test("过期订阅 {$subscription_id} 当前状态: {$status}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test('过期处理定时任务测试完成', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试清理定时任务
|
||||||
|
*/
|
||||||
|
private function test_cleanup_cron() {
|
||||||
|
$this->log_test('测试清理定时任务');
|
||||||
|
|
||||||
|
// 检查清理定时任务
|
||||||
|
$cleanup_hooks = array(
|
||||||
|
'yoone_cleanup_logs',
|
||||||
|
'yoone_cleanup_expired_tokens',
|
||||||
|
'yoone_cleanup_temp_data'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($cleanup_hooks as $hook) {
|
||||||
|
if (!wp_next_scheduled($hook)) {
|
||||||
|
$this->log_test("清理定时任务 {$hook} 未调度", 'warning');
|
||||||
|
} else {
|
||||||
|
$next_run = wp_next_scheduled($hook);
|
||||||
|
$this->log_test("清理定时任务 {$hook} 下次运行: " . date('Y-m-d H:i:s', $next_run));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试日志清理
|
||||||
|
if (class_exists('Yoone_Logger')) {
|
||||||
|
$log_count_before = $this->count_log_entries();
|
||||||
|
$this->log_test("清理前日志条目数: {$log_count_before}");
|
||||||
|
|
||||||
|
// 这里可以模拟清理过程,但不实际删除数据
|
||||||
|
$this->log_test('日志清理功能可用');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试过期令牌清理
|
||||||
|
if (class_exists('Yoone_Payment_Token')) {
|
||||||
|
$expired_tokens = $this->count_expired_tokens();
|
||||||
|
$this->log_test("过期支付令牌数量: {$expired_tokens}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test('清理定时任务测试完成', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试定时任务调度
|
||||||
|
*/
|
||||||
|
private function test_cron_scheduling() {
|
||||||
|
$this->log_test('测试定时任务调度');
|
||||||
|
|
||||||
|
// 获取所有已调度的定时任务
|
||||||
|
$cron_array = _get_cron_array();
|
||||||
|
$yoone_crons = array();
|
||||||
|
|
||||||
|
foreach ($cron_array as $timestamp => $cron) {
|
||||||
|
foreach ($cron as $hook => $dings) {
|
||||||
|
if (strpos($hook, 'yoone_') === 0) {
|
||||||
|
$yoone_crons[] = array(
|
||||||
|
'hook' => $hook,
|
||||||
|
'timestamp' => $timestamp,
|
||||||
|
'next_run' => date('Y-m-d H:i:s', $timestamp)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test("找到 " . count($yoone_crons) . " 个Yoone定时任务");
|
||||||
|
|
||||||
|
foreach ($yoone_crons as $cron) {
|
||||||
|
$this->log_test("定时任务: {$cron['hook']} - 下次运行: {$cron['next_run']}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查定时任务间隔
|
||||||
|
$this->check_cron_intervals();
|
||||||
|
|
||||||
|
$this->log_test('定时任务调度测试完成', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取需要续费的订阅
|
||||||
|
*/
|
||||||
|
private function get_due_subscriptions() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$results = $wpdb->get_col($wpdb->prepare("
|
||||||
|
SELECT id FROM {$wpdb->prefix}yoone_subscriptions
|
||||||
|
WHERE status = 'active'
|
||||||
|
AND next_payment_date <= %s
|
||||||
|
", current_time('mysql')));
|
||||||
|
|
||||||
|
return array_intersect($results, $this->test_subscription_ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取过期订阅
|
||||||
|
*/
|
||||||
|
private function get_expired_subscriptions() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$results = $wpdb->get_col($wpdb->prepare("
|
||||||
|
SELECT id FROM {$wpdb->prefix}yoone_subscriptions
|
||||||
|
WHERE status IN ('pending-cancel', 'expired')
|
||||||
|
AND next_payment_date < %s
|
||||||
|
", date('Y-m-d H:i:s', strtotime('-7 days'))));
|
||||||
|
|
||||||
|
return array_intersect($results, $this->test_subscription_ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计日志条目数
|
||||||
|
*/
|
||||||
|
private function count_log_entries() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$count = $wpdb->get_var("
|
||||||
|
SELECT COUNT(*) FROM {$wpdb->prefix}yoone_logs
|
||||||
|
WHERE created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
|
||||||
|
");
|
||||||
|
|
||||||
|
return intval($count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计过期令牌数
|
||||||
|
*/
|
||||||
|
private function count_expired_tokens() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$count = $wpdb->get_var("
|
||||||
|
SELECT COUNT(*) FROM {$wpdb->prefix}yoone_payment_tokens
|
||||||
|
WHERE expires_at IS NOT NULL AND expires_at < NOW()
|
||||||
|
");
|
||||||
|
|
||||||
|
return intval($count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查定时任务间隔
|
||||||
|
*/
|
||||||
|
private function check_cron_intervals() {
|
||||||
|
$expected_intervals = array(
|
||||||
|
'yoone_process_subscription_renewals' => 'hourly',
|
||||||
|
'yoone_process_expired_subscriptions' => 'daily',
|
||||||
|
'yoone_cleanup_logs' => 'weekly'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($expected_intervals as $hook => $expected_interval) {
|
||||||
|
$schedules = wp_get_schedules();
|
||||||
|
$next_run = wp_next_scheduled($hook);
|
||||||
|
|
||||||
|
if ($next_run) {
|
||||||
|
$this->log_test("定时任务 {$hook} 调度正常");
|
||||||
|
} else {
|
||||||
|
$this->log_test("定时任务 {$hook} 未正确调度", 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理测试数据
|
||||||
|
*/
|
||||||
|
private function cleanup_test_data() {
|
||||||
|
$this->log_test('清理定时任务测试数据');
|
||||||
|
|
||||||
|
// 删除测试订阅
|
||||||
|
foreach ($this->test_subscription_ids as $subscription_id) {
|
||||||
|
$subscription = new Yoone_Subscription($subscription_id);
|
||||||
|
$subscription->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除测试客户
|
||||||
|
if ($this->customer_id) {
|
||||||
|
wp_delete_user($this->customer_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test('定时任务测试数据清理完成', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录测试结果
|
||||||
|
*/
|
||||||
|
private function log_test($message, $type = 'info') {
|
||||||
|
$this->test_results[] = array(
|
||||||
|
'timestamp' => current_time('mysql'),
|
||||||
|
'message' => $message,
|
||||||
|
'type' => $type
|
||||||
|
);
|
||||||
|
|
||||||
|
// 同时记录到日志系统
|
||||||
|
Yoone_Logger::info('定时任务测试: ' . $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取测试结果
|
||||||
|
*/
|
||||||
|
public function get_test_results() {
|
||||||
|
return $this->test_results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,290 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 支付集成测试脚本
|
||||||
|
*
|
||||||
|
* @package Yoone_Subscriptions
|
||||||
|
* @subpackage Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付集成测试类
|
||||||
|
*/
|
||||||
|
class Yoone_Payment_Integration_Test {
|
||||||
|
|
||||||
|
private $test_results = array();
|
||||||
|
private $customer_id;
|
||||||
|
private $payment_token_id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行完整的支付集成测试
|
||||||
|
*/
|
||||||
|
public function run_full_test() {
|
||||||
|
$this->log_test('开始支付集成测试');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 环境检查
|
||||||
|
$this->test_environment();
|
||||||
|
|
||||||
|
// 2. 创建测试数据
|
||||||
|
$this->setup_test_data();
|
||||||
|
|
||||||
|
// 3. 测试支付令牌创建
|
||||||
|
$this->test_payment_token_creation();
|
||||||
|
|
||||||
|
// 4. 测试支付令牌验证
|
||||||
|
$this->test_payment_token_validation();
|
||||||
|
|
||||||
|
// 5. 测试支付网关配置
|
||||||
|
$this->test_payment_gateway_config();
|
||||||
|
|
||||||
|
// 6. 测试支付处理(模拟)
|
||||||
|
$this->test_payment_processing();
|
||||||
|
|
||||||
|
// 7. 测试支付令牌管理
|
||||||
|
$this->test_payment_token_management();
|
||||||
|
|
||||||
|
// 8. 清理测试数据
|
||||||
|
$this->cleanup_test_data();
|
||||||
|
|
||||||
|
$this->log_test('支付集成测试完成');
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->log_test('测试失败: ' . $e->getMessage(), 'error');
|
||||||
|
$this->cleanup_test_data();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->test_results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 环境检查
|
||||||
|
*/
|
||||||
|
private function test_environment() {
|
||||||
|
$this->log_test('检查支付环境');
|
||||||
|
|
||||||
|
// 检查必要的类是否存在
|
||||||
|
$required_classes = array(
|
||||||
|
'Yoone_Payment_Token',
|
||||||
|
'Yoone_Moneris_Gateway'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($required_classes as $class) {
|
||||||
|
if (!class_exists($class)) {
|
||||||
|
throw new Exception("必需的类 {$class} 不存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查数据库表
|
||||||
|
global $wpdb;
|
||||||
|
$table = $wpdb->prefix . 'yoone_payment_tokens';
|
||||||
|
$exists = $wpdb->get_var("SHOW TABLES LIKE '{$table}'");
|
||||||
|
if (!$exists) {
|
||||||
|
throw new Exception("数据库表 {$table} 不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test('支付环境检查通过', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建测试数据
|
||||||
|
*/
|
||||||
|
private function setup_test_data() {
|
||||||
|
$this->log_test('创建支付测试数据');
|
||||||
|
|
||||||
|
// 创建测试客户
|
||||||
|
$this->customer_id = Yoone_Test_Config::create_test_customer();
|
||||||
|
if (!$this->customer_id) {
|
||||||
|
throw new Exception('创建测试客户失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test("支付测试数据创建成功 - 客户ID: {$this->customer_id}", 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试支付令牌创建
|
||||||
|
*/
|
||||||
|
private function test_payment_token_creation() {
|
||||||
|
$this->log_test('测试支付令牌创建');
|
||||||
|
|
||||||
|
$token = new Yoone_Payment_Token();
|
||||||
|
$token->set_customer_id($this->customer_id);
|
||||||
|
$token->set_gateway_id('moneris');
|
||||||
|
$token->set_token('test_token_' . time());
|
||||||
|
$token->set_token_type('credit_card');
|
||||||
|
$token->set_card_type('visa');
|
||||||
|
$token->set_last_four('4242');
|
||||||
|
$token->set_expiry_month('12');
|
||||||
|
$token->set_expiry_year('2025');
|
||||||
|
$token->set_default(true);
|
||||||
|
|
||||||
|
// 保存令牌
|
||||||
|
$this->payment_token_id = $token->save();
|
||||||
|
|
||||||
|
if (!$this->payment_token_id) {
|
||||||
|
throw new Exception('支付令牌创建失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证令牌数据
|
||||||
|
$saved_token = new Yoone_Payment_Token($this->payment_token_id);
|
||||||
|
if ($saved_token->get_customer_id() !== $this->customer_id) {
|
||||||
|
throw new Exception('支付令牌数据验证失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test("支付令牌创建成功 - ID: {$this->payment_token_id}", 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试支付令牌验证
|
||||||
|
*/
|
||||||
|
private function test_payment_token_validation() {
|
||||||
|
$this->log_test('测试支付令牌验证');
|
||||||
|
|
||||||
|
$token = new Yoone_Payment_Token($this->payment_token_id);
|
||||||
|
|
||||||
|
// 测试令牌有效性
|
||||||
|
if (!$token->is_valid()) {
|
||||||
|
throw new Exception('支付令牌验证失败 - 令牌无效');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试过期检查
|
||||||
|
if ($token->is_expired()) {
|
||||||
|
throw new Exception('支付令牌验证失败 - 令牌已过期');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test('支付令牌验证通过', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试支付网关配置
|
||||||
|
*/
|
||||||
|
private function test_payment_gateway_config() {
|
||||||
|
$this->log_test('测试支付网关配置');
|
||||||
|
|
||||||
|
// 检查Moneris网关是否可用
|
||||||
|
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
|
||||||
|
|
||||||
|
if (!isset($available_gateways['moneris'])) {
|
||||||
|
$this->log_test('Moneris支付网关未启用,跳过配置测试', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$gateway = $available_gateways['moneris'];
|
||||||
|
|
||||||
|
// 检查基本配置
|
||||||
|
if (empty($gateway->get_option('store_id'))) {
|
||||||
|
$this->log_test('Moneris Store ID未配置', 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($gateway->get_option('api_token'))) {
|
||||||
|
$this->log_test('Moneris API Token未配置', 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test('支付网关配置检查完成', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试支付处理(模拟)
|
||||||
|
*/
|
||||||
|
private function test_payment_processing() {
|
||||||
|
$this->log_test('测试支付处理(模拟)');
|
||||||
|
|
||||||
|
// 由于这是测试环境,我们只模拟支付处理流程
|
||||||
|
// 实际的支付处理需要真实的Moneris凭据和测试环境
|
||||||
|
|
||||||
|
$token = new Yoone_Payment_Token($this->payment_token_id);
|
||||||
|
|
||||||
|
// 模拟支付数据
|
||||||
|
$payment_data = array(
|
||||||
|
'amount' => 29.99,
|
||||||
|
'currency' => 'CAD',
|
||||||
|
'description' => '测试支付',
|
||||||
|
'customer_id' => $this->customer_id,
|
||||||
|
'payment_token' => $token->get_token()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 验证支付数据
|
||||||
|
if (empty($payment_data['amount']) || $payment_data['amount'] <= 0) {
|
||||||
|
throw new Exception('支付金额无效');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($payment_data['payment_token'])) {
|
||||||
|
throw new Exception('支付令牌为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test('支付处理模拟测试通过', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试支付令牌管理
|
||||||
|
*/
|
||||||
|
private function test_payment_token_management() {
|
||||||
|
$this->log_test('测试支付令牌管理');
|
||||||
|
|
||||||
|
// 测试获取客户令牌
|
||||||
|
$customer_tokens = Yoone_Payment_Token::get_customer_tokens($this->customer_id);
|
||||||
|
if (empty($customer_tokens)) {
|
||||||
|
throw new Exception('获取客户支付令牌失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试获取默认令牌
|
||||||
|
$default_token = Yoone_Payment_Token::get_default_token($this->customer_id, 'moneris');
|
||||||
|
if (!$default_token || $default_token->get_id() !== $this->payment_token_id) {
|
||||||
|
throw new Exception('获取默认支付令牌失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试令牌显示
|
||||||
|
$token = new Yoone_Payment_Token($this->payment_token_id);
|
||||||
|
$display_info = $token->get_display_name();
|
||||||
|
if (empty($display_info)) {
|
||||||
|
throw new Exception('支付令牌显示信息为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test('支付令牌管理测试通过', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理测试数据
|
||||||
|
*/
|
||||||
|
private function cleanup_test_data() {
|
||||||
|
$this->log_test('清理支付测试数据');
|
||||||
|
|
||||||
|
// 删除测试支付令牌
|
||||||
|
if ($this->payment_token_id) {
|
||||||
|
$token = new Yoone_Payment_Token($this->payment_token_id);
|
||||||
|
$token->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除测试客户
|
||||||
|
if ($this->customer_id) {
|
||||||
|
wp_delete_user($this->customer_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test('支付测试数据清理完成', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录测试结果
|
||||||
|
*/
|
||||||
|
private function log_test($message, $type = 'info') {
|
||||||
|
$this->test_results[] = array(
|
||||||
|
'timestamp' => current_time('mysql'),
|
||||||
|
'message' => $message,
|
||||||
|
'type' => $type
|
||||||
|
);
|
||||||
|
|
||||||
|
// 同时记录到日志系统
|
||||||
|
Yoone_Logger::info('支付集成测试: ' . $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取测试结果
|
||||||
|
*/
|
||||||
|
public function get_test_results() {
|
||||||
|
return $this->test_results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,322 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 订阅流程测试脚本
|
||||||
|
*
|
||||||
|
* @package Yoone_Subscriptions
|
||||||
|
* @subpackage Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅流程测试类
|
||||||
|
*/
|
||||||
|
class Yoone_Subscription_Flow_Test {
|
||||||
|
|
||||||
|
private $test_results = array();
|
||||||
|
private $customer_id;
|
||||||
|
private $product_id;
|
||||||
|
private $subscription_id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行完整的订阅流程测试
|
||||||
|
*/
|
||||||
|
public function run_full_test() {
|
||||||
|
$this->log_test('开始订阅流程测试');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 环境检查
|
||||||
|
$this->test_environment();
|
||||||
|
|
||||||
|
// 2. 创建测试数据
|
||||||
|
$this->setup_test_data();
|
||||||
|
|
||||||
|
// 3. 测试订阅创建
|
||||||
|
$this->test_subscription_creation();
|
||||||
|
|
||||||
|
// 4. 测试订阅激活
|
||||||
|
$this->test_subscription_activation();
|
||||||
|
|
||||||
|
// 5. 测试订阅暂停
|
||||||
|
$this->test_subscription_pause();
|
||||||
|
|
||||||
|
// 6. 测试订阅恢复
|
||||||
|
$this->test_subscription_resume();
|
||||||
|
|
||||||
|
// 7. 测试续费处理
|
||||||
|
$this->test_subscription_renewal();
|
||||||
|
|
||||||
|
// 8. 测试订阅取消
|
||||||
|
$this->test_subscription_cancellation();
|
||||||
|
|
||||||
|
// 9. 清理测试数据
|
||||||
|
$this->cleanup_test_data();
|
||||||
|
|
||||||
|
$this->log_test('订阅流程测试完成');
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->log_test('测试失败: ' . $e->getMessage(), 'error');
|
||||||
|
$this->cleanup_test_data();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->test_results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 环境检查
|
||||||
|
*/
|
||||||
|
private function test_environment() {
|
||||||
|
$this->log_test('检查测试环境');
|
||||||
|
|
||||||
|
// 检查必要的类是否存在
|
||||||
|
$required_classes = array(
|
||||||
|
'Yoone_Subscription',
|
||||||
|
'Yoone_Subscription_Manager',
|
||||||
|
'Yoone_Payment_Token',
|
||||||
|
'WC_Product'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($required_classes as $class) {
|
||||||
|
if (!class_exists($class)) {
|
||||||
|
throw new Exception("必需的类 {$class} 不存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查数据库表
|
||||||
|
global $wpdb;
|
||||||
|
$tables = array(
|
||||||
|
$wpdb->prefix . 'yoone_subscriptions',
|
||||||
|
$wpdb->prefix . 'yoone_subscription_items',
|
||||||
|
$wpdb->prefix . 'yoone_payment_tokens'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
$exists = $wpdb->get_var("SHOW TABLES LIKE '{$table}'");
|
||||||
|
if (!$exists) {
|
||||||
|
throw new Exception("数据库表 {$table} 不存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test('环境检查通过', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建测试数据
|
||||||
|
*/
|
||||||
|
private function setup_test_data() {
|
||||||
|
$this->log_test('创建测试数据');
|
||||||
|
|
||||||
|
// 创建测试客户
|
||||||
|
$this->customer_id = Yoone_Test_Config::create_test_customer();
|
||||||
|
if (!$this->customer_id) {
|
||||||
|
throw new Exception('创建测试客户失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建测试产品
|
||||||
|
$this->product_id = Yoone_Test_Config::create_test_product('subscription');
|
||||||
|
if (!$this->product_id) {
|
||||||
|
throw new Exception('创建测试产品失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test("测试数据创建成功 - 客户ID: {$this->customer_id}, 产品ID: {$this->product_id}", 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试订阅创建
|
||||||
|
*/
|
||||||
|
private function test_subscription_creation() {
|
||||||
|
$this->log_test('测试订阅创建');
|
||||||
|
|
||||||
|
$subscription = new Yoone_Subscription();
|
||||||
|
$subscription->set_customer_id($this->customer_id);
|
||||||
|
$subscription->set_status('pending');
|
||||||
|
$subscription->set_billing_period('month');
|
||||||
|
$subscription->set_billing_interval(1);
|
||||||
|
$subscription->set_start_date(current_time('mysql'));
|
||||||
|
$subscription->set_total(29.99);
|
||||||
|
$subscription->set_currency('CAD');
|
||||||
|
|
||||||
|
// 添加订阅商品
|
||||||
|
$subscription->add_item(array(
|
||||||
|
'product_id' => $this->product_id,
|
||||||
|
'quantity' => 1,
|
||||||
|
'line_total' => 29.99
|
||||||
|
));
|
||||||
|
|
||||||
|
// 保存订阅
|
||||||
|
$this->subscription_id = $subscription->save();
|
||||||
|
|
||||||
|
if (!$this->subscription_id) {
|
||||||
|
throw new Exception('订阅创建失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证订阅数据
|
||||||
|
$saved_subscription = new Yoone_Subscription($this->subscription_id);
|
||||||
|
if ($saved_subscription->get_customer_id() !== $this->customer_id) {
|
||||||
|
throw new Exception('订阅数据验证失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test("订阅创建成功 - ID: {$this->subscription_id}", 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试订阅激活
|
||||||
|
*/
|
||||||
|
private function test_subscription_activation() {
|
||||||
|
$this->log_test('测试订阅激活');
|
||||||
|
|
||||||
|
$subscription = new Yoone_Subscription($this->subscription_id);
|
||||||
|
$result = $subscription->activate();
|
||||||
|
|
||||||
|
if (!$result) {
|
||||||
|
throw new Exception('订阅激活失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证状态
|
||||||
|
$subscription = new Yoone_Subscription($this->subscription_id);
|
||||||
|
if ($subscription->get_status() !== 'active') {
|
||||||
|
throw new Exception('订阅状态验证失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test('订阅激活成功', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试订阅暂停
|
||||||
|
*/
|
||||||
|
private function test_subscription_pause() {
|
||||||
|
$this->log_test('测试订阅暂停');
|
||||||
|
|
||||||
|
$subscription = new Yoone_Subscription($this->subscription_id);
|
||||||
|
$result = $subscription->pause();
|
||||||
|
|
||||||
|
if (!$result) {
|
||||||
|
throw new Exception('订阅暂停失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证状态
|
||||||
|
$subscription = new Yoone_Subscription($this->subscription_id);
|
||||||
|
if ($subscription->get_status() !== 'on-hold') {
|
||||||
|
throw new Exception('订阅暂停状态验证失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test('订阅暂停成功', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试订阅恢复
|
||||||
|
*/
|
||||||
|
private function test_subscription_resume() {
|
||||||
|
$this->log_test('测试订阅恢复');
|
||||||
|
|
||||||
|
$subscription = new Yoone_Subscription($this->subscription_id);
|
||||||
|
$result = $subscription->resume();
|
||||||
|
|
||||||
|
if (!$result) {
|
||||||
|
throw new Exception('订阅恢复失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证状态
|
||||||
|
$subscription = new Yoone_Subscription($this->subscription_id);
|
||||||
|
if ($subscription->get_status() !== 'active') {
|
||||||
|
throw new Exception('订阅恢复状态验证失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test('订阅恢复成功', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试续费处理
|
||||||
|
*/
|
||||||
|
private function test_subscription_renewal() {
|
||||||
|
$this->log_test('测试续费处理');
|
||||||
|
|
||||||
|
$subscription = new Yoone_Subscription($this->subscription_id);
|
||||||
|
|
||||||
|
// 设置下次付款日期为过去时间以触发续费
|
||||||
|
$past_date = date('Y-m-d H:i:s', strtotime('-1 day'));
|
||||||
|
$subscription->set_next_payment_date($past_date);
|
||||||
|
$subscription->save();
|
||||||
|
|
||||||
|
// 模拟续费处理
|
||||||
|
$renewal_result = $subscription->process_renewal();
|
||||||
|
|
||||||
|
if (!$renewal_result) {
|
||||||
|
// 续费可能因为没有支付方式而失败,这在测试中是正常的
|
||||||
|
$this->log_test('续费处理完成(可能因缺少支付方式而失败,这是正常的)', 'warning');
|
||||||
|
} else {
|
||||||
|
$this->log_test('续费处理成功', 'success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试订阅取消
|
||||||
|
*/
|
||||||
|
private function test_subscription_cancellation() {
|
||||||
|
$this->log_test('测试订阅取消');
|
||||||
|
|
||||||
|
$subscription = new Yoone_Subscription($this->subscription_id);
|
||||||
|
$result = $subscription->cancel();
|
||||||
|
|
||||||
|
if (!$result) {
|
||||||
|
throw new Exception('订阅取消失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证状态
|
||||||
|
$subscription = new Yoone_Subscription($this->subscription_id);
|
||||||
|
if ($subscription->get_status() !== 'cancelled') {
|
||||||
|
throw new Exception('订阅取消状态验证失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test('订阅取消成功', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理测试数据
|
||||||
|
*/
|
||||||
|
private function cleanup_test_data() {
|
||||||
|
$this->log_test('清理测试数据');
|
||||||
|
|
||||||
|
// 删除测试订阅
|
||||||
|
if ($this->subscription_id) {
|
||||||
|
$subscription = new Yoone_Subscription($this->subscription_id);
|
||||||
|
$subscription->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除测试产品
|
||||||
|
if ($this->product_id) {
|
||||||
|
wp_delete_post($this->product_id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除测试客户
|
||||||
|
if ($this->customer_id) {
|
||||||
|
wp_delete_user($this->customer_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log_test('测试数据清理完成', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录测试结果
|
||||||
|
*/
|
||||||
|
private function log_test($message, $type = 'info') {
|
||||||
|
$this->test_results[] = array(
|
||||||
|
'timestamp' => current_time('mysql'),
|
||||||
|
'message' => $message,
|
||||||
|
'type' => $type
|
||||||
|
);
|
||||||
|
|
||||||
|
// 同时记录到日志系统
|
||||||
|
Yoone_Logger::info('订阅流程测试: ' . $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取测试结果
|
||||||
|
*/
|
||||||
|
public function get_test_results() {
|
||||||
|
return $this->test_results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,458 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Yoone Subscriptions
|
||||||
|
* Plugin URI: https://yoone.ca
|
||||||
|
* Description: 专业的WooCommerce混装产品和订阅管理插件,支持Moneris支付网关
|
||||||
|
* Version: 1.0.0
|
||||||
|
* Author: Yoone Team
|
||||||
|
* Author URI: https://yoone.ca
|
||||||
|
* Text Domain: yoone-subscriptions
|
||||||
|
* Domain Path: /languages
|
||||||
|
* Requires at least: 5.0
|
||||||
|
* Tested up to: 6.4
|
||||||
|
* Requires PHP: 7.4
|
||||||
|
* WC requires at least: 5.0
|
||||||
|
* WC tested up to: 8.0
|
||||||
|
* License: GPL v2 or later
|
||||||
|
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||||
|
* Network: false
|
||||||
|
*
|
||||||
|
* @package YooneSubscriptions
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查WooCommerce是否激活
|
||||||
|
if (!in_array('woocommerce/woocommerce.php', apply_filters('active_plugins', get_option('active_plugins')))) {
|
||||||
|
add_action('admin_notices', function() {
|
||||||
|
echo '<div class="notice notice-error"><p><strong>Yoone Subscriptions</strong> 需要 WooCommerce 插件才能正常工作。</p></div>';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义插件常量
|
||||||
|
define('YOONE_SUBSCRIPTIONS_VERSION', '1.0.0');
|
||||||
|
define('YOONE_SUBSCRIPTIONS_PLUGIN_FILE', __FILE__);
|
||||||
|
define('YOONE_SUBSCRIPTIONS_PLUGIN_BASENAME', plugin_basename(__FILE__));
|
||||||
|
define('YOONE_SUBSCRIPTIONS_PLUGIN_PATH', plugin_dir_path(__FILE__));
|
||||||
|
define('YOONE_SUBSCRIPTIONS_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||||
|
define('YOONE_SUBSCRIPTIONS_INCLUDES_PATH', YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/');
|
||||||
|
define('YOONE_SUBSCRIPTIONS_TEMPLATES_PATH', YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'templates/');
|
||||||
|
define('YOONE_SUBSCRIPTIONS_ASSETS_URL', YOONE_SUBSCRIPTIONS_PLUGIN_URL . 'assets/');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主插件类
|
||||||
|
*/
|
||||||
|
final class Yoone_Subscriptions {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单例实例
|
||||||
|
*/
|
||||||
|
private static $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单例实例
|
||||||
|
*/
|
||||||
|
public static function instance() {
|
||||||
|
if (is_null(self::$instance)) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
private function __construct() {
|
||||||
|
$this->init_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化钩子
|
||||||
|
*/
|
||||||
|
private function init_hooks() {
|
||||||
|
// 插件激活和停用钩子
|
||||||
|
register_activation_hook(__FILE__, array($this, 'activate'));
|
||||||
|
register_deactivation_hook(__FILE__, array($this, 'deactivate'));
|
||||||
|
|
||||||
|
// 插件加载完成后初始化
|
||||||
|
add_action('plugins_loaded', array($this, 'init'), 10);
|
||||||
|
|
||||||
|
// 检查WooCommerce依赖
|
||||||
|
add_action('admin_notices', array($this, 'check_woocommerce_dependency'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件初始化
|
||||||
|
*/
|
||||||
|
public function init() {
|
||||||
|
// 检查WooCommerce是否激活
|
||||||
|
if (!$this->is_woocommerce_active()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载文本域
|
||||||
|
$this->load_textdomain();
|
||||||
|
|
||||||
|
// 包含核心文件
|
||||||
|
$this->includes();
|
||||||
|
|
||||||
|
// 初始化类
|
||||||
|
$this->init_classes();
|
||||||
|
|
||||||
|
// 初始化钩子
|
||||||
|
$this->init_plugin_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查WooCommerce是否激活
|
||||||
|
*/
|
||||||
|
private function is_woocommerce_active() {
|
||||||
|
return class_exists('WooCommerce');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查WooCommerce依赖
|
||||||
|
*/
|
||||||
|
public function check_woocommerce_dependency() {
|
||||||
|
if (!$this->is_woocommerce_active()) {
|
||||||
|
echo '<div class="notice notice-error"><p>';
|
||||||
|
echo __('Yoone Subscriptions 需要 WooCommerce 插件才能正常工作。', 'yoone-subscriptions');
|
||||||
|
echo '</p></div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载文本域
|
||||||
|
*/
|
||||||
|
private function load_textdomain() {
|
||||||
|
load_plugin_textdomain(
|
||||||
|
'yoone-subscriptions',
|
||||||
|
false,
|
||||||
|
dirname(YOONE_SUBSCRIPTIONS_PLUGIN_BASENAME) . '/languages'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 包含核心文件
|
||||||
|
*/
|
||||||
|
private function includes() {
|
||||||
|
// 核心抽象类和接口
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/abstracts/abstract-yoone-data.php';
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/interfaces/interface-yoone-subscription.php';
|
||||||
|
|
||||||
|
// 核心类
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/class-yoone-install.php';
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/class-yoone-ajax.php';
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/class-yoone-api.php';
|
||||||
|
|
||||||
|
// 混装产品层
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/bundle/class-yoone-bundle-product.php';
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/bundle/class-yoone-bundle-cart.php';
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/bundle/class-yoone-bundle-admin.php';
|
||||||
|
|
||||||
|
// 订阅层
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/subscription/class-yoone-subscription.php';
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/subscription/class-yoone-subscription-manager.php';
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/subscription/class-yoone-subscription-scheduler.php';
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/subscription/class-yoone-subscription-admin.php';
|
||||||
|
|
||||||
|
// 支付层
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/payment/class-yoone-moneris-gateway.php';
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/payment/class-yoone-payment-token.php';
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/payment/class-yoone-payment-tokens.php';
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/payment/class-yoone-payment-scheduler.php';
|
||||||
|
|
||||||
|
// 前端类
|
||||||
|
if (!is_admin()) {
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/frontend/class-yoone-frontend.php';
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/frontend/class-yoone-shortcodes.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 管理后台类
|
||||||
|
if (is_admin()) {
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/admin/class-yoone-admin.php';
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/admin/class-yoone-admin-menus.php';
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/admin/class-yoone-admin-settings.php';
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/admin/class-yoone-admin-logs.php';
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/class-yoone-log-analyzer.php';
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'tests/test-config.php';
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'tests/test-subscription-flow.php';
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'tests/class-yoone-test-suite.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具类
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/class-yoone-logger.php';
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/class-yoone-cache.php';
|
||||||
|
require_once YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'includes/class-yoone-helper.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化类
|
||||||
|
*/
|
||||||
|
private function init_classes() {
|
||||||
|
// 初始化前端类
|
||||||
|
new Yoone_Frontend();
|
||||||
|
|
||||||
|
// 初始化后端类
|
||||||
|
if (is_admin()) {
|
||||||
|
new Yoone_Admin();
|
||||||
|
new Yoone_Admin_Logs();
|
||||||
|
new Yoone_Test_Suite();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化AJAX类
|
||||||
|
new Yoone_Ajax();
|
||||||
|
|
||||||
|
// 初始化计划任务类
|
||||||
|
new Yoone_Cron();
|
||||||
|
|
||||||
|
// 注册支付网关
|
||||||
|
add_filter('woocommerce_payment_gateways', array($this, 'add_payment_gateways'));
|
||||||
|
|
||||||
|
// 注册产品类型
|
||||||
|
add_filter('product_type_selector', array($this, 'add_product_types'));
|
||||||
|
add_action('woocommerce_product_data_tabs', array($this, 'add_product_data_tabs'));
|
||||||
|
add_action('woocommerce_product_data_panels', array($this, 'add_product_data_panels'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化插件钩子
|
||||||
|
*/
|
||||||
|
private function init_plugin_hooks() {
|
||||||
|
// 产品类型和数据面板
|
||||||
|
add_filter('product_type_selector', array($this, 'add_product_types'));
|
||||||
|
add_filter('woocommerce_product_data_tabs', array($this, 'add_product_data_tabs'));
|
||||||
|
add_action('woocommerce_product_data_panels', array($this, 'add_product_data_panels'));
|
||||||
|
|
||||||
|
// 订阅状态和端点
|
||||||
|
add_action('init', array($this, 'register_subscription_statuses'));
|
||||||
|
add_action('init', array($this, 'add_subscription_endpoints'));
|
||||||
|
|
||||||
|
// 管理菜单
|
||||||
|
add_action('admin_menu', array($this, 'add_admin_menus'));
|
||||||
|
|
||||||
|
// 订单状态变化处理
|
||||||
|
add_action('woocommerce_order_status_changed', array($this, 'handle_order_status_change'), 10, 4);
|
||||||
|
|
||||||
|
// 产品元数据保存
|
||||||
|
add_action('woocommerce_process_product_meta', array($this, 'save_product_meta'));
|
||||||
|
|
||||||
|
// 我的账户菜单项
|
||||||
|
add_filter('woocommerce_account_menu_items', array($this, 'add_account_menu_items'));
|
||||||
|
add_action('woocommerce_account_subscriptions_endpoint', array($this, 'subscriptions_endpoint_content'));
|
||||||
|
|
||||||
|
// 订阅续费处理
|
||||||
|
add_action('yoone_process_subscription_renewals', array($this, 'process_subscription_renewals'));
|
||||||
|
|
||||||
|
// 清理过期令牌
|
||||||
|
add_action('yoone_cleanup_expired_tokens', array($this, 'cleanup_expired_tokens'));
|
||||||
|
|
||||||
|
// 文本域加载
|
||||||
|
add_action('plugins_loaded', array($this, 'load_textdomain'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加Moneris支付网关
|
||||||
|
*/
|
||||||
|
public function add_moneris_gateway($gateways) {
|
||||||
|
$gateways[] = 'Yoone_Moneris_Gateway';
|
||||||
|
return $gateways;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化核心钩子
|
||||||
|
*/
|
||||||
|
private function init_core_hooks() {
|
||||||
|
// WooCommerce初始化后的钩子
|
||||||
|
add_action('woocommerce_init', array($this, 'woocommerce_init'));
|
||||||
|
|
||||||
|
// 加载模板钩子
|
||||||
|
add_filter('woocommerce_locate_template', array($this, 'locate_template'), 10, 3);
|
||||||
|
|
||||||
|
// 脚本和样式
|
||||||
|
add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts'));
|
||||||
|
add_action('admin_enqueue_scripts', array($this, 'admin_enqueue_scripts'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WooCommerce初始化
|
||||||
|
*/
|
||||||
|
public function woocommerce_init() {
|
||||||
|
// 注册自定义产品类型
|
||||||
|
add_filter('product_type_selector', array($this, 'add_bundle_product_type'));
|
||||||
|
|
||||||
|
// 注册自定义订单状态
|
||||||
|
add_action('init', array($this, 'register_subscription_statuses'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加混装产品类型
|
||||||
|
*/
|
||||||
|
public function add_bundle_product_type($types) {
|
||||||
|
$types['bundle'] = __('混装产品', 'yoone-subscriptions');
|
||||||
|
return $types;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册订阅状态
|
||||||
|
*/
|
||||||
|
public function register_subscription_statuses() {
|
||||||
|
register_post_status('yoone-active', array(
|
||||||
|
'label' => __('激活', 'yoone-subscriptions'),
|
||||||
|
'public' => false,
|
||||||
|
'show_in_admin_status_list' => true,
|
||||||
|
'label_count' => _n_noop('激活 <span class="count">(%s)</span>', '激活 <span class="count">(%s)</span>', 'yoone-subscriptions')
|
||||||
|
));
|
||||||
|
|
||||||
|
register_post_status('yoone-paused', array(
|
||||||
|
'label' => __('暂停', 'yoone-subscriptions'),
|
||||||
|
'public' => false,
|
||||||
|
'show_in_admin_status_list' => true,
|
||||||
|
'label_count' => _n_noop('暂停 <span class="count">(%s)</span>', '暂停 <span class="count">(%s)</span>', 'yoone-subscriptions')
|
||||||
|
));
|
||||||
|
|
||||||
|
register_post_status('yoone-cancelled', array(
|
||||||
|
'label' => __('已取消', 'yoone-subscriptions'),
|
||||||
|
'public' => false,
|
||||||
|
'show_in_admin_status_list' => true,
|
||||||
|
'label_count' => _n_noop('已取消 <span class="count">(%s)</span>', '已取消 <span class="count">(%s)</span>', 'yoone-subscriptions')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定位模板文件
|
||||||
|
*/
|
||||||
|
public function locate_template($template, $template_name, $template_path) {
|
||||||
|
if (strpos($template_name, 'yoone-') === 0) {
|
||||||
|
$plugin_template = YOONE_SUBSCRIPTIONS_PLUGIN_PATH . 'templates/' . $template_name;
|
||||||
|
if (file_exists($plugin_template)) {
|
||||||
|
return $plugin_template;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $template;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载前端脚本和样式
|
||||||
|
*/
|
||||||
|
public function enqueue_scripts() {
|
||||||
|
wp_enqueue_style(
|
||||||
|
'yoone-subscriptions-frontend',
|
||||||
|
YOONE_SUBSCRIPTIONS_PLUGIN_URL . 'assets/css/frontend.css',
|
||||||
|
array(),
|
||||||
|
YOONE_SUBSCRIPTIONS_VERSION
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_enqueue_script(
|
||||||
|
'yoone-subscriptions-frontend',
|
||||||
|
YOONE_SUBSCRIPTIONS_PLUGIN_URL . 'assets/js/frontend.js',
|
||||||
|
array('jquery', 'wc-add-to-cart'),
|
||||||
|
YOONE_SUBSCRIPTIONS_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// 本地化脚本
|
||||||
|
wp_localize_script('yoone-subscriptions-frontend', 'yoone_ajax', array(
|
||||||
|
'ajax_url' => admin_url('admin-ajax.php'),
|
||||||
|
'nonce' => wp_create_nonce('yoone_nonce'),
|
||||||
|
'i18n' => array(
|
||||||
|
'loading' => __('加载中...', 'yoone-subscriptions'),
|
||||||
|
'error' => __('发生错误,请重试', 'yoone-subscriptions')
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载后台脚本和样式
|
||||||
|
*/
|
||||||
|
public function admin_enqueue_scripts($hook) {
|
||||||
|
// 只在相关页面加载
|
||||||
|
if (strpos($hook, 'yoone') === false && !in_array($hook, array('post.php', 'post-new.php', 'edit.php'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_enqueue_style(
|
||||||
|
'yoone-subscriptions-admin',
|
||||||
|
YOONE_SUBSCRIPTIONS_PLUGIN_URL . 'assets/css/admin.css',
|
||||||
|
array(),
|
||||||
|
YOONE_SUBSCRIPTIONS_VERSION
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_enqueue_script(
|
||||||
|
'yoone-subscriptions-admin',
|
||||||
|
YOONE_SUBSCRIPTIONS_PLUGIN_URL . 'assets/js/admin.js',
|
||||||
|
array('jquery', 'wc-enhanced-select'),
|
||||||
|
YOONE_SUBSCRIPTIONS_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_localize_script('yoone-subscriptions-admin', 'yoone_admin', array(
|
||||||
|
'ajax_url' => admin_url('admin-ajax.php'),
|
||||||
|
'nonce' => wp_create_nonce('yoone_admin_nonce')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件激活
|
||||||
|
*/
|
||||||
|
public function activate() {
|
||||||
|
// 检查WooCommerce
|
||||||
|
if (!$this->is_woocommerce_active()) {
|
||||||
|
deactivate_plugins(YOONE_SUBSCRIPTIONS_PLUGIN_BASENAME);
|
||||||
|
wp_die(__('Yoone Subscriptions 需要 WooCommerce 插件才能激活。', 'yoone-subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建数据库表
|
||||||
|
Yoone_Install::activate();
|
||||||
|
|
||||||
|
// 设置默认选项
|
||||||
|
$this->set_default_options();
|
||||||
|
|
||||||
|
// 刷新重写规则
|
||||||
|
flush_rewrite_rules();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件停用
|
||||||
|
*/
|
||||||
|
public function deactivate() {
|
||||||
|
// 清理定时任务
|
||||||
|
wp_clear_scheduled_hook('yoone_process_subscriptions');
|
||||||
|
wp_clear_scheduled_hook('yoone_retry_failed_payments');
|
||||||
|
|
||||||
|
// 刷新重写规则
|
||||||
|
flush_rewrite_rules();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置默认选项
|
||||||
|
*/
|
||||||
|
private function set_default_options() {
|
||||||
|
$defaults = array(
|
||||||
|
'yoone_bundle_min_quantity' => 10,
|
||||||
|
'yoone_subscription_billing_cycle' => 'monthly',
|
||||||
|
'yoone_payment_retry_attempts' => 3,
|
||||||
|
'yoone_payment_retry_interval' => 24
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($defaults as $option => $value) {
|
||||||
|
if (get_option($option) === false) {
|
||||||
|
add_option($option, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件主实例
|
||||||
|
*/
|
||||||
|
function yoone_subscriptions() {
|
||||||
|
return Yoone_Subscriptions::instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化插件
|
||||||
|
yoone_subscriptions();
|
||||||
Loading…
Reference in New Issue