const http = require ( 'http' ) ;
const fs = require ( 'fs' ) ;
const path = require ( 'path' ) ;
const mysql = require ( 'mysql2/promise' ) ;
const OssUploader = require ( './oss-uploader' ) ;
const port = 3008 ;
// 数据库配置
const dbConfig = {
host : process . env . DB_HOST || '1.95.162.61' ,
user : process . env . DB_USER || 'root' ,
password : process . env . DB_PASSWORD || 'schl@2025' ,
database : process . env . DB_NAME || 'wechat_app' ,
waitForConnections : true ,
connectionLimit : 20 ,
queueLimit : 0 ,
connectTimeout : 10000 ,
timezone : '+08:00'
} ;
// 创建数据库连接池
const pool = mysql . createPool ( dbConfig ) ;
// 测试数据库连接
async function testDbConnection ( ) {
try {
const connection = await pool . getConnection ( ) ;
console . log ( '数据库连接成功' ) ;
connection . release ( ) ;
} catch ( error ) {
console . error ( '数据库连接失败:' , error . message ) ;
}
}
testDbConnection ( ) ;
const server = http . createServer ( async ( req , res ) => {
// 健康检查端点
if ( req . method === 'GET' && req . url === '/health' ) {
try {
// 测试数据库连接
const [ rows ] = await pool . execute ( 'SELECT 1' ) ;
// 测试OSS连接
const ossStatus = await OssUploader . testConnection ( ) ;
res . writeHead ( 200 , {
'Content-Type' : 'application/json' ,
'Access-Control-Allow-Origin' : '*' ,
'Access-Control-Allow-Methods' : 'POST, GET, OPTIONS' ,
'Access-Control-Allow-Headers' : 'Content-Type'
} ) ;
res . end ( JSON . stringify ( {
success : true ,
status : 'healthy' ,
database : 'connected' ,
oss : ossStatus . success ? 'connected' : 'disconnected' ,
timestamp : new Date ( ) . toISOString ( )
} ) ) ;
} catch ( error ) {
console . error ( '健康检查失败:' , error . message ) ;
res . writeHead ( 503 , {
'Content-Type' : 'application/json' ,
'Access-Control-Allow-Origin' : '*' ,
'Access-Control-Allow-Methods' : 'POST, GET, OPTIONS' ,
'Access-Control-Allow-Headers' : 'Content-Type'
} ) ;
res . end ( JSON . stringify ( {
success : false ,
status : 'unhealthy' ,
error : error . message ,
timestamp : new Date ( ) . toISOString ( )
} ) ) ;
}
return ;
}
// 登录接口
if ( req . method === 'POST' && req . url === '/login' ) {
try {
let body = '' ;
req . on ( 'data' , chunk => {
body += chunk . toString ( ) ;
} ) ;
req . on ( 'end' , async ( ) => {
try {
// 解析表单数据
const formData = new URLSearchParams ( body ) ;
const userName = formData . get ( 'userName' ) ;
const password = formData . get ( 'password' ) ;
const projectName = formData . get ( 'projectName' ) ;
console . log ( '接收到的登录请求:' , { userName , projectName } ) ;
// 验证输入
if ( ! userName || ! password || ! projectName ) {
res . writeHead ( 400 , {
'Content-Type' : 'application/json' ,
'Access-Control-Allow-Origin' : '*' ,
'Access-Control-Allow-Methods' : 'POST, GET, OPTIONS' ,
'Access-Control-Allow-Headers' : 'Content-Type'
} ) ;
res . end ( JSON . stringify ( {
success : false ,
error : '请输入用户名、密码和职位名称'
} ) ) ;
return ;
}
// 连接到userlogin数据库
const userloginPool = mysql . createPool ( {
... dbConfig ,
database : 'userlogin'
} ) ;
// 验证用户名、密码和职位名称
const [ loginRows ] = await userloginPool . execute (
'SELECT * FROM login WHERE userName = ? AND password = ? AND projectName = ?' ,
[ userName , password , projectName ]
) ;
if ( loginRows . length === 0 ) {
res . writeHead ( 401 , {
'Content-Type' : 'application/json' ,
'Access-Control-Allow-Origin' : '*' ,
'Access-Control-Allow-Methods' : 'POST, GET, OPTIONS' ,
'Access-Control-Allow-Headers' : 'Content-Type'
} ) ;
res . end ( JSON . stringify ( {
success : false ,
error : '用户名、密码或职位名称错误'
} ) ) ;
return ;
}
const loginInfo = loginRows [ 0 ] ;
const managerId = loginInfo . managerId ;
// 获取用户详细信息
const [ personnelRows ] = await userloginPool . execute (
'SELECT * FROM personnel WHERE managerId = ?' ,
[ managerId ]
) ;
let userInfo = {
userName : loginInfo . userName ,
projectName : loginInfo . projectName ,
managerId : loginInfo . managerId
} ;
if ( personnelRows . length > 0 ) {
const personnelInfo = personnelRows [ 0 ] ;
userInfo = {
... userInfo ,
name : personnelInfo . name ,
phoneNumber : personnelInfo . phoneNumber ,
managercompany : personnelInfo . managercompany ,
managerdepartment : personnelInfo . managerdepartment ,
organization : personnelInfo . organization ,
alias : personnelInfo . alias
} ;
}
console . log ( '登录成功,用户信息:' , userInfo ) ;
// 返回成功响应
res . writeHead ( 200 , {
'Content-Type' : 'application/json' ,
'Access-Control-Allow-Origin' : '*' ,
'Access-Control-Allow-Methods' : 'POST, GET, OPTIONS' ,
'Access-Control-Allow-Headers' : 'Content-Type'
} ) ;
res . end ( JSON . stringify ( {
success : true ,
user : userInfo
} ) ) ;
// 关闭临时连接池
await userloginPool . end ( ) ;
} catch ( error ) {
console . error ( '处理登录请求失败:' , error . message ) ;
res . writeHead ( 500 , {
'Content-Type' : 'application/json' ,
'Access-Control-Allow-Origin' : '*' ,
'Access-Control-Allow-Methods' : 'POST, GET, OPTIONS' ,
'Access-Control-Allow-Headers' : 'Content-Type'
} ) ;
res . end ( JSON . stringify ( {
success : false ,
error : '登录失败,请重试'
} ) ) ;
}
} ) ;
} catch ( error ) {
console . error ( '登录请求处理失败:' , error . message ) ;
res . writeHead ( 500 , {
'Content-Type' : 'application/json' ,
'Access-Control-Allow-Origin' : '*' ,
'Access-Control-Allow-Methods' : 'POST, GET, OPTIONS' ,
'Access-Control-Allow-Headers' : 'Content-Type'
} ) ;
res . end ( JSON . stringify ( {
success : false ,
error : '登录失败,请重试'
} ) ) ;
}
return ;
}
// 获取二维码合集接口
if ( req . method === 'GET' && req . url . startsWith ( '/getQrCollection' ) ) {
try {
// 解析URL参数,获取用户信息
const urlParams = new URLSearchParams ( req . url . split ( '?' ) [ 1 ] || '' ) ;
const userName = urlParams . get ( 'userName' ) ;
const projectName = urlParams . get ( 'projectName' ) ;
console . log ( '获取二维码合集请求,用户信息:' , { userName , projectName } ) ;
// 判断用户角色:如果职位名称包含"管理员"或"Admin",则视为管理员
const isAdmin = projectName && ( projectName . includes ( '管理员' ) || projectName . includes ( 'Admin' ) ) ;
console . log ( '用户角色判断:' , { isAdmin } ) ;
// 构建SQL查询
let query , params ;
if ( isAdmin ) {
// 管理员可以查看所有二维码
query = 'SELECT inviter, inviter_phone as inviterPhone, projectName as inviterProjectName, sessionId, qrCodeUrl, company, phoneNumber, DATE_FORMAT(issueDate, "%Y-%m-%d %H:%i:%s") as createdAt, issueDate FROM certificate WHERE qrCodeUrl IS NOT NULL GROUP BY sessionId ORDER BY issueDate DESC' ;
params = [ ] ;
} else if ( userName ) {
// 采购员只能查看自己的二维码
query = 'SELECT inviter, inviter_phone as inviterPhone, projectName as inviterProjectName, sessionId, qrCodeUrl, company, phoneNumber, DATE_FORMAT(issueDate, "%Y-%m-%d %H:%i:%s") as createdAt, issueDate FROM certificate WHERE inviter = ? AND qrCodeUrl IS NOT NULL GROUP BY sessionId ORDER BY issueDate DESC' ;
params = [ userName ] ;
} else {
// 未登录用户,返回空列表
res . writeHead ( 200 , {
'Content-Type' : 'application/json' ,
'Access-Control-Allow-Origin' : '*' ,
'Access-Control-Allow-Methods' : 'POST, GET, OPTIONS' ,
'Access-Control-Allow-Headers' : 'Content-Type'
} ) ;
res . end ( JSON . stringify ( {
success : true ,
qrCodes : [ ]
} ) ) ;
return ;
}
// 从数据库获取合格证信息
const [ rows ] = await pool . execute ( query , params ) ;
// 生成二维码URL列表
const qrCodes = rows . map ( row => {
// 生成包含会话ID的URL
const url = ` http://8.137.125.67:3008/certificate.html?sessionId= ${ encodeURIComponent ( row . sessionId ) } &inviter= ${ encodeURIComponent ( row . inviter ) } &inviterPhone= ${ encodeURIComponent ( row . inviterPhone ) } &inviterProjectName= ${ encodeURIComponent ( row . inviterProjectName ) } ` ;
return {
inviter : row . inviter ,
inviterPhone : row . inviterPhone ,
inviterProjectName : row . inviterProjectName ,
sessionId : row . sessionId ,
qrCodeUrl : row . qrCodeUrl ,
company : row . company ,
phoneNumber : row . phoneNumber ,
issueDate : row . issueDate ,
createdAt : row . createdAt ,
url : url
} ;
} ) ;
console . log ( '获取二维码合集成功,数量:' , qrCodes . length ) ;
// 获取所有邀请者列表(仅管理员)
let invitees = [ ] ;
if ( isAdmin ) {
const [ inviteeRows ] = await pool . execute (
'SELECT DISTINCT inviter, inviter_phone as inviterPhone, projectName as inviterProjectName FROM certificate WHERE inviter IS NOT NULL AND qrCodeUrl IS NOT NULL'
) ;
invitees = inviteeRows ;
}
res . writeHead ( 200 , {
'Content-Type' : 'application/json' ,
'Access-Control-Allow-Origin' : '*' ,
'Access-Control-Allow-Methods' : 'POST, GET, OPTIONS' ,
'Access-Control-Allow-Headers' : 'Content-Type'
} ) ;
res . end ( JSON . stringify ( {
success : true ,
qrCodes : qrCodes ,
isAdmin : isAdmin ,
invitees : invitees
} ) ) ;
} catch ( error ) {
console . error ( '获取二维码合集失败:' , error . message ) ;
res . writeHead ( 500 , {
'Content-Type' : 'application/json' ,
'Access-Control-Allow-Origin' : '*' ,
'Access-Control-Allow-Methods' : 'POST, GET, OPTIONS' ,
'Access-Control-Allow-Headers' : 'Content-Type'
} ) ;
res . end ( JSON . stringify ( {
success : false ,
error : '获取二维码合集失败,请重试'
} ) ) ;
}
return ;
}
// 处理GET请求,获取最新的合格证信息
if ( req . method === 'GET' && req . url . startsWith ( '/getLatestCertificate' ) ) {
try {
// 解析URL参数,获取sessionId
const urlParams = new URLSearchParams ( req . url . split ( '?' ) [ 1 ] || '' ) ;
const sessionId = urlParams . get ( 'sessionId' ) ;
console . log ( '获取合格证信息,sessionId:' , sessionId ) ;
// 从数据库获取对应会话的最新合格证信息
let query , params ;
if ( sessionId ) {
query = 'SELECT company as subjectName, phoneNumber as contact, productName, grossWeight as weight, commitBasis as basis, origin, DATE_FORMAT(issueDate, "%Y-%m-%d") as date, signature, qrCodeUrl, inviter, inviter_phone as inviterPhone, projectName as inviterProjectName FROM certificate WHERE sessionId = ? ORDER BY id DESC LIMIT 1' ;
params = [ sessionId ] ;
} else {
query = 'SELECT company as subjectName, phoneNumber as contact, productName, grossWeight as weight, commitBasis as basis, origin, DATE_FORMAT(issueDate, "%Y-%m-%d") as date, signature, qrCodeUrl, inviter, inviter_phone as inviterPhone, projectName as inviterProjectName FROM certificate WHERE sessionId IS NULL ORDER BY id DESC LIMIT 1' ;
params = [ ] ;
}
const [ rows ] = await pool . execute ( query , params ) ;
const certificate = rows . length > 0 ? rows [ 0 ] : null ;
console . log ( '从数据库获取的最新合格证信息:' , certificate ) ;
// 返回成功响应
res . writeHead ( 200 , {
'Content-Type' : 'application/json' ,
'Access-Control-Allow-Origin' : '*' ,
'Access-Control-Allow-Methods' : 'POST, GET, OPTIONS' ,
'Access-Control-Allow-Headers' : 'Content-Type'
} ) ;
res . end ( JSON . stringify ( { success : true , certificate } ) ) ;
} catch ( error ) {
console . error ( '获取合格证信息失败:' , error . message ) ;
res . writeHead ( 500 , {
'Content-Type' : 'application/json' ,
'Access-Control-Allow-Origin' : '*' ,
'Access-Control-Allow-Methods' : 'POST, GET, OPTIONS' ,
'Access-Control-Allow-Headers' : 'Content-Type'
} ) ;
res . end ( JSON . stringify ( { success : false , error : '获取失败,请重试' } ) ) ;
}
return ;
}
// 处理POST请求
if ( req . method === 'POST' && req . url === '/submit' ) {
try {
let body = '' ;
req . on ( 'data' , chunk => {
body += chunk . toString ( ) ;
} ) ;
req . on ( 'end' , async ( ) => {
try {
// 解析表单数据
const formData = new URLSearchParams ( body ) ;
// 使用当前时间作为开具日期,数据库连接已配置为北京时间
const now = new Date ( ) ;
const sessionId = formData . get ( 'sessionId' ) ;
const certificate = {
subjectName : formData . get ( 'subjectName' ) ,
contact : formData . get ( 'contact' ) ,
productName : formData . get ( 'productName' ) ,
weight : formData . get ( 'weight' ) ,
basis : formData . get ( 'basis' ) ,
origin : formData . get ( 'origin' ) ,
date : now ,
signature : formData . get ( 'signature' ) ,
sessionId : sessionId ,
inviter : formData . get ( 'inviter' ) ,
inviterPhone : formData . get ( 'inviterPhone' ) ,
inviterProjectName : formData . get ( 'inviterProjectName' )
} ;
console . log ( '接收到的表单数据:' , {
subjectName : certificate . subjectName ,
contact : certificate . contact ,
productName : certificate . productName ,
weight : certificate . weight ,
basis : certificate . basis ,
origin : certificate . origin ,
date : certificate . date . toISOString ( ) ,
hasSignature : ! ! certificate . signature ,
sessionId : sessionId ,
inviter : certificate . inviter ,
inviterPhone : certificate . inviterPhone ,
inviterProjectName : certificate . inviterProjectName
} ) ;
// 处理手写签名,上传到OSS
let signatureUrl = null ;
if ( certificate . signature ) {
try {
// 从base64字符串中提取图片数据
const base64Data = certificate . signature . replace ( /^data:image\/png;base64,/ , '' ) ;
const buffer = Buffer . from ( base64Data , 'base64' ) ;
// 生成唯一的文件名,包含合格证信息的标识
const timestamp = Date . now ( ) ;
const filename = ` certificate_signature_ ${ timestamp } .png ` ;
// 上传到OSS,指定文件夹为certificate/signatures
signatureUrl = await OssUploader . uploadBuffer ( buffer , filename , 'certificate/signatures' , 'image' ) ;
console . log ( '手写签名上传到OSS成功:' , signatureUrl ) ;
} catch ( error ) {
console . error ( '上传手写签名到OSS失败:' , error . message ) ;
// 即使上传失败,也继续处理,将签名数据存储为base64
signatureUrl = certificate . signature ;
}
}
// 生成查看页面的URL,包含会话ID
const viewUrl = ` http://8.137.125.67:3008/view.html?sessionId= ${ encodeURIComponent ( sessionId ) } ` ;
// 生成二维码并上传到OSS
let qrCodeUrl = null ;
try {
// 使用Google Charts API生成二维码
const qrCodeApiUrl = ` https://api.qrserver.com/v1/create-qr-code/?size=300x300&data= ${ encodeURIComponent ( viewUrl ) } ` ;
// 下载二维码图片
const https = require ( 'https' ) ;
const qrCodeBuffer = await new Promise ( ( resolve , reject ) => {
https . get ( qrCodeApiUrl , ( response ) => {
const chunks = [ ] ;
response . on ( 'data' , ( chunk ) => chunks . push ( chunk ) ) ;
response . on ( 'end' , ( ) => resolve ( Buffer . concat ( chunks ) ) ) ;
response . on ( 'error' , reject ) ;
} ) . on ( 'error' , reject ) ;
} ) ;
// 生成唯一的文件名
const timestamp = Date . now ( ) ;
const qrCodeFilename = ` certificate_qrcode_ ${ timestamp } .png ` ;
// 上传到OSS,指定文件夹为certificate/qrcodes
qrCodeUrl = await OssUploader . uploadBuffer ( qrCodeBuffer , qrCodeFilename , 'certificate/qrcodes' , 'image' ) ;
console . log ( '二维码上传到OSS成功:' , qrCodeUrl ) ;
} catch ( error ) {
console . error ( '上传二维码到OSS失败:' , error . message ) ;
// 即使上传失败,也继续处理
}
// 插入数据到数据库
await pool . execute (
'INSERT INTO certificate (company, phoneNumber, productName, grossWeight, commitBasis, origin, issueDate, signature, sessionId, qrCodeUrl, inviter, inviter_phone, projectName) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' ,
[ certificate . subjectName , certificate . contact , certificate . productName , certificate . weight , certificate . basis , certificate . origin , certificate . date , signatureUrl , certificate . sessionId , qrCodeUrl , certificate . inviter , certificate . inviterPhone , certificate . inviterProjectName ]
) ;
console . log ( '数据插入数据库成功' ) ;
// 返回成功响应,确保返回的signature是OSS URL,日期只显示到日
const responseCertificate = {
... certificate ,
signature : signatureUrl ,
qrCodeUrl : qrCodeUrl ,
inviter : certificate . inviter ,
inviterPhone : certificate . inviterPhone ,
inviterProjectName : certificate . inviterProjectName ,
// 格式化日期为YYYY-MM-DD格式
date : certificate . date . toISOString ( ) . split ( 'T' ) [ 0 ]
} ;
res . writeHead ( 200 , {
'Content-Type' : 'application/json' ,
'Access-Control-Allow-Origin' : '*' ,
'Access-Control-Allow-Methods' : 'POST, GET, OPTIONS' ,
'Access-Control-Allow-Headers' : 'Content-Type'
} ) ;
res . end ( JSON . stringify ( { success : true , certificate : responseCertificate } ) ) ;
} catch ( error ) {
console . error ( '处理表单数据失败:' , error . message ) ;
res . writeHead ( 500 , {
'Content-Type' : 'application/json' ,
'Access-Control-Allow-Origin' : '*' ,
'Access-Control-Allow-Methods' : 'POST, GET, OPTIONS' ,
'Access-Control-Allow-Headers' : 'Content-Type'
} ) ;
res . end ( JSON . stringify ( { success : false , error : '处理数据失败,请重试' } ) ) ;
}
} ) ;
} catch ( error ) {
console . error ( '保存合格证信息失败:' , error . message ) ;
res . writeHead ( 500 , {
'Content-Type' : 'application/json' ,
'Access-Control-Allow-Origin' : '*' ,
'Access-Control-Allow-Methods' : 'POST, GET, OPTIONS' ,
'Access-Control-Allow-Headers' : 'Content-Type'
} ) ;
res . end ( JSON . stringify ( { success : false , error : '保存失败,请重试' } ) ) ;
}
return ;
}
// 处理GET请求
// 解析请求路径,移除查询参数
let filePath = '.' + req . url . split ( '?' ) [ 0 ] ;
if ( filePath === './' ) {
filePath = './invite.html' ;
}
// 获取文件扩展名
const extname = String ( path . extname ( filePath ) ) . toLowerCase ( ) ;
// 定义MIME类型
const mimeTypes = {
'.html' : 'text/html' ,
'.js' : 'text/javascript' ,
'.css' : 'text/css' ,
'.json' : 'application/json' ,
'.png' : 'image/png' ,
'.jpg' : 'image/jpg' ,
'.gif' : 'image/gif' ,
'.svg' : 'image/svg+xml' ,
'.wav' : 'audio/wav' ,
'.mp4' : 'video/mp4' ,
'.woff' : 'application/font-woff' ,
'.ttf' : 'application/font-ttf' ,
'.eot' : 'application/vnd.ms-fontobject' ,
'.otf' : 'application/font-otf' ,
'.wasm' : 'application/wasm'
} ;
// 获取对应的MIME类型
const contentType = mimeTypes [ extname ] || 'application/octet-stream' ;
// 读取文件
fs . readFile ( filePath , ( error , content ) => {
if ( error ) {
if ( error . code == 'ENOENT' ) {
// 文件不存在
fs . readFile ( './404.html' , ( error , content ) => {
res . writeHead ( 404 , { 'Content-Type' : 'text/html' } ) ;
res . end ( content , 'utf-8' ) ;
} ) ;
} else {
// 服务器错误
res . writeHead ( 500 ) ;
res . end ( 'Sorry, check with the site admin for error: ' + error . code + ' ..\n' ) ;
res . end ( ) ;
}
} else {
// 成功读取文件
res . writeHead ( 200 , { 'Content-Type' : contentType } ) ;
res . end ( content , 'utf-8' ) ;
}
} ) ;
} ) ;
// 启动服务器
server . listen ( port , ( ) => {
console . log ( ` Server running at http://localhost: ${ port } / ` ) ;
} ) ;
console . log ( ` Server starting on port ${ port } ... ` ) ;