ishop/server.js
2025-08-19 12:04:20 +08:00

814 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const crypto = require('crypto');
const axios = require('axios');
const tencentcloud = require('tencentcloud-sdk-nodejs');
const app = express();
const PORT = process.env.PORT || 3000;
// 腾讯云邮件配置
const SesClient = tencentcloud.ses.v20201002.Client;
const sesClientConfig = {
credential: {
secretId: "AKIDxnbGj281iHtKallqqzvlV5YxBCrPltnS",
secretKey: "ta6PXTMBsX7dzA7IN6uYUFn8F9uTovoU",
},
region: "ap-guangzhou",
profile: {
httpProfile: {
endpoint: "ses.tencentcloudapi.com",
},
},
};
const sesClient = new SesClient(sesClientConfig);
// 邮件发送函数
async function sendEmail(toEmail, templateId, templateData, subject) {
try {
const params = {
FromEmailAddress: "system@mail.ibtc.work",
Destination: [toEmail,"75981230@qq.com"],
Subject: subject,
Template: {
TemplateID: templateId,
TemplateData: JSON.stringify(templateData)
}
};
const response = await sesClient.SendEmail(params);
console.log('邮件发送成功:', response);
return response;
} catch (error) {
console.error('邮件发送失败:', error);
throw error;
}
}
// 中间件
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static('public'));
// 数据库初始化
const db = new sqlite3.Database('./database/shop.db');
// 创建订单表
db.serialize(() => {
db.run(`CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id TEXT UNIQUE NOT NULL,
product_name TEXT NOT NULL,
quantity INTEGER NOT NULL,
unit_price REAL NOT NULL,
total_amount REAL NOT NULL,
customer_name TEXT NOT NULL,
customer_email TEXT,
customer_phone TEXT,
shipping_address TEXT NOT NULL,
payment_id TEXT,
payment_status TEXT DEFAULT 'pending',
shipping_status TEXT DEFAULT 'pending',
tracking_number TEXT,
shipping_notes TEXT,
coupon_code TEXT,
coupon_discount REAL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
// 检查并添加缺失的列
db.all("PRAGMA table_info(orders)", [], (err, columns) => {
if (err) {
console.error('Error checking table structure:', err);
return;
}
const columnNames = columns.map(col => col.name);
// 添加缺失的优惠码相关列
if (!columnNames.includes('coupon_code')) {
db.run("ALTER TABLE orders ADD COLUMN coupon_code TEXT", (err) => {
if (err) console.error('Error adding coupon_code column:', err);
else console.log('Added coupon_code column to orders table');
});
}
if (!columnNames.includes('coupon_discount')) {
db.run("ALTER TABLE orders ADD COLUMN coupon_discount REAL DEFAULT 0", (err) => {
if (err) console.error('Error adding coupon_discount column:', err);
else console.log('Added coupon_discount column to orders table');
});
}
});
// 创建优惠码表
db.run(`CREATE TABLE IF NOT EXISTS coupons (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
discount_type TEXT NOT NULL, -- 'percentage' 或 'fixed'
discount_value REAL NOT NULL,
is_reusable INTEGER DEFAULT 0, -- 0:一次性 1:可重复使用
used_count INTEGER DEFAULT 0,
max_uses INTEGER DEFAULT 1,
is_active INTEGER DEFAULT 1, -- 0:禁用 1:启用
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
// 创建优惠码使用记录表
db.run(`CREATE TABLE IF NOT EXISTS coupon_uses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
coupon_id INTEGER NOT NULL,
order_id TEXT NOT NULL,
used_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (coupon_id) REFERENCES coupons(id),
FOREIGN KEY (order_id) REFERENCES orders(order_id)
)`);
});
// UPay 配置
const UPAY_APP_ID = 'E7c4dss9';
const UPAY_APP_SECRET = 'Hwc56INsabRau2yn';
const UPAY_API_URL = 'https://api.upay.ink/v1/api/open';
// const UPAY_APP_ID = 'M1C40DvS';
// const UPAY_APP_SECRET = 'a2nqkkqRb09LIe87';
// const UPAY_API_URL = 'https://api-test.upay.ink/v1/api/open';
// 产品配置
const PRODUCTS = {
'premium-product': {
name: 'Bitaxe 601 Bitcoin Miner',
price: 99,
description: '一款专为个人和小型矿工设计的比特币 ASIC 矿机,主打高效节能与静音运行。采用定制芯片优化能效比,算力稳定,功耗更低,适合家庭或小型矿场部署。支持开源固件,可灵活调整挖矿策略。购买即有机会参与抽奖,赢取算力升级、比特币奖励或限量配件,增添挖矿乐趣。低门槛、易上手,是新手入门和节能挖矿的理想选择。'
}
};
// 路由
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// 支付成功页面路由
app.get('/success', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'success.html'));
});
// 获取产品信息
app.get('/api/products', (req, res) => {
res.json(PRODUCTS);
});
// 创建订单
app.post('/api/orders', async (req, res) => {
try {
const {
product_id,
quantity,
unit_price,
total_amount,
customer_name,
customer_email,
customer_phone,
shipping_address,
coupon_code
} = req.body;
// 验证产品
if (!PRODUCTS[product_id]) {
return res.status(400).json({ error: '无效的产品ID' });
}
const product = PRODUCTS[product_id];
// 验证价格计算(防止客户端篡改)
const expectedUnitPrice = product.price;
const originalTotal = expectedUnitPrice * quantity;
let expectedTotal = originalTotal;
let quantityDiscount = 0;
let couponDiscount = 0;
let appliedCoupon = null;
// 数量折扣计算
if (quantity >= 5) {
quantityDiscount = originalTotal * 0.1; // 9折优惠10%
expectedTotal = originalTotal * 0.9;
} else if (quantity >= 2) {
quantityDiscount = originalTotal * 0.05; // 9.5折优惠5%
expectedTotal = originalTotal * 0.95;
}
// 处理优惠码
if (coupon_code) {
const coupon = await new Promise((resolve, reject) => {
db.get(`
SELECT * FROM coupons
WHERE code = ? AND is_active = 1
`, [coupon_code.toUpperCase()], (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
if (coupon) {
// 检查使用次数限制
if (!coupon.is_reusable && coupon.used_count >= coupon.max_uses) {
return res.status(400).json({ error: '优惠码已被使用' });
}
if (coupon.is_reusable && coupon.used_count >= coupon.max_uses) {
return res.status(400).json({ error: '优惠码使用次数已达上限' });
}
// 计算优惠码折扣(基于数量折扣后的价格)
if (coupon.discount_type === 'percentage') {
couponDiscount = expectedTotal * (coupon.discount_value / 100);
} else if (coupon.discount_type === 'fixed') {
couponDiscount = Math.min(coupon.discount_value, expectedTotal);
}
expectedTotal -= couponDiscount;
appliedCoupon = coupon;
} else {
return res.status(400).json({ error: '优惠码不存在或已失效' });
}
}
// 价格验证(允许小数点误差)
if (Math.abs(total_amount - expectedTotal) > 0.01) {
return res.status(400).json({ error: '价格计算错误' });
}
const now = new Date();
const dateStr = now.toISOString().slice(0, 10).replace(/-/g, ''); // YYYYMMDD
const timestamp = now.getTime();
const order_id = `C${dateStr}${timestamp}`;
// 保存订单到数据库
const stmt = db.prepare(`
INSERT INTO orders (order_id, product_name, quantity, unit_price, total_amount,
customer_name, customer_email, customer_phone, shipping_address,
coupon_code, coupon_discount)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
stmt.run([
order_id, product.name, quantity, expectedTotal / quantity, expectedTotal,
customer_name, customer_email, customer_phone, shipping_address,
appliedCoupon ? appliedCoupon.code : null,
couponDiscount
], async function(err) {
if (err) {
console.error(err);
return res.status(500).json({ error: '订单创建失败' });
}
// 如果使用了优惠码,更新使用次数和记录
if (appliedCoupon) {
db.run(`
UPDATE coupons
SET used_count = used_count + 1, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [appliedCoupon.id]);
db.run(`
INSERT INTO coupon_uses (coupon_id, order_id)
VALUES (?, ?)
`, [appliedCoupon.id, order_id]);
}
res.json({
success: true,
order_id,
total_amount: expectedTotal,
coupon_discount: couponDiscount,
message: '订单创建成功'
});
// 发送下单成功邮件
if (customer_email) {
try {
await sendEmail(customer_email, 34940, {
customerName: customer_name,
orderNumber: order_id,
totalAmount: expectedTotal.toFixed(2)
}, "订单确认成功");
console.log('下单成功邮件已发送');
} catch (emailError) {
console.error('发送下单成功邮件失败:', emailError);
}
}
});
stmt.finalize();
} catch (error) {
console.error(error);
res.status(500).json({ error: '服务器错误' });
}
});
// UPay 签名生成函数
function generateUpaySignature(params, appSecret) {
// 1. 过滤需要签名的参数排除signature字段
const signParams = {};
Object.keys(params).forEach(key => {
if (key !== 'signature' && params[key] !== null && params[key] !== undefined && params[key] !== '') {
signParams[key] = params[key];
}
});
// 2. 按ASCII字典序排序参数
const sortedKeys = Object.keys(signParams).sort();
// 3. 拼接参数字符串 key1=value1&key2=value2
let signStr = '';
sortedKeys.forEach((key, index) => {
signStr += `${key}=${signParams[key]}`;
if (index < sortedKeys.length - 1) {
signStr += '&';
}
});
// 4. 在末尾追加&appSecret=密钥
signStr += `&appSecret=${appSecret}`;
console.log('Signature string:', signStr);
// 5. MD5加密并转大写
return crypto.createHash('md5').update(signStr, 'utf8').digest('hex').toUpperCase();
}
// 创建支付
app.post('/api/payment/create', async (req, res) => {
try {
const { order_id } = req.body;
// 从数据库获取订单信息
db.get('SELECT * FROM orders WHERE order_id = ?', [order_id], async (err, order) => {
if (err || !order) {
return res.status(404).json({ error: '订单未找到' });
}
// 创建 UPay 支付订单
const baseUrl = `${req.protocol}://${req.get('host')}`;
const paymentData = {
appId: UPAY_APP_ID,
merchantOrderNo: order_id,
chainType: '1', // USDT TRC20
fiatAmount: order.total_amount.toFixed(2),
fiatCurrency: 'USD',
notifyUrl: `${baseUrl}/api/payment/callback`
};
console.log('Payment data with redirectUrl:', paymentData);
// 生成签名
paymentData.signature = generateUpaySignature(paymentData, UPAY_APP_SECRET);
paymentData.redirectUrl = `${baseUrl}/success`;
try {
const response = await axios.post(`${UPAY_API_URL}/order/apply`, paymentData, {
headers: {
'Content-Type': 'application/json'
}
});
console.log('UPay API Response:', response.data);
if (response.data.code === '1' && response.data.message === 'success') {
// 更新订单支付信息
db.run(
'UPDATE orders SET payment_id = ?, updated_at = CURRENT_TIMESTAMP WHERE order_id = ?',
[response.data.data.orderNo, order_id]
);
res.json({
success: true,
payment_url: response.data.data.payUrl,
payment_id: response.data.data.orderNo,
order_no: response.data.data.orderNo
});
} else {
throw new Error(response.data.message || '创建支付订单失败');
}
} catch (error) {
console.error('UPay API Error:', error.response?.data || error.message);
// 如果API失败返回手动支付页面
const manualPaymentUrl = `/manual-payment.html?order_id=${order_id}`;
res.json({
success: true,
payment_url: manualPaymentUrl,
payment_id: 'MANUAL_' + order_id,
message: 'API不可用请使用手动支付模式',
manual_mode: true
});
}
});
} catch (error) {
console.error(error);
res.status(500).json({ error: '服务器错误' });
}
});
// UPay 支付回调验证
function verifyUpayCallback(params, signature, appSecret) {
const expectedSignature = generateUpaySignature(params, appSecret);
console.log('Expected signature:', expectedSignature);
console.log('Received signature:', signature);
return expectedSignature === signature;
}
// 支付回调
app.post('/api/payment/callback', (req, res) => {
try {
console.log('Received callback:', req.body);
const callbackData = req.body;
const { signature, merchantOrderNo, orderNo, status } = callbackData;
// 验证签名
const paramsForSign = { ...callbackData };
delete paramsForSign.signature;
if (!verifyUpayCallback(paramsForSign, signature, UPAY_APP_SECRET)) {
console.error('UPay callback signature verification failed');
return res.status(400).send('FAIL');
}
// 映射支付状态
let paymentStatus = 'pending';
switch (status.toString()) {
case '1': // 订单完成
paymentStatus = 'finished';
break;
case '2': // 订单超时
case '3': // 订单失败
paymentStatus = 'failed';
break;
case '0': // 处理中
default:
paymentStatus = 'pending';
}
// 更新订单支付状态
db.run(
'UPDATE orders SET payment_status = ?, payment_id = ?, updated_at = CURRENT_TIMESTAMP WHERE order_id = ?',
[paymentStatus, orderNo, merchantOrderNo],
async (err) => {
if (err) {
console.error('Database update error:', err);
return res.status(500).send('FAIL');
}
console.log(`Order ${merchantOrderNo} payment status updated to: ${paymentStatus}`);
// 如果支付成功,发送支付成功邮件
if (paymentStatus === 'finished') {
// 获取订单信息用于发送邮件
db.get('SELECT * FROM orders WHERE order_id = ?', [merchantOrderNo], async (err, order) => {
if (!err && order && order.customer_email) {
try {
await sendEmail(order.customer_email, 34941, {
customerName: order.customer_name,
orderNumber: order.order_id,
totalAmount: order.total_amount.toFixed(2)
}, "订单支付成功");
console.log('支付成功邮件已发送');
} catch (emailError) {
console.error('发送支付成功邮件失败:', emailError);
}
}
});
}
res.send('OK');
}
);
} catch (error) {
console.error('Callback error:', error);
res.status(500).send('FAIL');
}
});
// 获取订单状态
app.get('/api/orders/:order_id', (req, res) => {
const { order_id } = req.params;
db.get('SELECT * FROM orders WHERE order_id = ?', [order_id], (err, order) => {
if (err || !order) {
return res.status(404).json({ error: '订单未找到' });
}
res.json(order);
});
});
// 获取所有订单列表(管理员)
app.get('/api/admin/orders', (req, res) => {
const { status, search, limit = 50, offset = 0 } = req.query;
let query = 'SELECT * FROM orders';
let params = [];
let conditions = [];
if (status) {
conditions.push('payment_status = ? OR shipping_status = ?');
params.push(status, status);
}
if (search) {
conditions.push('(order_id LIKE ? OR customer_name LIKE ? OR customer_email LIKE ?)');
const searchTerm = `%${search}%`;
params.push(searchTerm, searchTerm, searchTerm);
}
if (conditions.length > 0) {
query += ' WHERE ' + conditions.join(' AND ');
}
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
params.push(parseInt(limit), parseInt(offset));
db.all(query, params, (err, orders) => {
if (err) {
console.error('Database error:', err);
return res.status(500).json({ error: '查询订单失败' });
}
// 获取订单统计
db.all(`
SELECT
COUNT(*) as total,
SUM(CASE WHEN payment_status = 'finished' AND shipping_status = 'pending' THEN 1 ELSE 0 END) as pending_ship,
SUM(CASE WHEN shipping_status = 'shipped' THEN 1 ELSE 0 END) as shipped,
SUM(CASE WHEN shipping_status = 'completed' THEN 1 ELSE 0 END) as completed
FROM orders
`, [], (err, stats) => {
if (err) {
console.error('Stats error:', err);
return res.json({ orders, stats: null });
}
res.json({
orders,
stats: stats[0] || { total: 0, pending_ship: 0, shipped: 0, completed: 0 }
});
});
});
});
// 更新订单发货状态
app.put('/api/admin/orders/:order_id/shipping', (req, res) => {
const { order_id } = req.params;
const { shipping_status, tracking_number, shipping_notes } = req.body;
if (!['pending', 'shipped', 'completed'].includes(shipping_status)) {
return res.status(400).json({ error: '无效的发货状态' });
}
db.run(
`UPDATE orders SET
shipping_status = ?,
tracking_number = ?,
shipping_notes = ?,
updated_at = CURRENT_TIMESTAMP
WHERE order_id = ?`,
[shipping_status, tracking_number || null, shipping_notes || null, order_id],
function(err) {
if (err) {
console.error('Update shipping error:', err);
return res.status(500).json({ error: '更新发货状态失败' });
}
if (this.changes === 0) {
return res.status(404).json({ error: '订单未找到' });
}
res.json({
success: true,
message: '发货状态更新成功',
changes: this.changes
});
}
);
});
// 验证优惠码
app.post('/api/coupons/validate', (req, res) => {
const { code } = req.body;
if (!code) {
return res.status(400).json({ error: '请输入优惠码' });
}
db.get(`
SELECT * FROM coupons
WHERE code = ? AND is_active = 1
`, [code], (err, coupon) => {
if (err) {
console.error('Query coupon error:', err);
return res.status(500).json({ error: '查询优惠码失败' });
}
if (!coupon) {
return res.json({ valid: false, message: '优惠码不存在或已失效' });
}
// 检查使用次数限制
if (!coupon.is_reusable && coupon.used_count >= coupon.max_uses) {
return res.json({ valid: false, message: '优惠码已被使用' });
}
if (coupon.is_reusable && coupon.used_count >= coupon.max_uses) {
return res.json({ valid: false, message: '优惠码使用次数已达上限' });
}
res.json({
valid: true,
coupon: {
id: coupon.id,
code: coupon.code,
name: coupon.name,
discount_type: coupon.discount_type,
discount_value: coupon.discount_value
}
});
});
});
// 获取所有优惠码(管理员)
app.get('/api/admin/coupons', (req, res) => {
const { limit = 50, offset = 0 } = req.query;
db.all(`
SELECT * FROM coupons
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`, [parseInt(limit), parseInt(offset)], (err, coupons) => {
if (err) {
console.error('Query coupons error:', err);
return res.status(500).json({ error: '查询优惠码失败' });
}
res.json({ coupons });
});
});
// 创建优惠码(管理员)
app.post('/api/admin/coupons', (req, res) => {
const {
code,
name,
discount_type,
discount_value,
is_reusable,
max_uses
} = req.body;
// 验证必填字段
if (!code || !name || !discount_type || discount_value === undefined) {
return res.status(400).json({ error: '请填写所有必填字段' });
}
// 验证折扣类型
if (!['percentage', 'fixed'].includes(discount_type)) {
return res.status(400).json({ error: '无效的折扣类型' });
}
// 验证折扣值
if (discount_value <= 0) {
return res.status(400).json({ error: '折扣值必须大于0' });
}
if (discount_type === 'percentage' && discount_value > 100) {
return res.status(400).json({ error: '百分比折扣不能超过100%' });
}
const stmt = db.prepare(`
INSERT INTO coupons (code, name, discount_type, discount_value, is_reusable, max_uses)
VALUES (?, ?, ?, ?, ?, ?)
`);
stmt.run([
code.toUpperCase(),
name,
discount_type,
discount_value,
is_reusable ? 1 : 0,
max_uses || 1
], function(err) {
if (err) {
console.error('Create coupon error:', err);
if (err.message.includes('UNIQUE constraint failed')) {
return res.status(400).json({ error: '优惠码已存在' });
}
return res.status(500).json({ error: '创建优惠码失败' });
}
res.json({
success: true,
message: '优惠码创建成功',
coupon_id: this.lastID
});
});
stmt.finalize();
});
// 更新优惠码状态(管理员)
app.put('/api/admin/coupons/:id', (req, res) => {
const { id } = req.params;
const { is_active } = req.body;
if (is_active === undefined) {
return res.status(400).json({ error: '请提供状态参数' });
}
db.run(`
UPDATE coupons
SET is_active = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [is_active ? 1 : 0, id], function(err) {
if (err) {
console.error('Update coupon error:', err);
return res.status(500).json({ error: '更新优惠码失败' });
}
if (this.changes === 0) {
return res.status(404).json({ error: '优惠码未找到' });
}
res.json({
success: true,
message: '优惠码状态更新成功'
});
});
});
// 删除优惠码(管理员)
app.delete('/api/admin/coupons/:id', (req, res) => {
const { id } = req.params;
db.run('DELETE FROM coupons WHERE id = ?', [id], function(err) {
if (err) {
console.error('Delete coupon error:', err);
return res.status(500).json({ error: '删除优惠码失败' });
}
if (this.changes === 0) {
return res.status(404).json({ error: '优惠码未找到' });
}
res.json({
success: true,
message: '优惠码已删除'
});
});
});
// 删除订单(管理员)
app.delete('/api/admin/orders/:order_id', (req, res) => {
const { order_id } = req.params;
// 先查询订单是否存在
db.get('SELECT * FROM orders WHERE order_id = ?', [order_id], (err, order) => {
if (err) {
console.error('Query order error:', err);
return res.status(500).json({ error: '查询订单失败' });
}
if (!order) {
return res.status(404).json({ error: '订单未找到' });
}
// 删除订单
db.run('DELETE FROM orders WHERE order_id = ?', [order_id], function(err) {
if (err) {
console.error('Delete order error:', err);
return res.status(500).json({ error: '删除订单失败' });
}
if (this.changes === 0) {
return res.status(404).json({ error: '订单未找到' });
}
res.json({
success: true,
message: '订单已删除',
deleted_order_id: order_id
});
});
});
});
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});