befly 3.15.1 → 3.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,3 @@
1
- const DB_DIALECT_SET = new Set(["mysql", "postgresql", "sqlite"]);
2
1
  function isNonEmptyString(value) {
3
2
  return typeof value === "string" && value.trim().length > 0;
4
3
  }
@@ -8,39 +7,11 @@ function isValidPort(value) {
8
7
  function isValidNonNegativeInt(value) {
9
8
  return typeof value === "number" && Number.isFinite(value) && value >= 0 && Math.floor(value) === value;
10
9
  }
11
- function normalizeDbDialect(dialect) {
12
- const d = String(dialect || "")
13
- .toLowerCase()
14
- .trim();
15
- if (d === "postgresql")
16
- return "postgresql";
17
- if (d === "sqlite")
18
- return "sqlite";
19
- if (d === "mysql")
20
- return "mysql";
21
- return d;
22
- }
23
10
  function validateDbConfig(db) {
24
11
  if (!db) {
25
12
  throw new Error("配置错误:缺少 db 配置(config.db)");
26
13
  }
27
- const dialect = normalizeDbDialect(db.dialect);
28
- if (!DB_DIALECT_SET.has(dialect)) {
29
- throw new Error(`配置错误:db.dialect 只允许 mysql/postgresql/sqlite,当前值=${String(db.dialect)}`);
30
- }
31
- // sqlite:database 表示文件路径或 ":memory:";这里要求非空。
32
- if (dialect === "sqlite") {
33
- if (!isNonEmptyString(db.database)) {
34
- throw new Error(`配置错误:db.dialect=sqlite 时必须设置 db.database(sqlite 文件路径或 :memory:),当前值=${String(db.database)}`);
35
- }
36
- if (db.poolMax !== undefined) {
37
- if (typeof db.poolMax !== "number" || !Number.isFinite(db.poolMax) || db.poolMax <= 0) {
38
- throw new Error(`配置错误:db.poolMax 必须为正数,当前值=${String(db.poolMax)}`);
39
- }
40
- }
41
- return;
42
- }
43
- // mysql/postgresql:必须提供 host/port/username/password/database
14
+ // MySQL:必须提供 host/port/username/password/database
44
15
  if (!isNonEmptyString(db.host)) {
45
16
  throw new Error(`配置错误:db.host 必须为非空字符串,当前值=${String(db.host)}`);
46
17
  }
@@ -88,10 +88,13 @@ const LOWER_CAMEL_CASE_REGEX = /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
88
88
  * 必须为中文、数字、字母、下划线、短横线、空格
89
89
  */
90
90
  const FIELD_NAME_REGEX = /^[\u4e00-\u9fa5a-zA-Z0-9 _-]+$/;
91
- /**
92
- * VARCHAR 最大长度限制
93
- */
94
- const MAX_VARCHAR_LENGTH = 65535;
91
+ // MySQL VARCHAR 最大长度(utf8mb4 下受行大小限制;本项目采用保守上限,用于表定义校验)
92
+ const MAX_VARCHAR_LENGTH = 16383;
93
+ // 项目索引策略(更简单、更保守):
94
+ // - index=true: 只允许 max<=500
95
+ // - unique=true: 只允许 max<=180
96
+ const MAX_INDEX_STRING_LENGTH_FOR_INDEX = 500;
97
+ const MAX_INDEX_STRING_LENGTH_FOR_UNIQUE = 180;
95
98
  /**
96
99
  * 检查表定义文件
97
100
  * @throws 当检查失败时抛出异常
@@ -261,6 +264,16 @@ export async function checkTable(tables) {
261
264
  Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
262
265
  hasError = true;
263
266
  }
267
+ else {
268
+ if (field.index === true && fieldMax > MAX_INDEX_STRING_LENGTH_FOR_INDEX) {
269
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 设置了 index=true,` + `但 max=${fieldMax} 超出允许范围(要求 <= ${MAX_INDEX_STRING_LENGTH_FOR_INDEX})`);
270
+ hasError = true;
271
+ }
272
+ if (field.unique === true && fieldMax > MAX_INDEX_STRING_LENGTH_FOR_UNIQUE) {
273
+ Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 设置了 unique=true,` + `但 max=${fieldMax} 超出允许范围(要求 <= ${MAX_INDEX_STRING_LENGTH_FOR_UNIQUE})`);
274
+ hasError = true;
275
+ }
276
+ }
264
277
  // default 规则(table 定义专用):
265
278
  // - string:default 若存在且非 null,必须为 string
266
279
  // - array_*_string:default 若存在且非 null,必须为 string,且建议为 JSON 数组字符串(如 "[]")
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@ import { syncApi } from "./sync/syncApi";
25
25
  import { syncCache } from "./sync/syncCache";
26
26
  import { syncDev } from "./sync/syncDev";
27
27
  import { syncMenu } from "./sync/syncMenu";
28
- import { syncTable } from "./sync/syncTable";
28
+ import { SyncTable } from "./sync/syncTable";
29
29
  // 工具
30
30
  import { calcPerfTime } from "./utils/calcPerfTime";
31
31
  import { getProcessRole } from "./utils/processInfo";
@@ -92,7 +92,7 @@ export class Befly {
92
92
  // 注意:这里不做兼容别名(例如 dbHelper=db),要求上下文必须注入标准字段。
93
93
  this.assertStartContextReady();
94
94
  // 5. 自动同步 (仅主进程执行,避免集群模式下重复执行)
95
- await syncTable(this.context, tables);
95
+ await new SyncTable(this.context).run(tables);
96
96
  await syncApi(this.context, apis);
97
97
  await syncMenu(this.context, checkedMenus);
98
98
  const devEmail = this.config.devEmail;
@@ -18,6 +18,4 @@ export declare class CacheKeys {
18
18
  * - member: url.pathname(例如 /api/user/login;与 method 无关)
19
19
  */
20
20
  static roleApis(roleCode: string): string;
21
- /** 表结构缓存 */
22
- static tableColumns(table: string): string;
23
21
  }
@@ -26,8 +26,4 @@ export class CacheKeys {
26
26
  static roleApis(roleCode) {
27
27
  return `role:apis:${roleCode}`;
28
28
  }
29
- /** 表结构缓存 */
30
- static tableColumns(table) {
31
- return `table:columns:${table}`;
32
- }
33
29
  }
@@ -14,6 +14,8 @@ export declare class Connect {
14
14
  private static sqlConnectedAt;
15
15
  private static redisConnectedAt;
16
16
  private static sqlPoolMax;
17
+ private static mysqlVersionText;
18
+ private static mysqlVersionMajor;
17
19
  /**
18
20
  * 连接 SQL 数据库
19
21
  * @returns SQL 客户端实例
@@ -69,6 +71,8 @@ export declare class Connect {
69
71
  connectedAt: number | null;
70
72
  uptime: number | null;
71
73
  poolMax: number;
74
+ mysqlVersionText: string | null;
75
+ mysqlVersionMajor: number | null;
72
76
  };
73
77
  redis: {
74
78
  connected: boolean;
@@ -15,6 +15,9 @@ export class Connect {
15
15
  static sqlConnectedAt = null;
16
16
  static redisConnectedAt = null;
17
17
  static sqlPoolMax = 1;
18
+ // MySQL 版本信息(用于启动期校验与监控)
19
+ static mysqlVersionText = null;
20
+ static mysqlVersionMajor = null;
18
21
  // ========================================
19
22
  // SQL 连接管理
20
23
  // ========================================
@@ -24,51 +27,50 @@ export class Connect {
24
27
  */
25
28
  static async connectSql(dbConfig) {
26
29
  const config = dbConfig || {};
27
- // 构建数据库连接字符串
28
- const dialect = config.dialect || "mysql";
29
- const host = config.host || "127.0.0.1";
30
- const port = config.port || 3306;
31
- const user = encodeURIComponent(config.username || "root");
32
- const password = encodeURIComponent(config.password || "root");
33
- const database = encodeURIComponent(config.database || "befly_demo");
34
- let finalUrl;
35
- if (dialect === "sqlite") {
36
- finalUrl = database;
30
+ // 构建 MySQL 连接字符串(不做隐式默认;缺失直接报错,避免连错库/错密码)
31
+ const host = typeof config.host === "string" ? config.host.trim() : "";
32
+ const port = typeof config.port === "number" ? config.port : NaN;
33
+ const username = typeof config.username === "string" ? config.username.trim() : "";
34
+ const password = config.password === undefined ? "" : typeof config.password === "string" ? config.password : "";
35
+ const database = typeof config.database === "string" ? config.database.trim() : "";
36
+ if (!host) {
37
+ throw new Error("数据库配置不完整:db.host 缺失");
37
38
  }
38
- else {
39
- if (!host || !database) {
40
- throw new Error("数据库配置不完整,请检查配置参数");
41
- }
42
- finalUrl = `${dialect}://${user}:${password}@${host}:${port}/${database}`;
39
+ if (!Number.isFinite(port) || port < 1 || port > 65535) {
40
+ throw new Error(`数据库配置不完整:db.port 非法(当前值:${String(config.port)})`);
43
41
  }
44
- let sql;
45
- if (dialect === "sqlite") {
46
- sql = new SQL(finalUrl);
42
+ if (!username) {
43
+ throw new Error("数据库配置不完整:db.username 缺失");
47
44
  }
48
- else {
49
- sql = new SQL({
50
- url: finalUrl,
51
- max: config.poolMax ?? 1,
52
- bigint: false
53
- });
45
+ if (!database) {
46
+ throw new Error("数据库配置不完整:db.database 缺失");
54
47
  }
48
+ const user = encodeURIComponent(username);
49
+ const pass = encodeURIComponent(password);
50
+ const db = encodeURIComponent(database);
51
+ const finalUrl = `mysql://${user}:${pass}@${host}:${port}/${db}`;
52
+ const sql = new SQL({
53
+ url: finalUrl,
54
+ max: config.poolMax ?? 1,
55
+ bigint: false
56
+ });
55
57
  try {
56
58
  const timeout = 30000;
57
59
  const healthCheckPromise = (async () => {
58
- let version = "";
59
- if (dialect === "sqlite") {
60
- const v = await sql `SELECT sqlite_version() AS version`;
61
- version = v?.[0]?.version;
62
- }
63
- else if (dialect === "postgresql") {
64
- const v = await sql `SELECT version() AS version`;
65
- version = v?.[0]?.version;
60
+ const v = await sql `SELECT VERSION() AS version`;
61
+ const versionText = typeof v?.[0]?.version === "string" ? v?.[0]?.version : String(v?.[0]?.version || "");
62
+ // 常见格式:8.0.36 / 8.0.36-xxx
63
+ const majorText = versionText.split(".")[0];
64
+ const major = Number(majorText);
65
+ if (!Number.isFinite(major)) {
66
+ throw new Error(`无法解析 MySQL 版本信息: ${versionText}`);
66
67
  }
67
- else {
68
- const v = await sql `SELECT VERSION() AS version`;
69
- version = v?.[0]?.version;
68
+ if (major < 8) {
69
+ throw new Error(`仅支持 MySQL 8.0+,当前版本:${versionText}`);
70
70
  }
71
- return version;
71
+ this.mysqlVersionText = versionText;
72
+ this.mysqlVersionMajor = major;
73
+ return versionText;
72
74
  })();
73
75
  const timeoutPromise = new Promise((_, reject) => {
74
76
  setTimeout(() => {
@@ -132,11 +134,13 @@ export class Connect {
132
134
  const password = config.password || "";
133
135
  const db = config.db || 0;
134
136
  let auth = "";
135
- if (username && password) {
136
- auth = `${username}:${password}@`;
137
+ const encodedUsername = username ? encodeURIComponent(username) : "";
138
+ const encodedPassword = password ? encodeURIComponent(password) : "";
139
+ if (encodedUsername && encodedPassword) {
140
+ auth = `${encodedUsername}:${encodedPassword}@`;
137
141
  }
138
- else if (password) {
139
- auth = `:${password}@`;
142
+ else if (encodedPassword) {
143
+ auth = `:${encodedPassword}@`;
140
144
  }
141
145
  const url = `redis://${auth}${host}:${port}/${db}`;
142
146
  const redis = new RedisClient(url, {
@@ -228,7 +232,9 @@ export class Connect {
228
232
  connected: this.sqlClient !== null,
229
233
  connectedAt: this.sqlConnectedAt,
230
234
  uptime: this.sqlConnectedAt ? now - this.sqlConnectedAt : null,
231
- poolMax: this.sqlPoolMax
235
+ poolMax: this.sqlPoolMax,
236
+ mysqlVersionText: this.mysqlVersionText,
237
+ mysqlVersionMajor: this.mysqlVersionMajor
232
238
  },
233
239
  redis: {
234
240
  connected: this.redisClient !== null,
@@ -261,5 +267,7 @@ export class Connect {
261
267
  this.sqlConnectedAt = null;
262
268
  this.redisConnectedAt = null;
263
269
  this.sqlPoolMax = 1;
270
+ this.mysqlVersionText = null;
271
+ this.mysqlVersionMajor = null;
264
272
  }
265
273
  }
@@ -4,7 +4,6 @@
4
4
  */
5
5
  import type { WhereConditions, SqlValue } from "../types/common";
6
6
  import type { QueryOptions, InsertOptions, UpdateOptions, DeleteOptions, DbPageResult, DbListResult, TransactionCallback, DbResult, ListSql } from "../types/database";
7
- import type { DbDialect } from "./dbDialect";
8
7
  type RedisCacheLike = {
9
8
  getObject<T = unknown>(key: string): Promise<T | null>;
10
9
  setObject<T = unknown>(key: string, obj: T, ttl?: number | null): Promise<string | null>;
@@ -18,7 +17,7 @@ type SqlClientLike = {
18
17
  */
19
18
  export declare class DbHelper {
20
19
  private redis;
21
- private dialect;
20
+ private dbName;
22
21
  private sql;
23
22
  private isTransaction;
24
23
  /**
@@ -28,8 +27,8 @@ export declare class DbHelper {
28
27
  */
29
28
  constructor(options: {
30
29
  redis: RedisCacheLike;
30
+ dbName: string;
31
31
  sql?: SqlClientLike | null;
32
- dialect?: DbDialect;
33
32
  });
34
33
  private createSqlBuilder;
35
34
  private getTableColumns;
@@ -4,16 +4,22 @@
4
4
  */
5
5
  import { convertBigIntFields } from "../utils/convertBigIntFields";
6
6
  import { fieldClear } from "../utils/fieldClear";
7
- import { toSqlParams } from "../utils/sqlParams";
8
- import { toNumberFromSql } from "../utils/sqlResult";
7
+ import { toNumberFromSql, toSqlParams } from "../utils/sqlUtil";
9
8
  import { arrayKeysToCamel, isPlainObject, keysToCamel, snakeCase } from "../utils/util";
10
- import { CacheKeys } from "./cacheKeys";
11
- import { MySqlDialect } from "./dbDialect";
12
9
  import { DbUtils } from "./dbUtils";
13
10
  import { Logger } from "./logger";
14
11
  import { SqlBuilder } from "./sqlBuilder";
15
12
  import { SqlCheck } from "./sqlCheck";
16
- const TABLE_COLUMNS_CACHE_TTL_SECONDS = 3600;
13
+ function quoteIdentMySql(identifier) {
14
+ if (typeof identifier !== "string") {
15
+ throw new Error(`quoteIdentifier 需要字符串类型标识符 (identifier: ${String(identifier)})`);
16
+ }
17
+ const trimmed = identifier.trim();
18
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
19
+ throw new Error(`无效的 SQL 标识符: ${trimmed}`);
20
+ }
21
+ return `\`${trimmed}\``;
22
+ }
17
23
  function hasBegin(sql) {
18
24
  return typeof sql.begin === "function";
19
25
  }
@@ -35,7 +41,7 @@ class DbSqlError extends Error {
35
41
  */
36
42
  export class DbHelper {
37
43
  redis;
38
- dialect;
44
+ dbName;
39
45
  sql = null;
40
46
  isTransaction = false;
41
47
  /**
@@ -45,38 +51,35 @@ export class DbHelper {
45
51
  */
46
52
  constructor(options) {
47
53
  this.redis = options.redis;
54
+ if (typeof options.dbName !== "string" || options.dbName.trim() === "") {
55
+ throw new Error("DbHelper 初始化失败:dbName 必须为非空字符串");
56
+ }
57
+ this.dbName = options.dbName;
48
58
  this.sql = options.sql || null;
49
59
  this.isTransaction = Boolean(options.sql);
50
- // 默认使用 MySQL 方言(当前 core 的表结构/语法也主要基于 MySQL)
51
- this.dialect = options.dialect ? options.dialect : new MySqlDialect();
52
60
  }
53
61
  createSqlBuilder() {
54
- return new SqlBuilder({ quoteIdent: this.dialect.quoteIdent.bind(this.dialect) });
62
+ return new SqlBuilder({ quoteIdent: quoteIdentMySql });
55
63
  }
56
64
  /**
57
- * 获取表的所有字段名(Redis 缓存)
65
+ * 获取表的所有字段名
58
66
  * @param table - 表名(下划线格式)
59
67
  * @returns 字段名数组(下划线格式)
60
68
  */
61
69
  async getTableColumns(table) {
62
- // 1. 先查 Redis 缓存
63
- const cacheKey = CacheKeys.tableColumns(table);
64
- const columns = await this.redis.getObject(cacheKey);
65
- if (columns && columns.length > 0) {
66
- return columns;
67
- }
68
- // 2. 缓存未命中,查询数据库
69
- const query = this.dialect.getTableColumnsQuery(table);
70
- const execRes = await this.executeSelect(query.sql, query.params);
70
+ // 查询数据库
71
+ const quotedTable = quoteIdentMySql(table);
72
+ const execRes = await this.executeSelect(`SHOW COLUMNS FROM ${quotedTable}`, []);
71
73
  const result = execRes.data;
72
74
  if (!result || result.length === 0) {
73
75
  throw new Error(`表 ${table} 不存在或没有字段`);
74
76
  }
75
- const columnNames = this.dialect.getTableColumnsFromResult(result);
76
- // 3. 写入 Redis 缓存
77
- const cacheRes = await this.redis.setObject(cacheKey, columnNames, TABLE_COLUMNS_CACHE_TTL_SECONDS);
78
- if (cacheRes === null) {
79
- Logger.warn({ table: table, cacheKey: cacheKey, msg: "表字段缓存写入 Redis 失败" });
77
+ const columnNames = [];
78
+ for (const row of result) {
79
+ const name = row["Field"];
80
+ if (typeof name === "string" && name.length > 0) {
81
+ columnNames.push(name);
82
+ }
80
83
  }
81
84
  return columnNames;
82
85
  }
@@ -215,8 +218,7 @@ export class DbHelper {
215
218
  async tableExists(tableName) {
216
219
  // 将表名转换为下划线格式
217
220
  const snakeTableName = snakeCase(tableName);
218
- const query = this.dialect.tableExistsQuery(snakeTableName);
219
- const execRes = await this.executeSelect(query.sql, query.params);
221
+ const execRes = await this.executeSelect("SELECT COUNT(*) as count FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", [snakeTableName]);
220
222
  const exists = (execRes.data?.[0]?.count || 0) > 0;
221
223
  return {
222
224
  data: exists,
@@ -608,7 +610,7 @@ export class DbHelper {
608
610
  table: snakeTable,
609
611
  idField: "id",
610
612
  ids: ids,
611
- quoteIdent: this.dialect.quoteIdent.bind(this.dialect)
613
+ quoteIdent: quoteIdentMySql
612
614
  });
613
615
  const execRes = await this.executeRun(query.sql, query.params);
614
616
  const changes = toNumberFromSql(execRes.data?.changes);
@@ -649,7 +651,7 @@ export class DbHelper {
649
651
  idField: "id",
650
652
  rows: processedList,
651
653
  fields: fields,
652
- quoteIdent: this.dialect.quoteIdent.bind(this.dialect),
654
+ quoteIdent: quoteIdentMySql,
653
655
  updatedAtField: "updated_at",
654
656
  updatedAtValue: now,
655
657
  stateField: "state",
@@ -766,7 +768,7 @@ export class DbHelper {
766
768
  // 使用 Bun SQL 的 begin 方法开启事务
767
769
  // begin 方法会自动处理 commit/rollback
768
770
  return await sql.begin(async (tx) => {
769
- const trans = new DbHelper({ redis: this.redis, sql: tx, dialect: this.dialect });
771
+ const trans = new DbHelper({ redis: this.redis, dbName: this.dbName, sql: tx });
770
772
  return await callback(trans);
771
773
  });
772
774
  }
@@ -781,29 +783,29 @@ export class DbHelper {
781
783
  * @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
782
784
  */
783
785
  async exists(options) {
784
- const prepareOptions = {
785
- table: options.table,
786
- page: 1,
787
- limit: 1
788
- };
789
- if (options.where !== undefined)
790
- prepareOptions.where = options.where;
791
- if (options.joins !== undefined)
792
- prepareOptions.joins = options.joins;
793
- const { table, where, tableQualifier } = await this.prepareQueryOptions(prepareOptions);
794
- // 使用 COUNT(1) 性能更好
795
- const builder = this.createSqlBuilder()
796
- .selectRaw("COUNT(1) as cnt")
797
- .from(table)
798
- .where(DbUtils.addDefaultStateFilter(where, tableQualifier, false))
799
- .limit(1);
786
+ if (Array.isArray(options.joins) && options.joins.length > 0) {
787
+ throw new Error("exists 不支持 joins(请使用显式 query 或拆分查询)");
788
+ }
789
+ const rawTable = typeof options.table === "string" ? options.table.trim() : "";
790
+ if (!rawTable) {
791
+ throw new Error("exists.table 不能为空");
792
+ }
793
+ if (rawTable.includes(" ")) {
794
+ throw new Error(`exists 不支持别名表写法(table: ${rawTable})`);
795
+ }
796
+ if (rawTable.includes(".")) {
797
+ throw new Error(`exists 不支持 schema.table 写法(table: ${rawTable})`);
798
+ }
799
+ const snakeTable = snakeCase(rawTable);
800
+ const cleanWhere = fieldClear(options.where || {}, { excludeValues: [null, undefined] });
801
+ const snakeWhere = DbUtils.whereKeysToSnake(cleanWhere);
802
+ const whereFiltered = DbUtils.addDefaultStateFilter(snakeWhere, snakeTable, false);
803
+ // 使用 COUNT(1) 实现:语义清晰、适配现有返回结构
804
+ const builder = this.createSqlBuilder().selectRaw("COUNT(1) as cnt").from(snakeTable).where(whereFiltered).limit(1);
800
805
  const { sql, params } = builder.toSelectSql();
801
806
  const execRes = await this.executeSelect(sql, params);
802
807
  const exists = (execRes.data?.[0]?.cnt || 0) > 0;
803
- return {
804
- data: exists,
805
- sql: execRes.sql
806
- };
808
+ return { data: exists, sql: execRes.sql };
807
809
  }
808
810
  /**
809
811
  * 查询单个字段值(带字段名验证)
@@ -811,6 +813,20 @@ export class DbHelper {
811
813
  */
812
814
  async getFieldValue(options) {
813
815
  const field = options.field;
816
+ if (Array.isArray(options.joins) && options.joins.length > 0) {
817
+ throw new Error("getFieldValue 不支持 joins(请使用 getOne/getList 并自行取字段)");
818
+ }
819
+ const rawTable = typeof options.table === "string" ? options.table.trim() : "";
820
+ if (!rawTable) {
821
+ throw new Error("getFieldValue.table 不能为空");
822
+ }
823
+ if (rawTable.includes(" ")) {
824
+ throw new Error(`getFieldValue 不支持别名表写法(table: ${rawTable})`);
825
+ }
826
+ if (rawTable.includes(".")) {
827
+ throw new Error(`getFieldValue 不支持 schema.table 写法(table: ${rawTable})`);
828
+ }
829
+ // (其余逻辑保持不变)
814
830
  // 验证字段名格式(只允许字母、数字、下划线)
815
831
  if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field)) {
816
832
  throw new Error(`无效的字段名: ${field},只允许字母、数字和下划线`);
@@ -895,8 +911,8 @@ export class DbHelper {
895
911
  const builder = this.createSqlBuilder().where(whereFiltered);
896
912
  const { sql: whereClause, params: whereParams } = builder.getWhereConditions();
897
913
  // 构建安全的 UPDATE SQL(表名和字段名使用反引号转义,已经是下划线格式)
898
- const quotedTable = this.dialect.quoteIdent(snakeTable);
899
- const quotedField = this.dialect.quoteIdent(snakeField);
914
+ const quotedTable = quoteIdentMySql(snakeTable);
915
+ const quotedField = quoteIdentMySql(snakeField);
900
916
  const sql = whereClause ? `UPDATE ${quotedTable} SET ${quotedField} = ${quotedField} + ? WHERE ${whereClause}` : `UPDATE ${quotedTable} SET ${quotedField} = ${quotedField} + ?`;
901
917
  const execRes = await this.executeRun(sql, [value, ...whereParams]);
902
918
  const changes = toNumberFromSql(execRes.data?.changes);
@@ -1,5 +1,6 @@
1
1
  import { fieldClear } from "../utils/fieldClear";
2
2
  import { keysToSnake, snakeCase } from "../utils/util";
3
+ import { SqlCheck } from "./sqlCheck";
3
4
  export class DbUtils {
4
5
  static parseTableRef(tableRef) {
5
6
  if (typeof tableRef !== "string") {
@@ -92,11 +93,7 @@ export class DbUtils {
92
93
  // 情况2:指定包含字段
93
94
  if (classified.type === "include") {
94
95
  return classified.fields.map((field) => {
95
- // 保留函数和特殊字段
96
- if (field.includes("(") || field.includes(" ")) {
97
- return field;
98
- }
99
- return snakeCase(field);
96
+ return DbUtils.processJoinField(field);
100
97
  });
101
98
  }
102
99
  // 情况3:排除字段
@@ -124,6 +121,11 @@ export class DbUtils {
124
121
  if (fields.some((f) => !f || typeof f !== "string" || f.trim() === "")) {
125
122
  throw new Error("fields 不能包含空字符串或无效值");
126
123
  }
124
+ // 统一禁止函数/表达式:复杂表达式请使用 selectRaw/whereRaw
125
+ for (const rawField of fields) {
126
+ const checkField = rawField.startsWith("!") ? rawField.substring(1) : rawField;
127
+ SqlCheck.assertNoExprField(checkField);
128
+ }
127
129
  // 统计包含字段和排除字段
128
130
  const includeFields = fields.filter((f) => !f.startsWith("!"));
129
131
  const excludeFields = fields.filter((f) => f.startsWith("!"));
@@ -161,8 +163,10 @@ export class DbUtils {
161
163
  });
162
164
  }
163
165
  static processJoinField(field) {
164
- // 跳过函数、星号、已处理的字段
165
- if (field.includes("(") || field === "*" || field.startsWith("`")) {
166
+ // 统一禁止函数/表达式:复杂表达式请使用 SqlBuilder.selectRaw
167
+ SqlCheck.assertNoExprField(field);
168
+ // 跳过星号、已引用字段
169
+ if (field === "*" || field.startsWith("`")) {
166
170
  return field;
167
171
  }
168
172
  // 处理别名 AS
@@ -200,6 +204,8 @@ export class DbUtils {
200
204
  if (key === "$or" || key === "$and") {
201
205
  return key;
202
206
  }
207
+ // 统一禁止函数/表达式:复杂表达式请使用 whereRaw
208
+ SqlCheck.assertNoExprField(key);
203
209
  // 处理带操作符的字段名(如 user.userId$gt)
204
210
  if (key.includes("$")) {
205
211
  const lastDollarIndex = key.lastIndexOf("$");
@@ -328,6 +334,8 @@ export class DbUtils {
328
334
  result[key] = Array.isArray(value) ? value.map((item) => DbUtils.whereKeysToSnake(item)) : [];
329
335
  continue;
330
336
  }
337
+ // 统一禁止函数/表达式:复杂表达式请使用 whereRaw
338
+ SqlCheck.assertNoExprField(key);
331
339
  // 处理带操作符的字段名(如 userId$gt)
332
340
  if (key.includes("$")) {
333
341
  const lastDollarIndex = key.lastIndexOf("$");
@@ -3,7 +3,6 @@
3
3
  * 初始化数据库连接和 SQL 管理器
4
4
  */
5
5
  import { Connect } from "../lib/connect";
6
- import { getDialectByName } from "../lib/dbDialect";
7
6
  import { DbHelper } from "../lib/dbHelper";
8
7
  import { Logger } from "../lib/logger";
9
8
  /**
@@ -15,16 +14,17 @@ const dbPlugin = {
15
14
  deps: ["logger", "redis"],
16
15
  async handler(befly) {
17
16
  const env = befly.config?.nodeEnv;
17
+ const dbName = String(befly.config?.db?.database || "");
18
+ if (!dbName) {
19
+ throw new Error("数据库初始化失败:befly.config.db.database 缺失");
20
+ }
18
21
  if (!befly.redis) {
19
22
  throw new Error("Redis 未初始化");
20
23
  }
21
24
  try {
22
25
  const sql = Connect.getSql();
23
- const rawDbDialect = befly.config?.db?.dialect;
24
- const dialectName = rawDbDialect === "postgresql" || rawDbDialect === "sqlite" ? rawDbDialect : "mysql";
25
- const dialect = getDialectByName(dialectName);
26
26
  // 创建数据库管理器实例
27
- const dbManager = new DbHelper({ redis: befly.redis, sql: sql, dialect: dialect });
27
+ const dbManager = new DbHelper({ redis: befly.redis, dbName: dbName, sql: sql });
28
28
  return dbManager;
29
29
  }
30
30
  catch (error) {
@@ -145,6 +145,8 @@ export async function syncDev(ctx, config = {}) {
145
145
  code: roleConfig.code,
146
146
  name: roleConfig.name,
147
147
  description: roleConfig.description,
148
+ menus: [],
149
+ apis: [],
148
150
  sort: roleConfig.sort
149
151
  }
150
152
  });