feat: 添加订阅系统核心组件和测试框架

新增支付网关接口、订阅接口和抽象数据类
添加测试配置、支付集成测试和订阅流程测试
实现日志系统和混装产品前端模板
完善README文档说明系统架构和功能
This commit is contained in:
tikkhun 2025-11-04 10:33:45 +08:00
commit b676ae7469
52 changed files with 22183 additions and 0 deletions

210
README.md Normal file
View File

@ -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/实际开发/订阅制` 下的规范文档,并在代码中搜索对应模块的实现以获取最新内容。

855
assets/css/yoone-admin.css Normal file
View File

@ -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;
}
}

View File

@ -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;
}
}

501
assets/js/admin-logs.js Normal file
View File

@ -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);

View File

@ -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);

733
assets/js/yoone-admin.js Normal file
View File

@ -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);

379
assets/js/yoone-frontend.js Normal file
View File

@ -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);

View File

@ -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);
}

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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>';
}
}
}
}

View File

@ -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>';
}
}
}
}

View File

@ -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();

View File

@ -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));
}
}

View File

@ -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>';
}
}
}

View File

@ -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 连接测试失败');
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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';
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}
}

276
run-tests.php Normal file
View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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' => __('&laquo;'),
'next_text' => __('&raquo;'),
'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>

View File

@ -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>

View File

@ -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' => __('&laquo;'),
'next_text' => __('&raquo;'),
'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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">&ndash;</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;
}
}
?>

View File

@ -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>

View File

@ -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">
&larr; <?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;
}
}
?>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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();
}

View File

@ -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;
}
}

223
tests/test-config.php Normal file
View File

@ -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()
);
}
}

418
tests/test-cron-jobs.php Normal file
View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

458
yoone-subscriptions.php Normal file
View File

@ -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();