refactor: 项目架构重构 - 目录标准化 + 清理冗余
- 目录重命名: backend/wdkj-server/ → server/, frontend/ai-dimension/ → client/ - 删除 20+ 冗余文件(WDKJ 旧脚本、Windows 脚本、过时文档、设计稿) - 更新 package.json 元数据(移除 wdkj 命名) - 完善三级 .gitignore(根 + server + client) - 重写 README.md 和 CHANGELOG.md - 工具脚本移至 scripts/
This commit is contained in:
Executable
+96
@@ -0,0 +1,96 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
/**
|
||||
* 日志级别
|
||||
*/
|
||||
const LOG_LEVELS = {
|
||||
debug: 0,
|
||||
info: 1,
|
||||
warn: 2,
|
||||
error: 3
|
||||
}
|
||||
|
||||
/**
|
||||
* 日志器
|
||||
*/
|
||||
class Logger {
|
||||
constructor(level = 'info', logDir = './logs') {
|
||||
this.level = LOG_LEVELS[level] || LOG_LEVELS.info
|
||||
this.logDir = logDir
|
||||
|
||||
// 确保日志目录存在
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日志消息
|
||||
*/
|
||||
formatMessage(level, message, meta = {}) {
|
||||
const timestamp = new Date().toISOString()
|
||||
const metaStr = Object.keys(meta).length > 0 ? JSON.stringify(meta) : ''
|
||||
return `[${timestamp}] [${level.toUpperCase()}] ${message} ${metaStr}\n`
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入日志文件
|
||||
*/
|
||||
writeToFile(level, message, meta) {
|
||||
const date = new Date().toISOString().split('T')[0]
|
||||
const logFile = path.join(this.logDir, `${date}.log`)
|
||||
const logMessage = this.formatMessage(level, message, meta)
|
||||
|
||||
fs.appendFile(logFile, logMessage, (err) => {
|
||||
if (err) console.error('写入日志失败:', err)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录日志
|
||||
*/
|
||||
log(level, message, meta = {}) {
|
||||
if (LOG_LEVELS[level] < this.level) return
|
||||
|
||||
const formattedMessage = this.formatMessage(level, message, meta)
|
||||
|
||||
// 控制台输出
|
||||
switch (level) {
|
||||
case 'error':
|
||||
console.error(formattedMessage.trim())
|
||||
break
|
||||
case 'warn':
|
||||
console.warn(formattedMessage.trim())
|
||||
break
|
||||
default:
|
||||
console.log(formattedMessage.trim())
|
||||
}
|
||||
|
||||
// 文件输出(生产环境)
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
this.writeToFile(level, message, meta)
|
||||
}
|
||||
}
|
||||
|
||||
debug(message, meta) {
|
||||
this.log('debug', message, meta)
|
||||
}
|
||||
|
||||
info(message, meta) {
|
||||
this.log('info', message, meta)
|
||||
}
|
||||
|
||||
warn(message, meta) {
|
||||
this.log('warn', message, meta)
|
||||
}
|
||||
|
||||
error(message, meta) {
|
||||
this.log('error', message, meta)
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
const logger = new Logger(process.env.LOG_LEVEL || 'info')
|
||||
|
||||
module.exports = logger
|
||||
Executable
+95
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 统一响应格式
|
||||
*/
|
||||
class ApiResponse {
|
||||
/**
|
||||
* 成功响应
|
||||
*/
|
||||
static success(res, data = null, message = '操作成功', statusCode = 200) {
|
||||
return res.status(statusCode).json({
|
||||
success: true,
|
||||
message,
|
||||
data,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误响应
|
||||
*/
|
||||
static error(res, error = '操作失败', statusCode = 400, details = null) {
|
||||
return res.status(statusCode).json({
|
||||
success: false,
|
||||
error,
|
||||
details,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页响应
|
||||
*/
|
||||
static paginated(res, items, pagination, message = '查询成功') {
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message,
|
||||
data: {
|
||||
items,
|
||||
pagination: {
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
total: pagination.total,
|
||||
totalPages: Math.ceil(pagination.total / pagination.limit)
|
||||
}
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 未授权响应
|
||||
*/
|
||||
static unauthorized(res, error = '未授权访问') {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁止访问响应
|
||||
*/
|
||||
static forbidden(res, error = '权限不足') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源不存在响应
|
||||
*/
|
||||
static notFound(res, error = '资源不存在') {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务器错误响应
|
||||
*/
|
||||
static serverError(res, error = '服务器内部错误') {
|
||||
console.error('Server Error:', error)
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ApiResponse
|
||||
Executable
+235
@@ -0,0 +1,235 @@
|
||||
const axios = require('axios')
|
||||
const crypto = require('crypto')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
/**
|
||||
* 微信小程序 API 工具类
|
||||
*/
|
||||
class WeChatService {
|
||||
constructor() {
|
||||
this.appId = process.env.WX_APPID
|
||||
this.secret = process.env.WX_SECRET
|
||||
this.mchId = process.env.WX_PAY_MCH_ID
|
||||
this.apiKey = process.env.WX_PAY_API_KEY // APIv3 Key
|
||||
this.serialNo = process.env.WX_PAY_SERIAL_NO
|
||||
this.notifyUrl = process.env.WX_PAY_NOTIFY_URL
|
||||
|
||||
// 加载证书
|
||||
try {
|
||||
const certPath = path.join(__dirname, '../../certs/apiclient_cert.pem')
|
||||
const keyPath = path.join(__dirname, '../../certs/apiclient_key.pem')
|
||||
this.privateKey = fs.readFileSync(keyPath)
|
||||
this.certificate = fs.readFileSync(certPath)
|
||||
} catch (error) {
|
||||
console.error('微信支付证书加载失败,请检查 certs 目录:', error.message)
|
||||
}
|
||||
|
||||
// 缓存 access_token
|
||||
this.accessToken = null
|
||||
this.tokenExpireTime = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Access Token
|
||||
*/
|
||||
async getAccessToken() {
|
||||
// 检查缓存
|
||||
if (this.accessToken && Date.now() < this.tokenExpireTime) {
|
||||
return this.accessToken
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get('https://api.weixin.qq.com/cgi-bin/token', {
|
||||
params: {
|
||||
grant_type: 'client_credential',
|
||||
appid: this.appId,
|
||||
secret: this.secret
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.errcode) {
|
||||
throw new Error(response.data.errmsg)
|
||||
}
|
||||
|
||||
this.accessToken = response.data.access_token
|
||||
this.tokenExpireTime = Date.now() + (response.data.expires_in - 300) * 1000 // 提前5分钟过期
|
||||
|
||||
return this.accessToken
|
||||
} catch (error) {
|
||||
console.error('获取 Access Token 失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 code 换取 openid 和 session_key
|
||||
*/
|
||||
async code2Session(code) {
|
||||
try {
|
||||
const response = await axios.get('https://api.weixin.qq.com/sns/jscode2session', {
|
||||
params: {
|
||||
appid: this.appId,
|
||||
secret: this.secret,
|
||||
js_code: code,
|
||||
grant_type: 'authorization_code'
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.errcode && response.data.errcode !== 0) {
|
||||
throw new Error(response.data.errmsg)
|
||||
}
|
||||
|
||||
return {
|
||||
openid: response.data.openid,
|
||||
sessionKey: response.data.session_key,
|
||||
unionid: response.data.unionid
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('code2Session 失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机字符串
|
||||
*/
|
||||
generateNonceStr(length = 32) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
let result = ''
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成订单号
|
||||
*/
|
||||
generateOrderNo(prefix = 'WD') {
|
||||
const date = new Date()
|
||||
const dateStr = date.getFullYear().toString() +
|
||||
String(date.getMonth() + 1).padStart(2, '0') +
|
||||
String(date.getDate()).padStart(2, '0')
|
||||
const randomStr = Math.random().toString(36).substr(2, 6).toUpperCase()
|
||||
return `${prefix}${dateStr}${randomStr}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名
|
||||
*/
|
||||
generateSign(method, url, timestamp, nonceStr, body = '') {
|
||||
const signStr = `${method}\n${url}\n${timestamp}\n${nonceStr}\n${body}\n`
|
||||
const sign = crypto.createSign('RSA-SHA256')
|
||||
sign.update(signStr)
|
||||
sign.end()
|
||||
return sign.sign(this.privateKey, 'base64')
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建支付订单(小程序支付 - 真实流程)
|
||||
*/
|
||||
async createPaymentOrder(orderData) {
|
||||
const { openid, orderNo, body, amount } = orderData
|
||||
|
||||
const nonceStr = this.generateNonceStr()
|
||||
const timestamp = Math.floor(Date.now() / 1000).toString()
|
||||
|
||||
// 1. 统一下单 (JSAPI)
|
||||
const url = '/v3/pay/transactions/jsapi'
|
||||
const fullUrl = `https://api.mch.weixin.qq.com${url}`
|
||||
|
||||
const orderParams = {
|
||||
appid: this.appId,
|
||||
mchid: this.mchId,
|
||||
description: body,
|
||||
out_trade_no: orderNo,
|
||||
notify_url: this.notifyUrl,
|
||||
amount: {
|
||||
total: Math.round(amount * 100), // 转换为分
|
||||
currency: 'CNY'
|
||||
},
|
||||
payer: {
|
||||
openid: openid
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 注意:实际生产环境需要处理网络请求和签名
|
||||
// const response = await axios.post(fullUrl, orderParams, { ... })
|
||||
// const prepayId = response.data.prepay_id
|
||||
|
||||
// 模拟获取 prepay_id
|
||||
const prepayId = `wx_mock_${Date.now()}`
|
||||
|
||||
// 2. 生成前端调起支付的签名
|
||||
const paySignStr = `${this.appId}\n${timestamp}\n${nonceStr}\nprepay_id=${prepayId}\n`
|
||||
const paySign = crypto.createSign('RSA-SHA256')
|
||||
paySign.update(paySignStr)
|
||||
paySign.end()
|
||||
const signature = paySign.sign(this.privateKey, 'base64')
|
||||
|
||||
return {
|
||||
appId: this.appId,
|
||||
timeStamp: timestamp,
|
||||
nonceStr,
|
||||
package: `prepay_id=${prepayId}`,
|
||||
signType: 'RSA',
|
||||
paySign: signature,
|
||||
orderNo
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('微信支付下单失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请退款
|
||||
*/
|
||||
async refundOrder(transactionId, outRefundNo, totalAmount, refundAmount, reason = '用户申请退款') {
|
||||
const url = '/v3/refund/domestic/refunds'
|
||||
const fullUrl = `https://api.mch.weixin.qq.com${url}`
|
||||
|
||||
const nonceStr = this.generateNonceStr()
|
||||
const timestamp = Math.floor(Date.now() / 1000).toString()
|
||||
|
||||
const refundParams = {
|
||||
transaction_id: transactionId,
|
||||
out_refund_no: outRefundNo,
|
||||
reason: reason,
|
||||
notify_url: this.notifyUrl.replace('/callback', '/refund-callback'), // 假设退款回调地址
|
||||
amount: {
|
||||
refund: Math.round(refundAmount * 100),
|
||||
total: Math.round(totalAmount * 100),
|
||||
currency: 'CNY'
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 注意:实际生产环境需要处理网络请求和签名
|
||||
// const sign = this.generateSign('POST', url, timestamp, nonceStr, JSON.stringify(refundParams))
|
||||
// const response = await axios.post(fullUrl, refundParams, { ... })
|
||||
|
||||
console.log('[WeChat] 模拟退款请求:', refundParams)
|
||||
return { success: true, message: '退款申请已提交' }
|
||||
} catch (error) {
|
||||
console.error('微信支付退款失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证支付回调签名
|
||||
*/
|
||||
verifyCallbackSignature(data, sign) {
|
||||
// 实际应使用微信支付 V3 的签名验证方法
|
||||
// 这里仅作示例
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
const weChatService = new WeChatService()
|
||||
|
||||
module.exports = weChatService
|
||||
Reference in New Issue
Block a user