refactor: migrate to Tencent Cloud backend

这个提交包含在:
Guoguo
2026-04-28 22:56:47 +08:00
父节点 267c75718b
当前提交 444c91c0b0
修改 102 个文件,包含 17927 行新增1997 行删除
+52
查看文件
@@ -0,0 +1,52 @@
const crypto = require('crypto')
const jwt = require('jsonwebtoken')
const config = require('../config')
const { one } = require('./db')
function hashPassword(password, salt) {
return crypto.createHash('sha256').update(String(password) + ':' + salt).digest('hex')
}
function randomHex(bytes) {
return crypto.randomBytes(bytes).toString('hex')
}
function signUser(user) {
return jwt.sign({ type: 'user', user_id: user.user_id, openid: user.openid }, config.jwt.secret, { expiresIn: config.jwt.expiresIn })
}
function signAdmin(admin) {
return jwt.sign({ type: 'admin', admin_id: admin.admin_id, username: admin.username, role: admin.role }, config.jwt.adminSecret, { expiresIn: config.jwt.expiresIn })
}
function readBearer(headers) {
const auth = headers.authorization || headers.Authorization || ''
const match = auth.match(/^Bearer\s+(.+)$/i)
return match ? match[1] : ''
}
async function requireUser(ctx) {
const token = readBearer(ctx.headers)
if (!token) return null
try {
const payload = jwt.verify(token, config.jwt.secret)
if (payload.type !== 'user') return null
return await one('SELECT * FROM users WHERE user_id = :user_id AND status = 1', { user_id: payload.user_id })
} catch (err) {
return null
}
}
async function requireAdmin(ctx) {
const token = readBearer(ctx.headers) || (ctx.body && ctx.body.token)
if (!token) return null
try {
const payload = jwt.verify(token, config.jwt.adminSecret)
if (payload.type !== 'admin') return null
return await one('SELECT * FROM admin_accounts WHERE admin_id = :admin_id AND status = 1', { admin_id: payload.admin_id })
} catch (err) {
return null
}
}
module.exports = { hashPassword, randomHex, signUser, signAdmin, requireUser, requireAdmin }
+38
查看文件
@@ -0,0 +1,38 @@
const COS = require('cos-nodejs-sdk-v5')
const config = require('../config')
let client
function getClient() {
if (!client) {
client = new COS({
SecretId: config.cos.secretId,
SecretKey: config.cos.secretKey
})
}
return client
}
function getObjectUrl(key, expiresSeconds) {
return getClient().getObjectUrl({
Bucket: config.cos.bucket,
Region: config.cos.region,
Key: key,
Sign: true,
Expires: expiresSeconds || 600
})
}
function getPutObjectUrl(key, contentType, expiresSeconds) {
return getClient().getObjectUrl({
Bucket: config.cos.bucket,
Region: config.cos.region,
Key: key,
Method: 'PUT',
Sign: true,
Expires: expiresSeconds || 600,
Headers: contentType ? { 'Content-Type': contentType } : undefined
})
}
module.exports = { getClient, getObjectUrl, getPutObjectUrl }
+48
查看文件
@@ -0,0 +1,48 @@
const mysql = require('mysql2/promise')
const config = require('../config')
let pool
function getPool() {
if (!pool) {
pool = mysql.createPool({
host: config.db.host,
port: config.db.port,
user: config.db.user,
password: config.db.password,
database: config.db.database,
waitForConnections: true,
connectionLimit: 5,
namedPlaceholders: true,
timezone: '+08:00'
})
}
return pool
}
async function query(sql, params) {
const [rows] = await getPool().execute(sql, params || {})
return rows
}
async function one(sql, params) {
const rows = await query(sql, params)
return rows[0] || null
}
async function transaction(work) {
const conn = await getPool().getConnection()
try {
await conn.beginTransaction()
const result = await work(conn)
await conn.commit()
return result
} catch (err) {
await conn.rollback()
throw err
} finally {
conn.release()
}
}
module.exports = { getPool, query, one, transaction }
+16
查看文件
@@ -0,0 +1,16 @@
const { query } = require('./db')
async function writeLog(options) {
await query(
'INSERT INTO operation_logs (user_id, admin_id, action, detail, ip) VALUES (:user_id, :admin_id, :action, :detail, :ip)',
{
user_id: options.user_id || null,
admin_id: options.admin_id || null,
action: options.action,
detail: options.detail || '',
ip: options.ip || ''
}
)
}
module.exports = { writeLog }
+45
查看文件
@@ -0,0 +1,45 @@
function normalizeHeaders(headers) {
const result = {}
Object.keys(headers || {}).forEach(key => {
result[key] = headers[key]
result[key.toLowerCase()] = headers[key]
})
return result
}
function parseBody(event) {
if (!event.body) return {}
if (typeof event.body === 'object') return event.body
const raw = event.isBase64Encoded ? Buffer.from(event.body, 'base64').toString('utf8') : event.body
if (!raw) return {}
try { return JSON.parse(raw) } catch (err) { return {} }
}
function parseQuery(event) {
if (event.queryStringParameters) return event.queryStringParameters || {}
if (event.query) return event.query || {}
return {}
}
function getPath(event) {
return event.path || event.Path || event.requestContext && event.requestContext.path || '/'
}
function getMethod(event) {
return String(event.httpMethod || event.method || event.requestContext && event.requestContext.httpMethod || 'GET').toUpperCase()
}
function createContext(event) {
return {
event,
method: getMethod(event),
path: getPath(event),
headers: normalizeHeaders(event.headers),
query: parseQuery(event),
body: parseBody(event),
params: {},
ip: event.requestContext && event.requestContext.sourceIp || ''
}
}
module.exports = { createContext }
+23
查看文件
@@ -0,0 +1,23 @@
function ok(data) {
return { code: 0, message: 'success', data: data || {} }
}
function fail(code, message, data) {
return { code, message, data: data || {} }
}
function http(statusCode, body, headers) {
return {
isBase64Encoded: false,
statusCode,
headers: Object.assign({
'Content-Type': 'application/json; charset=utf-8',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Device-Id, X-App-Version, X-Platform',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS'
}, headers || {}),
body: JSON.stringify(body)
}
}
module.exports = { ok, fail, http }
+33
查看文件
@@ -0,0 +1,33 @@
class Router {
constructor() {
this.routes = []
}
add(method, pattern, handler) {
const keys = []
const regex = new RegExp('^' + pattern.replace(/\/:(\w+)/g, function (_, key) {
keys.push(key)
return '/([^/]+)'
}) + '$')
this.routes.push({ method, regex, keys, handler })
}
get(pattern, handler) { this.add('GET', pattern, handler) }
post(pattern, handler) { this.add('POST', pattern, handler) }
put(pattern, handler) { this.add('PUT', pattern, handler) }
delete(pattern, handler) { this.add('DELETE', pattern, handler) }
match(method, path) {
for (const route of this.routes) {
if (route.method !== method) continue
const match = path.match(route.regex)
if (!match) continue
const params = {}
route.keys.forEach((key, index) => { params[key] = decodeURIComponent(match[index + 1]) })
return { handler: route.handler, params }
}
return null
}
}
module.exports = Router
+76
查看文件
@@ -0,0 +1,76 @@
const https = require('https')
const config = require('../config')
function requestJson(url) {
return new Promise((resolve, reject) => {
https.get(url, res => {
let raw = ''
res.on('data', chunk => { raw += chunk })
res.on('end', () => {
try { resolve(JSON.parse(raw)) } catch (err) { reject(err) }
})
}).on('error', reject)
})
}
function postJson(url, body) {
return new Promise((resolve, reject) => {
const data = JSON.stringify(body || {})
const req = https.request(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data)
}
}, res => {
let raw = ''
res.on('data', chunk => { raw += chunk })
res.on('end', () => {
try { resolve(JSON.parse(raw)) } catch (err) { reject(err) }
})
})
req.on('error', reject)
req.write(data)
req.end()
})
}
let accessTokenCache = null
async function getAccessToken() {
if (accessTokenCache && accessTokenCache.expiresAt > Date.now() + 60000) return accessTokenCache.token
if (!config.wechat.appid || !config.wechat.secret) throw new Error('WECHAT_APPID or WECHAT_SECRET is not configured')
const url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' + encodeURIComponent(config.wechat.appid) + '&secret=' + encodeURIComponent(config.wechat.secret)
const data = await requestJson(url)
if (!data.access_token) throw new Error(data.errmsg || 'wechat access_token failed')
accessTokenCache = {
token: data.access_token,
expiresAt: Date.now() + (Number(data.expires_in || 7200) * 1000)
}
return accessTokenCache.token
}
async function code2Session(code) {
if (config.nodeEnv === 'development' && (!code || code === 'local' || String(code).indexOf('dev_') === 0)) {
return { openid: 'dev_openid_' + String(code || 'local').slice(-8) }
}
if (!config.wechat.appid || !config.wechat.secret) {
if (config.nodeEnv === 'development') return { openid: 'dev_openid_' + String(code || 'local').slice(-8) }
throw new Error('WECHAT_APPID or WECHAT_SECRET is not configured')
}
const url = 'https://api.weixin.qq.com/sns/jscode2session?appid=' + encodeURIComponent(config.wechat.appid) + '&secret=' + encodeURIComponent(config.wechat.secret) + '&js_code=' + encodeURIComponent(code) + '&grant_type=authorization_code'
const data = await requestJson(url)
if (!data.openid) throw new Error(data.errmsg || 'wechat login failed')
return data
}
async function getPhoneNumber(code) {
if (!code) throw new Error('phone code required')
const token = await getAccessToken()
const url = 'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=' + encodeURIComponent(token)
const data = await postJson(url, { code })
if (data.errcode) throw new Error(data.errmsg || 'get phone number failed')
return data.phone_info || null
}
module.exports = { code2Session, getPhoneNumber }