CREATE TABLE users (
user VARCHAR(64) PRIMARY KEY, -- 登录用户名
pass VARCHAR(255) NOT NULL, -- 密码哈希
mail VARCHAR(255) NOT NULL, -- 邮箱
name VARCHAR(128) NOT NULL -- 用户显示名(昵称、真实姓名)
);
CREATE TABLE user_membership (
user VARCHAR(64) PRIMARY KEY,
level VARCHAR(20) NOT NULL,
expires_at DATETIME NOT NULL
);
public function registerUser($user, $pass, $mail, $name) {
// 检查用户名是否存在
$stmt = $this->pdo->prepare("SELECT COUNT(*) FROM users WHERE user = ?");
$stmt->execute([$user]);
if ($stmt->fetchColumn() > 0) {
return ['success' => false, 'msg' => '用户名已存在'];
}
// 密码加密
$hash = password_hash($pass, PASSWORD_DEFAULT);
// 插入用户
$stmt = $this->pdo->prepare("INSERT INTO users (user, pass, mail, name) VALUES (?, ?, ?, ?)");
$res = $stmt->execute([$user, $hash, $mail, $name]);
if (!$res) {
return ['success' => false, 'msg' => '用户创建失败'];
}
// 赋予基础会员等级,默认 30 天有效期
$level = 'free';
$expires_at = date('Y-m-d H:i:s', strtotime('+30 days'));
$stmt = $this->pdo->prepare("INSERT INTO user_membership (user, level, expires_at) VALUES (?, ?, ?)");
$stmt->execute([$user, $level, $expires_at]);
return ['success' => true];
}
getUserData() 方法中也要返回 name
public function getUserData($user, $requireGroups = true) {
$stmt = $this->pdo->prepare("SELECT level, expires_at FROM user_membership WHERE user = ?");
$stmt->execute([$user]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
// 取用户显示名
$stmtName = $this->pdo->prepare("SELECT name, mail FROM users WHERE user = ?");
$stmtName->execute([$user]);
$userInfo = $stmtName->fetch(PDO::FETCH_ASSOC);
$groups = ['user'];
if ($row && strtotime($row['expires_at']) > time()) {
$groups[] = $row['level'];
}
return [
'user' => $user,
'name' => $userInfo ? $userInfo['name'] : $user,
'mail' => $userInfo ? $userInfo['mail'] : '',
'grps' => $groups
];
}
下面是一个相对完整的 DokuWiki Auth 插件示例,支持:
- MySQL 用户表(含
name
、mail
、pass
) - 会员等级管理(单一等级+到期)
- 用户注册(带昵称)
- 会员升级、降级
- 登录验证
getUserData()
返回完整信息和组- 简单示例的数据库连接(PDO)
你可以根据实际调整数据库连接参数和逻辑。
<?php
/**
* DokuWiki Plugin authmembership (Auth Component)
*/
use dokuwiki\Extension\AuthPlugin;
class auth_plugin_authmembership extends AuthPlugin {
/** @var PDO */
protected $pdo;
public function __construct() {
$this->cando['login'] = true;
$this->cando['logout'] = true;
$this->cando['getUserData'] = true;
$this->cando['checkPass'] = true;
try {
$this->pdo = new PDO('mysql:host=localhost;dbname=dokuwiki;charset=utf8mb4', 'dbuser', 'dbpass');
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
msg("Database connection failed: " . $e->getMessage(), -1);
}
}
/**
* 验证用户名密码
*/
public function checkPass($user, $pass) {
$stmt = $this->pdo->prepare("SELECT pass FROM users WHERE user = ?");
$stmt->execute([$user]);
$hash = $stmt->fetchColumn();
return $hash && password_verify($pass, $hash);
}
/**
* 获取用户数据(包括显示名、邮箱、组)
*/
public function getUserData($user, $requireGroups = true) {
$stmt = $this->pdo->prepare("SELECT level, expires_at FROM user_membership WHERE user = ?");
$stmt->execute([$user]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$stmtName = $this->pdo->prepare("SELECT name, mail FROM users WHERE user = ?");
$stmtName->execute([$user]);
$userInfo = $stmtName->fetch(PDO::FETCH_ASSOC);
$groups = ['user'];
if ($row && strtotime($row['expires_at']) > time()) {
$groups[] = $row['level'];
}
return [
'user' => $user,
'name' => $userInfo ? $userInfo['name'] : $user,
'mail' => $userInfo ? $userInfo['mail'] : '',
'grps' => $groups
];
}
/**
* 用户注册
* @param string $user 用户名
* @param string $pass 明文密码
* @param string $mail 邮箱
* @param string $name 显示名
* @return array ['success'=>bool, 'msg'=>string]
*/
public function registerUser($user, $pass, $mail, $name) {
// 检查用户名是否存在
$stmt = $this->pdo->prepare("SELECT COUNT(*) FROM users WHERE user = ?");
$stmt->execute([$user]);
if ($stmt->fetchColumn() > 0) {
return ['success' => false, 'msg' => '用户名已存在'];
}
// 密码加密
$hash = password_hash($pass, PASSWORD_DEFAULT);
// 插入用户
$stmt = $this->pdo->prepare("INSERT INTO users (user, pass, mail, name) VALUES (?, ?, ?, ?)");
$res = $stmt->execute([$user, $hash, $mail, $name]);
if (!$res) {
return ['success' => false, 'msg' => '用户创建失败'];
}
// 赋予基础会员等级,默认 30 天有效期
$level = 'free';
$expires_at = date('Y-m-d H:i:s', strtotime('+30 days'));
$stmt = $this->pdo->prepare("INSERT INTO user_membership (user, level, expires_at) VALUES (?, ?, ?)");
$stmt->execute([$user, $level, $expires_at]);
return ['success' => true, 'msg' => '注册成功'];
}
/**
* 升级或续费会员等级(覆盖当前等级和到期时间)
* @param string $user
* @param string $newLevel
* @param string $newExpiry 格式为 Y-m-d H:i:s
* @return bool
*/
public function upgradeMembership($user, $newLevel, $newExpiry) {
$stmt = $this->pdo->prepare("REPLACE INTO user_membership (user, level, expires_at) VALUES (?, ?, ?)");
return $stmt->execute([$user, $newLevel, $newExpiry]);
}
/**
* 降级为免费用户
* @param string $user
* @return bool
*/
public function downgradeToFree($user) {
$stmt = $this->pdo->prepare("DELETE FROM user_membership WHERE user = ?");
return $stmt->execute([$user]);
}
/**
* 获取当前会员状态
* @param string $user
* @return array|null
*/
public function getMembership($user) {
$stmt = $this->pdo->prepare("SELECT level, expires_at FROM user_membership WHERE user = ?");
$stmt->execute([$user]);
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
}
}
数据库示例表结构
CREATE TABLE users (
user VARCHAR(64) PRIMARY KEY,
pass VARCHAR(255) NOT NULL,
mail VARCHAR(255) NOT NULL,
name VARCHAR(128) NOT NULL
);
CREATE TABLE user_membership (
user VARCHAR(64) PRIMARY KEY,
level VARCHAR(20) NOT NULL,
expires_at DATETIME NOT NULL
);
如果你还需要:
- 后台管理界面代码示例(会员管理)
- 会员过期检测脚本(cron)
- 注册表单 HTML/JS 示例
都可以告诉我,我帮你继续完善!
您说:
我需要点击价格表格表后引导到注册,注册后有能跳转到 stripe checkout
ChatGPT 说:
明白,你想实现的完整流程是:
- 用户点击价格表里的某个会员等级或产品按钮(如“购买”)
- 如果用户未登录或未注册,先跳转到注册页面
- 用户注册成功后自动登录
- 注册成功后跳转到 Stripe Checkout 页面完成支付
- 支付成功后,Stripe Webhook 更新会员状态
下面给你一个示范设计思路和代码实现方向(基于 PHP + DokuWiki + Stripe):
设计要点
1. 价格表按钮
- 价格表按钮链接,指向类似
/register?plan=vip
的注册页面(plan
为价格计划标识)
2. 注册页面
- 注册表单收集用户名、邮箱、密码、显示名
- 隐藏字段存储
plan
信息,注册成功后使用该信息跳转支付
3. 注册处理
- 注册成功后,自动调用登录函数创建用户会话
- 跳转到后端生成的 Stripe Checkout 会话地址
4. Stripe Checkout
- 使用 Stripe PHP SDK 创建 Checkout Session,设置成功和取消跳转地址
- 用户支付成功后,Stripe Webhook 接收通知,更新会员等级到期时间
代码示例
价格表按钮示例 (HTML)
<!-- 用户点击后,跳转到注册页面并传递计划参数 -->
<a href="/doku.php?id=register&plan=vip" class="btn-buy">购买 VIP 会员</a>
注册页面示例处理逻辑(伪代码)
// 假设你在注册处理页面:
$plan = $_GET['plan'] ?? 'free'; // 获取计划
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$user = $_POST['user'];
$pass = $_POST['pass'];
$mail = $_POST['mail'];
$name = $_POST['name'];
$plan = $_POST['plan'] ?? 'free';
$result = $authPlugin->registerUser($user, $pass, $mail, $name);
if ($result['success']) {
// 自动登录
$authPlugin->loginUser($user, $pass);
// 根据计划创建 Stripe Checkout Session
$checkoutUrl = createStripeCheckoutSession($user, $plan);
// 跳转到 Stripe Checkout
header("Location: $checkoutUrl");
exit;
} else {
echo "注册失败:" . $result['msg'];
}
}
Stripe Checkout 会话创建示例 (PHP)
require 'vendor/autoload.php';
\Stripe\Stripe::setApiKey('sk_test_...');
function createStripeCheckoutSession($user, $plan) {
$priceId = getPriceIdByPlan($plan); // 你自己定义,映射 plan => Stripe Price ID
$session = \Stripe\Checkout\Session::create([
'payment_method_types' => ['card'],
'line_items' => [[
'price' => $priceId,
'quantity' => 1,
]],
'mode' => 'subscription',
'customer_email' => getUserEmail($user), // 预填邮箱
'success_url' => 'https://yourdomain.com/doku.php?id=payment_success',
'cancel_url' => 'https://yourdomain.com/doku.php?id=payment_cancel',
'metadata' => ['user' => $user, 'plan' => $plan],
]);
return $session->url;
}
Stripe Webhook 示例 (PHP)
- 监听订阅支付成功事件
checkout.session.completed
- 从
metadata
取出用户名和计划 - 更新数据库会员等级和到期时间
$input = @file_get_contents('php://input'); $event = null;
try {
$event = \Stripe\Event::constructFrom(json_decode($input, true));
} catch(\UnexpectedValueException $e) {
http_response_code(400);
exit();
}
if ($event->type == 'checkout.session.completed') {
$session = $event->data->object;
$user = $session->metadata->user;
$plan = $session->metadata->plan;
// 根据 plan 设置会员等级和有效期
$newExpiry = date('Y-m-d H:i:s', strtotime('+30 days'));
// 你调用插件升级会员接口
$authPlugin->upgradeMembership($user, $plan, $newExpiry);
}
http_response_code(200);
---
## 总结
| 步骤 | 说明 |
| --- | --- |
| 价格表按钮 | 带 plan 参数跳转到注册页面 |
| 注册页面 | 处理注册,自动登录,跳转 Stripe |
| Stripe Checkout | 完成支付 |
| Webhook | 更新会员状态 |
---
如果你需要,我可以帮你写:
- 注册页面完整示例HTML + PHP处理
- Stripe 创建和 webhook 的完整示例
- DokuWiki 集成方案(插件或页面模板)
你看想要哪个,我帮你细化。
##### 您说:
如果是登录用户,点击价格表按钮就不需要注册了
###### ChatGPT 说:
理解,这种情况下的流程更合理:
- **未登录用户**点击价格表,跳转注册页面,注册成功后跳转 Stripe Checkout
- **已登录用户**点击价格表,直接跳转 Stripe Checkout,无需再注册或登录
---
## 实现思路示例
### 1\. 价格表按钮链接生成逻辑
<?php
// 伪代码示例
if ($auth->isLoggedIn()) {
// 登录用户直接跳转支付链接,携带计划参数
echo '<a href="/doku.php?id=checkout&plan=vip" class="btn-buy">购买 VIP 会员</a>';
} else {
// 未登录跳转注册页面,并传计划参数
echo '<a href="/doku.php?id=register&plan=vip" class="btn-buy">购买 VIP 会员</a>';
}
---
### 2\. 处理 checkout 页面(用于已登录用户发起支付)
<?php
// checkout.php 或 doku.php?id=checkout 的处理逻辑
session_start();
if (!isset($_SESSION['auth']['user'])) {
// 未登录跳转注册页面
header('Location: /doku.php?id=register&plan=' . urlencode($_GET['plan'] ?? 'free'));
exit;
}
$user = $_SESSION['auth']['user'];
$plan = $_GET['plan'] ?? 'free';
// 创建 Stripe Checkout 会话
$checkoutUrl = createStripeCheckoutSession($user, $plan);
// 跳转到 Stripe Checkout
header("Location: $checkoutUrl");
exit;
---
### 3\. 注册页面处理逻辑(未登录用户注册后跳转 Checkout)
<?php
// register.php 或 doku.php?id=register 的处理逻辑
$plan = $_GET['plan'] ?? 'free';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$user = $_POST['user'];
$pass = $_POST['pass'];
$mail = $_POST['mail'];
$name = $_POST['name'];
$plan = $_POST['plan'] ?? 'free';
$result = $authPlugin->registerUser($user, $pass, $mail, $name);
if ($result['success']) {
// 自动登录
$authPlugin->loginUser($user, $pass);
// 跳转到支付
header('Location: /doku.php?id=checkout&plan=' . urlencode($plan));
exit;
} else {
echo "注册失败:" . $result['msg'];
}
}
---
### 4\. 登录状态判断(Auth 插件或模板中)
你可以用 DokuWiki 的全局变量 $USERINFO
判断登录状态,比如:
global $USERINFO;
if (!empty($USERINFO['name'])) {
// 已登录
$user = $USERINFO['name'];
} else {
// 未登录
}
---
## 你需要我帮你写的具体代码点?
- 价格表按钮模板示例
- 注册页面 + 表单
- checkout 页面
- Stripe 创建会话的 PHP 函数
- webhook 处理
我都可以给你。你说说先要哪几个?
##### 您说:
需要保存购买记录
###### ChatGPT 说:
明白,你想要在用户每次通过 Stripe 支付购买会员等级时,**保存一条购买记录**,方便后台查看和管理。
---
## 设计思路
1. **新增数据库表** user_purchases
用于保存购买记录,字段示例:
CREATE TABLE user_purchases (
id INT AUTO_INCREMENT PRIMARY KEY,
user VARCHAR(64) NOT NULL,
plan VARCHAR(50) NOT NULL,
stripe_session_id VARCHAR(255) NOT NULL,
purchase_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
amount INT NOT NULL, -- 单位分
currency VARCHAR(10) NOT NULL,
status VARCHAR(20) NOT NULL -- pending, success, failed
);
2. **创建 Stripe Checkout 会话时**,插入一条状态为 pending
的购买记录。
3. **Stripe webhook** 监听支付成功事件,将对应购买记录状态更新为 success
,并根据情况调用升级会员接口。
4. **失败或取消的支付**可选更新为 failed
。
---
## 代码示例
### 1\. 创建 Stripe Checkout 会话时保存购买记录
function createStripeCheckoutSession($user, $plan) {
global $pdo; // 你的 PDO 数据库连接
$priceId = getPriceIdByPlan($plan);
$amount = getPriceAmountByPlan($plan); // 单位分,比如 999 表示9.99美元
$currency = 'usd';
\Stripe\Stripe::setApiKey('sk_test_...');
$session = \Stripe\Checkout\Session::create([
'payment_method_types' => ['card'],
'line_items' => [[
'price' => $priceId,
'quantity' => 1,
]],
'mode' => 'subscription',
'customer_email' => getUserEmail($user),
'success_url' => 'https://yourdomain.com/doku.php?id=payment_success',
'cancel_url' => 'https://yourdomain.com/doku.php?id=payment_cancel',
'metadata' => ['user' => $user, 'plan' => $plan],
]);
// 保存购买记录,状态为 pending
$stmt = $pdo->prepare("INSERT INTO user_purchases (user, plan, stripe_session_id, amount, currency, status) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([
$user,
$plan,
$session->id,
$amount,
$currency,
'pending'
]);
return $session->url;
}
---
### 2\. Stripe Webhook 事件中更新购买记录状态
if ($event->type == 'checkout.session.completed') {
$session = $event->data->object;
$user = $session->metadata->user;
$plan = $session->metadata->plan;
// 更新购买记录状态为 success
$stmt = $pdo->prepare("UPDATE user_purchases SET status = ? WHERE stripe_session_id = ?");
$stmt->execute(['success', $session->id]);
// 升级会员
$newExpiry = date('Y-m-d H:i:s', strtotime('+30 days'));
$authPlugin->upgradeMembership($user, $plan, $newExpiry);
} elseif ($event->type == 'checkout.session.expired' || $event->type == 'checkout.session.async_payment_failed') {
$session = $event->data->object;
// 标记失败或过期
$stmt = $pdo->prepare("UPDATE user_purchases SET status = ? WHERE stripe_session_id = ?");
$stmt->execute(['failed', $session->id]);
}
---
### 3\. 购买记录查询示例
function getUserPurchases($user) {
global $pdo;
$stmt = $pdo->prepare("SELECT * FROM user_purchases WHERE user = ? ORDER BY purchase_date DESC");
$stmt->execute([$user]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
---
## 总结
- 购买流程中插入 pending
状态的购买记录
- Webhook 根据支付结果更新状态,成功后调用升级会员
- 可在后台做购买记录管理和查询
---
如果你需要,我可以帮你写:
- 购买记录的管理后台示例页面
- Stripe Webhook 完整示例
- 价格表按钮、注册、支付整合的示例代码
告诉我想先看哪个,我帮你做!
##### 您说:
关于这个 dokuwiki 会员级别支付还有什么补充的地方?
###### ChatGPT 说:
你目前的 authmembership
插件已经实现了核心功能:登录验证、自定义会员组获取、会员升级与降级。针对“Dokuwiki 会员级别支付”系统,下面是**进一步可完善或需要考虑的补充点**:
---
## ✅ 你已具备的功能(已完成):
- ✅ 自定义 checkPass()
验证登录密码
- ✅ 通过 getUserData()
返回会员等级对应的组(grps
)
- ✅ 自动判断会员是否过期并排除组信息
- ✅ 提供 upgradeMembership()
与 downgradeToFree()
方法供 webhook 或后台使用
---
## 🔧 推荐扩展功能(建议补充)
### 1\. **支付记录管理(已提出)**
你可以在插件中集成:
public function logPurchase($user, $plan, $amount, $currency, $sessionId, $status = 'pending') {
$stmt = $this->pdo->prepare("
INSERT INTO user_purchases (user, plan, amount, currency, stripe_session_id, status)
VALUES (?, ?, ?, ?, ?, ?)
");
return $stmt->execute([$user, $plan, $amount, $currency, $sessionId, $status]);
}
- 补充更新状态、获取记录等函数。
---
### 2\. **自动到期降级脚本(CRON)**
Dokuwiki 不会自动调用到期逻辑。你应添加一个**CLI 脚本**定时执行降级:
// scripts/cron_expire_members.php
$pdo = new PDO(...);
$stmt = $pdo->prepare("DELETE FROM user_membership WHERE expires_at < NOW()");
$stmt->execute();
- 加入 crontab,比如每天凌晨执行:
0 3 * * * php /var/www/dokuwiki/lib/plugins/authmembership/scripts/cron_expire_members.php
---
### 3\. **注册与支付流程整合(建议插件封装)**
可以写一个页面动作插件(action.php
):
- 页面 doku.php?id=buy&plan=vip
引导到注册或 checkout
- 自动注册、登录、跳转 checkout
- 注册后存储计划字段、会话中保留 $_SESSION['plan']
信息
---
### 4\. **后台会员管理 UI(可选)**
如果你希望在 Dokuwiki 后台看到会员状态:
- 添加 Admin 插件模块(继承 DokuWiki_Admin_Plugin
)
- 展示用户、等级、到期日,支持搜索、手动降级/续期按钮等
- 提供分页查看购买记录
---
### 5\. **支持多设备/会话限制(高级功能)**
比如防止一个账号共享登录,你可:
- 在 login()
时记录用户 IP + UA
- 可存表:user_sessions(user, ip, user_agent, last_seen)
- 设定一个规则,例如最多允许 2 个设备同时在线
---
### 6\. **Webhook 安全性**
Webhook 处理时需校验签名,避免伪造请求:
$sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
$event = \Stripe\Webhook::constructEvent(
$payload, $sig_header, $your_endpoint_secret
);
---
### 7\. **补差价升级逻辑(可选)**
用户购买了低级别会员后,可以按比例升级为高级别。你需要:
- 获取当前剩余时间
- 按比例减去已用金额
- 设置新 plan 和到期时间
这涉及价格表的逻辑管理,可封装一个 calculateUpgradePrice($fromLevel, $toLevel, $daysLeft)
函数。
---
## 🧱 数据库补充建议
你目前的 user_membership
表结构建议如下:
CREATE TABLE user_membership (
user VARCHAR(64) PRIMARY KEY,
level VARCHAR(32) NOT NULL,
expires_at DATETIME NOT NULL
);
user_purchases
表推荐为:
CREATE TABLE user_purchases (
id INT AUTO_INCREMENT PRIMARY KEY,
user VARCHAR(64) NOT NULL,
plan VARCHAR(32) NOT NULL,
amount INT NOT NULL,
currency VARCHAR(10) NOT NULL,
stripe_session_id VARCHAR(255) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
---
## ✅ 最佳实践顺序建议:
| 阶段 | 优先级 | 功能 |
| --- | --- | --- |
| Stripe 支付接入 | 必须 | Checkout 创建 + Webhook + plan 映射逻辑 |
| 保存购买记录 | 必须 | 插入 user_purchases
,WebHook 更新状态 |
| 定时过期降级 | 高 | Cron 每天降级过期会员 |
| 补差价升级 | 中 | 保留升级时间与金额,动态计算升级金额 |
| 后台会员管理界面 | 中 | 搜索用户、查看等级、到期、支付记录 |
| 登录用户直接支付跳转 | 必须 | 注册与支付整合,逻辑已完成 |
---
如果你需要,我可以:
- 帮你写完整的 webhook 接收端
- 注册页 + 价格页整合跳转模板
- 定时任务脚本与 SQL 执行
- 管理页面基础 UI
只需告诉我你想要实现哪一部分,我来写。
##### 您说:
注册页 + 价格页整合跳转模板
###### ChatGPT 说:
好的,我帮你设计一个 **注册页 + 价格页整合跳转 Stripe Checkout 的示例流程**,基于 Dokuwiki 和 PHP。
---
# 功能需求简述
- 价格页列出会员等级及价格,点击按钮触发操作:
- 如果用户未登录,跳转到注册页
- 注册成功后,跳转到对应会员等级的 Stripe Checkout 页面
- 如果已登录,点击按钮直接跳转 Checkout
---
# 示例代码结构
假设你有两个页面:
- /doku.php?id=register
:注册页
- /doku.php?id=membership
:价格页(列会员计划和购买按钮)
- 额外一个 PHP 处理文件 purchase.php
用来处理跳转和创建 Stripe Session
---
## 1\. membership 页面(价格页)
价格页示例用 PHP 输出,主要逻辑是根据登录状态判断跳转链接:
<?php
session_start();
$user = $_SESSION['user'] ?? null; // 根据你的 auth 插件调整
$plans = [
'basic' => ['name'=>'Basic Plan', 'price' => 999],
'vip' => ['name'=>'VIP Plan', 'price' => 2999],
];
?>
<h2>会员价格表</h2>
<table border="1" cellpadding="10">
<tr><th>等级</th><th>价格(元)</th><th>操作</th></tr>
<?php foreach ($plans as $key => $plan): ?>
<tr>
<td><?=htmlspecialchars($plan['name'])?></td>
<td><?=($plan['price']/100)?> 元/月</td>
<td>
<?php if (!$user): ?>
<!-- 未登录跳转注册页,注册成功后跳转购买 -->
<a href="doku.php?id=register&redirect=membership&plan=<?=$key?>">注册购买</a>
<?php else: ?>
<!-- 登录状态直接跳转 purchase.php 创建 Checkout -->
<a href="purchase.php?plan=<?=$key?>">购买</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</table>
---
## 2\. register 页面(注册页)
这里演示一个简单的注册流程,成功注册后跳转到 purchase.php
触发 Stripe 购买:
<?php
session_start();
$redirect = $_GET['redirect'] ?? 'membership';
$plan = $_GET['plan'] ?? null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$user = $_POST['user'];
$pass = $_POST['pass'];
// TODO: 注册逻辑,例如写入数据库
// 示例密码用 password_hash
$hash = password_hash($pass, PASSWORD_DEFAULT);
// 假设有 PDO $pdo
$stmt = $pdo->prepare("INSERT INTO users (user, pass) VALUES (?, ?)");
$stmt->execute([$user, $hash]);
// 登录用户
$_SESSION['user'] = $user;
// 注册成功,跳转到购买页(或计划页)进行购买
if ($plan) {
header("Location: purchase.php?plan=" . urlencode($plan));
exit;
} else {
header("Location: doku.php?id=" . urlencode($redirect));
exit;
}
}
?>
<h2>注册账号</h2>
<form method="post">
<label>用户名: <input name="user" required></label><br>
<label>密码: <input type="password" name="pass" required></label><br>
<button type="submit">注册并购买</button>
</form>
---
## 3\. purchase.php (购买入口)
该文件负责:
- 判断用户是否登录,未登录跳到注册页
- 根据 plan
参数创建 Stripe Checkout 会话
- 重定向到 Stripe Checkout 页面
示例:
<?php
session_start();
require 'vendor/autoload.php'; // Stripe PHP SDK
$user = $_SESSION['user'] ?? null;
$plan = $_GET['plan'] ?? null;
if (!$user) {
// 未登录跳转注册页,注册成功后回到这里继续购买
$redirectUrl = 'purchase.php?plan=' . urlencode($plan);
header("Location: doku.php?id=register&redirect=" . urlencode($redirectUrl) . "&plan=" . urlencode($plan));
exit;
}
// 你的数据库连接
$pdo = new PDO('mysql:host=localhost;dbname=dokuwiki', 'dbuser', 'dbpass');
\Stripe\Stripe::setApiKey('sktest...');
$priceIds = [
'basic' => 'price_xxxx_basic',
'vip' => 'price_xxxx_vip',
];
if (!isset($priceIds[$plan])) {
die("无效的会员等级");
}
$session = \Stripe\Checkout\Session::create([
'payment_method_types' => ['card'],
'line_items' => [[
'price' => $priceIds[$plan],
'quantity' => 1,
]],
'mode' => 'subscription',
'customer_email' => getUserEmail($user), // 需要你实现这个函数
'success_url' => 'https://yourdomain.com/doku.php?id=payment_success';,
'cancel_url' => 'https://yourdomain.com/doku.php?id=payment_cancel';,
'metadata' => ['user' => $user, 'plan' => $plan],
]);
// 可选:保存购买记录,状态 pending
$stmt = $pdo->prepare("INSERT INTO user_purchases (user, plan, stripe_session_id, amount, currency, status) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([
$user,
$plan,
$session->id,
getPriceAmount($plan),
'usd',
'pending'
]);
header("Location: " . $session->url);
exit;
// helper 函数示例
function getUserEmail($user) {
global $pdo;
$stmt = $pdo->prepare("SELECT email FROM users WHERE user = ?");
$stmt->execute([$user]);
return $stmt->fetchColumn() ?: '';
}
function getPriceAmount($plan) {
$prices = [
'basic' => 999,
'vip' => 2999,
];
return $prices[$plan] ?? 0;
}
---
# 总结
- 价格页检测登录,未登录跳注册,注册成功跳购买
- 购买入口统一由 purchase.php
处理,创建 Stripe Session 并跳转
- 需要实现用户注册写入和邮箱查询等辅助逻辑
- 后续结合 Stripe Webhook 完成支付状态回调和会员升级
---
如果需要,我可以帮你写:
- 更完善的注册验证(邮箱格式、重复名检查)
- Stripe Webhook 接收示例
- 会员购买成功后的回调处理
告诉我!
4/4
##### 您说:
为什么这里一直用 id= 来设计一个功能, id 不应该是 wiki 页面吗?为何不用其他 url 参数
###### ChatGPT 说:
你说得很对!
---
### Dokuwiki 里的 id
参数
- Dokuwiki 里 URL 里 id=xxx
,传统上是指**Wiki页面的页面ID**,比如页面路径。
- 因此用 id=
传功能参数(如注册、会员购买)**在语义上是不规范的**,容易和页面冲突,且维护上也混乱。
---
### 为什么示例里用 id=
?
- 我示例是简单直接用 Dokuwiki 现成路由做示范,方便快速试验。
- 但严格讲,真正开发插件或复杂功能,**不应该滥用 id=
来传功能参数**。
---
### 更好的设计思路
1. **使用 Dokuwiki 插件的自定义动作(action)页面**
例如 /doku.php?action=register
、action=membership
、action=purchase
这样与页面ID区分开,语义更清晰。
2. **使用 GET 参数传递细分信息**
如 /doku.php?action=register&plan=vip
action 表示功能模块,plan 表示具体参数。
3. **使用更干净的路径形式(路由重写)**
在服务器层(nginx/apache)或 Dokuwiki 里实现伪静态,比如 /register
, /membership
, /purchase
。
---
### 结论
- 不建议用 id=
传功能参数,容易引起页面ID冲突和混淆。
- 推荐使用 action
或自定义参数名,明确区分页面内容和功能请求。
- 这样插件开发和后续维护都更规范、稳定。
---
如果你想,我可以帮你写一套基于 action
参数的 Dokuwiki 插件示例,做到注册/购买功能更合规!
##### 您说:
给我示例
###### ChatGPT 说:
好的,下面给你一个基于 Dokuwiki 插件框架,利用 action
参数区分功能页面的简洁示例。
这个示例展示如何用 action=register
、action=membership
、action=purchase
来分别处理注册页、会员价格页和购买入口,避免使用 id=
混淆页面与功能。
---
# 1\. 插件基本结构
创建一个名为 authmembership
的 Dokuwiki 插件(目录结构示例):
dokuwiki/lib/plugins/authmembership/
action.php
plugin.info.txt
---
# 2\. plugin.info.txt 内容
base authmembership
author YourName
email [email protected]
date 2025-06-10
name AuthMembership
desc 会员注册与购买流程插件示例
url https://example.com
---
# 3\. action.php 示例代码
<?php
// 入口文件,处理自定义 action
if(!defined('DOKU_INC')) die();
class action_plugin_authmembership extends DokuWiki_Action_Plugin {
public function register(Doku_Event_Handler $controller) {
// 注册 hook 监听所有页面请求的 ACTION
$controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'handleAction');
}
public function handleAction(Doku_Event $event, $param) {
global $ACT, $ID;
$action = $_GET['action'] ?? '';
if (!in_array($action, ['register', 'membership', 'purchase'])) {
return; // 不处理其它action,正常加载页面
}
// 禁止 Dokuwiki 默认页面渲染
$event->preventDefault();
$event->stopPropagation();
switch ($action) {
case 'register':
$this->renderRegister();
break;
case 'membership':
$this->renderMembership();
break;
case 'purchase':
$this->handlePurchase();
break;
}
}
protected function renderRegister() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$user = $_POST['user'] ?? '';
$pass = $_POST['pass'] ?? '';
if (!$user || !$pass) {
echo "<p>用户名和密码不能为空</p>";
$this->renderRegisterForm();
return;
}
// 注册逻辑(示例,需改为你的数据库写入逻辑)
// TODO: 这里写注册逻辑
// ...
echo "<p>注册成功!请<a href='?action=membership'>前往会员价格页</a>购买。</p>";
return;
}
$this->renderRegisterForm();
}
protected function renderRegisterForm() {
echo <<<HTML
<h2>注册账号</h2>
<form method="post">
<label>用户名: <input name="user" required></label><br>
<label>密码: <input type="password" name="pass" required></label><br>
<button type="submit">注册</button>
</form>
HTML;
}
protected function renderMembership() {
// 会员价格页示例
$plans = [
'basic' => ['name'=>'Basic Plan', 'price' => 999],
'vip' => ['name'=>'VIP Plan', 'price' => 2999],
];
$user = $_SESSION['user'] ?? null;
echo "<h2>会员价格表</h2>";
echo "<table border='1' cellpadding='10'><tr><th>等级</th><th>价格(元)</th><th>操作</th></tr>";
foreach ($plans as $key => $plan) {
echo "<tr>";
echo "<td>" . htmlspecialchars($plan['name']) . "</td>";
echo "<td>" . ($plan['price']/100) . " 元/月</td>";
if (!$user) {
echo "<td><a href='?action=register&redirect=membership&plan=$key'>注册购买</a></td>";
} else {
echo "<td><a href='?action=purchase&plan=$key'>购买</a></td>";
}
echo "</tr>";
}
echo "</table>";
}
protected function handlePurchase() {
session_start();
$user = $_SESSION['user'] ?? null;
$plan = $_GET['plan'] ?? null;
if (!$user) {
// 未登录跳注册,注册成功后跳购买
$redirectUrl = '?action=purchase&plan=' . urlencode($plan);
header("Location: ?action=register&redirect=" . urlencode($redirectUrl) . "&plan=" . urlencode($plan));
exit;
}
// 这里写调用 Stripe 创建 Checkout 会话逻辑
echo "<p>欢迎 $user ,准备购买会员计划:$plan</p>";
echo "<p>这里应该跳转到 Stripe Checkout</p>";
}
}
---
# 4\. 使用示例
- 访问 https://yourwiki/doku.php?action=register
进入注册页
- 访问 https://yourwiki/doku.php?action=membership
查看价格表
- 点击价格表的“购买”按钮,会调用 ?action=purchase&plan=xxx
做购买逻辑
---
# 总结
- **用 action
作为功能区分,避免用 id
误导页面内容**
- 插件通过监听 DOKUWIKI_STARTED
事件,拦截并自定义输出
- 各个功能页面逻辑集中管理,方便扩展