FrontTools/DBManager/DBManager.ts
Rain 3ab1f2db24 [入库]初次入库各种工具
- DBManager DB创建管理器
- FormatTimeTool
- ImageViewer工具
- InputArear工具
- OverlayPage工具
- 下拉刷新容器工具
- 视频查看器工具
2026-04-02 16:09:00 +08:00

349 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

export interface DBSchema {
dbName: string;
dbPath: string;
tableName: string;
columns: string; // 建表 SQL
indexes?: string[]; // 索引
jsonFields?: string[]; // 需要自动 JSON 序列化的字段名
primaryKey: string; // 主键
immutableFields?: string[]; // 只插入不更新的值
}
// 导入输入下面这个👇
// import { DBManager , DBSchema} from '@/src/service/DBManager/DBManager'
export class DBManager {
private tableColumns!: string[];
constructor(private config: DBSchema) {}
// 外部调用的开启数据库的接口
async open(): Promise<void> {
this.compileColumns();// 建立Scheme规范
await this.connectDB(); // 先链接
const sqlList = this.generateInitSqls(); // 后建表
if (sqlList.length > 0) { await this.executeSql(sqlList);} // 再建索引
}
/* ================================= 基础 CRUD 封装 ================================= */
async Upsert(data: any): Promise<void> {
const row = this.serialize(data);
const keys = Object.keys(row);
if (keys.length === 0) return;
const values = keys.map(k => this.formatValue(row[k]));
const insertSql =
`INSERT INTO ${this.config.tableName} (${keys.join(',')})
VALUES (${values.join(',')})`;
const updateCols = keys.filter(
k =>
k !== this.config.primaryKey &&
!this.config.immutableFields?.includes(k)
);
if (updateCols.length === 0) {
// 只有主键或不可变字段,直接插
return this.executeSql(insertSql);
}
const updateSet = updateCols
.map(k => `${k} = excluded.${k}`)
.join(',');
const sql = `
${insertSql}
ON CONFLICT(${this.config.primaryKey})
DO UPDATE SET ${updateSet}
`;
return this.executeSql(sql);
}
async UpsertOnlyChanged(data: any): Promise<void> {
const row = this.serialize(data);
const keys = Object.keys(row);
if (keys.length === 0) return;
const pk = this.config.primaryKey;
if (!pk || !keys.includes(pk)) {
throw new Error('[DB] UpsertOnlyChanged 需要 primaryKey');
}
const values = keys.map(k => this.formatValue(row[k]));
const insertSql =
`INSERT INTO ${this.config.tableName} (${keys.join(',')})
VALUES (${values.join(',')})`;
const updateCols = keys.filter(
k =>
k !== pk &&
!this.config.immutableFields?.includes(k)
);
if (updateCols.length === 0) {
return this.executeSql(insertSql);
}
// SET 子句
const setClause = updateCols
.map(k => `${k} = excluded.${k}`)
.join(', ');
// 变化检测(核心)
const diffWhere = updateCols
.map(k => `${k} IS NOT excluded.${k}`)
.join(' OR ');
const sql = `
${insertSql}
ON CONFLICT(${pk})
DO UPDATE SET ${setClause}
WHERE ${diffWhere}
`;
return this.executeSql(sql);
}
/** 批量保存 */
async saveBatch(list: any[]): Promise<void> {
if (!list || list.length === 0) return;
await this.executeSql("BEGIN TRANSACTION;");
try {
for (const item of list) { await this.Upsert(item);}
await this.executeSql("COMMIT;");
} catch (e) {
await this.executeSql("ROLLBACK;");
throw e;
}
}
/**
* 批量查询多个 target 的最新 N 条记录
* @param targetField 目标字段名 (如 'target_guid')
* @param targetIds 目标ID数组
* @param limit 每个 target 取多少条
* @param orderByField 排序字段 (默认按此字段倒序)
* @returns 所有结果的数组
*/
async findBatchLatest<T>(
targetField: string,
targetIds: string[],
limit: number = 50,
orderByField: string = 'timestamp'
): Promise<T[]> {
if (!targetIds || targetIds.length === 0) return [];
// 过滤掉无效的 IDundefined、null、空字符串
const validIds = targetIds.filter(id => id != null && id !== '');
if (validIds.length === 0) return [];
// 使用子查询 + UNION ALL每个子查询需要用括号包起来
const subQueries = validIds.map(id => {
const safeId = id.replace(/'/g, "''");
return `SELECT * FROM (
SELECT * FROM ${this.config.tableName}
WHERE ${targetField} = '${safeId}'
ORDER BY ${orderByField} DESC
LIMIT ${limit}
)`;
});
const sql = subQueries.join(' UNION ALL ');
const res = await this.selectRaw(sql);
return res.map(row => this.deserialize(row)) as T[];
}
/** 通用条件查询 */
async find<T>(options: {
where?: Record<string, any>,
columns?: string[], // 支持只查特定列,如 ['key', 'local_path']
orderBy?: string,
limit?: number,
offset?: number
}): Promise<T[]> {
const colStr = options.columns && options.columns.length > 0
? options.columns.join(',') : '*';
let sql = `SELECT ${colStr} FROM ${this.config.tableName}`;
const whereClause = this.buildWhere(options.where);
if (whereClause) sql += ` WHERE ${whereClause}`;
if (options.orderBy) sql += ` ORDER BY ${options.orderBy}`;
if (options.limit) sql += ` LIMIT ${options.limit}`;
if (options.offset) sql += ` OFFSET ${options.offset}`;
const res = await this.selectRaw(sql);
return res.map(row => this.deserialize(row)) as T[];
}
/** 通用删除 */
async delete(whereClause: string): Promise<void> {
const sql = `DELETE FROM ${this.config.tableName} WHERE ${whereClause}`;
return this.executeSql(sql);
}
/** 执行原生SQL查询 */
async queryRaw<T>(sql: string): Promise<T[]> {
const res = await this.selectRaw(sql);
return res.map(row => this.deserialize(row)) as T[];
}
/** 统计数量 */
async count(where?: Record<string, any>): Promise<number> {
let sql = `SELECT COUNT(*) as total FROM ${this.config.tableName}`;
const whereClause = this.buildWhere(where);
if (whereClause) sql += ` WHERE ${whereClause}`;
const res = await this.selectRaw(sql);
return res.length > 0 ? res[0].total : 0;
}
/**
* 替代手动写 "DELETE FROM table WHERE key='...'"
*/
async deleteBy(where: Record<string, any>): Promise<void> {
const whereClause = this.buildWhere(where);
if (!whereClause) {
console.warn('[DB] deleteBy 被调用但没有条件,操作被阻止以防止清空全表');
return;
}
const sql = `DELETE FROM ${this.config.tableName} WHERE ${whereClause}`;
return this.executeSql(sql);
}
/* ================================= 内部工具 ================================= */
private buildWhere(where?: Record<string, any>): string {
if (!where) return '';
const clauses: string[] = [];
for (const key in where) {
const val = where[key];
if (val === undefined) continue;
// 处理特殊符号,如 "age >": 18
if (key.includes(' ') || key.includes('<') || key.includes('>') || key.includes('LIKE'))
clauses.push(`${key} ${this.formatValue(val)}`);
else
clauses.push(`${key} = ${this.formatValue(val)}`);
}
return clauses.join(' AND ');
}
// 执行SQL语句
public executeSql(sql: string | string[]): Promise<void> {
return new Promise((resolve, reject) => {
plus.sqlite.executeSql({
name: this.config.dbName,
sql: sql,
success: () => resolve(),
fail: (e) => {
console.error(`[DB] SQL Error:`, e);
reject(e);
}
});
});
}
private selectRaw(sql: string): Promise<any[]> {
return new Promise((resolve, reject) => {
plus.sqlite.selectSql({
name: this.config.dbName,
sql,
success: (res) => resolve(res as any[]),
fail: (e) => {
console.error("SQL查询失败:", sql, e);
reject(e);
}
});
});
}
// 序列化数值
private formatValue(val: any): string {
if (typeof val === 'string') return `'${val.replace(/'/g, "''")}'`;
if (typeof val === 'boolean') return val ? '1' : '0';
if (val === null || val === undefined) return 'NULL';
return String(val);
}
/** 整体序列化*/
private serialize(data: any): Record<string, any> {
const row: any = {};
for (const col of this.tableColumns) {
let val = data[col];
if (val === undefined) continue;
// JSON 字段处理
if (this.config.jsonFields?.includes(col)) {
if (val !== null && typeof val === 'object') { val = JSON.stringify(val);}
}
// boolean 统一
if (typeof val === 'boolean')
val = val ? 1 : 0;
row[col] = val;
}
return row;
}
/** 整体反序列化 */
private deserialize(row: any) {
if (!row) return row;
this.config.jsonFields?.forEach(f => {
if (row[f] && typeof row[f] === 'string') {
try {
// 判断是否像 json 只有 {, [ 开头的才解析
if (row[f].startsWith('{') || row[f].startsWith('[')) {
row[f] = JSON.parse(row[f]);
}
} catch (e) {
console.warn(`[DB] JSON 解析错误 ${f}:`, e);
}
}
});
return row;
}
// 初始化数据库SQL样式
private generateInitSqls(): string[] {
const sqls: string[] = [];
const { tableName, columns, indexes } = this.config;
sqls.push(`CREATE TABLE IF NOT EXISTS ${tableName} (${columns})`);
if (indexes && indexes.length > 0) {
indexes.forEach((fieldStr) => {
const safeSuffix = fieldStr.replace(/[, ]+/g, '_');
const indexName = `idx_${tableName}_${safeSuffix}`;
sqls.push(`CREATE INDEX IF NOT EXISTS ${indexName} ON ${tableName} (${fieldStr})`);
});
}
return sqls;
}
private get realDbPath(): string {
const p = this.config.dbPath;
// 如果本来就以 .db/.sqlite/.sqlite3 结尾,就不再拼
return /(\.db|\.sqlite3?)$/i.test(p) ? p : `${p}.db`;
}
private compileColumns() {
this.tableColumns = this.config.columns
.split(',')
.map(c => c.trim().split(/\s+/)[0]);
}
private connectDB(): Promise<void> {
return new Promise((resolve, reject) => {
if (plus.sqlite.isOpenDatabase({ name: this.config.dbName, path: this.realDbPath })) {
return resolve();
}
plus.sqlite.openDatabase({
name: this.config.dbName,
path: this.realDbPath,
success: () => resolve(),
fail: (e) => reject(e)
});
});
}
}