unit uDataBase; interface uses System.SysUtils, System.Classes, FireDAC.Comp.Client, FireDAC.Stan.Param, FMX.Grid, FireDAC.Stan.Def, FireDAC.Stan.Intf, FireDAC.Stan.Option, FireDAC.Stan.Error, FireDAC.UI.Intf, FireDAC.Phys.Intf, FireDAC.Stan.Pool, FireDAC.Stan.Async, FireDAC.Phys, FireDAC.Phys.SQLite, FireDAC.Phys.SQLiteDef, FireDAC.Stan.ExprFuncs, FireDAC.Phys.SQLiteWrapper.Stat, FireDAC.FMXUI.Wait, FireDAC.DatS, FireDAC.DApt.Intf, FireDAC.DApt, Data.DB, FireDAC.Comp.DataSet, FireDAC.Comp.UI, uRecords, System.Generics.Collections, System.Rtti, System.TypInfo; type TSettingsDatabase = class private FConnection: TFDConnection; procedure InitializeDatabase; function GetSetting(const Name: string): string; procedure SetSetting(const Name, Value: string); function GetColumnsDefinition(Grid: TStringGrid): string; function GetColumnsList(Grid: TStringGrid): string; function GetValuesPlaceholders(Grid: TStringGrid): string; function CheckTableExists(const TableName: string): Boolean; procedure EnsureTableForRecord(const TableName: string; RecordTypeInfo: PTypeInfo); // function GetFieldDefinition(Field: TRttiField): string; function GetSQLType(Field: TRttiField): string; function TableHasColumn(const TableName, ColumnName: string): Boolean; public FChannel: string; constructor Create(const DatabasePath: string); destructor Destroy; override; function ReadSetting(const Name: string; Default: string = ''): string; procedure WriteSetting(const Name, Value: string); function getLoginData: TLogin; procedure SaveGridToTable(const TableName: string; Grid: TStringGrid); procedure LoadGridFromTable(const TableName: string; Grid: TStringGrid); procedure LoadUsers(var users: TList); procedure addGroupResponse(Name, Respons: string); procedure getGroupResponse(aName: string; const lbResponse: TStrings); procedure getGroupName(const lbName: TStrings); procedure delGroupName(aName: string); procedure delGroupResponse(aName, aResponse: string); procedure SaveRecordArray(const TableName: string; const Items: array of T); procedure LoadRecordArray(const TableName: string; var Items: TArray); end; implementation uses uGeneral; constructor TSettingsDatabase.Create(const DatabasePath: string); begin FConnection := TFDConnection.Create(nil); try FConnection.DriverName := 'SQLite'; FConnection.Params.Database := DatabasePath; FConnection.Connected := True; InitializeDatabase; except on E: Exception do begin FreeAndNil(FConnection); raise Exception.Create('Ошибка при подключении к базе данных: ' + E.Message); end; end; end; procedure TSettingsDatabase.delGroupName(aName: string); var Query: TFDQuery; begin Query := TFDQuery.Create(nil); try Query.Connection := FConnection; Query.SQL.Text := 'DELETE FROM GroupResponse WHERE Name = :name'; Query.ParamByName('name').AsString := aName; Query.ExecSQL; finally Query.Free; end; end; procedure TSettingsDatabase.delGroupResponse(aName, aResponse: string); var Query: TFDQuery; begin Query := TFDQuery.Create(nil); try Query.Connection := FConnection; Query.SQL.Text := 'DELETE FROM GroupResponse WHERE Name = :name AND Response = :response'; Query.ParamByName('name').AsString := aName; Query.ParamByName('response').AsString := aResponse; Query.ExecSQL; finally Query.Free; end; end; destructor TSettingsDatabase.Destroy; begin if Assigned(FConnection) then FConnection.Connected := False; FreeAndNil(FConnection); inherited; end; procedure TSettingsDatabase.InitializeDatabase; var FDQuery: TFDQuery; FieldExists: Boolean; begin FConnection.ExecSQL('CREATE TABLE IF NOT EXISTS params (' + ' name TEXT PRIMARY KEY,' + ' value TEXT' + ');'); FConnection.ExecSQL('CREATE TABLE IF NOT EXISTS users (' + ' id TEXT PRIMARY KEY,' + ' login TEXT,' + ' DisplayName TEXT,' + ' created_at DATETIME,' + ' follow_at DATETIME,' + ' isVip TEXT,' + ' isModer TEXT,' + ' isO TEXT,' + ' streamer TEXT' + ');'); FDQuery := TFDQuery.Create(nil); try FDQuery.Connection := FConnection; FieldExists := False; FDQuery.SQL.Text := 'PRAGMA table_info(users)'; FDQuery.Open; while not FDQuery.EOF do begin if FDQuery.FieldByName('name').AsString = 'streamer' then begin FieldExists := True; Break; end; FDQuery.Next; end; FDQuery.Close; if not FieldExists then FConnection.ExecSQL('ALTER TABLE users ADD COLUMN streamer TEXT'); finally FDQuery.Free; end; FConnection.ExecSQL('CREATE TABLE IF NOT EXISTS GroupResponse (ID INTEGER PRIMARY KEY, Name TEXT, Response TEXT);'); end; procedure TSettingsDatabase.LoadUsers(var users: TList); var Query: TFDQuery; UserItem: TUser; begin Query := TFDQuery.Create(nil); try Query.Connection := FConnection; Query.SQL.Text := 'SELECT * FROM users WHERE streamer = :streamer'; Query.ParamByName('streamer').AsString := FChannel; Query.Open; while not Query.EOF do begin UserItem.id := Query.FieldByName('id').AsString; UserItem.login := Query.FieldByName('login').AsString; UserItem.DisplayName := Query.FieldByName('DisplayName').AsString; UserItem.created_at := Query.FieldByName('created_at').AsDateTime; UserItem.follow_at := Query.FieldByName('follow_at').AsDateTime; UserItem.isVip := Query.FieldByName('isVip').AsString = 'True'; UserItem.isModer := Query.FieldByName('isModer').AsString = 'True'; UserItem.isO := Query.FieldByName('isO').AsString = 'True'; UserItem.isO_today := False; users.Add(UserItem); Query.Next; end; finally Query.Free; end; end; function TSettingsDatabase.GetColumnsDefinition(Grid: TStringGrid): string; var Col: Integer; begin Result := ''; for Col := 0 to Grid.ColumnCount - 1 do begin if Result <> '' then Result := Result + ', '; Result := Result + 'col' + IntToStr(Col) + ' TEXT'; end; end; function TSettingsDatabase.GetColumnsList(Grid: TStringGrid): string; var Col: Integer; begin Result := ''; for Col := 0 to Grid.ColumnCount - 1 do begin if Result <> '' then Result := Result + ', '; Result := Result + 'col' + IntToStr(Col); end; end; procedure TSettingsDatabase.getGroupName(const lbName: TStrings); var Query: TFDQuery; begin Query := TFDQuery.Create(nil); try Query.Connection := FConnection; lbName.Clear; Query.SQL.Text := 'SELECT DISTINCT Name FROM GroupResponse'; Query.Open; while not Query.EOF do begin lbName.Add(Query.FieldByName('Name').AsString); Query.Next; end; finally Query.Free; end; end; procedure TSettingsDatabase.getGroupResponse(aName: string; const lbResponse: TStrings); var Query: TFDQuery; begin Query := TFDQuery.Create(nil); try Query.Connection := FConnection; lbResponse.Clear; Query.SQL.Text := 'SELECT Response FROM GroupResponse WHERE Name = :name'; Query.ParamByName('name').AsString := aName; Query.Open; while not Query.EOF do begin lbResponse.Add(Query.FieldByName('Response').AsString); Query.Next; end; finally Query.Free; end; end; function TSettingsDatabase.getLoginData: TLogin; begin Result.TTV_Token_Bot := ReadSetting('edtBotToken'); Result.TTV_Token_Strimer := ReadSetting('edtBotTokenStreamer'); Result.TTV_Name_Bot := ReadSetting('edtBotName'); Result.TTV_Name_Strimer := ReadSetting('edtChannel'); Result.TTV_ClientID := ReadSetting('edtBotClientID'); Result.DA_ClientID := ReadSetting('edtDAClientID'); Result.DA_Client_Sictert := ReadSetting('edtDAClientSecret'); Result.DA_RedirectURL := ReadSetting('edtDARedirectURL'); Result.DA_Code := ReadSetting('edtDACode'); Result.AI_Gigachat_ClientID := ReadSetting('edtGPTClientID'); Result.AI_Gigachat_AutorizationCode := ReadSetting('edtGPTAC'); Result.AI_ChatGPT_Token := ReadSetting('edtGPTATChatGPT'); Result.AI_DeepSeek_Token := ReadSetting('edtGPTATDeepSeek'); end; function TSettingsDatabase.GetValuesPlaceholders(Grid: TStringGrid): string; var Col: Integer; begin Result := ''; for Col := 0 to Grid.ColumnCount - 1 do begin if Result <> '' then Result := Result + ', '; Result := Result + ':col' + IntToStr(Col); end; end; procedure TSettingsDatabase.addGroupResponse(Name, Respons: string); var Query: TFDQuery; begin Query := TFDQuery.Create(nil); try Query.Connection := FConnection; Query.SQL.Text := 'INSERT INTO GroupResponse (Name, Response) VALUES (:name, :response)'; Query.ParamByName('name').AsString := Name; Query.ParamByName('response').AsString := Respons; Query.ExecSQL; finally Query.Free; end; end; function TSettingsDatabase.CheckTableExists(const TableName: string): Boolean; var Query: TFDQuery; begin Result := False; Query := TFDQuery.Create(nil); try Query.Connection := FConnection; Query.SQL.Text := 'SELECT COUNT(*) FROM sqlite_master WHERE type=''table'' AND name=:TableName'; Query.ParamByName('TableName').AsString := TableName; Query.Open; Result := (Query.Fields[0].AsInteger > 0); finally Query.Free; end; end; procedure TSettingsDatabase.LoadGridFromTable(const TableName: string; Grid: TStringGrid); var Query: TFDQuery; Col, Row: Integer; begin if not CheckTableExists(TableName) then Exit; Query := TFDQuery.Create(nil); try Query.Connection := FConnection; Query.SQL.Text := 'SELECT * FROM ' + TableName; Query.Open; Grid.RowCount := 0; while not Query.EOF do begin Row := Grid.RowCount; Grid.RowCount := Grid.RowCount + 1; for Col := 0 to Grid.ColumnCount - 1 do Grid.Cells[Col, Row] := Query.FieldByName('col' + IntToStr(Col)).AsString; Query.Next; end; finally Query.Free; end; end; procedure TSettingsDatabase.SaveGridToTable(const TableName: string; Grid: TStringGrid); var Query: TFDQuery; Col, Row: Integer; begin FConnection.ExecSQL('DROP TABLE IF EXISTS ' + TableName); FConnection.ExecSQL('CREATE TABLE ' + TableName + ' (id INTEGER PRIMARY KEY AUTOINCREMENT, ' + GetColumnsDefinition(Grid) + ');'); Query := TFDQuery.Create(nil); try Query.Connection := FConnection; for Row := 0 to Grid.RowCount - 1 do begin Query.SQL.Text := 'INSERT INTO ' + TableName + ' (' + GetColumnsList(Grid) + ') VALUES (' + GetValuesPlaceholders(Grid) + ')'; for Col := 0 to Grid.ColumnCount - 1 do Query.ParamByName('col' + IntToStr(Col)).AsString := Grid.Cells[Col, Row]; Query.ExecSQL; end; finally Query.Free; end; end; function TSettingsDatabase.GetSetting(const Name: string): string; var Query: TFDQuery; begin Query := TFDQuery.Create(nil); try Query.Connection := FConnection; Query.SQL.Text := 'SELECT value FROM params WHERE name = :name'; Query.ParamByName('name').AsString := Name; Query.Open; if not Query.IsEmpty then Result := Query.FieldByName('value').AsString else Result := ''; finally Query.Free; end; end; procedure TSettingsDatabase.SetSetting(const Name, Value: string); begin FConnection.ExecSQL('INSERT OR REPLACE INTO params (name, value) VALUES (:name, :value)', [Name, Value]); end; function TSettingsDatabase.ReadSetting(const Name: string; Default: string = ''): string; begin Result := GetSetting(Name); if Result = '' then Result := Default; end; procedure TSettingsDatabase.WriteSetting(const Name, Value: string); begin SetSetting(Name, Value); end; function TSettingsDatabase.TableHasColumn(const TableName, ColumnName: string): Boolean; var Query: TFDQuery; begin Query := TFDQuery.Create(nil); try Query.Connection := FConnection; Query.SQL.Text := Format('PRAGMA table_info(%s)', [TableName]); Query.Open; Result := False; while not Query.EOF do begin if SameText(Query.FieldByName('name').AsString, ColumnName) then Exit(True); Query.Next; end; finally Query.Free; end; end; function TSettingsDatabase.GetSQLType(Field: TRttiField): string; begin case Field.FieldType.TypeKind of tkInteger, tkInt64: Result := 'INTEGER'; tkFloat: Result := 'REAL'; tkUString, tkString, tkWString, tkLString: Result := 'TEXT'; tkEnumeration: if Field.FieldType = TypeInfo(Boolean) then Result := 'BOOLEAN' else Result := 'INTEGER'; else raise Exception.CreateFmt('Unsupported type for field %s', [Field.Name]); end; end; procedure TSettingsDatabase.EnsureTableForRecord(const TableName: string; RecordTypeInfo: PTypeInfo); var Context: TRttiContext; RttiType: TRttiType; Field: TRttiField; FieldDefs: string; Query: TFDQuery; begin if not CheckTableExists(TableName) then begin Context := TRttiContext.Create; RttiType := Context.GetType(RecordTypeInfo); FieldDefs := ''; for Field in RttiType.GetFields do begin if FieldDefs <> '' then FieldDefs := FieldDefs + ', '; FieldDefs := FieldDefs + Field.Name + ' ' + GetSQLType(Field); end; FConnection.ExecSQL(Format('CREATE TABLE %s (%s)', [TableName, FieldDefs])); end else begin Context := TRttiContext.Create; RttiType := Context.GetType(RecordTypeInfo); Query := TFDQuery.Create(nil); try Query.Connection := FConnection; for Field in RttiType.GetFields do begin if not TableHasColumn(TableName, Field.Name) then begin Query.SQL.Text := Format('ALTER TABLE %s ADD COLUMN %s %s', [TableName, Field.Name, GetSQLType(Field)]); Query.ExecSQL; end; end; finally Query.Free; end; end; end; procedure TSettingsDatabase.SaveRecordArray(const TableName: string; const Items: array of T); var Context: TRttiContext; RttiType: TRttiType; Fields: TArray; Query: TFDQuery; Rec: T; Field: TRttiField; FieldNames, Placeholders: string; i, j: Integer; Value: TValue; Param: TFDParam; begin if Length(Items) = 0 then begin FConnection.ExecSQL('DELETE FROM ' + TableName); exit; end; EnsureTableForRecord(TableName, TypeInfo(T)); Context := TRttiContext.Create; RttiType := Context.GetType(TypeInfo(T)); Fields := RttiType.GetFields; Query := TFDQuery.Create(nil); try Query.Connection := FConnection; // Построение списка полей и плейсхолдеров FieldNames := ''; Placeholders := ''; for j := 0 to High(Fields) do begin if j > 0 then begin FieldNames := FieldNames + ', '; Placeholders := Placeholders + ', '; end; FieldNames := FieldNames + Fields[j].Name; Placeholders := Placeholders + ':' + Fields[j].Name; end; Query.SQL.Text := 'INSERT INTO ' + TableName + ' (' + FieldNames + ') VALUES (' + Placeholders + ')'; // Подготавливаем запрос (создает параметры) //Query.Prepare; // Устанавливаем размер массива параметров Query.Params.ArraySize := Length(Items); // Начало транзакции FConnection.StartTransaction; try // Очистка таблицы перед вставкой новых данных FConnection.ExecSQL('DELETE FROM ' + TableName); i := 0; for Rec in Items do begin for Field in Fields do begin Value := Field.GetValue(@Rec); // Находим параметр Param := Query.FindParam(Field.Name); if not Assigned(Param) then raise Exception.CreateFmt('Parameter "%s" not found in query', [Field.Name]); // Обработка NULL-значений if Value.IsEmpty then begin Param.Clear(i); end else begin // Обработка по типу case Field.FieldType.TypeKind of tkInteger, tkInt64: Param.AsIntegers[i] := Value.AsInteger; tkFloat: if (Field.FieldType = TypeInfo(TDateTime)) or (Field.FieldType = TypeInfo(TDate)) or (Field.FieldType = TypeInfo(TTime)) then Param.AsDateTimes[i] := Value.AsExtended else Param.AsFloats[i] := Value.AsExtended; tkEnumeration: if Field.FieldType = TypeInfo(Boolean) then Param.AsBooleans[i] := Value.AsBoolean else Param.AsIntegers[i] := Value.AsOrdinal; tkUString, tkString, tkWString, tkLString: Param.AsStrings[i] := Value.AsString; else Param.AsStrings[i] := Value.ToString; end; end; end; Inc(i); end; // Пакетное выполнение Query.Execute(Length(Items), 0); FConnection.Commit; except FConnection.Rollback; raise; end; finally Query.Free; end; end; procedure TSettingsDatabase.LoadRecordArray(const TableName: string; var Items: TArray); var Context: TRttiContext; RttiType: TRttiType; Fields: TArray; Query: TFDQuery; Rec: T; RecList: TList; Field: TRttiField; begin if not CheckTableExists(TableName) then begin SetLength(Items, 0); Exit; end; EnsureTableForRecord(TableName, TypeInfo(T)); Context := TRttiContext.Create; try RttiType := Context.GetType(TypeInfo(T)); Fields := RttiType.GetFields; Query := TFDQuery.Create(nil); RecList := TList.Create; try Query.Connection := FConnection; Query.SQL.Text := Format('SELECT * FROM %s', [TableName]); Query.Open; while not Query.EOF do begin Rec := Default(T); for Field in Fields do begin if Query.FindField(Field.Name) <> nil then Field.SetValue(@Rec, TValue.FromVariant(Query.FieldByName(Field.Name).Value)); end; RecList.Add(Rec); Query.Next; end; Items := RecList.ToArray; finally Query.Free; RecList.Free; end; finally Context.Free; end; end; end.