项目概述
一个完整的知识付费平台,包含:
- 内容体系:专栏 → 章节 → 文章/视频 三级结构
- 付费模式:单篇付费、专栏订阅、会员制
- 学习追踪:阅读进度、学习时长、完成率
- 社交功能:评论、点赞、收藏
内容结构设计
专栏(Column)
└── 章节(Chapter)
└── 文章(Article)
├── 文字内容
├── 音频
└── 视频付费逻辑
// 判断用户是否有权限访问内容
async function canAccessArticle(userId: string, articleId: string): Promise<boolean> {
const article = await Article.findById(articleId).populate('column');
// 免费内容直接可以访问
if (article.isFree) return true;
// 检查是否购买了单篇
const singlePurchase = await Purchase.findOne({
userId,
itemType: 'article',
itemId: articleId,
status: 'paid',
});
if (singlePurchase) return true;
// 检查是否购买了专栏
const columnPurchase = await Purchase.findOne({
userId,
itemType: 'column',
itemId: article.column._id,
status: 'paid',
});
if (columnPurchase) return true;
// 检查是否是有效会员
const membership = await Membership.findOne({
userId,
expiredAt: { $gt: new Date() },
});
if (membership) return true;
return false;
}学习进度追踪
// 记录阅读进度
app.post('/api/progress', authenticate, async (req, res) => {
const { articleId, readPercent, readDuration } = req.body;
await prisma.readingProgress.upsert({
where: {
userId_articleId: {
userId: req.user.id,
articleId,
},
},
update: {
readPercent: Math.max(readPercent, 0), // 只增不减
readDuration: { increment: readDuration },
lastReadAt: new Date(),
completed: readPercent >= 90, // 阅读 90% 视为完成
},
create: {
userId: req.user.id,
articleId,
readPercent,
readDuration,
completed: readPercent >= 90,
},
});
res.json({ success: true });
});微信支付集成
// 创建支付订单
app.post('/api/orders/create', authenticate, async (req, res) => {
const { itemType, itemId } = req.body;
// 获取商品信息
const item = itemType === 'article'
? await Article.findById(itemId)
: await Column.findById(itemId);
// 创建订单
const order = await Order.create({
userId: req.user.id,
itemType,
itemId,
amount: item.price,
status: 'pending',
orderNo: generateOrderNo(),
});
// 调用微信支付统一下单
const wxPayResult = await wxpay.unifiedOrder({
body: item.title,
out_trade_no: order.orderNo,
total_fee: Math.round(item.price * 100), // 转为分
openid: req.user.openid,
trade_type: 'JSAPI',
});
res.json({
orderId: order.id,
payParams: {
appId: wxPayResult.appid,
timeStamp: wxPayResult.timeStamp,
nonceStr: wxPayResult.nonceStr,
package: wxPayResult.package,
signType: 'MD5',
paySign: wxPayResult.paySign,
},
});
});