commit b676ae7469d430ce5d988a21cd2b5a08f9b8507a Author: tikkhun Date: Tue Nov 4 10:33:45 2025 +0800 feat: 添加订阅系统核心组件和测试框架 新增支付网关接口、订阅接口和抽象数据类 添加测试配置、支付集成测试和订阅流程测试 实现日志系统和混装产品前端模板 完善README文档说明系统架构和功能 diff --git a/README.md b/README.md new file mode 100644 index 0000000..aa883be --- /dev/null +++ b/README.md @@ -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/实际开发/订阅制` 下的规范文档,并在代码中搜索对应模块的实现以获取最新内容。 \ No newline at end of file diff --git a/assets/css/yoone-admin.css b/assets/css/yoone-admin.css new file mode 100644 index 0000000..717b8ec --- /dev/null +++ b/assets/css/yoone-admin.css @@ -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; + } +} \ No newline at end of file diff --git a/assets/css/yoone-frontend.css b/assets/css/yoone-frontend.css new file mode 100644 index 0000000..e59fd0a --- /dev/null +++ b/assets/css/yoone-frontend.css @@ -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; + } +} \ No newline at end of file diff --git a/assets/js/admin-logs.js b/assets/js/admin-logs.js new file mode 100644 index 0000000..97ab868 --- /dev/null +++ b/assets/js/admin-logs.js @@ -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 = $('').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 = $('').html( + '
' + + '

详细信息:

' + + '
' + this.formatJSON(context) + '
' + + '
' + ); + + $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 = '
日志级别分布
'; + + Object.keys(levels).forEach(function(level) { + var count = levels[level]; + var percentage = total > 0 ? (count / total * 100).toFixed(1) : 0; + + html += '
' + + '' + level + '' + + '
' + + '
' + + '
' + + '' + count + ' (' + percentage + '%)' + + '
'; + }); + + $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 = '
24小时日志趋势
'; + + Object.keys(hourlyStats).forEach(function(hour) { + var count = hourlyStats[hour]; + var height = maxCount > 0 ? (count / maxCount * 100) : 0; + + html += '
' + + '
' + + '' + hour + '' + + '
'; + }); + + html += '
'; + $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 = $('
').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)); + }); + + // 添加样式 + $(' + 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 '

' . __('设置已保存', 'yoone-subscriptions') . '

'; + } + + ?> +
+ + + + + + + + + + + + + + + +
+ +

+
+ +

+
+ +

+
+ +

+ +

+
+ ' . __('没有找到日志记录', 'yoone-subscriptions') . ''; + return; + } + + ?> + + + + + + + + + + + + + + + + + + + +
+ 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(); \ No newline at end of file diff --git a/includes/admin/class-yoone-admin.php b/includes/admin/class-yoone-admin.php new file mode 100644 index 0000000..aeaffc5 --- /dev/null +++ b/includes/admin/class-yoone-admin.php @@ -0,0 +1,754 @@ +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 '
'; + echo '

' . __('订阅管理', 'yoone-subscriptions') . '

'; + + $list_table->display(); + + echo '
'; + } + + /** + * 套装管理页面 + */ + 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 '
'; + echo '

' . __('套装管理', 'yoone-subscriptions'); + echo '' . __('添加套装', 'yoone-subscriptions') . ''; + echo '

'; + + $list_table->display(); + + echo '
'; + } + + /** + * 套装编辑页面 + */ + 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 '
'; + + 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 '
'; + echo '

' . __('套装产品', 'yoone-subscriptions') . '

'; + echo '
'; + echo '
'; + echo ''; + echo '
'; + echo '
'; + + echo '
'; + + // 订阅设置面板 + echo '
'; + + 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 '
'; + } + + /** + * 保存产品数据 + */ + 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 '

' . __('此订单没有相关订阅', 'yoone-subscriptions') . '

'; + return; + } + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + foreach ($subscriptions as $subscription) { + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo ''; + echo '
' . __('订阅ID', 'yoone-subscriptions') . '' . __('状态', 'yoone-subscriptions') . '' . __('金额', 'yoone-subscriptions') . '' . __('下次付款', 'yoone-subscriptions') . '' . __('操作', 'yoone-subscriptions') . '
#' . $subscription->id . '' . ucfirst($subscription->status) . '' . wc_price($subscription->total) . '' . ($subscription->next_payment_date ? date('Y-m-d', strtotime($subscription->next_payment_date)) : '-') . ''; + echo '' . __('查看', 'yoone-subscriptions') . ''; + echo '
'; + } + + /** + * 显示订单订阅信息 + */ + 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 '
'; + echo '

' . __('此订单包含订阅产品', 'yoone-subscriptions') . '

'; + echo '
'; + } + } + + /* + |-------------------------------------------------------------------------- + | 产品列表自定义列 + |-------------------------------------------------------------------------- + */ + + /** + * 添加产品列 + */ + 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 ''; + } else { + echo ''; + } + break; + + case 'yoone_bundle': + $product = wc_get_product($post_id); + if ($product && $product->get_type() === 'yoone_bundle') { + echo ''; + } else { + echo ''; + } + 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; + } +} \ No newline at end of file diff --git a/includes/admin/class-yoone-bundles-list-table.php b/includes/admin/class-yoone-bundles-list-table.php new file mode 100644 index 0000000..0d705c4 --- /dev/null +++ b/includes/admin/class-yoone-bundles-list-table.php @@ -0,0 +1,389 @@ + 'bundle', + 'plural' => 'bundles', + 'ajax' => false, + )); + } + + /** + * 获取列 + */ + public function get_columns() { + return array( + 'cb' => '', + '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 '' . esc_html($item->name) . ''; + + 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('', $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( + '%s', + $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'] = '' . __('编辑', 'yoone-subscriptions') . ''; + + 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'] = '' . __('停用', 'yoone-subscriptions') . ''; + } else { + $activate_url = wp_nonce_url( + admin_url('admin.php?page=yoone-bundles&action=activate&id=' . $item->id), + 'activate_bundle_' . $item->id + ); + $actions['activate'] = '' . __('激活', 'yoone-subscriptions') . ''; + } + + $duplicate_url = wp_nonce_url( + admin_url('admin.php?page=yoone-bundles&action=duplicate&id=' . $item->id), + 'duplicate_bundle_' . $item->id + ); + $actions['duplicate'] = '' . __('复制', 'yoone-subscriptions') . ''; + + $delete_url = wp_nonce_url( + admin_url('admin.php?page=yoone-bundles&action=delete&id=' . $item->id), + 'delete_bundle_' . $item->id + ); + $actions['delete'] = '' . __('删除', 'yoone-subscriptions') . ''; + + 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( + '%s (%d)', + 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( + '%s (%d)', + 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 '
'; + echo '

' . sprintf($messages[$action], $processed) . '

'; + echo '
'; + } + } + } +} \ No newline at end of file diff --git a/includes/admin/class-yoone-subscriptions-list-table.php b/includes/admin/class-yoone-subscriptions-list-table.php new file mode 100644 index 0000000..e24c946 --- /dev/null +++ b/includes/admin/class-yoone-subscriptions-list-table.php @@ -0,0 +1,409 @@ + 'subscription', + 'plural' => 'subscriptions', + 'ajax' => false, + )); + } + + /** + * 获取列 + */ + public function get_columns() { + return array( + 'cb' => '', + '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 '' . $item->display_name . '
' . $item->user_email . ''; + + 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('', $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( + '%s', + $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'] = '' . __('编辑', 'yoone-subscriptions') . ''; + + 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'] = '' . __('暂停', 'yoone-subscriptions') . ''; + } + + 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'] = '' . __('恢复', 'yoone-subscriptions') . ''; + } + + 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'] = '' . __('取消', 'yoone-subscriptions') . ''; + } + + $delete_url = wp_nonce_url( + admin_url('admin.php?page=yoone-subscriptions&action=delete&id=' . $item->id), + 'delete_subscription_' . $item->id + ); + $actions['delete'] = '' . __('删除', 'yoone-subscriptions') . ''; + + 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( + '%s (%d)', + 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( + '%s (%d)', + 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 '
'; + echo '

' . sprintf($messages[$action], $processed) . '

'; + echo '
'; + } + } + } +} \ No newline at end of file diff --git a/includes/bundle/class-yoone-bundle-frontend.php b/includes/bundle/class-yoone-bundle-frontend.php new file mode 100644 index 0000000..c7c0faf --- /dev/null +++ b/includes/bundle/class-yoone-bundle-frontend.php @@ -0,0 +1,318 @@ +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(); \ No newline at end of file diff --git a/includes/bundle/class-yoone-bundle.php b/includes/bundle/class-yoone-bundle.php new file mode 100644 index 0000000..817b21e --- /dev/null +++ b/includes/bundle/class-yoone-bundle.php @@ -0,0 +1,613 @@ + '', + '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)); + } +} \ No newline at end of file diff --git a/includes/class-yoone-admin.php b/includes/class-yoone-admin.php new file mode 100644 index 0000000..fc1405f --- /dev/null +++ b/includes/class-yoone-admin.php @@ -0,0 +1,870 @@ +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 '

' . __('订阅已更新', 'yoone-subscriptions') . '

'; + }); + } + } + + /** + * 混装产品页面 + */ + 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 '

' . __('设置已保存', 'yoone-subscriptions') . '

'; + }); + } + + /** + * 报告页面 + */ + 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 '
'; + + 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 '
'; + echo '

' . __('订阅周期', 'yoone-subscriptions') . '

'; + + $periods = get_post_meta($post->ID, '_yoone_subscription_periods', true); + if (!is_array($periods)) { + $periods = array(); + } + + echo '
'; + foreach ($periods as $key => $period) { + $this->render_subscription_period_row($key, $period); + } + echo '
'; + + echo ''; + echo '
'; + + echo '
'; + + // 混装设置面板 + echo '
'; + + 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 '
'; + echo '

' . __('混装项目', 'yoone-subscriptions') . '

'; + echo '
'; + echo ''; + echo '
'; + + echo '
'; + } + + /** + * 渲染订阅周期行 + */ + private function render_subscription_period_row($key, $period) { + echo '
'; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo '
'; + } + + /** + * 保存产品元数据 + */ + 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 '

' . __('此订单没有关联的订阅', 'yoone-subscriptions') . '

'; + return; + } + + foreach ($subscriptions as $subscription) { + echo '
'; + echo '

' . __('订阅ID:', 'yoone-subscriptions') . ' ' . $subscription->id . '

'; + echo '

' . __('状态:', 'yoone-subscriptions') . ' ' . Yoone_Frontend::get_subscription_status_label($subscription->status) . '

'; + echo '

' . __('下次付款:', 'yoone-subscriptions') . ' ' . $subscription->next_payment_date . '

'; + echo '

' . __('查看详情', 'yoone-subscriptions') . '

'; + echo '
'; + } + } + + /** + * 处理订单状态变化 + */ + 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' ? '' : '—'; + break; + case 'yoone_bundle': + $product = wc_get_product($post_id); + echo $product && $product->get_type() === 'yoone_bundle' ? '' : '—'; + 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 '

'; + echo __('Yoone Subscriptions 需要 WooCommerce 插件才能正常工作。', 'yoone-subscriptions'); + echo '

'; + } + } +} \ No newline at end of file diff --git a/includes/class-yoone-ajax.php b/includes/class-yoone-ajax.php new file mode 100644 index 0000000..d92467a --- /dev/null +++ b/includes/class-yoone-ajax.php @@ -0,0 +1,384 @@ +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 连接测试失败'); + } + } +} \ No newline at end of file diff --git a/includes/class-yoone-api.php b/includes/class-yoone-api.php new file mode 100644 index 0000000..356dd37 --- /dev/null +++ b/includes/class-yoone-api.php @@ -0,0 +1,533 @@ + 'GET', + 'callback' => array($this, 'get_subscriptions'), + 'permission_callback' => array($this, 'check_permissions'), + )); + + register_rest_route(self::API_NAMESPACE, '/subscriptions/(?P\d+)', array( + 'methods' => 'GET', + 'callback' => array($this, 'get_subscription'), + 'permission_callback' => array($this, 'check_permissions'), + )); + + register_rest_route(self::API_NAMESPACE, '/subscriptions/(?P\d+)', array( + 'methods' => 'PUT', + 'callback' => array($this, 'update_subscription'), + 'permission_callback' => array($this, 'check_permissions'), + )); + + register_rest_route(self::API_NAMESPACE, '/subscriptions/(?P\d+)/pause', array( + 'methods' => 'POST', + 'callback' => array($this, 'pause_subscription'), + 'permission_callback' => array($this, 'check_permissions'), + )); + + register_rest_route(self::API_NAMESPACE, '/subscriptions/(?P\d+)/resume', array( + 'methods' => 'POST', + 'callback' => array($this, 'resume_subscription'), + 'permission_callback' => array($this, 'check_permissions'), + )); + + register_rest_route(self::API_NAMESPACE, '/subscriptions/(?P\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\d+)', array( + 'methods' => 'GET', + 'callback' => array($this, 'get_bundle'), + 'permission_callback' => array($this, 'check_permissions'), + )); + + register_rest_route(self::API_NAMESPACE, '/bundles/(?P\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\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); + } +} \ No newline at end of file diff --git a/includes/class-yoone-cron.php b/includes/class-yoone-cron.php new file mode 100644 index 0000000..f7c9260 --- /dev/null +++ b/includes/class-yoone-cron.php @@ -0,0 +1,363 @@ +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(); + } +} \ No newline at end of file diff --git a/includes/class-yoone-frontend.php b/includes/class-yoone-frontend.php new file mode 100644 index 0000000..9c59ba6 --- /dev/null +++ b/includes/class-yoone-frontend.php @@ -0,0 +1,454 @@ +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 .= '
' . __('混装产品', 'yoone-subscriptions') . ''; + } + + if (isset($cart_item['yoone_subscription_period'])) { + $period = $cart_item['yoone_subscription_period']; + $name .= '
' . sprintf(__('订阅周期: %s', 'yoone-subscriptions'), $period) . ''; + } + + 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 '

' . __('请先登录查看订阅。', 'yoone-subscriptions') . '

'; + } + + 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'; + } +} \ No newline at end of file diff --git a/includes/class-yoone-helper.php b/includes/class-yoone-helper.php new file mode 100644 index 0000000..8380871 --- /dev/null +++ b/includes/class-yoone-helper.php @@ -0,0 +1,372 @@ +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; + } +} \ No newline at end of file diff --git a/includes/class-yoone-install.php b/includes/class-yoone-install.php new file mode 100644 index 0000000..556690e --- /dev/null +++ b/includes/class-yoone-install.php @@ -0,0 +1,292 @@ +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 . ' (%s)', $label . ' (%s)', 'yoone-subscriptions') + )); + } + } + + /** + * 检查数据库版本 + */ + public static function check_version() { + if (get_option('yoone_subscriptions_db_version') !== self::DB_VERSION) { + self::create_tables(); + } + } +} \ No newline at end of file diff --git a/includes/class-yoone-log-analyzer.php b/includes/class-yoone-log-analyzer.php new file mode 100644 index 0000000..4cd6f82 --- /dev/null +++ b/includes/class-yoone-log-analyzer.php @@ -0,0 +1,488 @@ + 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 = 'Yoone Subscriptions Log Analysis Report'; + $html .= ''; + + $html .= '

Yoone Subscriptions 日志分析报告

'; + $html .= '

生成时间: ' . $data['generated_at'] . '

'; + $html .= '

分析周期: ' . $data['period_days'] . ' 天

'; + + $html .= '
'; + $html .= '健康评分: ' . $data['health']['score'] . '/100 (' . $data['health']['text'] . ')'; + $html .= '
'; + + if (!empty($data['health']['issues'])) { + $html .= '

发现的问题

    '; + foreach ($data['health']['issues'] as $issue) { + $html .= '
  • ' . htmlspecialchars($issue) . '
  • '; + } + $html .= '
'; + } + + if (!empty($data['health']['recommendations'])) { + $html .= '

建议

    '; + foreach ($data['health']['recommendations'] as $rec) { + $html .= '
  • ' . htmlspecialchars($rec) . '
  • '; + } + $html .= '
'; + } + + if (!empty($data['anomalies'])) { + $html .= '

异常检测

'; + $html .= ''; + foreach ($data['anomalies'] as $anomaly) { + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + $html .= '
类型严重程度描述时间
' . htmlspecialchars($anomaly['type']) . '' . htmlspecialchars($anomaly['severity']) . '' . htmlspecialchars($anomaly['message']) . '' . htmlspecialchars($anomaly['timestamp']) . '
'; + } + + $html .= ''; + + return $html; + } +} \ No newline at end of file diff --git a/includes/class-yoone-logger.php b/includes/class-yoone-logger.php new file mode 100644 index 0000000..bcae068 --- /dev/null +++ b/includes/class-yoone-logger.php @@ -0,0 +1,314 @@ +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; + } +} \ No newline at end of file diff --git a/includes/frontend/class-yoone-frontend.php b/includes/frontend/class-yoone-frontend.php new file mode 100644 index 0000000..2703b04 --- /dev/null +++ b/includes/frontend/class-yoone-frontend.php @@ -0,0 +1,524 @@ +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 .= '
'; + $name .= '' . __('套装产品', 'yoone-subscriptions') . ''; + + if (!empty($bundle_data['items'])) { + $name .= '
    '; + foreach ($bundle_data['items'] as $item) { + $item_product = wc_get_product($item['product_id']); + if ($item_product) { + $name .= '
  • ' . $item_product->get_name() . ' × ' . $item['quantity'] . '
  • '; + } + } + $name .= '
'; + } + + if (isset($bundle_data['savings']) && $bundle_data['savings'] > 0) { + $name .= '' . sprintf(__('节省: %s', 'yoone-subscriptions'), wc_price($bundle_data['savings'])) . ''; + } + + $name .= '
'; + } + + 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 .= '
'; + $price .= '' . $this->format_subscription_string($subscription_data) . ''; + $price .= '
'; + } + + 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 '
'; + echo '

' . __('订阅信息', 'yoone-subscriptions') . '

'; + echo '

' . __('您的订单包含订阅产品,将会自动续费。', 'yoone-subscriptions') . '

'; + + 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 '
'; + echo '' . $product->get_name() . '
'; + echo '' . $this->format_subscription_string($subscription_data) . ''; + echo '
'; + } + } + + echo '
'; + } + + /* + |-------------------------------------------------------------------------- + | 我的账户功能 + |-------------------------------------------------------------------------- + */ + + /** + * 添加订阅菜单项 + */ + 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; + } +} \ No newline at end of file diff --git a/includes/interfaces/interface-yoone-payment-gateway.php b/includes/interfaces/interface-yoone-payment-gateway.php new file mode 100644 index 0000000..aa1243b --- /dev/null +++ b/includes/interfaces/interface-yoone-payment-gateway.php @@ -0,0 +1,101 @@ +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 '

' . __('测试模式已启用。您可以使用测试卡号:4242424242424242', 'yoone-subscriptions') . '

'; + } + + echo '
'; + + // 如果支持令牌化,显示保存的支付方式 + if ($this->supports('tokenization') && is_checkout()) { + $this->tokenization_script(); + $this->saved_payment_methods(); + } + + echo '
+ + +
'; + + echo '
+ + +
'; + + echo '
+ + +
'; + + // 保存支付方式选项 + if ($this->supports('tokenization') && is_checkout() && !is_add_payment_method_page()) { + echo '
+ + +
'; + } + + echo '
'; + } + + /** + * 验证字段 + */ + 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 .= ''; + $xml .= '' . htmlspecialchars($data['store_id']) . ''; + $xml .= '' . htmlspecialchars($data['api_token']) . ''; + $xml .= '<' . $endpoint . '>'; + + foreach ($data as $key => $value) { + if ($key !== 'store_id' && $key !== 'api_token') { + $xml .= '<' . $key . '>' . htmlspecialchars($value) . ''; + } + } + + $xml .= ''; + $xml .= ''; + + 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); + } +} \ No newline at end of file diff --git a/includes/payment/class-yoone-payment-token.php b/includes/payment/class-yoone-payment-token.php new file mode 100644 index 0000000..b73705b --- /dev/null +++ b/includes/payment/class-yoone-payment-token.php @@ -0,0 +1,534 @@ + 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; + } +} \ No newline at end of file diff --git a/includes/subscription/class-yoone-subscription.php b/includes/subscription/class-yoone-subscription.php new file mode 100644 index 0000000..7e844bd --- /dev/null +++ b/includes/subscription/class-yoone-subscription.php @@ -0,0 +1,871 @@ + 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; + } + } + } +} \ No newline at end of file diff --git a/run-tests.php b/run-tests.php new file mode 100644 index 0000000..445fe1c --- /dev/null +++ b/run-tests.php @@ -0,0 +1,276 @@ + + + + + + + Yoone Subscriptions 测试运行器 + + + +
+

🧪 Yoone Subscriptions 测试运行器

+ + '; + echo '

📋 测试环境信息

'; + echo ''; + + 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 ""; + } + } else { + echo ''; + } + + echo '
{$label}{$value}
无法获取环境信息 - Yoone_Test_Config类不存在
'; + echo '
'; + + // 运行测试的按钮 + echo '
'; + echo '

🚀 运行测试

'; + echo '

选择要运行的测试类型:

'; + + $test_types = array( + 'subscription' => '订阅功能测试', + 'payment' => '支付集成测试', + 'bundle' => '捆绑产品测试', + 'cron' => '定时任务测试', + 'all' => '运行所有测试' + ); + + foreach ($test_types as $type => $label) { + echo "{$label}"; + } + + echo '
'; + + // 处理测试运行 + if (isset($_GET['run_test'])) { + $test_type = sanitize_text_field($_GET['run_test']); + + echo '
'; + echo "

🔍 运行 {$test_types[$test_type]} 结果

"; + + if (class_exists('Yoone_Test_Suite')) { + $test_suite = new Yoone_Test_Suite(); + + try { + switch ($test_type) { + case 'subscription': + echo '

订阅功能测试

'; + $results = $test_suite->run_test_suite('subscription'); + break; + + case 'payment': + echo '

支付集成测试

'; + $results = $test_suite->run_test_suite('payment'); + break; + + case 'bundle': + echo '

捆绑产品测试

'; + $results = $test_suite->run_test_suite('bundle'); + break; + + case 'cron': + echo '

定时任务测试

'; + $results = $test_suite->run_test_suite('cron'); + break; + + case 'all': + echo '

运行所有测试

'; + $results = $test_suite->run_test_suite('all'); + break; + } + + // 显示测试结果 + if (empty($results['tests'])) { + echo '
没有测试结果
'; + } else { + // 显示摘要 + $summary = $results['summary']; + echo "
"; + echo "测试摘要:
"; + echo "总计: {$summary['total']} | 通过: {$summary['passed']} | 失败: {$summary['failed']} | 跳过: {$summary['skipped']}"; + echo "
"; + + // 显示详细结果 + foreach ($results['tests'] as $result) { + $class = $result['status'] === 'passed' ? 'success' : + ($result['status'] === 'failed' ? 'error' : 'warning'); + + echo "
"; + echo "{$result['name']}
"; + echo "状态: " . ($result['status'] === 'passed' ? '✅ 通过' : + ($result['status'] === 'failed' ? '❌ 失败' : '⚠️ 警告')) . "
"; + echo "描述: {$result['description']}"; + + if (!empty($result['error'])) { + echo "
错误: {$result['error']}"; + } + + if (!empty($result['result']) && $result['result'] !== true) { + echo "
结果: {$result['result']}"; + } + + echo "
"; + } + } + + } catch (Exception $e) { + echo "
"; + echo "测试运行失败
"; + echo "错误: " . $e->getMessage(); + echo "
"; + } + + } else { + echo '
Yoone_Test_Suite类不存在,请确保插件正确安装
'; + } + + echo '
'; + } + ?> + +
+

📚 测试说明

+
    +
  • 订阅功能测试: 测试订阅的创建、激活、暂停、恢复、续费和取消流程
  • +
  • 支付集成测试: 测试Moneris支付网关集成和支付令牌管理
  • +
  • 捆绑产品测试: 测试产品捆绑功能和价格计算
  • +
  • 定时任务测试: 测试自动续费、过期处理等定时任务
  • +
  • 运行所有测试: 执行完整的测试套件
  • +
+ +

⚠️ 注意事项

+
    +
  • 测试会创建临时数据,测试完成后会自动清理
  • +
  • 建议在开发环境中运行测试,避免影响生产数据
  • +
  • 某些测试可能需要有效的支付配置才能完全通过
  • +
+
+ +
+

🔧 快速操作

+ 刷新页面 + 管理后台测试 + 查看日志 +
+ + + \ No newline at end of file diff --git a/templates/admin/bundle-add.php b/templates/admin/bundle-add.php new file mode 100644 index 0000000..41d2c39 --- /dev/null +++ b/templates/admin/bundle-add.php @@ -0,0 +1,260 @@ + 'publish', + 'limit' => -1, + 'orderby' => 'title', + 'order' => 'ASC' +)); +?> + +
+

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

+
+ + + +

+
+ + + +

+
+ + + +

+
+ + + +

+
+ +

+

+ +
+ + +
+ +
+ +

+ +

+
+ + +
+ +

+ + + +

+
+ + + + \ No newline at end of file diff --git a/templates/admin/bundle-edit.php b/templates/admin/bundle-edit.php new file mode 100644 index 0000000..f268720 --- /dev/null +++ b/templates/admin/bundle-edit.php @@ -0,0 +1,327 @@ + 'publish', + 'limit' => -1, + 'orderby' => 'title', + 'order' => 'ASC' +)); +?> + +
+

id); ?>

+ +
+ id); ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

+
+ + + +

+
+ + + +

+
+ + + +

+
+ + + +

+
+ + + +

+
+ + + +

+
+ + + +

+
+ +

+

+ +
+ + +
+ + $item): ?> + product_id); ?> +
+ + + + + + + + + + +
+ + + + + + + + + +
+
+ + +
+ +

+ +

+
+ + +
+ +

+ + + +

+
+ + + + \ No newline at end of file diff --git a/templates/admin/bundle-list.php b/templates/admin/bundle-list.php new file mode 100644 index 0000000..36d54c3 --- /dev/null +++ b/templates/admin/bundle-list.php @@ -0,0 +1,298 @@ +delete( + $wpdb->prefix . 'yoone_bundle_items', + array('bundle_id' => $bundle_id) + ); + // 删除混装产品 + $wpdb->delete( + $wpdb->prefix . 'yoone_bundles', + array('id' => $bundle_id) + ); + } + echo '

' . sprintf(__('已删除 %d 个混装产品', 'yoone-subscriptions'), count($bundle_ids)) . '

'; + } +} + +// 获取混装产品列表 +$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" +); +?> + +
+

+ + + + + +
+

+
+ + + +
+

+
+ + + +
+ + + +
+ + + + + +
+ + +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + get_var($wpdb->prepare( + "SELECT COUNT(*) FROM {$wpdb->prefix}yoone_bundle_items WHERE bundle_id = %d", + $bundle->id + )); + ?> + + + + + + + + + + + + + + +
+ + +
+ + + #id; ?> + + + + name); ?> + + +
+ + + + | + + + + + + +
+
+ description, 10)); ?> + + discount_type === 'percentage'): ?> + discount_value; ?>% + discount_type === 'fixed'): ?> + discount_value); ?> + + + + + + + + status === 'active' ? __('启用', 'yoone-subscriptions') : __('禁用', 'yoone-subscriptions'); ?> + + + created_at)); ?> + + + + +
+
+ + + 1): ?> +
+
+ add_query_arg('paged', '%#%'), + 'format' => '', + 'prev_text' => __('«'), + 'next_text' => __('»'), + 'total' => $total_pages, + 'current' => $current_page + ); + echo paginate_links($pagination_args); + ?> +
+
+ +
+ + + + \ No newline at end of file diff --git a/templates/admin/subscription-edit.php b/templates/admin/subscription-edit.php new file mode 100644 index 0000000..5028ce8 --- /dev/null +++ b/templates/admin/subscription-edit.php @@ -0,0 +1,229 @@ + + +
+

id); ?>

+ +
+ id); ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +

+
+ + + user_id); + if ($user) { + echo ''; + echo esc_html($user->display_name . ' (' . $user->user_email . ')'); + echo ''; + } else { + _e('用户不存在', 'yoone-subscriptions'); + } + ?> +
+ + + product_id); + if ($product) { + echo ''; + echo esc_html($product->get_name()); + echo ''; + } else { + _e('产品不存在', 'yoone-subscriptions'); + } + ?> +
+ + + +

+
+ + + +

+
+ + + +

+
+ + + +

+
+ + + +

+
+ + + +

+
+ + + +

+
+ + + +

+
+ + + +

+
+ + +
+ + +

+ + 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 + )); + ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#ID; ?>post_date)); ?>order_total); ?>order_status); ?> + + + +
+ +

+ + + +

+
\ No newline at end of file diff --git a/templates/admin/subscription-list.php b/templates/admin/subscription-list.php new file mode 100644 index 0000000..e19781c --- /dev/null +++ b/templates/admin/subscription-list.php @@ -0,0 +1,215 @@ +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" +); +?> + +
+

+ + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ #id; ?> + + user_id): ?> + + display_name ?: $subscription->user_email); ?> + +
user_email); ?> + + + +
+ product_id): ?> + + product_name ?: __('未知产品', 'yoone-subscriptions')); ?> + + + + + + + status); ?> + + + billing_period, $subscription->billing_interval); ?> + + next_payment_date && $subscription->status === 'active'): ?> + next_payment_date)); ?> + + + + + created_at)); ?> + + + + +
+ + + 1): ?> +
+
+ add_query_arg('paged', '%#%'), + 'format' => '', + 'prev_text' => __('«'), + 'next_text' => __('»'), + 'total' => $total_pages, + 'current' => $current_page + ); + echo paginate_links($pagination_args); + ?> +
+
+ +
+ + \ No newline at end of file diff --git a/templates/bundle/bundle-options.php b/templates/bundle/bundle-options.php new file mode 100644 index 0000000..29bbd5f --- /dev/null +++ b/templates/bundle/bundle-options.php @@ -0,0 +1,267 @@ +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; +} +?> + +
+

+ +
+

get_description()); ?>

+ + get_discount_value() > 0): ?> +
+ get_discount_type() === 'percentage'): ?> + get_discount_value()); ?> + + get_discount_value())); ?> + +
+ +
+ +
+ + + +
+
+ get_image('thumbnail'); ?> +
+ +
+

get_name()); ?>

+

get_price_html(); ?>

+ + +

+ +
+ +
+ + +
+ +
+ + + get_price() * $item['default_quantity']); ?> + +
+
+ +
+ +
+
+
+ + - +
+ + get_discount_value() > 0): ?> +
+ + - +
+ + +
+ + - +
+
+ +
+ + + +
+
+ + get_min_quantity() || $bundle->get_max_quantity()): ?> +
+ get_min_quantity()): ?> +

+ get_min_quantity()); ?> +

+ + + get_max_quantity()): ?> +

+ get_max_quantity()); ?> +

+ +
+ +
+ + \ No newline at end of file diff --git a/templates/emails/subscription-cancelled.php b/templates/emails/subscription-cancelled.php new file mode 100644 index 0000000..9cc5b81 --- /dev/null +++ b/templates/emails/subscription-cancelled.php @@ -0,0 +1,326 @@ +product_id); +$product_name = $product ? $product->get_name() : __('未知产品', 'yoone-subscriptions'); + +?> + + +> + + + + <?php _e('订阅已取消', 'yoone-subscriptions'); ?> + + + + + + \ No newline at end of file diff --git a/templates/emails/subscription-renewal.php b/templates/emails/subscription-renewal.php new file mode 100644 index 0000000..ff5242c --- /dev/null +++ b/templates/emails/subscription-renewal.php @@ -0,0 +1,304 @@ +product_id); +$product_name = $product ? $product->get_name() : __('未知产品', 'yoone-subscriptions'); + +?> + + +> + + + + <?php _e('订阅续费成功', 'yoone-subscriptions'); ?> + + + + + + \ No newline at end of file diff --git a/templates/frontend/bundle-product.php b/templates/frontend/bundle-product.php new file mode 100644 index 0000000..df7e516 --- /dev/null +++ b/templates/frontend/bundle-product.php @@ -0,0 +1,472 @@ +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; + +?> + +
+ + +
+ + 0): ?> + + +
+ + + description): ?> +
+

+

description); ?>

+
+ + + +
+

+ +
+ + product_id); + if (!$item_product) continue; + + $item_price = $item_product->get_price(); + $item_total = $item_price * $item->quantity; + ?> + +
+
+ get_image('thumbnail'); ?> +
+ +
+
+ + product_name); ?> + +
+ + product_excerpt): ?> +

product_excerpt); ?>

+ + +
+ + quantity); ?> + + + + + quantity > 1): ?> + () + + +
+
+ +
+ + + +
+
+ +
+
+ + +
+
+
+ + +
+ +
+ + +
+ + 0): ?> +
+ + + () +
+ +
+
+ + +
+
+ +

+ +

+
+
+ +
+ + + + \ No newline at end of file diff --git a/templates/frontend/my-subscriptions.php b/templates/frontend/my-subscriptions.php new file mode 100644 index 0000000..3fc0489 --- /dev/null +++ b/templates/frontend/my-subscriptions.php @@ -0,0 +1,250 @@ +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 +)); + +?> + +
+

+ + + +
+

+

+ + + +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +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; + } +} +?> \ No newline at end of file diff --git a/templates/frontend/single-product/bundle-options.php b/templates/frontend/single-product/bundle-options.php new file mode 100644 index 0000000..78de7e7 --- /dev/null +++ b/templates/frontend/single-product/bundle-options.php @@ -0,0 +1,355 @@ + + +
+

get_name()); ?>

+ + get_description()): ?> +
+ get_description()); ?> +
+ + +
+

+ + $item): ?> + + +
+
+ + +
+ 0): ?> + + get_price() * $item['quantity']); ?> + + + 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)); + ?> + + + get_price() * $item['quantity']); ?> + +
+
+ +
+
+ get_image('thumbnail'); ?> +
+ +
+
+ get_short_description(), 20); ?> +
+ +
+ + +
+ + 0): ?> +
+ + + + + + + + + +
+ +
+
+
+ +
+ +
+
+
+ + - +
+ + get_discount_type() !== 'none' && $bundle->get_discount_value() > 0): ?> +
+ + + get_discount_type() === 'percentage'): ?> + get_discount_value()); ?> + + get_discount_value())); ?> + + +
+ + +
+ + - +
+ + +
+ +
+ +
+ + get_max_quantity()): ?> + max="get_max_quantity()); ?>" + + step="1" + /> + +
+
+
+ +
+ +
+
+ + \ No newline at end of file diff --git a/templates/frontend/subscription-details.php b/templates/frontend/subscription-details.php new file mode 100644 index 0000000..1e2e603 --- /dev/null +++ b/templates/frontend/subscription-details.php @@ -0,0 +1,355 @@ +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 +)); + +?> + +
+

id); ?>

+ +

+ + ← + +

+ + +
+

+ + + + + + + + + + + + + + + + + + + + trial_end_date): ?> + + + + + + next_payment_date && $subscription->status === 'active'): ?> + + + + + + end_date): ?> + + + + + + +
+ + status); ?> + +
+ product_id): ?> + + product_name ?: __('未知产品', 'yoone-subscriptions')); ?> + + + + +
+ + subscription_price); ?> + + + / billing_period, $subscription->billing_interval); ?> + +
+ +
+ +
+ +
+ +
+
+ + +
+

+ +
+ status === 'active'): ?> + + + + status === 'paused'): ?> + + + + + + status, array('active', 'paused'))): ?> + + + + +
+
+ + +
+

+ + +

+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +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; + } +} +?> \ No newline at end of file diff --git a/templates/frontend/subscription-options.php b/templates/frontend/subscription-options.php new file mode 100644 index 0000000..82db5b3 --- /dev/null +++ b/templates/frontend/subscription-options.php @@ -0,0 +1,514 @@ +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; + +?> + +
+ + +
+

+ +
+ +
+ +
+ + +
+ +
+
+
+ + + + + + + +
+ + + + \ No newline at end of file diff --git a/templates/myaccount/subscriptions.php b/templates/myaccount/subscriptions.php new file mode 100644 index 0000000..9fa7bcc --- /dev/null +++ b/templates/myaccount/subscriptions.php @@ -0,0 +1,374 @@ +get_results($wpdb->prepare(" + SELECT * FROM {$wpdb->prefix}yoone_subscriptions + WHERE customer_id = %d + ORDER BY created_at DESC +", $customer_id)); + +?> + +
+

+ + +
+

+ + + +
+ +
+ + id); + $subscription_items = $subscription_obj->get_items(); + $next_payment = $subscription_obj->get_next_payment_date(); + ?> + +
+
+
+

id); ?>

+ + get_status_label($subscription->status)); ?> + +
+ +
+ status === 'active'): ?> + + + + status === 'paused'): ?> + + + + + + status, array('active', 'paused'))): ?> + + + + + + + + +
+
+ +
+
+

+ +
    + + +
  • +
    + get_image('thumbnail'); ?> +
    +
    + get_name()); ?> + × + +
    +
  • + +
+ +
+ +
+
+ + total); ?> +
+ +
+ + + format_billing_period($subscription->billing_period, $subscription->billing_interval)); ?> + +
+ + status === 'active' && $next_payment): ?> +
+ + + + +
+ + +
+ + + created_at)); ?> + +
+ + payment_method): ?> +
+ + + get_payment_method_title($subscription->payment_method)); ?> + +
+ +
+
+ + status === 'active'): ?> +
+

+ +
+ + + + + + + + + + + +
+
+ +
+ +
+ +
+ +
+ +
+ + \ No newline at end of file diff --git a/templates/subscription/subscription-options.php b/templates/subscription/subscription-options.php new file mode 100644 index 0000000..d5043e5 --- /dev/null +++ b/templates/subscription/subscription-options.php @@ -0,0 +1,345 @@ +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; +} +?> + +
+

+ +
+ + + +
+ + + + + + +
+ + + + \ No newline at end of file diff --git a/tests/class-yoone-test-suite.php b/tests/class-yoone-test-suite.php new file mode 100644 index 0000000..c83b136 --- /dev/null +++ b/tests/class-yoone-test-suite.php @@ -0,0 +1,760 @@ + +
+

+ +
+ + + + + +
+ + + +
+

+
+ 总计: 0 + 通过: 0 + 失败: 0 + 跳过: 0 +
+
+
+
+ + + + + __('权限不足', '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(); +} \ No newline at end of file diff --git a/tests/test-bundle-products.php b/tests/test-bundle-products.php new file mode 100644 index 0000000..24ca51d --- /dev/null +++ b/tests/test-bundle-products.php @@ -0,0 +1,360 @@ +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; + } +} \ No newline at end of file diff --git a/tests/test-config.php b/tests/test-config.php new file mode 100644 index 0000000..d5a698e --- /dev/null +++ b/tests/test-config.php @@ -0,0 +1,223 @@ + 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() + ); + } +} \ No newline at end of file diff --git a/tests/test-cron-jobs.php b/tests/test-cron-jobs.php new file mode 100644 index 0000000..2f733c6 --- /dev/null +++ b/tests/test-cron-jobs.php @@ -0,0 +1,418 @@ +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; + } +} \ No newline at end of file diff --git a/tests/test-payment-integration.php b/tests/test-payment-integration.php new file mode 100644 index 0000000..d15608e --- /dev/null +++ b/tests/test-payment-integration.php @@ -0,0 +1,290 @@ +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; + } +} \ No newline at end of file diff --git a/tests/test-subscription-flow.php b/tests/test-subscription-flow.php new file mode 100644 index 0000000..b915b21 --- /dev/null +++ b/tests/test-subscription-flow.php @@ -0,0 +1,322 @@ +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; + } +} \ No newline at end of file diff --git a/yoone-subscriptions.php b/yoone-subscriptions.php new file mode 100644 index 0000000..710aa38 --- /dev/null +++ b/yoone-subscriptions.php @@ -0,0 +1,458 @@ +

Yoone Subscriptions 需要 WooCommerce 插件才能正常工作。

'; + }); + 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 '

'; + echo __('Yoone Subscriptions 需要 WooCommerce 插件才能正常工作。', 'yoone-subscriptions'); + echo '

'; + } + } + + /** + * 加载文本域 + */ + 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('激活 (%s)', '激活 (%s)', 'yoone-subscriptions') + )); + + register_post_status('yoone-paused', array( + 'label' => __('暂停', 'yoone-subscriptions'), + 'public' => false, + 'show_in_admin_status_list' => true, + 'label_count' => _n_noop('暂停 (%s)', '暂停 (%s)', 'yoone-subscriptions') + )); + + register_post_status('yoone-cancelled', array( + 'label' => __('已取消', 'yoone-subscriptions'), + 'public' => false, + 'show_in_admin_status_list' => true, + 'label_count' => _n_noop('已取消 (%s)', '已取消 (%s)', '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(); \ No newline at end of file