-
+
{{$t('已耗时:')}}
diff --git a/lib/client/src/views/project/release/publish-mixin.js b/lib/client/src/views/project/release/publish-mixin.js
index 16e2f2a75..e2f9bf82b 100644
--- a/lib/client/src/views/project/release/publish-mixin.js
+++ b/lib/client/src/views/project/release/publish-mixin.js
@@ -1,4 +1,5 @@
import dayjs from 'dayjs'
+import { safeJsonParse } from 'shared/security/xss-protection'
export default {
data () {
@@ -16,7 +17,7 @@ export default {
let time = info.createTime ? dayjs(info.createTime).format('YYYY-MM-DD HH:mm:ss') : '--'
let updateUser = info.updateUser
if (info.releaseType === 'FROM_V3') {
- const paasInfo = info.fromPaasInfo ? JSON.parse(info.fromPaasInfo) : {}
+ const paasInfo = safeJsonParse(info.fromPaasInfo, {})
time = paasInfo.updateTime || time
updateUser = paasInfo.updateUser || updateUser
}
diff --git a/lib/client/src/views/project/release/publish.vue b/lib/client/src/views/project/release/publish.vue
index a785a8d15..80acf9c0a 100644
--- a/lib/client/src/views/project/release/publish.vue
+++ b/lib/client/src/views/project/release/publish.vue
@@ -97,7 +97,7 @@
- {{ $t('最近{0}版本:', [typeMap[latestInfo.isOffline]]) }}
+ {{ $t('最近{0}版本:', [typeMap[latestInfo.isOffline]]) }}
,{{ latestInfo.status === 'successful' ? $t('查看成功日志') : (latestInfo.status === 'failed' ? $t('查看失败日志') : $t('查看日志'))}}
diff --git a/lib/client/src/views/system/index.vue b/lib/client/src/views/system/index.vue
index bb0151087..322a5eab1 100644
--- a/lib/client/src/views/system/index.vue
+++ b/lib/client/src/views/system/index.vue
@@ -59,7 +59,7 @@
diff --git a/lib/shared/security/xss-protection.js b/lib/shared/security/xss-protection.js
new file mode 100644
index 000000000..2a1990c79
--- /dev/null
+++ b/lib/shared/security/xss-protection.js
@@ -0,0 +1,41 @@
+/**
+ * XSS 防护工具函数
+ * 提供统一的XSS过滤和防护功能
+ **/
+/**
+ * HTML实体编码映射表
+ */
+const HTML_ENTITIES = {
+ '&': '&',
+ '<': '<', + '>': '>',
+ '"': '"',
+ "'": ''',
+ '/': '/'
+}
+
+/**
+ * 转义HTML特殊字符,防止XSS攻击
+ * @param {string} str 需要转义的字符串
+ * @returns {string} 转义后的安全字符串
+ */
+export function escapeHtml (str) {
+ if (typeof str !== 'string') {
+ return str
+ }
+ return str.replace(/[&
"'/]/g, (match) => HTML_ENTITIES[match])
+}
+
+/**
+ * 安全的JSON解析
+ * @param {string} jsonStr JSON字符串
+ * @param {*} defaultValue 默认值
+ * @returns {*} 解析后的对象或默认值
+ */
+export function safeJsonParse (jsonStr, defaultValue = null) {
+ try {
+ return JSON.parse(jsonStr)
+ } catch (e) {
+ return defaultValue
+ }
+}
From 193da88390128d2b561d4108a066fa09f5b7e9dc Mon Sep 17 00:00:00 2001
From: LivySara <1936808975@qq.com>
Date: 2026年1月22日 15:41:15 +0800
Subject: [PATCH 2/8] =?UTF-8?q?feat=EF=BC=9Asql=E6=B3=A8=E5=85=A5=E9=98=B2?=
=?UTF-8?q?=E6=8A=A4=20---story=3D130244026?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
lib/server/controller/open-api.js | 122 +++++++++++++++++-
lib/server/service/business/bk-base.js | 109 ++++++++++------
lib/server/service/business/data-source.js | 71 ++++++++--
.../service/business/preview-db-service.js | 42 ++++--
lib/shared/security/sql-protection.js | 12 ++
5 files changed, 297 insertions(+), 59 deletions(-)
create mode 100644 lib/shared/security/sql-protection.js
diff --git a/lib/server/controller/open-api.js b/lib/server/controller/open-api.js
index d5b5dda10..7f5535b98 100644
--- a/lib/server/controller/open-api.js
+++ b/lib/server/controller/open-api.js
@@ -17,6 +17,7 @@ import { myProject } from './iam'
import { getPreviewDbConfig, getTables, getTableDetail } from '../service/business/preview-db-service'
import DBEngineService from '../service/common/db-engine-service'
+import { logger } from '../logger'
const { createDemoProject } = require('./project')
@@ -260,25 +261,132 @@ export const getProjectTableCols = async (ctx) => {
}
}
+/**
+ * 增强的SQL安全检查函数
+ * @param {string} sql SQL语句
+ * @throws {Error} 如果检测到危险模式
+ */
+const enhancedSqlSecurityCheck = (sql) => {
+ if (!sql || typeof sql !== 'string') {
+ throw new Error('SQL语句不能为空')
+ }
+
+ const trimmedSql = sql.trim()
+ const upperCaseSql = trimmedSql.toUpperCase()
+
+ // 基本检查:必须以SELECT开头
+ if (!upperCaseSql.startsWith('SELECT')) {
+ throw new Error('仅支持SELECT查询语句')
+ }
+
+ // 危险关键词检查(更全面)
+ const dangerousKeywords = [
+ 'DROP DATABASE', 'TRUNCATE TABLE', 'DROP TABLE', 'CREATE TABLE',
+ 'DELETE FROM', 'ALTER TABLE', 'INSERT INTO', 'UPDATE SET',
+ 'GRANT', 'REVOKE', 'CREATE USER', 'DROP USER', 'SET PASSWORD',
+ 'LOAD DATA', 'OUTFILE', 'DUMPFILE', 'LOAD_FILE'
+ ]
+
+ for (const keyword of dangerousKeywords) {
+ if (upperCaseSql.includes(keyword)) {
+ throw new Error(`检测到危险关键词: ${keyword}`)
+ }
+ }
+
+ // 危险模式检查
+ const dangerousPatterns = [
+ /UNION\s+SELECT/i, // UNION注入
+ /;\s*(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|GRANT|REVOKE)/i, // 多语句注入
+ /--/, // SQL注释
+ /\/\*/, // 多行注释开始
+ /\*\//, // 多行注释结束
+ /XP_CMDSHELL/i, // SQL Server命令执行
+ /SP_EXECUTESQL/i, // SQL Server动态SQL
+ /EXEC\s*\(/i, // 执行函数
+ /EXECUTE\s*\(/i, // 执行函数
+ /INFORMATION_SCHEMA/i, // 信息模式
+ /PERFORMANCE_SCHEMA/i, // 性能模式
+ /MYSQL\./i, // MySQL系统表
+ /\@\@/, // 系统变量
+ /DATABASE\(\)/i, // 数据库函数
+ /USER\(\)/i, // 用户函数
+ /VERSION\(\)/i, // 版本函数
+ /BENCHMARK\s*\(/i, // 基准测试函数
+ /SLEEP\s*\(/i, // 延时函数
+ /WAITFOR\s+DELAY/i, // SQL Server延时
+ /PG_SLEEP\s*\(/i, // PostgreSQL延时
+ /DBMS_PIPE\.RECEIVE_MESSAGE/i, // Oracle延时
+ /INTO\s+OUTFILE/i, // 文件写入
+ /INTO\s+DUMPFILE/i, // 文件转储
+ /LOAD_FILE\s*\(/i, // 文件读取
+ /CHAR\s*\(/i, // 字符编码绕过
+ /ASCII\s*\(/i, // ASCII编码绕过
+ /HEX\s*\(/i, // 十六进制编码绕过
+ /UNHEX\s*\(/i, // 反十六进制编码
+ /CONCAT\s*\(/i, // 字符串拼接(可能用于绕过)
+ /SUBSTRING\s*\(/i, // 子字符串(可能用于盲注)
+ /MID\s*\(/i, // 中间字符串
+ /LEFT\s*\(/i, // 左侧字符串
+ /RIGHT\s*\(/i, // 右侧字符串
+ /IF\s*\(/i, // 条件函数(可能用于盲注)
+ /CASE\s+WHEN/i, // CASE语句(可能用于盲注)
+ /EXTRACTVALUE\s*\(/i, // XML函数(可能用于报错注入)
+ /UPDATEXML\s*\(/i, // XML更新函数
+ /NAME_CONST\s*\(/i, // 名称常量函数
+ /MULTIPOINT\s*\(/i, // 几何函数
+ /POLYGON\s*\(/i, // 多边形函数
+ /MULTIPOLYGON\s*\(/i, // 多多边形函数
+ /LINESTRING\s*\(/i, // 线字符串函数
+ /MULTILINESTRING\s*\(/i // 多线字符串函数
+ ]
+
+ for (const pattern of dangerousPatterns) {
+ if (pattern.test(sql)) {
+ throw new Error(`检测到危险的SQL模式: ${pattern.source}`)
+ }
+ }
+}
+
// 执行sql查询,获取预览环境用户db表下数据(当前仅会开放给bk-vision)
export const execQuerySql = async (ctx) => {
- // 涉及到表变更sql的关键词
- const sqlKeywords = ['DROP DATABASE', 'TRRUNCATE TABLE', 'DROP TABLE', 'CREATE TABLE', 'DELETE FROM', 'ALTER TABLE', 'INSERT INTO']
const post = ctx.request.body
const { projectId, sql } = post
- const upperCaseSql = sql && sql.toUpperCase()
- // 此接口只能执行查询语句,禁止执行任何涉及表变更的语句
- if (!upperCaseSql || !upperCaseSql.startsWith('SELECT') || sqlKeywords.indexOf(upperCaseSql) !== -1) {
- ctx.throw(400, 'sql语句未以SELECT开头或含有变更数据表的危险关键词', { code: 400 })
+ // 参数验证
+ if (!projectId) {
+ ctx.throw(400, '缺少必要参数 projectId', { code: 400 })
+ }
+
+ if (!sql || typeof sql !== 'string') {
+ ctx.throw(400, 'SQL语句不能为空且必须为字符串类型', { code: 400 })
}
try {
+ // 增强的安全检查
+ enhancedSqlSecurityCheck(sql)
+
const previewDbConfig = await getPreviewDbConfig(projectId)
const dbEngine = new DBEngineService(previewDbConfig)
const res = await dbEngine.execSql(sql)
success(ctx, res)
} catch (err) {
- ctx.throw(500, err.sqlMessage || err.message || err, { code: 500 })
+ // 记录详细的错误日志
+ logger.error({
+ message: 'SQL查询执行失败',
+ projectId,
+ sql: sql.substring(0, 200), // 只记录前200个字符,避免日志过长
+ error: err.message,
+ stack: err.stack,
+ timestamp: new Date().toISOString()
+ })
+
+ // 不暴露具体的SQL错误信息给客户端
+ const safeErrorMessage = err.message.includes('检测到危险') ||
+ err.message.includes('SQL语句') ||
+ err.message.includes('仅支持SELECT')
+ ? err.message
+ : '查询执行失败,请检查SQL语句'
+
+ ctx.throw(400, safeErrorMessage, { code: 400 })
}
}
diff --git a/lib/server/service/business/bk-base.js b/lib/server/service/business/bk-base.js
index 93568ffb2..a60c39858 100644
--- a/lib/server/service/business/bk-base.js
+++ b/lib/server/service/business/bk-base.js
@@ -9,6 +9,7 @@ import {
import {
generateToken
} from './token'
+import { escapeIdentifier } from '../../../shared/security/sql-protection'
const getProjectInfo = async (projectId, bkTicket) => {
const projectInfo = await LCDataService.findOne(
@@ -166,6 +167,20 @@ export const getTables = async (projectId, bkTicket, bkBizId) => {
}
}
+/**
+ * 验证排序方向
+ * @param {string} direction 排序方向
+ * @returns {string} 验证后的排序方向
+ */
+const validateSortDirection = (direction) => {
+ const allowedDirections = ['ASC', 'DESC']
+ const upperDirection = String(direction).toUpperCase()
+ if (!allowedDirections.includes(upperDirection)) {
+ throw new Error('排序方向错误:只支持 ASC 或 DESC')
+ }
+ return upperDirection
+}
+
/**
* 获取有权限的数据
* @param {*} projectId 项目id
@@ -175,43 +190,65 @@ export const getTables = async (projectId, bkTicket, bkBizId) => {
* @returns 表数据
*/
export const getTableDatas = async (projectId, queryData, tableFileName, bkTicket) => {
- // 构造查询 SQL
- const {
- page,
- pageSize,
- bkSortKey,
- bkSortValue,
- bkDataSourceType,
- ...others
- } = queryData
- let sql = `SELECT * FROM \`${tableFileName}\``
- Object
- .keys(others)
- .forEach((key, index) => {
- if (index <= 0) { - sql += ' WHERE' - } else { - sql += ' AND' + try { + // 构造查询 SQL - 使用参数化查询防止SQL注入 + const { + page, + pageSize, + bkSortKey, + bkSortValue, + bkDataSourceType, + ...others + } = queryData + + // 验证表名 + const safeTableName = escapeIdentifier(tableFileName) + let sql = `SELECT * FROM ${safeTableName}` + + Object + .keys(others) + .forEach((key, index) => {
+ if (index <= 0) { + sql += ' WHERE' + } else { + sql += ' AND' + } + sql += ` \`${key}\` LIKE '%${others[key]}%'` + }) + + // 构建ORDER BY子句 + if (bkSortKey && bkSortValue) { + const safeSortKey = escapeIdentifier(bkSortKey) + const safeSortDirection = validateSortDirection(bkSortValue) + sql += ` ORDER BY ${safeSortKey} ${safeSortDirection}` + } + + // 构建LIMIT子句 + if (page !== undefined && pageSize !== undefined) { + const pageNum = parseInt(page, 10) + const pageSizeNum = parseInt(pageSize, 10) + + // 验证分页参数 + if (isNaN(pageNum) || isNaN(pageSizeNum) || pageNum < 0 || pageSizeNum <= 0 || pageSizeNum> 1000) {
+ throw new Error('分页参数错误:页码不能为负数,每页大小必须在1-1000之间')
}
- sql += ` \`${key}\` LIKE '%${others[key]}%'`
- })
- if (bkSortKey && bkSortValue) {
- sql += ` ORDER BY \`${bkSortKey}\` ${bkSortValue}`
- }
- if (page && pageSize) {
- const index = page * pageSize
- sql += ` LIMIT ${index}, ${pageSize}`
- }
+
+ const offset = pageNum * pageSizeNum
+ sql += ` LIMIT ${offset}, ${pageSizeNum}`
+ }
- sql += ';'
- // 执行查询
- const multTableDatas = await execSQL(projectId, sql, bkTicket)
- // 过滤 bk-base 系统内置字段
- const list = (multTableDatas?.[0] || []).map((item) => {
- const { dtEventTimeStamp, localTime, thedate, dtEventTime, ...rest } = item
- return rest
- })
- return {
- list
+ sql += ';'
+ // 执行查询
+ const multTableDatas = await execSQL(projectId, sql, bkTicket)
+ // 过滤 bk-base 系统内置字段
+ const list = (multTableDatas?.[0] || []).map((item) => {
+ const { dtEventTimeStamp, localTime, thedate, dtEventTime, ...rest } = item
+ return rest
+ })
+ return {
+ list
+ }
+ } catch (error) {
+ throw new Error(error.message || error)
}
}
diff --git a/lib/server/service/business/data-source.js b/lib/server/service/business/data-source.js
index 6d6e18021..7b2ae19a4 100644
--- a/lib/server/service/business/data-source.js
+++ b/lib/server/service/business/data-source.js
@@ -108,11 +108,19 @@ export const getSqlByCondition = async (projectId, condition) => {
return generateSqlByCondition(condition, list)
}
-// 只能是查询语句检查
+// 只能是查询语句检查 - 增强版本
export const querySqlCheck = (sql) => {
- if (!/;$/.test(sql.trim())) {
+ if (!sql || typeof sql !== 'string') {
+ throw new Error('SQL语句不能为空')
+ }
+
+ const trimmedSql = sql.trim()
+
+ if (!/;$/.test(trimmedSql)) {
throw new Error(global.i18n.t('Sql 语句不完整,需要是【;】号结尾'))
}
+
+ // 基础检查列表(保持原有逻辑)
const checkList = [
{ check: (val) => !/^select/.test(val), message: global.i18n.t('仅支持 SELECT 查询语句,请修改后再试') },
{ check: (val) => /database\(\)/i.test(val), message: global.i18n.t('不允许出现 database() 函数') },
@@ -123,28 +131,75 @@ export const querySqlCheck = (sql) => {
{ check: (val) => /MYSQL\./i.test(val), message: global.i18n.t('不允许查询 MYSQL 内置表相关数据') },
{ check: (val) => /\@\@/i.test(val), message: global.i18n.t('不允许出现@@符号') }
]
+
+ // 增强的安全检查列表
+ const enhancedCheckList = [
+ { check: (val) => /union\s+select/i.test(val), message: global.i18n.t('不允许使用 UNION SELECT 语句') },
+ { check: (val) => /;\s*(drop|delete|update|insert|alter|create|grant|revoke)/i.test(val), message: global.i18n.t('检测到多语句注入尝试') },
+ { check: (val) => /--/.test(val), message: global.i18n.t('不允许使用 SQL 注释符号 --') },
+ { check: (val) => /\/\*/.test(val), message: global.i18n.t('不允许使用多行注释 /*') },
+ { check: (val) => /\*\//.test(val), message: global.i18n.t('不允许使用多行注释 */') },
+ { check: (val) => /xp_cmdshell/i.test(val), message: global.i18n.t('不允许使用 xp_cmdshell 函数') },
+ { check: (val) => /sp_executesql/i.test(val), message: global.i18n.t('不允许使用 sp_executesql 函数') },
+ { check: (val) => /exec\s*\(/i.test(val), message: global.i18n.t('不允许使用 EXEC 函数') },
+ { check: (val) => /execute\s*\(/i.test(val), message: global.i18n.t('不允许使用 EXECUTE 函数') },
+ { check: (val) => /load_file\s*\(/i.test(val), message: global.i18n.t('不允许使用 LOAD_FILE 函数') },
+ { check: (val) => /into\s+outfile/i.test(val), message: global.i18n.t('不允许使用 INTO OUTFILE 语句') },
+ { check: (val) => /into\s+dumpfile/i.test(val), message: global.i18n.t('不允许使用 INTO DUMPFILE 语句') },
+ { check: (val) => /benchmark\s*\(/i.test(val), message: global.i18n.t('不允许使用 BENCHMARK 函数') },
+ { check: (val) => /sleep\s*\(/i.test(val), message: global.i18n.t('不允许使用 SLEEP 函数') },
+ { check: (val) => /waitfor\s+delay/i.test(val), message: global.i18n.t('不允许使用 WAITFOR DELAY 语句') },
+ { check: (val) => /pg_sleep\s*\(/i.test(val), message: global.i18n.t('不允许使用 PG_SLEEP 函数') },
+ { check: (val) => /extractvalue\s*\(/i.test(val), message: global.i18n.t('不允许使用 EXTRACTVALUE 函数') },
+ { check: (val) => /updatexml\s*\(/i.test(val), message: global.i18n.t('不允许使用 UPDATEXML 函数') },
+ { check: (val) => /name_const\s*\(/i.test(val), message: global.i18n.t('不允许使用 NAME_CONST 函数') },
+ { check: (val) => /multipoint\s*\(/i.test(val), message: global.i18n.t('不允许使用 MULTIPOINT 函数') },
+ { check: (val) => /polygon\s*\(/i.test(val), message: global.i18n.t('不允许使用 POLYGON 函数') },
+ { check: (val) => /multipolygon\s*\(/i.test(val), message: global.i18n.t('不允许使用 MULTIPOLYGON 函数') },
+ { check: (val) => /linestring\s*\(/i.test(val), message: global.i18n.t('不允许使用 LINESTRING 函数') },
+ { check: (val) => /multilinestring\s*\(/i.test(val), message: global.i18n.t('不允许使用 MULTILINESTRING 函数') }
+ ]
+
const sqlArr = splitSql(sql)
sqlArr.forEach((sqlStr) => {
const lowerCaseSql = sqlStr.trim().toLowerCase()
if (lowerCaseSql) {
+ // 执行原有检查
checkList.forEach((checkItem) => {
if (checkItem.check(lowerCaseSql)) {
throw new Error(checkItem.message)
}
})
+
+ // 执行增强检查
+ enhancedCheckList.forEach((checkItem) => {
+ if (checkItem.check(lowerCaseSql)) {
+ throw new Error(checkItem.message)
+ }
+ })
}
})
}
// sql 解码
export const decodeSql = (sql, params) => {
- let decodedSql = decodeBase64(sql)
- const paramKeys = Object.keys(params || {})
- if (paramKeys.length> 0) {
- paramKeys.forEach((paramKey) => {
+ try {
+ let decodedSql = decodeBase64(sql)
+
+ // 验证解码后的SQL
+ if (!decodedSql || typeof decodedSql !== 'string') {
+ throw new Error('无效的SQL语句')
+ }
+
+ const paramKeys = Object.keys(params || {})
+ if (paramKeys.length> 0) {
+ paramKeys.forEach((paramKey) => {
const reg = new RegExp(`\\$\\{${paramKey}\\}`, 'g')
decodedSql = decodedSql.replace(reg, params[paramKey])
- })
+ })
+ }
+ return decodedSql
+ } catch (error) {
+ throw new Error(`SQL解码失败: ${error.message}`)
}
- return decodedSql
}
diff --git a/lib/server/service/business/preview-db-service.js b/lib/server/service/business/preview-db-service.js
index da2764d63..f307f1844 100644
--- a/lib/server/service/business/preview-db-service.js
+++ b/lib/server/service/business/preview-db-service.js
@@ -10,7 +10,7 @@
*/
import DBEngineService from '../common/db-engine-service'
-import { LCDataService, TABLE_FILE_NAME, getDataService } from '../common/data-service'
+import { LCDataService, TABLE_FILE_NAME, getDataService } from '../common/data-service'
import { EntitySchema, createConnection, EventSubscriber, Like } from 'typeorm'
import { RequestContext } from '../../middleware/request-context'
import { sm4Encrypt, sm4Decrypt, uuid } from '../../util'
@@ -19,16 +19,34 @@ import OnlineDBService from '../common/online-db-service'
import {
transferTimeByTimezoneOffset
} from './data-source'
+import { escapeIdentifier } from '../../../shared/security/sql-protection'
+const mysql = require('mysql2')
const dataBaseConf = require('../../conf/data-source')
/**
* 开启预览
* @param {*} projectId 应用id
+ * @param {*} dbName 数据库名称
*/
export const enablePerviewDb = async (projectId, dbName) => {
+ // 输入验证
+ if (!dbName || typeof dbName !== 'string') {
+ throw new Error('数据库名称必须是非空字符串')
+ }
+
+ // 标识符格式校验
+ try {
+ escapeIdentifier(dbName)
+ } catch (error) {
+ throw new Error('数据库名称格式错误:只允许字母、数字、下划线和连字符')
+ }
+
+ // 安全的数据库名称
+ const safeDbName = `bklesscode_${dbName}`
+
const dbInfo = {
projectId,
- dbName: `bklesscode_${dbName}`,
+ dbName: safeDbName,
userName: uuid(),
passWord: uuid()
}
@@ -36,12 +54,20 @@ export const enablePerviewDb = async (projectId, dbName) => {
// 创建用于预览的DB
const previewDbEngine = await getPreviewDbEngine()
await previewDbEngine.execCb(async (pool) => {
+ // 使用 mysql.escapeId 对数据库名进行安全转义
+ const escapedDbName = mysql.escapeId(dbInfo.dbName)
+
// 创建应用对应的预览数据库
- await pool.query(`CREATE DATABASE \`${dbInfo.dbName}\`;`)
- // 创建用户并授权对应的库
- await pool.query(`CREATE USER '${dbInfo.userName}'@'%' IDENTIFIED BY '${dbInfo.passWord}';`)
- await pool.query(`GRANT ALL ON ${dbInfo.dbName}.* TO '${dbInfo.userName}'@'%';`)
- await pool.query('FLUSH PRIVILEGES;')
+ await pool.query(`CREATE DATABASE ${escapedDbName}`)
+
+ // 创建用户并授权对应的库 - 使用 mysql.escape 转义所有值
+ const escapedUserName = mysql.escape(dbInfo.userName)
+ const escapedPassword = mysql.escape(dbInfo.passWord)
+ const escapedHost = mysql.escape('%')
+
+ await pool.query(`CREATE USER ${escapedUserName}@${escapedHost} IDENTIFIED BY ${escapedPassword}`)
+ await pool.query(`GRANT ALL ON ${escapedDbName}.* TO ${escapedUserName}@${escapedHost}`)
+ await pool.query('FLUSH PRIVILEGES')
})
// 加密
@@ -317,7 +343,7 @@ export const getThirdPartDBTables = async (projectId, page, pageSize, thirdPartD
BASE_COLUMNS()[0]
])
})
- updateSql += `ALTER TABLE \`${tableData.tableName}\` ADD COLUMN \`id\` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY;`
+ updateSql += `ALTER TABLE ${mysql.escapeId(tableData.tableName)} ADD COLUMN ${mysql.escapeId('id')} int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY;`
}
}
})
diff --git a/lib/shared/security/sql-protection.js b/lib/shared/security/sql-protection.js
new file mode 100644
index 000000000..6d6558da9
--- /dev/null
+++ b/lib/shared/security/sql-protection.js
@@ -0,0 +1,12 @@
+/**
+ * 安全的SQL标识符转义函数
+ * @param {string} identifier 标识符(表名、列名等)
+ * @returns {string} 转义后的标识符
+ */
+export const escapeIdentifier = (identifier) => {
+ // 只允许字母、数字、下划线
+ if (!/^[a-zA-Z0-9_-]+$/.test(identifier)) {
+ throw new Error('标识符格式错误:只允许字母、数字、下划线和连字符')
+ }
+ return `\`${identifier}\``
+}
From deec9d3c8643266965976ae7b38b3579f3ddd278 Mon Sep 17 00:00:00 2001
From: terlinhe
Date: 2026年1月26日 10:41:15 +0800
Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=E9=83=A8=E5=88=86=E4=BE=9D?=
=?UTF-8?q?=E8=B5=96=E5=8D=87=E7=BA=A7=20#=20Reviewed,=20transaction=20id:?=
=?UTF-8?q?=2072620?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
lib/server/model/project-code.js | 2 +-
lib/server/service/business/v3-service.js | 3 +--
package.json | 6 +++---
3 files changed, 5 insertions(+), 6 deletions(-)
diff --git a/lib/server/model/project-code.js b/lib/server/model/project-code.js
index 5555d7a61..33e9c33f3 100644
--- a/lib/server/model/project-code.js
+++ b/lib/server/model/project-code.js
@@ -616,7 +616,7 @@ const projectCode = {
await this.writePackageJSON(
path.join(targetPath, 'package.json'),
[
- { name: 'moment', version: '2.29.4' },
+ { name: 'moment', version: '2.30.1' },
{ name: 'animate.css', version: '4.1.1' },
{ name: 'swiper-element-animation', version: '1.1.0' },
{ name: 'swiper', version: '11.0.5' },
diff --git a/lib/server/service/business/v3-service.js b/lib/server/service/business/v3-service.js
index e38e9c991..b6522e9b7 100644
--- a/lib/server/service/business/v3-service.js
+++ b/lib/server/service/business/v3-service.js
@@ -228,8 +228,7 @@ export const uploadToBkRepo = async (projectId, version, versionId, releaseId) =
return new Promise(async (resolve, reject) => {
try {
shell.cd(STATIC_URL)
- const targz = `tar zcf bklesscode-proj-${projectId}.tar.gz -C ./${sourceDir} .`
- shell.exec(targz)
+ shell.cmd('tar', ['zcf', `bklesscode-proj-${projectId}.tar.gz`, '-C', `./${sourceDir}`, '.'])
shell.cd(projectPath)
// 根据压缩包大小计算md5值
diff --git a/package.json b/package.json
index 8d5677336..9e00bfcf8 100644
--- a/package.json
+++ b/package.json
@@ -177,8 +177,8 @@
"mavon-editor": "^2.9.0",
"md5": "^2.3.0",
"monaco-editor": "^0.52.2",
- "monaco-editor-webpack-plugin":"^7.1.0",
- "moment": "^2.29.1",
+ "monaco-editor-webpack-plugin": "^7.1.0",
+ "moment": "^2.30.1",
"mysql2": "~2.3.3",
"node-request-context": "~1.0.5",
"node-sql-parser": "^4.5.1",
@@ -200,7 +200,7 @@
"raw-loader": "^4.0.2",
"reflect-metadata": "^0.1.13",
"screenfull": "~5.0.2",
- "shelljs": "~0.8.4",
+ "shelljs": "^0.10.0",
"shx": "~0.3.2",
"swagger-ui-dist": "^5.11.7",
"swig": "~1.4.2",
From 7e5c1ffeba2d7ab6baf08f55026da6f3a2858efa Mon Sep 17 00:00:00 2001
From: terlinhe
Date: 2026年1月26日 11:28:07 +0800
Subject: [PATCH 4/8] =?UTF-8?q?fix:=20typeorm=E5=8D=87=E7=BA=A7=E4=BC=98?=
=?UTF-8?q?=E5=8C=96=20#=20Reviewed,=20transaction=20id:=2072631?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../project-init-code/lib/server/service/data-service.js | 5 +++--
.../project-init-code/lib/server/service/data-service.js | 5 +++--
lib/server/service/common/data-service.js | 5 +++--
3 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/lib/server/project-template/vue2/project-init-code/lib/server/service/data-service.js b/lib/server/project-template/vue2/project-init-code/lib/server/service/data-service.js
index c2f2a1aa3..cf6acd9b7 100644
--- a/lib/server/project-template/vue2/project-init-code/lib/server/service/data-service.js
+++ b/lib/server/project-template/vue2/project-init-code/lib/server/service/data-service.js
@@ -249,9 +249,10 @@ export function getDataService (name = 'default', customEntityMap) {
* @param {*} query 查询参数
* @returns 获取数据详情结果
*/
- findOne (tableFileName, query = {}) {
+ async findOne (tableFileName, query = {}) {
const repository = getRepositoryByName(tableFileName)
- return repository.findOne({ where: transformQuery(query) }) || {}
+ const result = await repository.findOne({ where: transformQuery(query) })
+ return result || {}
},
/**
diff --git a/lib/server/project-template/vue3/project-init-code/lib/server/service/data-service.js b/lib/server/project-template/vue3/project-init-code/lib/server/service/data-service.js
index c2f2a1aa3..cf6acd9b7 100644
--- a/lib/server/project-template/vue3/project-init-code/lib/server/service/data-service.js
+++ b/lib/server/project-template/vue3/project-init-code/lib/server/service/data-service.js
@@ -249,9 +249,10 @@ export function getDataService (name = 'default', customEntityMap) {
* @param {*} query 查询参数
* @returns 获取数据详情结果
*/
- findOne (tableFileName, query = {}) {
+ async findOne (tableFileName, query = {}) {
const repository = getRepositoryByName(tableFileName)
- return repository.findOne({ where: transformQuery(query) }) || {}
+ const result = await repository.findOne({ where: transformQuery(query) })
+ return result || {}
},
/**
diff --git a/lib/server/service/common/data-service.js b/lib/server/service/common/data-service.js
index 03e8d10ab..9d5654ed9 100644
--- a/lib/server/service/common/data-service.js
+++ b/lib/server/service/common/data-service.js
@@ -312,9 +312,10 @@ export function getDataService (name = 'default', customEntityMap) {
* @param {*} query 查询参数
* @returns 获取数据详情结果
*/
- findOne (tableFileName, query = { deleteFlag: 0 }) {
+ async findOne (tableFileName, query = { deleteFlag: 0 }) {
const repository = getRepositoryByName(tableFileName)
- return repository.findOne({ where: transformQuery(query) }) || {}
+ const result = await repository.findOne({ where: transformQuery(query) })
+ return result || {}
},
/**
* 添加
From e0ba7f50b69e1af71b6bac083d1c53b71d10265f Mon Sep 17 00:00:00 2001
From: terlinhe
Date: 2026年5月16日 20:46:52 +0800
Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=E5=A4=84=E7=90=86package.json?=
=?UTF-8?q?=E4=BE=9D=E8=B5=96=EF=BC=9Aswig=E3=80=81lodash=E3=80=81compress?=
=?UTF-8?q?ing=E3=80=81swiper?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
lib/client/index.html | 1 -
lib/client/preview.html | 1 -
lib/server/app.browser.js | 2 +-
lib/server/model/project-code.js | 2 +-
.../vue2/project-init-code/lib/server/app.browser.js | 2 +-
.../project-template/vue2/project-init-code/package.json | 2 +-
.../vue3/project-init-code/lib/server/app.browser.js | 2 +-
.../project-template/vue3/project-init-code/package.json | 2 +-
lib/server/router/index.js | 2 --
lib/server/service/business/open-api.js | 3 ++-
package.json | 8 ++++----
11 files changed, 12 insertions(+), 15 deletions(-)
diff --git a/lib/client/index.html b/lib/client/index.html
index 85e75bd03..3e42fe5e1 100644
--- a/lib/client/index.html
+++ b/lib/client/index.html
@@ -31,7 +31,6 @@
var BK_COMPONENT_API_URL = '<%= process.env.BK_COMPONENT_API_URL %>'
var BKPAAS_BK_DOMAIN = '<%= process.env.BKPAAS_BK_DOMAIN %>'
var BK_SHARED_RES_URL = '<%= process.env.BK_SHARED_RES_URL %>'
- var BK_API_URL_TMPL = '{{ BK_API_URL_TMPL }}'
var BKPAAS_ENGINE_REGION = '<%= process.env.BK_PAAS_ENGINE_REGION %>'
var BK_APP_APIGW_PREFIX = '<%= process.env.BK_API_GATEWAY_ORIGIN.replace('{api_name}', 'bk-lesscode') + '/' + process.env.BK_LESSCODE_ENVIRONMENT || '' %>'
var BK_IAM_HOST = '<%= process.env.BK_IAM_HOST %>'
diff --git a/lib/client/preview.html b/lib/client/preview.html
index af0f1d28c..34286d0ac 100644
--- a/lib/client/preview.html
+++ b/lib/client/preview.html
@@ -129,7 +129,6 @@
>
var BK_STATIC_URL = '<%= process.env.BK_STATIC_URL %>'
var BKPAAS_ENVIRONMENT = '<%= process.env.BK_PAAS_ENVIRONMENT %>'
- var BK_API_URL_TMPL = '{{ BK_API_URL_TMPL }}'
var BKPAAS_ENGINE_REGION = '<%= process.env.BK_PAAS_ENGINE_REGION %>'
var BK_APP_APIGW_PREFIX = '<%= process.env.BK_API_GATEWAY_ORIGIN.replace('{api_name}', 'bk-lesscode') + '/' + process.env.BK_LESSCODE_ENVIRONMENT || '' %>'
var BK_IAM_HOST = '<%= process.env.BK_IAM_HOST %>'
diff --git a/lib/server/app.browser.js b/lib/server/app.browser.js
index 8a93f531b..be53332ca 100644
--- a/lib/server/app.browser.js
+++ b/lib/server/app.browser.js
@@ -265,7 +265,7 @@ async function startServer () {
app.use(convert.compose(allowedMethods))
app.context.render = views(resolve(__dirname, '..', IS_DEV ? 'client' : 'client/dist'), {
- map: { html: 'swig' }
+ map: { html: 'ejs' }
})
app.use(historyApiFallback({
diff --git a/lib/server/model/project-code.js b/lib/server/model/project-code.js
index 33e9c33f3..e23a3ad0b 100644
--- a/lib/server/model/project-code.js
+++ b/lib/server/model/project-code.js
@@ -619,7 +619,7 @@ const projectCode = {
{ name: 'moment', version: '2.30.1' },
{ name: 'animate.css', version: '4.1.1' },
{ name: 'swiper-element-animation', version: '1.1.0' },
- { name: 'swiper', version: '11.0.5' },
+ { name: 'swiper', version: '^12.1.2' },
{ name: 'lucky-canvas', version: '1.7.27' }
]
)
diff --git a/lib/server/project-template/vue2/project-init-code/lib/server/app.browser.js b/lib/server/project-template/vue2/project-init-code/lib/server/app.browser.js
index be286d26f..af20e61ea 100644
--- a/lib/server/project-template/vue2/project-init-code/lib/server/app.browser.js
+++ b/lib/server/project-template/vue2/project-init-code/lib/server/app.browser.js
@@ -99,7 +99,7 @@ async function startServer () {
app.use(convert.compose(allowedMethods))
app.context.render = views(resolve(__dirname, '..', IS_DEV ? 'client' : 'client/dist'), {
- map: { html: 'swig' }
+ map: { html: 'ejs' }
})
app.use(historyApiFallback({
diff --git a/lib/server/project-template/vue2/project-init-code/package.json b/lib/server/project-template/vue2/project-init-code/package.json
index a52db45a6..fafca8630 100644
--- a/lib/server/project-template/vue2/project-init-code/package.json
+++ b/lib/server/project-template/vue2/project-init-code/package.json
@@ -120,6 +120,7 @@
"cheerio": "1.0.0-rc.2",
"chokidar": "~3.2.1",
"co-views": "~2.1.0",
+ "ejs": "^3.1.10",
"cookie": "~0.4.0",
"core-js": "^3.25.5",
"dayjs": "^1.9.3",
@@ -169,7 +170,6 @@
"query-string": "^7.1.1",
"reflect-metadata": "^0.1.13",
"shx": "~0.3.2",
- "swig": "~1.4.2",
"mavon-editor": "^2.9.0",
"typeorm": "~0.3.27",
"typescript": "^4.4.3",
diff --git a/lib/server/project-template/vue3/project-init-code/lib/server/app.browser.js b/lib/server/project-template/vue3/project-init-code/lib/server/app.browser.js
index be286d26f..af20e61ea 100644
--- a/lib/server/project-template/vue3/project-init-code/lib/server/app.browser.js
+++ b/lib/server/project-template/vue3/project-init-code/lib/server/app.browser.js
@@ -99,7 +99,7 @@ async function startServer () {
app.use(convert.compose(allowedMethods))
app.context.render = views(resolve(__dirname, '..', IS_DEV ? 'client' : 'client/dist'), {
- map: { html: 'swig' }
+ map: { html: 'ejs' }
})
app.use(historyApiFallback({
diff --git a/lib/server/project-template/vue3/project-init-code/package.json b/lib/server/project-template/vue3/project-init-code/package.json
index 82a726eb7..4527990da 100644
--- a/lib/server/project-template/vue3/project-init-code/package.json
+++ b/lib/server/project-template/vue3/project-init-code/package.json
@@ -119,6 +119,7 @@
"cheerio": "1.0.0-rc.2",
"chokidar": "~3.2.1",
"co-views": "~2.1.0",
+ "ejs": "^3.1.10",
"cookie": "~0.4.0",
"core-js": "^3.25.5",
"dayjs": "^1.9.3",
@@ -167,7 +168,6 @@
"query-string": "^7.1.1",
"reflect-metadata": "^0.1.13",
"shx": "~0.3.2",
- "swig": "~1.4.2",
"mavon-editor": "^2.9.0",
"typeorm": "~0.3.27",
"typescript": "^4.4.3",
diff --git a/lib/server/router/index.js b/lib/server/router/index.js
index 732e3c2fd..b0cf6aa70 100644
--- a/lib/server/router/index.js
+++ b/lib/server/router/index.js
@@ -8,7 +8,6 @@
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
-import httpConf from '../conf/http'
const KoaRouter = require('koa-router')
const fs = require('fs')
const path = require('path')
@@ -23,7 +22,6 @@ function getRenderTemplateName (ctx) {
}
const renderParams = {
STATIC_URL: '',
- BK_API_URL_TMPL: httpConf.apiGateWayUrlTmpl,
BKPAAS_BK_DOMAIN: process.env.BKPAAS_BK_DOMAIN,
BK_SHARED_RES_URL: process.env.BKPAAS_SHARED_RES_URL,
BK_COMPONENT_API_URL: process.env.BK_COMPONENT_API_URL
diff --git a/lib/server/service/business/open-api.js b/lib/server/service/business/open-api.js
index 355d05bb5..b9f3bf12e 100644
--- a/lib/server/service/business/open-api.js
+++ b/lib/server/service/business/open-api.js
@@ -9,6 +9,7 @@ import {
execApiGateWay,
decodeToken
} from '@bkui/apigateway-nodejs-sdk'
+import httpConf from '../../conf/http'
import openApiJson from '../../system-conf/open-api.json'
import {
transformVersionToNum
@@ -106,7 +107,7 @@ export const generateApiGateway = async () => {
// 更新资源文档
await execApiGateWay({
apiName: 'bk-apigateway',
- apiUrlTemp: process.env.BK_API_URL_TMPL,
+ apiUrlTemp: httpConf.apiGateWayUrlTmpl,
path: `/api/v1/apis/${apiName}/resource-docs/import/by-archive/`,
method: 'post',
stageName: 'prod',
diff --git a/package.json b/package.json
index 9e00bfcf8..2d4cc68d0 100644
--- a/package.json
+++ b/package.json
@@ -115,7 +115,8 @@
"change-case": "~4.1.1",
"cheerio": "1.0.0-rc.2",
"co-views": "~2.1.0",
- "compressing": "^1.6.3",
+ "ejs": "^3.1.10",
+ "compressing": "^1.10.5",
"continuation-local-storage": "~3.2.1",
"cookie": "~0.4.0",
"core-js": "^3.25.5",
@@ -162,7 +163,7 @@
"koa-static": "~5.0.0",
"koa-unless": "^1.0.7",
"koa2-connect-history-api-fallback": "~0.1.2",
- "lodash": "~4.17.15",
+ "lodash": "^4.18.0",
"lodash.clonedeep": "~4.5.0",
"log4js-json-layout": "^2.2.2",
"lucky-canvas": "^1.7.27",
@@ -203,8 +204,7 @@
"shelljs": "^0.10.0",
"shx": "~0.3.2",
"swagger-ui-dist": "^5.11.7",
- "swig": "~1.4.2",
- "swiper": "11.0.5",
+ "swiper": "^12.1.2",
"swiper-element-animation": "^1.1.0",
"transliteration": "~2.1.8",
"tslib": "^2.0.3",
From eed12c64eb46f0052af583794a3433142f0fe06f Mon Sep 17 00:00:00 2001
From: terlinhe
Date: 2026年5月16日 21:49:49 +0800
Subject: [PATCH 6/8] =?UTF-8?q?feat:=20=E5=A4=84=E7=90=86package.json?=
=?UTF-8?q?=E4=BE=9D=E8=B5=96=EF=BC=9Axlsx=E3=80=81path-to-regex?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.eslintrc.js | 1 -
bk.config.js | 13 +-
lib/client/src/common/util.js | 15 +-
.../components/custom-buttons.vue | 7 +-
.../preview/components/buttons/index.js | 7 +-
.../data-table/common/use-download-demo.ts | 8 +-
.../data-table/data-manage/render-data.vue | 30 ++--
.../data-table/table-design/create-table.vue | 27 ++--
.../data-table/table-design/edit-table.vue | 27 ++--
.../data-table/table-design/show-table.vue | 4 +-
.../table-design/table-list/list/index.vue | 4 +-
.../system/operation/stats/export-button.vue | 13 +-
lib/server/controller/data-source.js | 4 +-
lib/server/decorator/send/index.js | 27 +++-
.../vue2/project-init-code/bk.config.js | 13 ++
.../components/buttons/index.js | 7 +-
.../components/custom-buttons.vue | 7 +-
.../lib/shared/excel/index.js | 33 +++++
.../vue2/project-init-code/package.json | 4 +-
.../vue3/project-init-code/bk.config.js | 13 ++
.../components/buttons/index.js | 7 +-
.../components/custom-buttons.vue | 7 +-
.../lib/shared/excel/index.js | 33 +++++
.../vue3/project-init-code/package.json | 4 +-
.../data-parse/data-parser/xlsx-parser.js | 44 +++---
lib/shared/data-source/data-parse/index.js | 2 +-
.../data-parse/struct-parser/xlsx-parser.js | 48 +++---
lib/shared/data-source/helper.js | 29 ++--
lib/shared/excel/index.js | 138 ++++++++++++++++++
package.json | 4 +-
30 files changed, 398 insertions(+), 182 deletions(-)
create mode 100644 lib/server/project-template/vue2/project-init-code/lib/shared/excel/index.js
create mode 100644 lib/server/project-template/vue3/project-init-code/lib/shared/excel/index.js
create mode 100644 lib/shared/excel/index.js
diff --git a/.eslintrc.js b/.eslintrc.js
index 3b9407da9..85f1826f3 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -31,7 +31,6 @@ module.exports = {
ResizeSensor: false,
define: false,
BK_USER_MANAGE_HOST: false,
- BK_API_URL_TMPL: false,
BKPAAS_ENVIRONMENT: false,
BK_ITSM_URL: false,
BK_APP_APIGW_PREFIX: false,
diff --git a/bk.config.js b/bk.config.js
index f65a4b42b..6a20cdcb3 100644
--- a/bk.config.js
+++ b/bk.config.js
@@ -125,9 +125,16 @@ module.exports = {
chunks: 'all',
reuseExistingChunk: true
},
- xlsxTypeormMoment: {
- name: 'xlsx-typeorm-moment',
- test: /(xlsx)|(typeorm)|(moment)/,
+ exceljs: {
+ name: 'exceljs',
+ test: /[\\/]node_modules[\\/]exceljs[\\/]/,
+ priority: 2,
+ chunks: 'all',
+ reuseExistingChunk: true
+ },
+ typeormMoment: {
+ name: 'typeorm-moment',
+ test: /[\\/]node_modules[\\/](typeorm|moment)[\\/]/,
priority: 1,
chunks: 'all',
reuseExistingChunk: true
diff --git a/lib/client/src/common/util.js b/lib/client/src/common/util.js
index 825db23bf..aa5a0ab5a 100644
--- a/lib/client/src/common/util.js
+++ b/lib/client/src/common/util.js
@@ -25,6 +25,19 @@ export function transformHtmlToVnode (html) {
return htmlComponent._render()
}
+const DOWNLOAD_MIME_MAP = {
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ xls: 'application/vnd.ms-excel',
+ sql: 'text/plain',
+ json: 'application/json',
+ zip: 'application/zip'
+}
+
+function getDownloadMimeType (filename = '') {
+ const ext = filename.split('.').pop()?.toLowerCase()
+ return DOWNLOAD_MIME_MAP[ext] || 'application/octet-stream'
+}
+
/**
* 前端下载文件
* @param {*} source 文件内容
@@ -32,7 +45,7 @@ export function transformHtmlToVnode (html) {
*/
export function downloadFile (source, filename = 'lesscode.txt') {
const downloadEl = document.createElement('a')
- const blob = new Blob([source])
+ const blob = new Blob([source], { type: getDownloadMimeType(filename) })
downloadEl.download = filename
const url = URL.createObjectURL(blob)
downloadEl.href = url
diff --git a/lib/client/src/components/flow-form-comp/components/custom-buttons.vue b/lib/client/src/components/flow-form-comp/components/custom-buttons.vue
index 851a8cccb..2e35050d8 100644
--- a/lib/client/src/components/flow-form-comp/components/custom-buttons.vue
+++ b/lib/client/src/components/flow-form-comp/components/custom-buttons.vue
@@ -23,7 +23,7 @@
>
- import * as XLSX from 'xlsx'
+ import { downloadXlsxFromAoa } from 'shared/excel'
export default {
name: 'CustomButtons',
@@ -99,10 +99,7 @@
body.push(row)
})
- const wb = XLSX.utils.book_new()
- const ws = XLSX.utils.aoa_to_sheet([header, ...body])
- XLSX.utils.book_append_sheet(wb, ws)
- XLSX.writeFile(wb, `${this.nodeName || this.tableName}.xlsx`)
+ await downloadXlsxFromAoa(`${this.nodeName || this.tableName}.xlsx`, 'Sheet1', [header, ...body])
} catch (e) {
console.log(e.message || e)
} finally {
diff --git a/lib/client/src/components/render/pc/widget/data-manage-container/form-data-manage/preview/components/buttons/index.js b/lib/client/src/components/render/pc/widget/data-manage-container/form-data-manage/preview/components/buttons/index.js
index ba4fa9ce3..2f9f573ac 100644
--- a/lib/client/src/components/render/pc/widget/data-manage-container/form-data-manage/preview/components/buttons/index.js
+++ b/lib/client/src/components/render/pc/widget/data-manage-container/form-data-manage/preview/components/buttons/index.js
@@ -1,5 +1,5 @@
import { h } from 'bk-lesscode-render'
-import * as XLSX from 'xlsx'
+import { downloadXlsxFromAoa } from 'shared/excel'
import './index.postcss'
export default {
@@ -62,10 +62,7 @@ export default {
body.push(row)
})
- const wb = XLSX.utils.book_new()
- const ws = XLSX.utils.aoa_to_sheet([header, ...body])
- XLSX.utils.book_append_sheet(wb, ws)
- XLSX.writeFile(wb, `${this.tableName}.xlsx`)
+ await downloadXlsxFromAoa(`${this.tableName}.xlsx`, 'Sheet1', [header, ...body])
} catch (e) {
console.log(e.message || e)
} finally {
diff --git a/lib/client/src/views/project/data-source-manage/data-table/common/use-download-demo.ts b/lib/client/src/views/project/data-source-manage/data-table/common/use-download-demo.ts
index 2a3556312..4b230e706 100644
--- a/lib/client/src/views/project/data-source-manage/data-table/common/use-download-demo.ts
+++ b/lib/client/src/views/project/data-source-manage/data-table/common/use-download-demo.ts
@@ -110,18 +110,18 @@ const demoTable = [{
}]
// 下载表结构示例
-export const downloadStructTemplate = (type) => {
+export const downloadStructTemplate = async (type) => {
const fileName = type === 'sql' ? 'bklesscode-struct-demo.sql' : ''
- const files = generateExportStruct(demoTable, type, fileName)
+ const files = await generateExportStruct(demoTable, type, fileName)
files.forEach(({ name, content }) => {
downloadFile(content, name)
})
}
// 下载数据示例
-export const downloadDataTemplate = (type, demoData) => {
+export const downloadDataTemplate = async (type, demoData) => {
const fileName = type === 'sql' ? 'bklesscode-data-demo.sql' : ''
- const files = generateExportDatas(demoData, type, fileName)
+ const files = await generateExportDatas(demoData, type, fileName)
files.forEach(({ name, content }) => {
downloadFile(content, name)
})
diff --git a/lib/client/src/views/project/data-source-manage/data-table/data-manage/render-data.vue b/lib/client/src/views/project/data-source-manage/data-table/data-manage/render-data.vue
index b38b81585..b0b21ca7a 100644
--- a/lib/client/src/views/project/data-source-manage/data-table/data-manage/render-data.vue
+++ b/lib/client/src/views/project/data-source-manage/data-table/data-manage/render-data.vue
@@ -571,7 +571,7 @@
window.open(`/api/data-source/exportDatas/projectId/${projectId}/fileType/${fileType}/tableName/${activeTable.value.tableName}/environment/${environment.value.key}?x-timezone-offset=${new Date().getTimezoneOffset()}`)
}
- const exportSelectDatas = (fileType) => {
+ const exportSelectDatas = async (fileType) => {
// 生产 sql 语法不需要id
const datas = [{
tableName: activeTable.value.tableName,
@@ -588,7 +588,7 @@
transferTimezone(datas[0].list)
}
const fileName = fileType === DATA_FILE_TYPE.SQL ? `bklesscode-data-${projectId}.sql` : ''
- const files = generateExportDatas(datas, fileType, fileName)
+ const files = await generateExportDatas(datas, fileType, fileName)
files.forEach(({ name, content }) => {
downloadFile(content, name)
})
@@ -607,22 +607,16 @@
}
// 解析导入的数据
- const parseImport = ({ data, type }) => {
- return new Promise((resolve, reject) => {
- try {
- const [list] = handleImportData(
- [data],
- type,
- activeTable.value.columns.map(column => column.name)
- )
- resolve({
- data: list,
- message: type === DATA_FILE_TYPE.XLSX ? window.i18n.t('解析到【{0}】条数据,点击导入后插入到数据库', [list.length]) : ''
- })
- } catch (error) {
- reject(error)
- }
- })
+ const parseImport = async ({ data, type }) => {
+ const [list] = await handleImportData(
+ [data],
+ type,
+ activeTable.value.columns.map(column => column.name)
+ )
+ return {
+ data: list,
+ message: type === DATA_FILE_TYPE.XLSX ? window.i18n.t('解析到【{0}】条数据,点击导入后插入到数据库', [list.length]) : ''
+ }
}
// 执行导入
diff --git a/lib/client/src/views/project/data-source-manage/data-table/table-design/create-table.vue b/lib/client/src/views/project/data-source-manage/data-table/table-design/create-table.vue
index 33f229374..e83c36452 100644
--- a/lib/client/src/views/project/data-source-manage/data-table/table-design/create-table.vue
+++ b/lib/client/src/views/project/data-source-manage/data-table/table-design/create-table.vue
@@ -203,23 +203,16 @@
}
}
// 解析导入的表结构
- const parseImport = ({ data, type }) => {
- return new Promise((resolve, reject) => {
- try {
- const [tableInfo] = handleImportStruct([data], type)
- const columns = [
- ...BASE_COLUMNS(),
- ...tableInfo.columns.filter(column => !BASE_COLUMNS().find(baseColumn => baseColumn.name === column.name))
- ]
- // 过滤掉基础字段设置,使用系统内置
- resolve({
- data: columns,
- message: window.i18n.t('解析到【{0}】个字段,请点击导入后修改字段配置', [columns.length])
- })
- } catch (error) {
- reject(error)
- }
- })
+ const parseImport = async ({ data, type }) => {
+ const [tableInfo] = await handleImportStruct([data], type)
+ const columns = [
+ ...BASE_COLUMNS(),
+ ...tableInfo.columns.filter(column => !BASE_COLUMNS().find(baseColumn => baseColumn.name === column.name))
+ ]
+ return {
+ data: columns,
+ message: window.i18n.t('解析到【{0}】个字段,请点击导入后修改字段配置', [columns.length])
+ }
}
// 执行导入
const handleImport = (data) => {
diff --git a/lib/client/src/views/project/data-source-manage/data-table/table-design/edit-table.vue b/lib/client/src/views/project/data-source-manage/data-table/table-design/edit-table.vue
index fffbbf762..2cccad7ea 100644
--- a/lib/client/src/views/project/data-source-manage/data-table/table-design/edit-table.vue
+++ b/lib/client/src/views/project/data-source-manage/data-table/table-design/edit-table.vue
@@ -235,23 +235,16 @@
})
}
// 解析导入的表结构
- const parseImport = ({ data, type }) => {
- return new Promise((resolve, reject) => {
- try {
- const [tableInfo] = handleImportStruct([data], type)
- const columns = [
- ...BASE_COLUMNS(),
- ...tableInfo.columns.filter(column => !BASE_COLUMNS().find(baseColumn => baseColumn.name === column.name))
- ]
- // 过滤掉基础字段设置,使用系统内置
- resolve({
- data: columns,
- message: window.i18n.t('解析到【{0}】个字段,请点击导入后修改字段配置', [columns.length])
- })
- } catch (error) {
- reject(error)
- }
- })
+ const parseImport = async ({ data, type }) => {
+ const [tableInfo] = await handleImportStruct([data], type)
+ const columns = [
+ ...BASE_COLUMNS(),
+ ...tableInfo.columns.filter(column => !BASE_COLUMNS().find(baseColumn => baseColumn.name === column.name))
+ ]
+ return {
+ data: columns,
+ message: window.i18n.t('解析到【{0}】个字段,请点击导入后修改字段配置', [columns.length])
+ }
}
// 执行导入
const handleImport = (data) => {
diff --git a/lib/client/src/views/project/data-source-manage/data-table/table-design/show-table.vue b/lib/client/src/views/project/data-source-manage/data-table/table-design/show-table.vue
index 4ec6dee8b..acfb558de 100644
--- a/lib/client/src/views/project/data-source-manage/data-table/table-design/show-table.vue
+++ b/lib/client/src/views/project/data-source-manage/data-table/table-design/show-table.vue
@@ -115,13 +115,13 @@
})
}
- const exportTables = (fileType) => {
+ const exportTables = async (fileType) => {
const tables = [{
...tableStatus.basicInfo,
columns: tableStatus.data
}]
const fileName = fileType === 'sql' ? `bklesscode-struct-${tableStatus.basicInfo.tableName}.sql` : ''
- const files = generateExportStruct(tables, fileType, fileName)
+ const files = await generateExportStruct(tables, fileType, fileName)
files.forEach(({ name, content }) => {
downloadFile(content, name)
})
diff --git a/lib/client/src/views/project/data-source-manage/data-table/table-design/table-list/list/index.vue b/lib/client/src/views/project/data-source-manage/data-table/table-design/table-list/list/index.vue
index cebf4eac8..e47e6fb12 100644
--- a/lib/client/src/views/project/data-source-manage/data-table/table-design/table-list/list/index.vue
+++ b/lib/client/src/views/project/data-source-manage/data-table/table-design/table-list/list/index.vue
@@ -242,9 +242,9 @@
window.open(`/api/data-source/exportStruct/projectId/${projectId}/fileType/${fileType}`)
}
- const exportSelectTables = (fileType) => {
+ const exportSelectTables = async (fileType) => {
const fileName = fileType === 'sql' ? `bklesscode-struct-${projectId}.sql` : ''
- const files = generateExportStruct(listStatus.selectRows, fileType, fileName)
+ const files = await generateExportStruct(listStatus.selectRows, fileType, fileName)
files.forEach(({ name, content }) => {
downloadFile(content, name)
})
diff --git a/lib/client/src/views/system/operation/stats/export-button.vue b/lib/client/src/views/system/operation/stats/export-button.vue
index 2737c13a5..d0bc57d27 100644
--- a/lib/client/src/views/system/operation/stats/export-button.vue
+++ b/lib/client/src/views/system/operation/stats/export-button.vue
@@ -3,7 +3,7 @@
>
- import * as XLSX from 'xlsx'
+ import { downloadXlsxFromAoa } from 'shared/excel'
export default {
props: {
@@ -89,9 +89,9 @@
})
const data = [header, ...body]
- this.generateXlsx(`lesscode-stats-${this.name}.xlsx`, data, this.tableSheetName)
+ await this.generateXlsx(`lesscode-stats-${this.name}.xlsx`, data, this.tableSheetName)
},
- handleCommonTimeDimExport () {
+ async handleCommonTimeDimExport () {
const blocks = []
const max = this.list.length
this.list.forEach((block, index) => {
@@ -114,13 +114,10 @@
blocks.push([])
}
})
- this.generateXlsx(`lesscode-stats-${this.name}-${this.dim}.xlsx`, blocks, window.i18n.t('按时间'))
+ await this.generateXlsx(`lesscode-stats-${this.name}-${this.dim}.xlsx`, blocks, window.i18n.t('按时间'))
},
generateXlsx (fileName, data, sheetName) {
- const wb = XLSX.utils.book_new()
- const ws = XLSX.utils.aoa_to_sheet(data)
- XLSX.utils.book_append_sheet(wb, ws, sheetName)
- XLSX.writeFile(wb, fileName)
+ return downloadXlsxFromAoa(fileName, sheetName, data)
},
fistLetterUpper (str) {
return str.charAt(0).toUpperCase() + str.slice(1)
diff --git a/lib/server/controller/data-source.js b/lib/server/controller/data-source.js
index 282ef4c2e..3bf190771 100644
--- a/lib/server/controller/data-source.js
+++ b/lib/server/controller/data-source.js
@@ -724,7 +724,7 @@ export default class DataSourceController {
})
const fileName = fileType === 'sql' ? `lesscode-struct-${projectId}.sql` : ''
const zipName = `lesscode-struct-${projectId}`
- const fileList = generateExportStruct(tables, fileType, fileName)
+ const fileList = await generateExportStruct(tables, fileType, fileName)
return { fileList, zipName }
}
@@ -770,7 +770,7 @@ export default class DataSourceController {
const fileName = fileType === DATA_FILE_TYPE.SQL ? `lesscode-data-${projectId}.sql` : ''
const zipName = `lesscode-data-${projectId}`
- const fileList = generateExportDatas(datas, fileType, fileName)
+ const fileList = await generateExportDatas(datas, fileType, fileName)
return { fileList, zipName }
}
}
diff --git a/lib/server/decorator/send/index.js b/lib/server/decorator/send/index.js
index 5c55a9571..58fc235e0 100644
--- a/lib/server/decorator/send/index.js
+++ b/lib/server/decorator/send/index.js
@@ -14,6 +14,24 @@ const path = require('path')
const fse = require('fs-extra')
const { logger } = require('../../logger')
+const sanitizePathName = (name = '') => name.replace(/[/\\]/g, '_')
+
+const toWriteBuffer = (content) => {
+ if (Buffer.isBuffer(content)) {
+ return content
+ }
+ if (content instanceof ArrayBuffer) {
+ return Buffer.from(content)
+ }
+ if (ArrayBuffer.isView(content)) {
+ return Buffer.from(content.buffer, content.byteOffset, content.byteLength)
+ }
+ if (typeof content === 'string') {
+ return Buffer.from(content, 'utf8')
+ }
+ return Buffer.from(content)
+}
+
const outputError = (error, ctx) => {
// 结构化日志记录错误
logger.error(error)
@@ -67,8 +85,8 @@ export const OutputZip = () => {
try {
const { fileList } = await originValue.apply(this, [ctx])
let { zipName } = await originValue.apply(this, [ctx])
- // 去除路径名中的./\符号
- zipName = zipName?.replace(/[./\\]/g, '_')
+ // 去除路径分隔符,保留扩展名中的 .
+ zipName = sanitizePathName(zipName || '')
curDownLoadDirPath = path.join(downloadTempPath, zipName)
curDownLoadFilesPath = path.join(curDownLoadDirPath, zipName)
// 确保下载临时目录有
@@ -77,10 +95,9 @@ export const OutputZip = () => {
await fse.ensureDir(curDownLoadFilesPath)
// 生成 文件列表
fileList.forEach((file) => {
- let name = file.name || zipName
- name = name?.replace(/[./\\]/g, '_')
+ const name = sanitizePathName(file.name || zipName)
const filePath = path.join(curDownLoadFilesPath, name)
- fse.writeFileSync(filePath, file.content, 'utf8')
+ fse.writeFileSync(filePath, toWriteBuffer(file.content))
})
// 生成 zip
const curDownLoadZipPath = path.join(curDownLoadDirPath, `${zipName}.zip`)
diff --git a/lib/server/project-template/vue2/project-init-code/bk.config.js b/lib/server/project-template/vue2/project-init-code/bk.config.js
index 1850942a6..5a228dfbc 100644
--- a/lib/server/project-template/vue2/project-init-code/bk.config.js
+++ b/lib/server/project-template/vue2/project-init-code/bk.config.js
@@ -47,6 +47,19 @@ module.exports = {
target: serverAddress
}
]
+ },
+ optimization: {
+ splitChunks: {
+ cacheGroups: {
+ exceljs: {
+ name: 'exceljs',
+ test: /[\\/]node_modules[\\/]exceljs[\\/]/,
+ priority: 2,
+ chunks: 'all',
+ reuseExistingChunk: true
+ }
+ }
+ }
}
}
}
diff --git a/lib/server/project-template/vue2/project-init-code/lib/client/src/components/data-manage-container/form-data-manage/components/buttons/index.js b/lib/server/project-template/vue2/project-init-code/lib/client/src/components/data-manage-container/form-data-manage/components/buttons/index.js
index e79262c90..d670e5523 100644
--- a/lib/server/project-template/vue2/project-init-code/lib/client/src/components/data-manage-container/form-data-manage/components/buttons/index.js
+++ b/lib/server/project-template/vue2/project-init-code/lib/client/src/components/data-manage-container/form-data-manage/components/buttons/index.js
@@ -1,4 +1,4 @@
-import * as XLSX from 'xlsx'
+import { downloadXlsxFromAoa } from 'shared/excel'
import './index.postcss'
export default {
@@ -61,10 +61,7 @@ export default {
body.push(row)
})
- const wb = XLSX.utils.book_new()
- const ws = XLSX.utils.aoa_to_sheet([header, ...body])
- XLSX.utils.book_append_sheet(wb, ws)
- XLSX.writeFile(wb, `${this.tableName}.xlsx`)
+ await downloadXlsxFromAoa(`${this.tableName}.xlsx`, 'Sheet1', [header, ...body])
} catch (e) {
console.log(e.message || e)
} finally {
diff --git a/lib/server/project-template/vue2/project-init-code/lib/client/src/components/flow-form-comp/components/custom-buttons.vue b/lib/server/project-template/vue2/project-init-code/lib/client/src/components/flow-form-comp/components/custom-buttons.vue
index 37eff329c..2f09af5ad 100644
--- a/lib/server/project-template/vue2/project-init-code/lib/client/src/components/flow-form-comp/components/custom-buttons.vue
+++ b/lib/server/project-template/vue2/project-init-code/lib/client/src/components/flow-form-comp/components/custom-buttons.vue
@@ -24,7 +24,7 @@
>
- import * as XLSX from 'xlsx'
+ import { downloadXlsxFromAoa } from 'shared/excel'
export default {
name: 'CustomButtons',
@@ -100,10 +100,7 @@
body.push(row)
})
- const wb = XLSX.utils.book_new()
- const ws = XLSX.utils.aoa_to_sheet([header, ...body])
- XLSX.utils.book_append_sheet(wb, ws)
- XLSX.writeFile(wb, `${this.nodeName || this.tableName}.xlsx`)
+ await downloadXlsxFromAoa(`${this.nodeName || this.tableName}.xlsx`, 'Sheet1', [header, ...body])
} catch (e) {
console.log(e.message || e)
} finally {
diff --git a/lib/server/project-template/vue2/project-init-code/lib/shared/excel/index.js b/lib/server/project-template/vue2/project-init-code/lib/shared/excel/index.js
new file mode 100644
index 000000000..a63f121c3
--- /dev/null
+++ b/lib/server/project-template/vue2/project-init-code/lib/shared/excel/index.js
@@ -0,0 +1,33 @@
+/**
+ * Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community Edition) available.
+ * Copyright (C) 2025 Tencent. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+import ExcelJS from 'exceljs'
+
+const XLSX_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+
+/**
+ * 浏览器端下载 xlsx
+ * @param {string} fileName
+ * @param {string} sheetName
+ * @param {Array>} rows
+ */
+export async function downloadXlsxFromAoa (fileName, sheetName, rows) {
+ const workbook = new ExcelJS.Workbook()
+ const worksheet = workbook.addWorksheet(sheetName || 'Sheet1')
+ worksheet.addRows(rows)
+ const buffer = await workbook.xlsx.writeBuffer()
+ const blob = new Blob([buffer], { type: XLSX_MIME })
+ const url = URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = fileName
+ link.click()
+ URL.revokeObjectURL(url)
+}
diff --git a/lib/server/project-template/vue2/project-init-code/package.json b/lib/server/project-template/vue2/project-init-code/package.json
index fafca8630..3ac6ba988 100644
--- a/lib/server/project-template/vue2/project-init-code/package.json
+++ b/lib/server/project-template/vue2/project-init-code/package.json
@@ -157,7 +157,7 @@
"node-request-context": "~1.0.5",
"nodemon": "~1.19.3",
"ora": "~4.0.2",
- "path-to-regexp": "^8.3.0",
+ "path-to-regexp": "^8.4.2",
"postcss": "~8.4.31",
"postcss-import": "^15.0.0",
"postcss-mixins": "^9.0.4",
@@ -180,7 +180,7 @@
"vue-json-viewer": "^2.2.22",
"vue-router": "~3.1.3",
"vuex": "~3.1.1",
- "xlsx": "^0.18.5"
+ "exceljs": "^4.4.0"
},
"overrides": {
"@babel/runtime": "7.23.9",
diff --git a/lib/server/project-template/vue3/project-init-code/bk.config.js b/lib/server/project-template/vue3/project-init-code/bk.config.js
index 0dd422f14..acd75aaa2 100644
--- a/lib/server/project-template/vue3/project-init-code/bk.config.js
+++ b/lib/server/project-template/vue3/project-init-code/bk.config.js
@@ -48,6 +48,19 @@ module.exports = {
target: serverAddress
}
]
+ },
+ optimization: {
+ splitChunks: {
+ cacheGroups: {
+ exceljs: {
+ name: 'exceljs',
+ test: /[\\/]node_modules[\\/]exceljs[\\/]/,
+ priority: 2,
+ chunks: 'all',
+ reuseExistingChunk: true
+ }
+ }
+ }
}
}
}
diff --git a/lib/server/project-template/vue3/project-init-code/lib/client/src/components/data-manage-container/form-data-manage/components/buttons/index.js b/lib/server/project-template/vue3/project-init-code/lib/client/src/components/data-manage-container/form-data-manage/components/buttons/index.js
index 147f7d15f..fb8299d8d 100644
--- a/lib/server/project-template/vue3/project-init-code/lib/client/src/components/data-manage-container/form-data-manage/components/buttons/index.js
+++ b/lib/server/project-template/vue3/project-init-code/lib/client/src/components/data-manage-container/form-data-manage/components/buttons/index.js
@@ -1,5 +1,5 @@
import { h, resolveComponent } from 'vue'
-import * as XLSX from 'xlsx'
+import { downloadXlsxFromAoa } from 'shared/excel'
import './index.postcss'
export default {
@@ -62,10 +62,7 @@ export default {
body.push(row)
})
- const wb = XLSX.utils.book_new()
- const ws = XLSX.utils.aoa_to_sheet([header, ...body])
- XLSX.utils.book_append_sheet(wb, ws)
- XLSX.writeFile(wb, `${this.tableName}.xlsx`)
+ await downloadXlsxFromAoa(`${this.tableName}.xlsx`, 'Sheet1', [header, ...body])
} catch (e) {
console.log(e.message || e)
} finally {
diff --git a/lib/server/project-template/vue3/project-init-code/lib/client/src/components/flow-form-comp/components/custom-buttons.vue b/lib/server/project-template/vue3/project-init-code/lib/client/src/components/flow-form-comp/components/custom-buttons.vue
index 37eff329c..2f09af5ad 100644
--- a/lib/server/project-template/vue3/project-init-code/lib/client/src/components/flow-form-comp/components/custom-buttons.vue
+++ b/lib/server/project-template/vue3/project-init-code/lib/client/src/components/flow-form-comp/components/custom-buttons.vue
@@ -24,7 +24,7 @@
>
- import * as XLSX from 'xlsx'
+ import { downloadXlsxFromAoa } from 'shared/excel'
export default {
name: 'CustomButtons',
@@ -100,10 +100,7 @@
body.push(row)
})
- const wb = XLSX.utils.book_new()
- const ws = XLSX.utils.aoa_to_sheet([header, ...body])
- XLSX.utils.book_append_sheet(wb, ws)
- XLSX.writeFile(wb, `${this.nodeName || this.tableName}.xlsx`)
+ await downloadXlsxFromAoa(`${this.nodeName || this.tableName}.xlsx`, 'Sheet1', [header, ...body])
} catch (e) {
console.log(e.message || e)
} finally {
diff --git a/lib/server/project-template/vue3/project-init-code/lib/shared/excel/index.js b/lib/server/project-template/vue3/project-init-code/lib/shared/excel/index.js
new file mode 100644
index 000000000..a63f121c3
--- /dev/null
+++ b/lib/server/project-template/vue3/project-init-code/lib/shared/excel/index.js
@@ -0,0 +1,33 @@
+/**
+ * Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community Edition) available.
+ * Copyright (C) 2025 Tencent. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+import ExcelJS from 'exceljs'
+
+const XLSX_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+
+/**
+ * 浏览器端下载 xlsx
+ * @param {string} fileName
+ * @param {string} sheetName
+ * @param {Array>} rows
+ */
+export async function downloadXlsxFromAoa (fileName, sheetName, rows) {
+ const workbook = new ExcelJS.Workbook()
+ const worksheet = workbook.addWorksheet(sheetName || 'Sheet1')
+ worksheet.addRows(rows)
+ const buffer = await workbook.xlsx.writeBuffer()
+ const blob = new Blob([buffer], { type: XLSX_MIME })
+ const url = URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = fileName
+ link.click()
+ URL.revokeObjectURL(url)
+}
diff --git a/lib/server/project-template/vue3/project-init-code/package.json b/lib/server/project-template/vue3/project-init-code/package.json
index 4527990da..13e7a5f7d 100644
--- a/lib/server/project-template/vue3/project-init-code/package.json
+++ b/lib/server/project-template/vue3/project-init-code/package.json
@@ -155,7 +155,7 @@
"node-request-context": "~1.0.5",
"nodemon": "~1.19.3",
"ora": "~4.0.2",
- "path-to-regexp": "^8.3.0",
+ "path-to-regexp": "^8.4.2",
"postcss": "~8.4.31",
"postcss-import": "^15.0.0",
"postcss-mixins": "^9.0.4",
@@ -178,7 +178,7 @@
"vue-json-viewer": "^2.2.22",
"vue-router": "~4.1.6",
"vuex": "^4.1.0",
- "xlsx": "^0.18.5",
+ "exceljs": "^4.4.0",
"less": "^4.2.0"
},
"overrides": {
diff --git a/lib/shared/data-source/data-parse/data-parser/xlsx-parser.js b/lib/shared/data-source/data-parse/data-parser/xlsx-parser.js
index 9c7596489..67b78c221 100644
--- a/lib/shared/data-source/data-parse/data-parser/xlsx-parser.js
+++ b/lib/shared/data-source/data-parse/data-parser/xlsx-parser.js
@@ -8,7 +8,10 @@
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
-import * as XLSX from 'xlsx'
+import {
+ buildXlsxBufferFromAoa,
+ parseFirstSheetToJson
+} from '../../../excel'
import { getTypeByValue } from '../../../util'
import { DATA_TYPES } from '../../../constant'
@@ -17,20 +20,18 @@ import { DATA_TYPES } from '../../../constant'
* @param {*} xlsxs xlsx 文件
* @returns 数据 json
*/
-function transformXlsx2Json (xlsxs) {
- return xlsxs.map(({ content }) => {
- const workBook = XLSX.read(content, { type: 'binary', cellDates: true })
- // 读取第一个 sheet
- return XLSX.utils.sheet_to_json(workBook.Sheets[workBook.SheetNames[0]], { raw: false })
- })
+async function transformXlsx2Json (xlsxs) {
+ const result = []
+ for (const { content } of xlsxs) {
+ result.push(await parseFirstSheetToJson(content, { raw: false, cellDates: true }))
+ }
+ return result
}
-function transformJson2Xlsx (finalDatas) {
- return finalDatas.map(({ tableName, list }) => {
- // 生成表头
+async function transformJson2Xlsx (finalDatas) {
+ const result = []
+ for (const { tableName, list } of finalDatas) {
const header = Object.keys(list[0])
-
- // 生成字段值
const body = []
list.forEach((data) => {
const dataValues = header.map((key) => {
@@ -42,15 +43,10 @@ function transformJson2Xlsx (finalDatas) {
})
body.push(dataValues)
})
-
- // 构造 xlsx 文件
- const workBook = XLSX.utils.book_new()
- const ws = XLSX.utils.aoa_to_sheet([header, ...body])
- XLSX.utils.book_append_sheet(workBook, ws, tableName)
- const content = XLSX.write(workBook, { bookType: 'xlsx', bookSST: false, type: 'buffer' })
-
- return { tableName, content }
- })
+ const content = await buildXlsxBufferFromAoa(tableName, [header, ...body])
+ result.push({ tableName, content })
+ }
+ return result
}
/**
@@ -61,12 +57,12 @@ export class DataXlsxParser {
this.xlsxs = xlsxs
}
- set (that = {}) {
- that.finalDatas = transformXlsx2Json(this.xlsxs)
+ async set (that = {}) {
+ that.finalDatas = await transformXlsx2Json(this.xlsxs)
return that
}
- export (that) {
+ async export (that) {
return transformJson2Xlsx(that.finalDatas)
}
}
diff --git a/lib/shared/data-source/data-parse/index.js b/lib/shared/data-source/data-parse/index.js
index 8aa19e9c6..e9abf7cd4 100644
--- a/lib/shared/data-source/data-parse/index.js
+++ b/lib/shared/data-source/data-parse/index.js
@@ -49,7 +49,7 @@ export class DataParse {
* @param {*} parser 具体执行导出的实例
* @returns 返回导出结果
*/
- export (parser) {
+ async export (parser) {
return parser.export(this)
}
}
diff --git a/lib/shared/data-source/data-parse/struct-parser/xlsx-parser.js b/lib/shared/data-source/data-parse/struct-parser/xlsx-parser.js
index a7c15d627..6a4d5e48d 100644
--- a/lib/shared/data-source/data-parse/struct-parser/xlsx-parser.js
+++ b/lib/shared/data-source/data-parse/struct-parser/xlsx-parser.js
@@ -8,7 +8,10 @@
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
-import * as XLSX from 'xlsx'
+import {
+ buildXlsxBufferFromAoa,
+ parseFirstSheetToJson
+} from '../../../excel'
import {
ORM_KEYS
} from '../../constant'
@@ -18,16 +21,16 @@ import {
* @param {[{ tableName, content }]} xlsxs xlsx 数据
* @returns [{ tableName: 表名, columns: 列信息 }]
*/
-function transformXlsx2Json (xlsxs) {
- return xlsxs.map(({ tableName, content }) => {
- const workBook = XLSX.read(content, { type: 'binary', cellDates: true })
- // 读取第一个 sheet
- const columns = XLSX.utils.sheet_to_json(workBook.Sheets[workBook.SheetNames[0]])
- return {
+async function transformXlsx2Json (xlsxs) {
+ const result = []
+ for (const { tableName, content } of xlsxs) {
+ const columns = await parseFirstSheetToJson(content, { cellDates: true })
+ result.push({
tableName,
columns
- }
- })
+ })
+ }
+ return result
}
/**
@@ -35,12 +38,10 @@ function transformXlsx2Json (xlsxs) {
* @param {*} finalDatas table json
* @returns [{ tableName: 表名, content: 文件内容 }]
*/
-function transformJson2Xlsx (finalDatas) {
- return finalDatas.map(({ tableName, columns }) => {
- // 生成表头
+async function transformJson2Xlsx (finalDatas) {
+ const result = []
+ for (const { tableName, columns } of finalDatas) {
const header = ORM_KEYS.filter(x => x !== 'columnId')
-
- // 生成字段值
const body = []
columns.forEach((column) => {
const columnValue = header.map((key) => {
@@ -48,15 +49,10 @@ function transformJson2Xlsx (finalDatas) {
})
body.push(columnValue)
})
-
- // 构造 xlsx 文件
- const workBook = XLSX.utils.book_new()
- const ws = XLSX.utils.aoa_to_sheet([header, ...body])
- XLSX.utils.book_append_sheet(workBook, ws, tableName)
- const content = XLSX.write(workBook, { bookType: 'xlsx', bookSST: false, type: 'buffer' })
-
- return { tableName, content }
- })
+ const content = await buildXlsxBufferFromAoa(tableName, [header, ...body])
+ result.push({ tableName, content })
+ }
+ return result
}
/**
@@ -67,12 +63,12 @@ export class StructXlsxParser {
this.xlsxs = xlsxs
}
- set (that = {}) {
- that.finalDatas = transformXlsx2Json(this.xlsxs)
+ async set (that = {}) {
+ that.finalDatas = await transformXlsx2Json(this.xlsxs)
return that
}
- export (that) {
+ async export (that) {
return transformJson2Xlsx(that.finalDatas)
}
}
diff --git a/lib/shared/data-source/helper.js b/lib/shared/data-source/helper.js
index 889879669..8ba6349dd 100644
--- a/lib/shared/data-source/helper.js
+++ b/lib/shared/data-source/helper.js
@@ -45,16 +45,16 @@ import {
* @param {*} name 导出文件名
* @returns 返回导出内容 [{ name, conent }]
*/
-export const generateExportStruct = (tables, fileType, name) => {
+export const generateExportStruct = async (tables, fileType, name) => {
const dataParse = new DataParse()
const structJsonParser = new StructJsonParser(tables)
if (fileType === DATA_FILE_TYPE.SQL) {
const structSqlParser = new StructSqlParser()
- const content = dataParse.import(structJsonParser).export(structSqlParser)
+ const content = await dataParse.import(structJsonParser).export(structSqlParser)
return [{ content, name }]
} else {
const structXlsxParser = new StructXlsxParser()
- const fileContents = dataParse.import(structJsonParser).export(structXlsxParser)
+ const fileContents = await dataParse.import(structJsonParser).export(structXlsxParser)
return fileContents.map(({ tableName, content }) => {
return {
name: name || `${tableName}.xlsx`,
@@ -71,16 +71,16 @@ export const generateExportStruct = (tables, fileType, name) => {
* @param {*} name 导出文件名
* @returns 返回导出内容 [{ name, conent }]
*/
-export const generateExportDatas = (datas, fileType, name) => {
+export const generateExportDatas = async (datas, fileType, name) => {
const dataParse = new DataParse()
const dataJsonParser = new DataJsonParser(datas)
if (fileType === DATA_FILE_TYPE.SQL) {
const dataSqlParser = new DataSqlParser()
- const content = dataParse.import(dataJsonParser).export(dataSqlParser)
+ const content = await dataParse.import(dataJsonParser).export(dataSqlParser)
return [{ content, name }]
} else {
const dataXlsxParser = new DataXlsxParser()
- const fileContents = dataParse.import(dataJsonParser).export(dataXlsxParser)
+ const fileContents = await dataParse.import(dataJsonParser).export(dataXlsxParser)
return fileContents.map(({ tableName, content }) => {
return {
name: name || `${tableName}.xlsx`,
@@ -96,18 +96,16 @@ export const generateExportDatas = (datas, fileType, name) => {
* @param {*} fileType 文件类型
* @returns 表结构 json
*/
-export const handleImportStruct = (files, fileType) => {
+export const handleImportStruct = async (files, fileType) => {
const dataParse = new DataParse()
const structJsonParser = new StructJsonParser()
let tableStructs = []
if (fileType === DATA_FILE_TYPE.SQL) {
- tableStructs = dataParse
- .set(new StructSqlParser(files))
- .export(structJsonParser)
+ await dataParse.set(new StructSqlParser(files))
+ tableStructs = await dataParse.export(structJsonParser)
} else {
- tableStructs = dataParse
- .set(new StructXlsxParser(files))
- .export(structJsonParser)
+ await dataParse.set(new StructXlsxParser(files))
+ tableStructs = await dataParse.export(structJsonParser)
}
// 执行校验
checkTable(tableStructs)
@@ -121,7 +119,7 @@ export const handleImportStruct = (files, fileType) => {
* @param {*} columns 字段信息
* @returns 数据 json or sql
*/
-export const handleImportData = (files, fileType, columns) => {
+export const handleImportData = async (files, fileType, columns) => {
const dataParse = new DataParse()
const dataJsonParser = new DataJsonParser()
if (fileType === DATA_FILE_TYPE.SQL) {
@@ -136,7 +134,8 @@ export const handleImportData = (files, fileType, columns) => {
return files
} else {
const dataXlsxParser = new DataXlsxParser(files)
- return dataParse.set(dataXlsxParser).export(dataJsonParser)
+ await dataParse.set(dataXlsxParser)
+ return dataParse.export(dataJsonParser)
}
}
diff --git a/lib/shared/excel/index.js b/lib/shared/excel/index.js
new file mode 100644
index 000000000..91886b87f
--- /dev/null
+++ b/lib/shared/excel/index.js
@@ -0,0 +1,138 @@
+/**
+ * Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community Edition) available.
+ * Copyright (C) 2025 Tencent. All rights reserved.
+ * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://opensource.org/licenses/MIT
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+import ExcelJS from 'exceljs'
+
+const XLSX_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+
+function binaryStringToUint8Array (content) {
+ const bytes = new Uint8Array(content.length)
+ for (let i = 0; i < content.length; i++) { + bytes[i] = content.charCodeAt(i) & 0xff + } + return bytes +} + +function normalizeLoadInput (content) { + if (typeof content === 'string') { + return binaryStringToUint8Array(content) + } + return content +} + +function getCellValue (cell, { raw = false } = {}) { + if (!cell) { + return '' + } + const { value } = cell + if (value === null || value === undefined) { + return '' + } + if (!raw) { + if (cell.text !== undefined && cell.text !== null && cell.text !== '') { + return cell.text + } + if (value instanceof Date) { + return value + } + if (typeof value === 'object') { + if (value.text !== undefined) { + return value.text + } + if (value.result !== undefined) { + return value.result + } + if (value.richText) { + return value.richText.map(item => item.text).join('')
+ }
+ }
+ return value
+ }
+ if (typeof value === 'object' && value.result !== undefined) {
+ return value.result
+ }
+ return value
+}
+
+/**
+ * 解析 xlsx 第一个 sheet 为对象数组(首行作为表头)
+ * @param {string|ArrayBuffer|Uint8Array} content
+ * @param {{ raw?: boolean, cellDates?: boolean }} options
+ */
+export async function parseFirstSheetToJson (content, options = {}) {
+ const { raw = false } = options
+ const workbook = new ExcelJS.Workbook()
+ await workbook.xlsx.load(normalizeLoadInput(content))
+ const worksheet = workbook.worksheets[0]
+ if (!worksheet) {
+ return []
+ }
+
+ const headerMap = {}
+ const headerRow = worksheet.getRow(1)
+ headerRow.eachCell({ includeEmpty: true }, (cell, colNumber) => {
+ const header = getCellValue(cell, { raw })
+ if (header !== '' && header !== undefined && header !== null) {
+ headerMap[colNumber] = header
+ }
+ })
+
+ const result = []
+ const rowCount = worksheet.rowCount
+ for (let rowNumber = 2; rowNumber <= rowCount; rowNumber++) { + const row = worksheet.getRow(rowNumber) + const record = {} + let hasValue = false + row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
+ const key = headerMap[colNumber]
+ if (key === undefined) {
+ return
+ }
+ const cellValue = getCellValue(cell, { raw })
+ if (cellValue !== '' && cellValue !== undefined && cellValue !== null) {
+ hasValue = true
+ }
+ record[key] = cellValue
+ })
+ if (hasValue) {
+ result.push(record)
+ }
+ }
+ return result
+}
+
+/**
+ * 由二维数组生成 xlsx Buffer
+ * @param {string} sheetName
+ * @param {Array>} rows
+ */
+export async function buildXlsxBufferFromAoa (sheetName, rows) {
+ const workbook = new ExcelJS.Workbook()
+ const worksheet = workbook.addWorksheet(sheetName || 'Sheet1')
+ worksheet.addRows(rows)
+ return workbook.xlsx.writeBuffer()
+}
+
+/**
+ * 浏览器端下载 xlsx
+ * @param {string} fileName
+ * @param {string} sheetName
+ * @param {Array>} rows
+ */
+export async function downloadXlsxFromAoa (fileName, sheetName, rows) {
+ const buffer = await buildXlsxBufferFromAoa(sheetName, rows)
+ const blob = new Blob([buffer], { type: XLSX_MIME })
+ const url = URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = fileName
+ link.click()
+ URL.revokeObjectURL(url)
+}
diff --git a/package.json b/package.json
index 2d4cc68d0..71afdcfbe 100644
--- a/package.json
+++ b/package.json
@@ -186,7 +186,7 @@
"nodemon": "~1.19.3",
"node-fetch": "^2.6.13",
"npm": "6.14.15",
- "path-to-regexp": "^8.3.0",
+ "path-to-regexp": "^8.4.2",
"pinyin": "^2.11.2",
"postcss": "~8.4.31",
"postcss-import": "^15.0.0",
@@ -226,7 +226,7 @@
"vue-template-compiler": "~2.7.14",
"vuedraggable": "~2.23.2",
"vuex": "~3.1.1",
- "xlsx": "^0.18.5"
+ "exceljs": "^4.4.0"
},
"overrides": {
"@babel/runtime": "7.23.9",
From 6a9b5746d5ffac9f273a571e85eed0a03e6ffcc2 Mon Sep 17 00:00:00 2001
From: terlinhe
Date: 2026年5月17日 23:19:20 +0800
Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=E5=A4=84=E7=90=86package.json?=
=?UTF-8?q?=E4=BE=9D=E8=B5=96=EF=BC=9Aaxios=E3=80=81koa?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
lib/client/src/api/index.js | 46 +++++++---
lib/client/src/api/request-error.js | 89 ++++++++++++++++++-
lib/server/controller/page.js | 6 +-
lib/server/middleware/error.js | 17 +++-
.../vue2/project-init-code/package.json | 4 +-
.../vue3/project-init-code/package.json | 4 +-
package.json | 4 +-
7 files changed, 143 insertions(+), 27 deletions(-)
diff --git a/lib/client/src/api/index.js b/lib/client/src/api/index.js
index a28faf300..18ddad99c 100644
--- a/lib/client/src/api/index.js
+++ b/lib/client/src/api/index.js
@@ -12,7 +12,7 @@
import axios from 'axios'
import cookie from 'cookie'
-import RequestError from './request-error'
+import RequestError, { formatErrorMessage } from './request-error'
import CachedPromise from './cached-promise'
import RequestQueue from './request-queue'
import { bus } from '../common/bus'
@@ -20,6 +20,19 @@ import { bkMessage } from 'bk-magic-vue'
import { showLoginModal } from '@blueking/login-modal'
+const getResponseMessage = (response) => {
+ const body = response?.data
+ if (body && typeof body === 'object') {
+ const bodyMessage = body.message ?? body.msg ?? body.data?.message
+ if (bodyMessage != null && bodyMessage !== '') {
+ return formatErrorMessage(bodyMessage)
+ }
+ }
+ return response?.statusText || formatErrorMessage()
+}
+
+const rejectRequestError = (code, message, response) => Promise.reject(new RequestError(code, message, response))
+
// 解析错误
axios.interceptors.response.use(
// 后端 API 响应成功(http status 200)
@@ -42,33 +55,38 @@ axios.interceptors.response.use(
bus.$emit('not-exist', data.message)
return data
// 后端业务处理报错
- default:
- const { code, message = window.i18n.t('系统错误') } = response.data
- throw new RequestError(code, message, response)
+ default: {
+ const body = response.data
+ const { code } = body
+ const message = body.message ?? body.msg ?? body.data?.message
+ return rejectRequestError(code, formatErrorMessage(message), response)
+ }
}
},
// 解析 http status code (非 200)
error => {
+ if (error instanceof RequestError) {
+ return Promise.reject(error)
+ }
const { response } = error
if (response) {
- // 默认提示 http 状态码错误标记
- let message = response.statusText
- // 兼容后端响应时通过body返回错误信息
- if (response.data && response.data.message) {
- message = response.data.message
- }
- return Promise.reject(new RequestError(response.status || -1, message, response))
+ return rejectRequestError(response.status || -1, getResponseMessage(response), response)
}
- return Promise.reject(new RequestError(-1, window.i18n.t('{0} 无法访问', [process.env.BK_AJAX_URL_PREFIX])))
+ return rejectRequestError(-1, window.i18n.t('{0} 无法访问', [process.env.BK_AJAX_URL_PREFIX]))
}
)
// 处理错误
axios.interceptors.response.use(undefined, error => {
+ const requestError = error instanceof RequestError
+ ? error
+ : (error?.response
+ ? new RequestError(error.response.status || -1, getResponseMessage(error.response), error.response)
+ : error)
const {
code,
message,
response
- } = error
+ } = requestError
switch (code) {
// 用户登录状态失效
case 401:
@@ -87,7 +105,7 @@ axios.interceptors.response.use(undefined, error => {
bkMessage({ theme: 'error', message, ellipsisLine: 3, limit: 1 })
}
}
- return Promise.reject(error)
+ return Promise.reject(requestError)
})
const http = {
diff --git a/lib/client/src/api/request-error.js b/lib/client/src/api/request-error.js
index b9a25a73b..3c788342b 100644
--- a/lib/client/src/api/request-error.js
+++ b/lib/client/src/api/request-error.js
@@ -1,8 +1,93 @@
+const DEFAULT_ERROR_MESSAGE = '系统错误'
+
+const getDefaultMessage = () => {
+ try {
+ return window.i18n?.t?.(DEFAULT_ERROR_MESSAGE) || DEFAULT_ERROR_MESSAGE
+ } catch {
+ return DEFAULT_ERROR_MESSAGE
+ }
+}
+
+/**
+ * 将接口返回的 message 转为可展示的字符串(避免 [object Object])
+ */
+export const formatErrorMessage = (message, fallback = getDefaultMessage()) => {
+ if (message == null || message === '') {
+ return fallback
+ }
+ if (typeof message === 'string') {
+ return message
+ }
+ if (typeof message === 'number' || typeof message === 'boolean') {
+ return String(message)
+ }
+ if (message instanceof Error) {
+ return formatErrorMessage(message.message, fallback)
+ }
+ if (Array.isArray(message)) {
+ if (typeof message[0] === 'string' && window.i18n?.t) {
+ try {
+ return window.i18n.t(...message)
+ } catch {
+ // fall through
+ }
+ }
+ return message
+ .map(item => formatErrorMessage(item, ''))
+ .filter(Boolean)
+ .join('; ') || fallback
+ }
+ if (typeof message === 'object') {
+ if (Object.keys(message).length === 0) {
+ return fallback
+ }
+ if (message.message != null) {
+ return formatErrorMessage(message.message, fallback)
+ }
+ if (message.msg != null) {
+ return formatErrorMessage(message.msg, fallback)
+ }
+ if (message.content != null) {
+ return formatErrorMessage(message.content, fallback)
+ }
+ if (message.detail != null) {
+ return formatErrorMessage(message.detail, fallback)
+ }
+ if (message.error != null) {
+ return formatErrorMessage(message.error, fallback)
+ }
+ if (typeof message.key === 'string' && window.i18n?.t) {
+ try {
+ return window.i18n.t(message.key, message.params || message.values || message.args)
+ } catch {
+ // fall through
+ }
+ }
+ const locale = window.i18n?.locale || 'zh-cn'
+ const localeMessage = message[locale] ?? message['zh-cn'] ?? message.en ?? message.zh
+ if (localeMessage != null) {
+ return formatErrorMessage(localeMessage, fallback)
+ }
+ const stringValues = Object.values(message).filter(value => typeof value === 'string')
+ if (stringValues.length === 1) {
+ return stringValues[0]
+ }
+ try {
+ return JSON.stringify(message)
+ } catch {
+ return fallback
+ }
+ }
+ return String(message)
+}
+
export default class RequestError extends Error {
constructor (code, message, response) {
- super()
+ const displayMessage = formatErrorMessage(message)
+ super(displayMessage)
+ this.name = 'RequestError'
this.code = code
- this.message = message
+ this.message = displayMessage
this.response = response
}
}
diff --git a/lib/server/controller/page.js b/lib/server/controller/page.js
index 1832b4713..c4bb924ce 100644
--- a/lib/server/controller/page.js
+++ b/lib/server/controller/page.js
@@ -624,7 +624,7 @@ export const pageLockStatus = async ctx => {
})
} catch (error) {
ctx.throwError({
- message: error
+ message: error?.message || error
})
}
}
@@ -646,7 +646,7 @@ export const updatePageActive = async ctx => {
})
} catch (error) {
ctx.throwError({
- message: error
+ message: error?.message || error
})
}
}
@@ -675,7 +675,7 @@ export const occupyPage = async ctx => {
})
} catch (error) {
ctx.throwError({
- message: error
+ message: error?.message || error
})
}
}
diff --git a/lib/server/middleware/error.js b/lib/server/middleware/error.js
index 726df0045..91fd191c4 100644
--- a/lib/server/middleware/error.js
+++ b/lib/server/middleware/error.js
@@ -9,6 +9,19 @@
* specific language governing permissions and limitations under the License.
*/
+const formatErrorMessage = (message) => {
+ if (message == null || message === '') {
+ return global.i18n?.t?.('服务器错误') || '服务器错误'
+ }
+ if (typeof message === 'string') {
+ return message
+ }
+ if (message instanceof Error) {
+ return message.message || global.i18n?.t?.('服务器错误') || '服务器错误'
+ }
+ return String(message)
+}
+
module.exports = () => {
return async function (ctx, next) {
/**
@@ -26,7 +39,7 @@ module.exports = () => {
message,
code
} = error
- throw new global.BusinessError(message, code, status)
+ throw new global.BusinessError(formatErrorMessage(message), code, status)
}
}
/**
@@ -50,7 +63,7 @@ module.exports = () => {
ctx.status = status
ctx.body = {
code,
- message,
+ message: formatErrorMessage(message),
data
}
diff --git a/lib/server/project-template/vue2/project-init-code/package.json b/lib/server/project-template/vue2/project-init-code/package.json
index 3ac6ba988..1dfbfca11 100644
--- a/lib/server/project-template/vue2/project-init-code/package.json
+++ b/lib/server/project-template/vue2/project-init-code/package.json
@@ -108,7 +108,7 @@
"acorn": "~7.2.0",
"ansi_up": "^5.0.0",
"app-root-path": "~3.0.0",
- "axios": "~1.12.0",
+ "axios": "^1.15.2",
"babel-eslint": "~10.0.3",
"babel-plugin-parameter-decorator": "^1.0.16",
"better-npm-run": "~0.1.1",
@@ -139,7 +139,7 @@
"jsonp": "~0.2.1",
"js-base64": "^3.7.7",
"js-cookie": "3.0.1",
- "koa": "~2.8.2",
+ "koa": "^2.16.4",
"koa-body": "^4.2.0",
"koa-bodyparser": "~4.2.1",
"koa-convert": "~1.2.0",
diff --git a/lib/server/project-template/vue3/project-init-code/package.json b/lib/server/project-template/vue3/project-init-code/package.json
index 13e7a5f7d..6346f3bd8 100644
--- a/lib/server/project-template/vue3/project-init-code/package.json
+++ b/lib/server/project-template/vue3/project-init-code/package.json
@@ -108,7 +108,7 @@
"acorn": "~7.2.0",
"ansi_up": "^5.0.0",
"app-root-path": "~3.0.0",
- "axios": "~1.12.0",
+ "axios": "^1.15.2",
"babel-eslint": "~10.0.3",
"babel-plugin-parameter-decorator": "^1.0.16",
"better-npm-run": "~0.1.1",
@@ -137,7 +137,7 @@
"jsonp": "~0.2.1",
"js-base64": "^3.7.7",
"js-cookie": "3.0.1",
- "koa": "~2.8.2",
+ "koa": "^2.16.4",
"koa-body": "^4.2.0",
"koa-bodyparser": "~4.2.1",
"koa-convert": "~1.2.0",
diff --git a/package.json b/package.json
index 71afdcfbe..2072a6091 100644
--- a/package.json
+++ b/package.json
@@ -104,7 +104,7 @@
"app-root-path": "~3.0.0",
"async-validator": "~1.8.1",
"bk-lesscode-render": "1.0.0-beta.4",
- "axios": "~1.12.0",
+ "axios": "^1.15.2",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-parameter-decorator": "^1.0.16",
"better-npm-run": "~0.1.1",
@@ -148,7 +148,7 @@
"js-base64": "^3.7.7",
"js-cookie": "3.0.1",
"jsdom": "~16.7.0",
- "koa": "~2.8.2",
+ "koa": "^2.16.4",
"koa-body": "^4.2.0",
"koa-bodyparser": "~4.2.1",
"koa-convert": "~1.2.0",
From 5d2be22d72481c571937ab6316c7bb1eff82ec20 Mon Sep 17 00:00:00 2001
From: luofann
Date: Thu, 2 Jul 2026 11:23:12 +0800
Subject: [PATCH 8/8] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=A1=A8=E5=8D=95?=
=?UTF-8?q?=E5=AE=B9=E5=99=A8=E5=86=85textarea=E7=BB=84=E4=BB=B6=E8=AE=BE?=
=?UTF-8?q?=E7=BD=AEresizable=E5=90=8E=E4=B8=8D=E8=83=BD=E8=87=AA=E9=80=82?=
=?UTF-8?q?=E5=BA=94=E9=AB=98=E5=BA=A6=E7=9A=84=E9=97=AE=E9=A2=98=20#=20Re?=
=?UTF-8?q?viewed,=20transaction=20id:=2082718?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
lib/client/src/form-engine/fields/textarea/index.js | 1 +
lib/client/src/form-engine/fields/textarea/index.postcss | 8 ++++++++
.../src/components/form-engine/fields/textarea/index.js | 1 +
.../components/form-engine/fields/textarea/index.postcss | 8 ++++++++
4 files changed, 18 insertions(+)
create mode 100644 lib/client/src/form-engine/fields/textarea/index.postcss
create mode 100644 lib/server/project-template/vue3/project-init-code/lib/client/src/components/form-engine/fields/textarea/index.postcss
diff --git a/lib/client/src/form-engine/fields/textarea/index.js b/lib/client/src/form-engine/fields/textarea/index.js
index 7a4263503..d4437287b 100644
--- a/lib/client/src/form-engine/fields/textarea/index.js
+++ b/lib/client/src/form-engine/fields/textarea/index.js
@@ -1,4 +1,5 @@
import { h } from 'bk-lesscode-render'
+import './index.postcss'
export default {
name: 'bkform-engine-textarea',
diff --git a/lib/client/src/form-engine/fields/textarea/index.postcss b/lib/client/src/form-engine/fields/textarea/index.postcss
new file mode 100644
index 000000000..7e3d41d85
--- /dev/null
+++ b/lib/client/src/form-engine/fields/textarea/index.postcss
@@ -0,0 +1,8 @@
+.bkform-engine-field-widget {
+ .bk-lesscode-textarea,
+ .bk-textarea {
+ textarea {
+ flex: 1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/server/project-template/vue3/project-init-code/lib/client/src/components/form-engine/fields/textarea/index.js b/lib/server/project-template/vue3/project-init-code/lib/client/src/components/form-engine/fields/textarea/index.js
index bce14121d..1d4b91305 100644
--- a/lib/server/project-template/vue3/project-init-code/lib/client/src/components/form-engine/fields/textarea/index.js
+++ b/lib/server/project-template/vue3/project-init-code/lib/client/src/components/form-engine/fields/textarea/index.js
@@ -1,4 +1,5 @@
import { h, resolveComponent } from 'vue'
+import './index.postcss'
export default {
name: 'bkform-engine-textarea',
diff --git a/lib/server/project-template/vue3/project-init-code/lib/client/src/components/form-engine/fields/textarea/index.postcss b/lib/server/project-template/vue3/project-init-code/lib/client/src/components/form-engine/fields/textarea/index.postcss
new file mode 100644
index 000000000..7e3d41d85
--- /dev/null
+++ b/lib/server/project-template/vue3/project-init-code/lib/client/src/components/form-engine/fields/textarea/index.postcss
@@ -0,0 +1,8 @@
+.bkform-engine-field-widget {
+ .bk-lesscode-textarea,
+ .bk-textarea {
+ textarea {
+ flex: 1;
+ }
+ }
+}
\ No newline at end of file