Skip to content
Toggle navigation
Projects
Groups
Snippets
Help
YYY
/
saas-v2-report-tool
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit ed210527
authored
Jun 05, 2026
by
niuxing
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
初始化报表工具
1 parent
6d624c29
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
298 additions
and
13 deletions
server/index.js
server/routes/query.js
server/routes/sql-manage.js
server/scheduler.js
server/sql-analyzer.js
src/App.jsx
src/components/SqlManagerModal.jsx
server/index.js
View file @
ed21052
...
@@ -11,7 +11,7 @@ import schedulerRouter from './routes/scheduler.js'
...
@@ -11,7 +11,7 @@ import schedulerRouter from './routes/scheduler.js'
const
__dirname
=
path
.
dirname
(
fileURLToPath
(
import
.
meta
.
url
))
const
__dirname
=
path
.
dirname
(
fileURLToPath
(
import
.
meta
.
url
))
const
app
=
express
()
const
app
=
express
()
const
PORT
=
4000
const
PORT
=
4000
const
HOSTNAME
=
"
0.0.0.0
"
const
HOSTNAME
=
"
localhost
"
// 中间件
// 中间件
app
.
use
(
cors
())
app
.
use
(
cors
())
...
...
server/routes/query.js
View file @
ed21052
import
{
Router
}
from
'express'
import
{
Router
}
from
'express'
import
{
v4
as
uuidv4
}
from
'uuid'
import
{
v4
as
uuidv4
}
from
'uuid'
import
{
executeQuery
}
from
'../db.js'
import
{
executeQuery
}
from
'../db.js'
import
{
analyzeSql
}
from
'../sql-analyzer.js'
import
{
analyzeSql
,
validateSelectOnly
}
from
'../sql-analyzer.js'
import
{
setCache
,
getCache
,
searchInCache
}
from
'../cache.js'
import
{
setCache
,
getCache
,
searchInCache
}
from
'../cache.js'
const
router
=
Router
()
const
router
=
Router
()
...
@@ -16,6 +16,13 @@ router.post('/check', async (req, res) => {
...
@@ -16,6 +16,13 @@ router.post('/check', async (req, res) => {
if
(
!
connId
||
!
sql
)
{
if
(
!
connId
||
!
sql
)
{
return
res
.
json
({
success
:
false
,
message
:
'缺少参数'
})
return
res
.
json
({
success
:
false
,
message
:
'缺少参数'
})
}
}
// 校验SQL必须为SELECT语句
const
sqlCheck
=
validateSelectOnly
(
sql
)
if
(
!
sqlCheck
.
allowed
)
{
return
res
.
json
({
success
:
false
,
message
:
sqlCheck
.
reason
})
}
const
result
=
await
analyzeSql
(
connId
,
sql
)
const
result
=
await
analyzeSql
(
connId
,
sql
)
res
.
json
({
success
:
true
,
data
:
result
})
res
.
json
({
success
:
true
,
data
:
result
})
}
catch
(
e
)
{
}
catch
(
e
)
{
...
@@ -34,6 +41,12 @@ router.post('/execute', async (req, res) => {
...
@@ -34,6 +41,12 @@ router.post('/execute', async (req, res) => {
return
res
.
json
({
success
:
false
,
message
:
'缺少参数'
})
return
res
.
json
({
success
:
false
,
message
:
'缺少参数'
})
}
}
// 校验SQL必须为SELECT语句
const
sqlCheck
=
validateSelectOnly
(
sql
)
if
(
!
sqlCheck
.
allowed
)
{
return
res
.
json
({
success
:
false
,
message
:
sqlCheck
.
reason
})
}
const
startTime
=
Date
.
now
()
const
startTime
=
Date
.
now
()
const
data
=
await
executeQuery
(
connId
,
sql
)
const
data
=
await
executeQuery
(
connId
,
sql
)
const
duration
=
Date
.
now
()
-
startTime
const
duration
=
Date
.
now
()
-
startTime
...
...
server/routes/sql-manage.js
View file @
ed21052
...
@@ -9,6 +9,7 @@ import { startScriptTask, stopScriptTask, reloadAllScriptTasks, executeScriptTas
...
@@ -9,6 +9,7 @@ import { startScriptTask, stopScriptTask, reloadAllScriptTasks, executeScriptTas
import
{
getPool
}
from
'../db.js'
import
{
getPool
}
from
'../db.js'
import
{
buildSqlWithDateRange
}
from
'../condition-builder.js'
import
{
buildSqlWithDateRange
}
from
'../condition-builder.js'
import
{
sendReportMail
,
loadSmtpConfig
}
from
'../mail.js'
import
{
sendReportMail
,
loadSmtpConfig
}
from
'../mail.js'
import
{
validateSelectOnly
}
from
'../sql-analyzer.js'
const
__dirname
=
path
.
dirname
(
fileURLToPath
(
import
.
meta
.
url
))
const
__dirname
=
path
.
dirname
(
fileURLToPath
(
import
.
meta
.
url
))
const
SCRIPTS_FILE
=
path
.
join
(
__dirname
,
'..'
,
'sql-scripts.json'
)
const
SCRIPTS_FILE
=
path
.
join
(
__dirname
,
'..'
,
'sql-scripts.json'
)
...
@@ -60,6 +61,12 @@ router.post('/', (req, res) => {
...
@@ -60,6 +61,12 @@ router.post('/', (req, res) => {
return
res
.
json
({
success
:
false
,
message
:
'脚本名称和SQL内容不能为空'
})
return
res
.
json
({
success
:
false
,
message
:
'脚本名称和SQL内容不能为空'
})
}
}
// 校验SQL必须为SELECT语句
const
sqlCheck
=
validateSelectOnly
(
sql
)
if
(
!
sqlCheck
.
allowed
)
{
return
res
.
json
({
success
:
false
,
message
:
sqlCheck
.
reason
})
}
const
scripts
=
loadScripts
()
const
scripts
=
loadScripts
()
const
id
=
`script_
${
Date
.
now
()}
`
const
id
=
`script_
${
Date
.
now
()}
`
const
now
=
new
Date
().
toISOString
()
const
now
=
new
Date
().
toISOString
()
...
@@ -114,6 +121,15 @@ router.put('/:id', (req, res) => {
...
@@ -114,6 +121,15 @@ router.put('/:id', (req, res) => {
return
res
.
json
({
success
:
false
,
message
:
'脚本不存在'
})
return
res
.
json
({
success
:
false
,
message
:
'脚本不存在'
})
}
}
// 校验SQL必须为SELECT语句(仅当sql被更新时校验)
const
newSql
=
sql
!==
undefined
?
sql
:
scripts
[
idx
].
sql
if
(
newSql
)
{
const
sqlCheck
=
validateSelectOnly
(
newSql
)
if
(
!
sqlCheck
.
allowed
)
{
return
res
.
json
({
success
:
false
,
message
:
sqlCheck
.
reason
})
}
}
// 先停止旧的定时任务
// 先停止旧的定时任务
stopScriptTask
(
id
)
stopScriptTask
(
id
)
...
@@ -168,6 +184,13 @@ router.post('/:id/run', async (req, res) => {
...
@@ -168,6 +184,13 @@ router.post('/:id/run', async (req, res) => {
const
scripts
=
loadScripts
()
const
scripts
=
loadScripts
()
const
script
=
scripts
.
find
(
s
=>
s
.
id
===
req
.
params
.
id
)
const
script
=
scripts
.
find
(
s
=>
s
.
id
===
req
.
params
.
id
)
if
(
!
script
)
return
res
.
json
({
success
:
false
,
message
:
'脚本不存在'
})
if
(
!
script
)
return
res
.
json
({
success
:
false
,
message
:
'脚本不存在'
})
// 校验SQL必须为SELECT语句
const
sqlCheck
=
validateSelectOnly
(
script
.
sql
)
if
(
!
sqlCheck
.
allowed
)
{
return
res
.
json
({
success
:
false
,
message
:
sqlCheck
.
reason
})
}
executeScriptTask
(
script
)
executeScriptTask
(
script
)
res
.
json
({
success
:
true
,
message
:
'任务已触发执行'
})
res
.
json
({
success
:
true
,
message
:
'任务已触发执行'
})
}
catch
(
e
)
{
}
catch
(
e
)
{
...
@@ -188,6 +211,12 @@ router.post('/:id/send-mail', async (req, res) => {
...
@@ -188,6 +211,12 @@ router.post('/:id/send-mail', async (req, res) => {
return
res
.
json
({
success
:
false
,
message
:
'请指定收件人邮箱'
})
return
res
.
json
({
success
:
false
,
message
:
'请指定收件人邮箱'
})
}
}
// 校验SQL必须为SELECT语句
const
sqlCheck
=
validateSelectOnly
(
script
.
sql
)
if
(
!
sqlCheck
.
allowed
)
{
return
res
.
json
({
success
:
false
,
message
:
sqlCheck
.
reason
})
}
// 验证SMTP配置
// 验证SMTP配置
const
smtpConfig
=
loadSmtpConfig
()
const
smtpConfig
=
loadSmtpConfig
()
if
(
!
smtpConfig
)
{
if
(
!
smtpConfig
)
{
...
...
server/scheduler.js
View file @
ed21052
...
@@ -7,6 +7,7 @@ import xlsx from 'xlsx'
...
@@ -7,6 +7,7 @@ import xlsx from 'xlsx'
import
{
getPool
}
from
'./db.js'
import
{
getPool
}
from
'./db.js'
import
{
buildSqlWithDateRange
}
from
'./condition-builder.js'
import
{
buildSqlWithDateRange
}
from
'./condition-builder.js'
import
{
sendReportMail
}
from
'./mail.js'
import
{
sendReportMail
}
from
'./mail.js'
import
{
validateSelectOnly
}
from
'./sql-analyzer.js'
const
__dirname
=
path
.
dirname
(
fileURLToPath
(
import
.
meta
.
url
))
const
__dirname
=
path
.
dirname
(
fileURLToPath
(
import
.
meta
.
url
))
const
SCRIPTS_FILE
=
path
.
join
(
__dirname
,
'..'
,
'sql-scripts.json'
)
const
SCRIPTS_FILE
=
path
.
join
(
__dirname
,
'..'
,
'sql-scripts.json'
)
...
@@ -66,6 +67,14 @@ export async function executeScriptTask(script) {
...
@@ -66,6 +67,14 @@ export async function executeScriptTask(script) {
console
.
log
(
`[定时任务] 开始执行:
${
script
.
name
}
(
${
new
Date
().
toLocaleString
()}
)`
)
console
.
log
(
`[定时任务] 开始执行:
${
script
.
name
}
(
${
new
Date
().
toLocaleString
()}
)`
)
try
{
try
{
// 校验SQL必须为SELECT语句
const
sqlCheck
=
validateSelectOnly
(
script
.
sql
)
if
(
!
sqlCheck
.
allowed
)
{
updateScriptStatus
(
script
.
id
,
'failed'
,
sqlCheck
.
reason
)
console
.
error
(
`[定时任务]
${
script
.
name
}
: SQL校验失败 -
${
sqlCheck
.
reason
}
`
)
return
}
// 1. 解析动态条件
// 1. 解析动态条件
const
resolvedConditions
=
resolveDynamicValues
(
script
.
conditions
||
[])
const
resolvedConditions
=
resolveDynamicValues
(
script
.
conditions
||
[])
...
...
server/sql-analyzer.js
View file @
ed21052
...
@@ -122,14 +122,132 @@ export async function analyzeSql(connId, sql) {
...
@@ -122,14 +122,132 @@ export async function analyzeSql(connId, sql) {
* 检测SQL类型
* 检测SQL类型
*/
*/
function
detectSqlType
(
sql
)
{
function
detectSqlType
(
sql
)
{
const
trimmed
=
sql
.
trim
().
toUpperCase
()
const
normalized
=
normalizeSqlForDetection
(
sql
)
if
(
trimmed
.
startsWith
(
'SELECT'
))
return
'SELECT'
if
(
/^
\s
*SELECT
\b
/i
.
test
(
normalized
))
return
'SELECT'
if
(
trimmed
.
startsWith
(
'INSERT'
))
return
'INSERT'
if
(
/^
\s
*WITH
\b
/i
.
test
(
normalized
))
return
'SELECT'
// CTE + SELECT
if
(
trimmed
.
startsWith
(
'UPDATE'
))
return
'UPDATE'
if
(
/^
\s
*INSERT
\b
/i
.
test
(
normalized
))
return
'INSERT'
if
(
trimmed
.
startsWith
(
'DELETE'
))
return
'DELETE'
if
(
/^
\s
*UPDATE
\b
/i
.
test
(
normalized
))
return
'UPDATE'
if
(
trimmed
.
startsWith
(
'CREATE'
))
return
'CREATE'
if
(
/^
\s
*DELETE
\b
/i
.
test
(
normalized
))
return
'DELETE'
if
(
trimmed
.
startsWith
(
'ALTER'
))
return
'ALTER'
if
(
/^
\s
*REPLACE
\b
/i
.
test
(
normalized
))
return
'REPLACE'
if
(
trimmed
.
startsWith
(
'DROP'
))
return
'DROP'
if
(
/^
\s
*CREATE
\b
/i
.
test
(
normalized
))
return
'CREATE'
if
(
trimmed
.
startsWith
(
'TRUNCATE'
))
return
'TRUNCATE'
if
(
/^
\s
*ALTER
\b
/i
.
test
(
normalized
))
return
'ALTER'
if
(
/^
\s
*DROP
\b
/i
.
test
(
normalized
))
return
'DROP'
if
(
/^
\s
*TRUNCATE
\b
/i
.
test
(
normalized
))
return
'TRUNCATE'
if
(
/^
\s
*LOAD
\s
+DATA
\b
/i
.
test
(
normalized
))
return
'LOAD_DATA'
if
(
/^
\s
*CALL
\b
/i
.
test
(
normalized
))
return
'CALL'
if
(
/^
\s
*GRANT
\b
/i
.
test
(
normalized
))
return
'GRANT'
if
(
/^
\s
*REVOKE
\b
/i
.
test
(
normalized
))
return
'REVOKE'
return
'OTHER'
return
'OTHER'
}
}
/**
* 去除SQL前缀注释和空白,用于类型检测
*/
function
normalizeSqlForDetection
(
sql
)
{
// 去除单行注释 (-- 和 #)
let
normalized
=
sql
.
replace
(
/--
[^\n]
*/g
,
' '
).
replace
(
/#
[^\n]
*/g
,
' '
)
// 去除多行注释
normalized
=
normalized
.
replace
(
/
\/\*[\s\S]
*
?\*\/
/g
,
' '
)
return
normalized
.
trim
()
}
/**
* 校验SQL是否为只读SELECT语句
* 支持检测多语句场景(分号分隔)
* @param {string} sql
* @returns {{ allowed: boolean, reason: string|null, detectedType: string }}
*/
export
function
validateSelectOnly
(
sql
)
{
if
(
!
sql
||
!
sql
.
trim
())
{
return
{
allowed
:
false
,
reason
:
'SQL内容不能为空'
,
detectedType
:
'EMPTY'
}
}
// 分号分隔的多语句逐条检测
const
statements
=
splitSqlStatements
(
sql
)
for
(
let
i
=
0
;
i
<
statements
.
length
;
i
++
)
{
const
stmt
=
statements
[
i
]
if
(
!
stmt
.
trim
())
continue
const
type
=
detectSqlType
(
stmt
)
if
(
type
===
'SELECT'
)
continue
// SELECT允许
// 非SELECT语句,拒绝
const
DANGEROUS_LABELS
=
{
'INSERT'
:
'INSERT(插入数据)'
,
'UPDATE'
:
'UPDATE(更新数据)'
,
'DELETE'
:
'DELETE(删除数据)'
,
'REPLACE'
:
'REPLACE(替换数据)'
,
'CREATE'
:
'CREATE(创建对象)'
,
'ALTER'
:
'ALTER(修改结构)'
,
'DROP'
:
'DROP(删除对象)'
,
'TRUNCATE'
:
'TRUNCATE(清空表)'
,
'LOAD_DATA'
:
'LOAD DATA(导入数据)'
,
'CALL'
:
'CALL(调用存储过程)'
,
'GRANT'
:
'GRANT(授权)'
,
'REVOKE'
:
'REVOKE(回收权限)'
,
'OTHER'
:
'未知类型'
,
}
const
label
=
DANGEROUS_LABELS
[
type
]
||
type
return
{
allowed
:
false
,
reason
:
`脚本仅允许SELECT查询语句,检测到第
${
i
+
1
}
条语句为
${
label
}
,不允许执行`
,
detectedType
:
type
,
}
}
return
{
allowed
:
true
,
reason
:
null
,
detectedType
:
'SELECT'
}
}
/**
* 按分号分割SQL语句(忽略引号内的分号)
*/
function
splitSqlStatements
(
sql
)
{
const
statements
=
[]
let
current
=
''
let
inSingleQuote
=
false
let
inDoubleQuote
=
false
for
(
let
i
=
0
;
i
<
sql
.
length
;
i
++
)
{
const
ch
=
sql
[
i
]
const
next
=
sql
[
i
+
1
]
if
(
ch
===
"'"
&&
!
inDoubleQuote
)
{
if
(
inSingleQuote
&&
next
===
"'"
)
{
// 转义单引号 ''
current
+=
ch
+
next
i
++
continue
}
inSingleQuote
=
!
inSingleQuote
current
+=
ch
continue
}
if
(
ch
===
'"'
&&
!
inSingleQuote
)
{
if
(
inDoubleQuote
&&
next
===
'"'
)
{
current
+=
ch
+
next
i
++
continue
}
inDoubleQuote
=
!
inDoubleQuote
current
+=
ch
continue
}
if
(
ch
===
';'
&&
!
inSingleQuote
&&
!
inDoubleQuote
)
{
statements
.
push
(
current
)
current
=
''
continue
}
current
+=
ch
}
if
(
current
.
trim
())
{
statements
.
push
(
current
)
}
return
statements
}
src/App.jsx
View file @
ed21052
...
@@ -24,6 +24,42 @@ import SmtpConfigModal from './components/SmtpConfigModal'
...
@@ -24,6 +24,42 @@ import SmtpConfigModal from './components/SmtpConfigModal'
import
ResultTable
from
'./components/ResultTable'
import
ResultTable
from
'./components/ResultTable'
import
request
from
'./utils/request'
import
request
from
'./utils/request'
// 前端SQL类型检测(与后端 sql-analyzer.js 保持一致)
const
BLOCKED_SQL_TYPES
=
[
'INSERT'
,
'UPDATE'
,
'DELETE'
,
'REPLACE'
,
'CREATE'
,
'ALTER'
,
'DROP'
,
'TRUNCATE'
,
'LOAD DATA'
,
'CALL'
,
'GRANT'
,
'REVOKE'
]
function
detectSqlTypeFrontend
(
sql
)
{
if
(
!
sql
||
!
sql
.
trim
())
return
{
type
:
'EMPTY'
,
isSelect
:
true
}
const
cleaned
=
sql
.
replace
(
/--
[^\n]
*/g
,
' '
).
replace
(
/#
[^\n]
*/g
,
' '
).
replace
(
/
\/\*[\s\S]
*
?\*\/
/g
,
' '
).
trim
()
const
upper
=
cleaned
.
toUpperCase
()
if
(
/^
\s
*SELECT
\b
/
.
test
(
upper
))
return
{
type
:
'SELECT'
,
isSelect
:
true
}
if
(
/^
\s
*WITH
\b
/
.
test
(
upper
))
return
{
type
:
'SELECT'
,
isSelect
:
true
}
for
(
const
kw
of
BLOCKED_SQL_TYPES
)
{
const
regex
=
new
RegExp
(
`^\\s*
${
kw
.
replace
(
/
\s
+/g
,
'\\s+'
)}
\\b`
,
'i'
)
if
(
regex
.
test
(
upper
))
return
{
type
:
kw
.
replace
(
/
\s
+/g
,
'_'
),
isSelect
:
false
,
label
:
kw
}
}
return
{
type
:
'OTHER'
,
isSelect
:
false
,
label
:
'未知类型'
}
}
function
validateSelectOnlyFrontend
(
sql
)
{
if
(
!
sql
||
!
sql
.
trim
())
return
{
allowed
:
true
,
reason
:
null
}
const
stmts
=
sql
.
split
(
';'
).
filter
(
s
=>
s
.
trim
())
for
(
let
i
=
0
;
i
<
stmts
.
length
;
i
++
)
{
const
det
=
detectSqlTypeFrontend
(
stmts
[
i
])
if
(
!
det
.
isSelect
)
{
return
{
allowed
:
false
,
reason
:
`仅允许SELECT查询语句,检测到第
${
i
+
1
}
条语句为
${
det
.
label
}
,不允许执行`
}
}
}
return
{
allowed
:
true
,
reason
:
null
}
}
// 清空右侧查询结果
function
clearQueryResults
(
setQueryId
,
setResultColumns
,
setResultTotal
,
setResultDuration
)
{
setQueryId
(
null
)
setResultColumns
([])
setResultTotal
(
0
)
setResultDuration
(
null
)
}
export
default
function
App
()
{
export
default
function
App
()
{
// 数据库连接状态
// 数据库连接状态
const
[
connId
,
setConnId
]
=
useState
(
null
)
const
[
connId
,
setConnId
]
=
useState
(
null
)
...
@@ -137,6 +173,15 @@ export default function App() {
...
@@ -137,6 +173,15 @@ export default function App() {
}
}
const
sql
=
buildFinalSql
()
const
sql
=
buildFinalSql
()
// 校验SQL必须为SELECT语句
const
sqlCheck
=
validateSelectOnlyFrontend
(
sql
)
if
(
!
sqlCheck
.
allowed
)
{
clearQueryResults
(
setQueryId
,
setResultColumns
,
setResultTotal
,
setResultDuration
)
showError
(
sqlCheck
.
reason
,
sql
,
'SQL校验'
)
return
}
setFinalSql
(
sql
)
setFinalSql
(
sql
)
try
{
try
{
...
@@ -145,9 +190,11 @@ export default function App() {
...
@@ -145,9 +190,11 @@ export default function App() {
setCheckResult
(
res
.
data
)
setCheckResult
(
res
.
data
)
setCheckModalOpen
(
true
)
setCheckModalOpen
(
true
)
}
else
{
}
else
{
clearQueryResults
(
setQueryId
,
setResultColumns
,
setResultTotal
,
setResultDuration
)
showError
(
res
.
message
,
sql
,
'预检查阶段'
)
showError
(
res
.
message
,
sql
,
'预检查阶段'
)
}
}
}
catch
(
e
)
{
}
catch
(
e
)
{
clearQueryResults
(
setQueryId
,
setResultColumns
,
setResultTotal
,
setResultDuration
)
showError
(
e
.
message
||
'检查失败'
,
sql
,
'预检查阶段'
)
showError
(
e
.
message
||
'检查失败'
,
sql
,
'预检查阶段'
)
}
}
}
}
...
@@ -155,6 +202,15 @@ export default function App() {
...
@@ -155,6 +202,15 @@ export default function App() {
// 确认执行
// 确认执行
const
handleConfirmExecute
=
async
()
=>
{
const
handleConfirmExecute
=
async
()
=>
{
setCheckModalOpen
(
false
)
setCheckModalOpen
(
false
)
// 校验SQL必须为SELECT语句(双保险)
const
sqlCheck
=
validateSelectOnlyFrontend
(
finalSql
)
if
(
!
sqlCheck
.
allowed
)
{
clearQueryResults
(
setQueryId
,
setResultColumns
,
setResultTotal
,
setResultDuration
)
showError
(
sqlCheck
.
reason
,
finalSql
,
'SQL校验'
)
return
}
setExecLoading
(
true
)
setExecLoading
(
true
)
try
{
try
{
...
@@ -173,9 +229,11 @@ export default function App() {
...
@@ -173,9 +229,11 @@ export default function App() {
// 执行成功后自动同步参数到脚本
// 执行成功后自动同步参数到脚本
syncConditionsToScript
()
syncConditionsToScript
()
}
else
{
}
else
{
clearQueryResults
(
setQueryId
,
setResultColumns
,
setResultTotal
,
setResultDuration
)
showError
(
res
.
message
,
finalSql
,
'确认执行阶段'
)
showError
(
res
.
message
,
finalSql
,
'确认执行阶段'
)
}
}
}
catch
(
e
)
{
}
catch
(
e
)
{
clearQueryResults
(
setQueryId
,
setResultColumns
,
setResultTotal
,
setResultDuration
)
showError
(
e
.
message
||
'执行失败'
,
finalSql
,
'确认执行阶段'
)
showError
(
e
.
message
||
'执行失败'
,
finalSql
,
'确认执行阶段'
)
}
finally
{
}
finally
{
setExecLoading
(
false
)
setExecLoading
(
false
)
...
@@ -193,6 +251,15 @@ export default function App() {
...
@@ -193,6 +251,15 @@ export default function App() {
}
}
const
sql
=
buildFinalSql
()
const
sql
=
buildFinalSql
()
// 校验SQL必须为SELECT语句
const
sqlCheck
=
validateSelectOnlyFrontend
(
sql
)
if
(
!
sqlCheck
.
allowed
)
{
clearQueryResults
(
setQueryId
,
setResultColumns
,
setResultTotal
,
setResultDuration
)
showError
(
sqlCheck
.
reason
,
sql
,
'SQL校验'
)
return
}
setFinalSql
(
sql
)
setFinalSql
(
sql
)
setExecLoading
(
true
)
setExecLoading
(
true
)
...
@@ -212,9 +279,11 @@ export default function App() {
...
@@ -212,9 +279,11 @@ export default function App() {
// 执行成功后自动同步参数到脚本
// 执行成功后自动同步参数到脚本
syncConditionsToScript
()
syncConditionsToScript
()
}
else
{
}
else
{
clearQueryResults
(
setQueryId
,
setResultColumns
,
setResultTotal
,
setResultDuration
)
showError
(
res
.
message
,
sql
,
'执行阶段'
)
showError
(
res
.
message
,
sql
,
'执行阶段'
)
}
}
}
catch
(
e
)
{
}
catch
(
e
)
{
clearQueryResults
(
setQueryId
,
setResultColumns
,
setResultTotal
,
setResultDuration
)
showError
(
e
.
message
||
'执行失败'
,
sql
,
'执行阶段'
)
showError
(
e
.
message
||
'执行失败'
,
sql
,
'执行阶段'
)
}
finally
{
}
finally
{
setExecLoading
(
false
)
setExecLoading
(
false
)
...
...
src/components/SqlManagerModal.jsx
View file @
ed21052
...
@@ -8,7 +8,7 @@ import {
...
@@ -8,7 +8,7 @@ import {
PlusOutlined
,
DeleteOutlined
,
EditOutlined
,
ImportOutlined
,
PlusOutlined
,
DeleteOutlined
,
EditOutlined
,
ImportOutlined
,
SearchOutlined
,
SaveOutlined
,
MailOutlined
,
ClockCircleOutlined
,
SearchOutlined
,
SaveOutlined
,
MailOutlined
,
ClockCircleOutlined
,
SettingOutlined
,
PlayCircleOutlined
,
CheckCircleOutlined
,
SettingOutlined
,
PlayCircleOutlined
,
CheckCircleOutlined
,
CloseCircleOutlined
,
UploadOutlined
,
CloseCircleOutlined
,
UploadOutlined
,
WarningOutlined
,
}
from
'@ant-design/icons'
}
from
'@ant-design/icons'
import
dayjs
from
'dayjs'
import
dayjs
from
'dayjs'
import
request
from
'../utils/request'
import
request
from
'../utils/request'
...
@@ -51,6 +51,36 @@ const validateEmails = (rule, value) => {
...
@@ -51,6 +51,36 @@ const validateEmails = (rule, value) => {
return
Promise
.
resolve
()
return
Promise
.
resolve
()
}
}
// 前端SQL类型检测(简化版,与后端 sql-analyzer.js 保持一致)
const
BLOCKED_SQL_TYPES
=
[
'INSERT'
,
'UPDATE'
,
'DELETE'
,
'REPLACE'
,
'CREATE'
,
'ALTER'
,
'DROP'
,
'TRUNCATE'
,
'LOAD DATA'
,
'CALL'
,
'GRANT'
,
'REVOKE'
]
function
detectSqlTypeFrontend
(
sql
)
{
if
(
!
sql
||
!
sql
.
trim
())
return
{
type
:
'EMPTY'
,
isSelect
:
true
,
label
:
''
}
// 去除注释
const
cleaned
=
sql
.
replace
(
/--
[^\n]
*/g
,
' '
).
replace
(
/#
[^\n]
*/g
,
' '
).
replace
(
/
\/\*[\s\S]
*
?\*\/
/g
,
' '
).
trim
()
const
upper
=
cleaned
.
toUpperCase
()
if
(
/^
\s
*SELECT
\b
/
.
test
(
upper
))
return
{
type
:
'SELECT'
,
isSelect
:
true
,
label
:
'SELECT(查询)'
}
if
(
/^
\s
*WITH
\b
/
.
test
(
upper
))
return
{
type
:
'SELECT'
,
isSelect
:
true
,
label
:
'CTE + SELECT(查询)'
}
for
(
const
kw
of
BLOCKED_SQL_TYPES
)
{
const
regex
=
new
RegExp
(
`^\\s*
${
kw
.
replace
(
/
\s
+/g
,
'\\s+'
)}
\\b`
,
'i'
)
if
(
regex
.
test
(
upper
))
return
{
type
:
kw
.
replace
(
/
\s
+/g
,
'_'
),
isSelect
:
false
,
label
:
`
${
kw
}
`
}
}
return
{
type
:
'OTHER'
,
isSelect
:
false
,
label
:
'未知类型'
}
}
function
validateSelectOnlyFrontend
(
sql
)
{
if
(
!
sql
||
!
sql
.
trim
())
return
{
allowed
:
true
,
reason
:
null
}
// 按分号分割(简化版)
const
stmts
=
sql
.
split
(
';'
).
filter
(
s
=>
s
.
trim
())
for
(
let
i
=
0
;
i
<
stmts
.
length
;
i
++
)
{
const
det
=
detectSqlTypeFrontend
(
stmts
[
i
])
if
(
!
det
.
isSelect
)
{
return
{
allowed
:
false
,
reason
:
`脚本仅允许SELECT查询语句,检测到第
${
i
+
1
}
条语句为
${
det
.
label
}
,不允许保存和执行`
}
}
}
return
{
allowed
:
true
,
reason
:
null
}
}
export
default
function
SqlManagerModal
({
open
,
onClose
,
onLoadSql
,
currentSql
,
currentConditions
,
connId
,
currentScriptId
,
onScriptIdChange
})
{
export
default
function
SqlManagerModal
({
open
,
onClose
,
onLoadSql
,
currentSql
,
currentConditions
,
connId
,
currentScriptId
,
onScriptIdChange
})
{
const
[
scripts
,
setScripts
]
=
useState
([])
const
[
scripts
,
setScripts
]
=
useState
([])
const
[
loading
,
setLoading
]
=
useState
(
false
)
const
[
loading
,
setLoading
]
=
useState
(
false
)
...
@@ -548,10 +578,27 @@ export default function SqlManagerModal({ open, onClose, onLoadSql, currentSql,
...
@@ -548,10 +578,27 @@ export default function SqlManagerModal({ open, onClose, onLoadSql, currentSql,
<
Form
.
Item
name=
"sql"
label=
"SQL内容"
rules=
{
[{
required
:
true
,
message
:
'SQL内容不能为空'
}]
}
>
<
Form
.
Item
name=
"sql"
label=
"SQL内容"
rules=
{
[{
required
:
true
,
message
:
'SQL内容不能为空'
}]
}
>
<
TextArea
<
TextArea
rows=
{
6
}
rows=
{
6
}
placeholder=
"
SQL脚本内容
"
placeholder=
"
仅允许 SELECT 查询语句,不支持 INSERT/UPDATE/DELETE/DDL 等数据变更操作
"
style=
{
{
fontFamily
:
'Menlo, Monaco, Consolas, monospace'
,
fontSize
:
12
}
}
style=
{
{
fontFamily
:
'Menlo, Monaco, Consolas, monospace'
,
fontSize
:
12
}
}
/>
/>
</
Form
.
Item
>
</
Form
.
Item
>
<
Form
.
Item
shouldUpdate=
{
(
prev
,
cur
)
=>
prev
.
sql
!==
cur
.
sql
}
noStyle
>
{
({
getFieldValue
})
=>
{
const
sqlVal
=
getFieldValue
(
'sql'
)
||
''
const
check
=
validateSelectOnlyFrontend
(
sqlVal
)
if
(
sqlVal
.
trim
()
&&
!
check
.
allowed
)
{
return
(
<
div
style=
{
{
padding
:
'6px 12px'
,
marginBottom
:
12
,
background
:
'#fff2f0'
,
border
:
'1px solid #ffccc7'
,
borderRadius
:
6
,
fontSize
:
12
,
color
:
'#cf1322'
,
}
}
>
<
WarningOutlined
style=
{
{
marginRight
:
4
}
}
/>
{
check
.
reason
}
</
div
>
)
}
return
null
}
}
</
Form
.
Item
>
<
div
style=
{
{
textAlign
:
'right'
,
marginBottom
:
8
}
}
>
<
div
style=
{
{
textAlign
:
'right'
,
marginBottom
:
8
}
}
>
<
Button
size=
"small"
icon=
{
<
SearchOutlined
/>
}
onClick=
{
handleParseConditionsInDetail
}
>
<
Button
size=
"small"
icon=
{
<
SearchOutlined
/>
}
onClick=
{
handleParseConditionsInDetail
}
>
从SQL解析参数
从SQL解析参数
...
...
Write
Preview
Markdown
is supported
Attach a file
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to post a comment