| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492 |
- import Router from '@koa/router';
- import { Context } from 'koa';
- import * as PaymentService from './payment.service';
- import { BadRequestError } from '../../middleware/errorHandler';
- import { authMiddleware } from '../../middleware/auth';
- import { prisma } from '../../models';
- import { safeParseInt } from '../../utils/safe-parse';
- const router = new Router();
- // 创建支付订单
- router.post('/create', authMiddleware, async (ctx: Context) => {
- const userId = safeParseInt(ctx.state.user.userId);
- const { planId, paymentMethod, period = 'monthly', returnUrl } = ctx.request.body as {
- planId: number;
- paymentMethod: 'alipay' | 'wechat' | 'mock';
- period?: 'monthly' | 'yearly';
- returnUrl?: string;
- };
- if (!planId) {
- throw new BadRequestError('请选择套餐');
- }
- if (!['alipay', 'wechat', 'mock'].includes(paymentMethod)) {
- throw new BadRequestError('请选择支付方式');
- }
- const result = await PaymentService.createPaymentOrder(userId, planId, paymentMethod, period, returnUrl);
- ctx.body = {
- code: 0,
- message: '订单创建成功',
- data: result
- };
- });
- // 创建 Token 包支付订单
- router.post('/token-packs/create', authMiddleware, async (ctx: Context) => {
- const userId = safeParseInt(ctx.state.user.userId);
- const { quantity, paymentMethod, returnUrl } = ctx.request.body as {
- quantity: number;
- paymentMethod: 'alipay' | 'wechat' | 'mock';
- returnUrl?: string;
- };
- if (!quantity || quantity < 1) {
- throw new BadRequestError('购买数量至少为1');
- }
- if (!['alipay', 'wechat', 'mock'].includes(paymentMethod)) {
- throw new BadRequestError('请选择支付方式');
- }
- const result = await PaymentService.createTokenPackOrder(userId, quantity, paymentMethod, returnUrl);
- ctx.body = {
- code: 0,
- message: '订单创建成功',
- data: result
- };
- });
- // 模拟支付(仅开发环境)
- router.post('/mock', authMiddleware, async (ctx: Context) => {
- if (process.env.NODE_ENV === 'production') {
- throw new BadRequestError('生产环境不可用');
- }
- const userId = safeParseInt(ctx.state.user.userId);
- const { orderNo } = ctx.request.body as { orderNo: string };
- if (!orderNo) {
- throw new BadRequestError('订单号不能为空');
- }
- const result = await PaymentService.mockPaymentSuccess(orderNo, userId);
- ctx.body = {
- code: 0,
- message: result.message,
- data: result
- };
- });
- // 支付宝异步通知回调
- router.post('/alipay/notify', async (ctx: Context) => {
- const params = ctx.request.body as Record<string, string>;
- console.log('[Alipay Notify] 收到异步通知:', params);
- // 验证签名
- const signVerified = PaymentService.verifyAlipaySign(params);
- if (!signVerified) {
- console.error('[Alipay Notify] 签名验证失败');
- ctx.status = 400;
- ctx.body = 'fail';
- return;
- }
- const { out_trade_no, trade_status, trade_no } = params;
- try {
- if (trade_status === 'TRADE_SUCCESS' || trade_status === 'TRADE_FINISHED') {
- // 支付成功
- await PaymentService.handlePaymentCallback(out_trade_no, trade_no, 'success');
- console.log('[Alipay Notify] 订单支付成功:', out_trade_no);
- ctx.body = 'success';
- } else if (trade_status === 'WAIT_BUYER_PAY') {
- // 等待买家付款
- console.log('[Alipay Notify] 等待买家付款:', out_trade_no);
- ctx.body = 'success';
- } else {
- // 其他状态视为失败
- await PaymentService.handlePaymentCallback(out_trade_no, trade_no, 'failed');
- console.log('[Alipay Notify] 订单支付失败:', out_trade_no, trade_status);
- ctx.body = 'success';
- }
- } catch (error) {
- console.error('[Alipay Notify] 处理回调失败:', error);
- ctx.status = 500;
- ctx.body = 'fail';
- }
- });
- // 支付宝同步回调(用户从支付宝页面返回)
- router.get('/alipay/return', async (ctx: Context) => {
- const params = ctx.query as Record<string, string>;
- console.log('[Alipay Return] 收到同步返回:', params);
- // 验证签名
- const signVerified = PaymentService.verifyAlipaySign(params);
- if (!signVerified) {
- console.error('[Alipay Return] 签名验证失败');
- // 重定向到支付结果页面(失败)
- const failUrl = `${process.env.ALIPAY_RETURN_URL || '/#/pages/payment-result/index'}?status=failed&error_msg=签名验证失败`;
- ctx.redirect(failUrl);
- return;
- }
- const { out_trade_no, trade_status } = params;
- // 根据支付状态重定向
- if (trade_status === 'TRADE_SUCCESS' || trade_status === 'TRADE_FINISHED') {
- // 支付成功
- const successUrl = `${process.env.ALIPAY_RETURN_URL || '/#/pages/payment-result/index'}?status=success&orderNo=${out_trade_no}&trade_status=${trade_status}`;
- ctx.redirect(successUrl);
- } else {
- // 支付未完成
- const failUrl = `${process.env.ALIPAY_RETURN_URL || '/#/pages/payment-result/index'}?status=failed&orderNo=${out_trade_no}`;
- ctx.redirect(failUrl);
- }
- });
- // 微信支付回调(V3 加密格式)
- router.post('/wechat/notify', async (ctx: Context) => {
- try {
- const body = ctx.request.body as any;
- console.log('[WeChat Notify] 收到回调:', JSON.stringify(body));
- // WeChat V3 回调是加密的,需要先解密 resource.ciphertext
- const eventType = body.event_type;
- const decrypted = PaymentService.decryptWechatNotify(body);
- if (!decrypted) {
- console.error('[WeChat Notify] 回调解密失败,返回 500');
- ctx.status = 500;
- ctx.body = { code: 'FAIL', message: '解密失败' };
- return;
- }
- const { out_trade_no, transaction_id, trade_state } = decrypted;
- if (!out_trade_no) {
- console.error('[WeChat Notify] 解密后缺少 out_trade_no:', JSON.stringify(decrypted));
- ctx.status = 400;
- ctx.body = { code: 'FAIL', message: '缺少订单号' };
- return;
- }
- if (trade_state === 'SUCCESS') {
- await PaymentService.handlePaymentCallback(out_trade_no, transaction_id, 'success');
- console.log('[WeChat Notify] 订单支付成功:', out_trade_no);
- } else {
- await PaymentService.handlePaymentCallback(out_trade_no, transaction_id, 'failed');
- console.log('[WeChat Notify] 订单支付失败:', out_trade_no, trade_state);
- }
- ctx.body = { code: 'SUCCESS', message: '成功' };
- } catch (error) {
- console.error('[WeChat Notify] 处理回调失败:', error);
- ctx.status = 500;
- ctx.body = { code: 'FAIL', message: '处理失败' };
- }
- });
- // 查询微信支付订单状态
- router.get('/wechat/query/:orderNo', authMiddleware, async (ctx: Context) => {
- const { orderNo } = ctx.params;
- const result = await PaymentService.queryWechatOrder(orderNo);
- if (!result) {
- ctx.body = {
- code: -1,
- message: '未配置微信支付或查询失败',
- data: null
- };
- return;
- }
- ctx.body = {
- code: 0,
- message: 'success',
- data: result
- };
- });
- // 获取订单列表
- router.get('/orders', authMiddleware, async (ctx: Context) => {
- const userId = safeParseInt(ctx.state.user.userId);
- const { page = '1', pageSize = '20' } = ctx.query as { page?: string; pageSize?: string };
- const result = await PaymentService.getOrderList(
- userId,
- Number(page) || 1,
- Number(pageSize) || 20
- );
- ctx.body = {
- code: 0,
- message: 'success',
- data: result
- };
- });
- // 获取订单详情(含主动同步微信支付状态)
- router.get('/orders/:orderNo', authMiddleware, async (ctx: Context) => {
- const userId = safeParseInt(ctx.state.user.userId);
- const { orderNo } = ctx.params;
- const result = await PaymentService.getOrderDetailWithSync(orderNo, userId);
- ctx.body = {
- code: 0,
- message: 'success',
- data: result
- };
- });
- // 生成支付宝扫码支付二维码
- router.post('/alipay/qrcode', authMiddleware, async (ctx: Context) => {
- const userId = parseInt(ctx.state.user.userId);
- const { planId } = ctx.request.body as { planId: number };
- if (!planId) {
- throw new BadRequestError('请选择套餐');
- }
- // 获取套餐信息
- const plan = await prisma.subscriptionPlan.findUnique({
- where: { id: planId }
- });
- if (!plan) {
- throw new BadRequestError('套餐不存在');
- }
- // 生成订单
- const orderNo = PaymentService.generateOrderNo();
- const amount = Number(plan.priceMonthly);
- // 创建订单记录
- await prisma.order.create({
- data: {
- userId,
- orderNo,
- planId,
- productType: 'monthly',
- amount,
- status: 'pending',
- paymentMethod: 'alipay',
- }
- });
- // 生成支付二维码链接
- const qrcodeUrl = await PaymentService.generateAlipayQrcode(orderNo, amount, plan.name);
- ctx.body = {
- code: 0,
- message: 'success',
- data: {
- orderNo,
- amount,
- planName: plan.name,
- qrcode: qrcodeUrl
- }
- };
- });
- // ==================== 微信 JSAPI 支付(公众号内支付) ====================
- // 生成微信 OAuth 授权链接(前端跳转到微信获取 code)
- router.get('/wechat/oauth-url', async (ctx: Context) => {
- let { redirect } = ctx.query as { redirect?: string };
- // 修复HTML实体编码问题(Nginx或Koa可能把 / 编码成 /)
- if (redirect) {
- redirect = redirect.replace(//|//gi, '/').replace(/:|:/gi, ':');
- }
- // OAuth授权必须使用公众号appId
- const appId = process.env.WECHAT_PUBLIC_APP_ID || process.env.WECHAT_APP_ID;
- if (!appId) {
- ctx.body = { code: -1, message: '未配置微信公众号', data: null };
- return;
- }
- // 构造 OAuth 授权链接
- // scope=snsapi_base 静默授权,不需要用户确认,但只能获取 openid
- const baseUrl = redirect || (process.env.BASE_URL || 'https://book.rrbrr.com');
- const redirectUri = encodeURIComponent(baseUrl);
- const state = Math.random().toString(36).substring(2, 10);
- const oauthUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_base&state=${state}#wechat_redirect`;
-
- console.log('[WeChat OAuth] 构造授权链接:', { appId, baseUrl, redirectUri: decodeURIComponent(redirectUri), oauthUrl });
- ctx.body = {
- code: 0,
- message: 'success',
- data: { oauthUrl, state }
- };
- });
- // 通过 OAuth code 获取 openid
- router.post('/wechat/openid', async (ctx: Context) => {
- const { code } = ctx.request.body as { code: string };
- if (!code) {
- ctx.body = { code: -1, message: '授权码不能为空', data: null };
- return;
- }
- const openid = await PaymentService.getWechatOpenid(code);
- if (!openid) {
- ctx.body = { code: -1, message: '获取用户信息失败', data: null };
- return;
- }
- ctx.body = {
- code: 0,
- message: 'success',
- data: { openid }
- };
- });
- // 获取微信 JS-SDK 签名(用于 wx.config)
- router.get('/wechat/jssdk-sign', async (ctx: Context) => {
- const { url } = ctx.query as { url?: string };
-
- if (!url) {
- ctx.body = { code: -1, message: '缺少url参数', data: null };
- return;
- }
- const signature = await PaymentService.generateJsSdkSignature(url);
-
- if (!signature) {
- ctx.body = { code: -1, message: '生成签名失败', data: null };
- return;
- }
- ctx.body = {
- code: 0,
- message: 'success',
- data: signature
- };
- });
- // 创建微信 JSAPI 支付订单(公众号内支付)
- router.post('/wechat/jsapi', authMiddleware, async (ctx: Context) => {
- const userId = safeParseInt(ctx.state.user.userId);
- const rawBody = ctx.request.body as any;
- // ===== DEBUG: 完整请求日志 =====
- console.log('[JSAPI-DEBUG] ========== 收到JSAPI支付请求 ==========');
- console.log('[JSAPI-DEBUG] userId:', userId);
- console.log('[JSAPI-DEBUG] body keys:', Object.keys(rawBody));
- console.log('[JSAPI-DEBUG] body:', JSON.stringify(rawBody, null, 2));
- console.log('[JSAPI-DEBUG] headers:', JSON.stringify({
- 'content-type': ctx.request.headers['content-type'],
- 'user-agent': ctx.request.headers['user-agent'],
- 'authorization': ctx.request.headers['authorization'] ? '***' : 'none',
- }));
- const { planId, productType, quantity, period = 'monthly', openid } = rawBody as {
- planId?: number;
- productType?: string;
- quantity?: number;
- period?: 'monthly' | 'yearly';
- openid: string;
- };
- console.log('[JSAPI-DEBUG] 解析后参数:', JSON.stringify({ planId, productType, quantity, period, openid: openid ? openid.substring(0, 8) + '...' : 'null' }));
- if (!openid) {
- console.log('[JSAPI-DEBUG] ❌ 缺少openid');
- throw new BadRequestError('缺少用户标识(openid),请在微信中打开');
- }
- // Token 包支付
- if (productType === 'token-pack' && quantity) {
- const orderNo = PaymentService.generateOrderNo();
- const packPrice = 4.80; // 100分钟×0.048
- const amount = Math.round(packPrice * quantity * 100) / 100;
- const subject = `${quantity}包积分包(共${100 * quantity}分钟)`;
- await prisma.order.create({
- data: {
- userId, orderNo, planId: null,
- productType: 'token-pack', amount,
- status: 'pending', paymentMethod: 'wechat',
- }
- });
- const payParams = await PaymentService.generateWechatJsapiPayment(orderNo, amount, subject, openid);
- console.log('[JSAPI-DEBUG] ✅ Token包支付创建成功, payParams keys:', Object.keys(payParams || {}));
- console.log('[JSAPI-DEBUG] 返回前端数据:', JSON.stringify({ orderNo, amount, payParams: { ...payParams, paySign: payParams?.paySign?.substring(0, 20) + '...' } }));
- ctx.body = {
- code: 0, message: '订单创建成功',
- data: { orderNo, amount, planName: subject, payParams }
- };
- return;
- }
- if (!planId) {
- throw new BadRequestError('请选择套餐');
- }
- // 获取套餐信息
- const plan = await prisma.subscriptionPlan.findUnique({
- where: { id: planId }
- });
- if (!plan) {
- throw new BadRequestError('套餐不存在');
- }
- // 生成订单
- const orderNo = PaymentService.generateOrderNo();
- const amount = period === 'yearly' ? Number(plan.priceYearly) : Number(plan.priceMonthly);
- // 创建订单记录
- await prisma.order.create({
- data: {
- userId,
- orderNo,
- planId,
- productType: period,
- amount,
- status: 'pending',
- paymentMethod: 'wechat',
- }
- });
- // 调用微信 JSAPI 支付
- const payParams = await PaymentService.generateWechatJsapiPayment(
- orderNo,
- amount,
- plan.name,
- openid
- );
- console.log('[JSAPI-DEBUG] ✅ 套餐支付创建成功, payParams keys:', Object.keys(payParams || {}));
- console.log('[JSAPI-DEBUG] 返回前端数据:', JSON.stringify({ orderNo, amount, planName: plan.name, payParams: { ...payParams, paySign: payParams?.paySign?.substring(0, 20) + '...' } }));
- ctx.body = {
- code: 0,
- message: '订单创建成功',
- data: {
- orderNo,
- amount,
- planName: plan.name,
- payParams // 用于 wx.chooseWXPay 的参数
- }
- };
- });
- export default router;
|