From 9da2fcfbcacf01bb9c04dde0bc78472901296f35 Mon Sep 17 00:00:00 2001 From: Alex Ant Date: Tue, 1 Jul 2025 20:31:21 +0500 Subject: [PATCH] migration --- ... => V1.0.5__migrate_orders_to_postgres.ts} | 8 +- .../src/migrator/migrationManager.ts | 22 ++- .../ListOfDocuments/ListOfDocumentsPage.vue | 23 ++- .../shared/ui/BaseDocument/BaseDocument.vue | 2 - .../src/shared/ui/CardStyles/index.scss | 20 ++- .../ui/MeetCompactCard/ui/MeetCompactCard.vue | 7 + components/factory/package.json | 3 +- .../src/Services/Databazor/SearchService.ts | 136 ++++++++++++------ scripts/release-info.md | 2 +- 9 files changed, 143 insertions(+), 80 deletions(-) rename components/controller/migrations/{V1.0.4__migrate_orders_to_postgres_test.ts => V1.0.5__migrate_orders_to_postgres.ts} (98%) diff --git a/components/controller/migrations/V1.0.4__migrate_orders_to_postgres_test.ts b/components/controller/migrations/V1.0.5__migrate_orders_to_postgres.ts similarity index 98% rename from components/controller/migrations/V1.0.4__migrate_orders_to_postgres_test.ts rename to components/controller/migrations/V1.0.5__migrate_orders_to_postgres.ts index 6b77393d198..86e61629cf1 100644 --- a/components/controller/migrations/V1.0.4__migrate_orders_to_postgres_test.ts +++ b/components/controller/migrations/V1.0.5__migrate_orders_to_postgres.ts @@ -7,16 +7,18 @@ import { PaymentStatusEnum } from '../src/domain/gateway/enums/payment-status.en import { PaymentTypeEnum, PaymentDirectionEnum, getPaymentDirection } from '../src/domain/gateway/enums/payment-type.enum'; import type { PaymentDomainInterface } from '../src/domain/gateway/interfaces/payment-domain.interface'; import { sha256 } from '../src/utils/sha256'; +import { generator } from '../src/services/document.service'; export default { name: 'Миграция платежей из MongoDB в PostgreSQL (унифицированная модель)', - isTest: true, // Включаем тестовый режим - validUntil: new Date('2024-12-31'), // Действует до конца года + validUntil: new Date('2025-07-03'), // Действует до async up(): Promise { console.log('Выполнение миграции: Перенос платежей из MongoDB в PostgreSQL (унифицированная модель)'); try { + await generator.connect(config.mongoose.url); + // Инициализируем подключение к PostgreSQL const dataSource = new DataSource({ type: 'postgres', @@ -36,7 +38,7 @@ export default { const paymentRepository = new TypeOrmPaymentRepository(dataSource.getRepository(PaymentEntity)); // Получаем все заказы из MongoDB - const orders = await Order.find().populate('user'); + const orders = await Order.find(); console.log(`Найдено ${orders.length} заказов в MongoDB`); let migratedCount = 0; diff --git a/components/controller/src/migrator/migrationManager.ts b/components/controller/src/migrator/migrationManager.ts index 365c152cc70..34cc58f4439 100644 --- a/components/controller/src/migrator/migrationManager.ts +++ b/components/controller/src/migrator/migrationManager.ts @@ -145,9 +145,7 @@ export class MigrationManager { }); } - async runMigration(migration: Migration, version: string, description: string): Promise { - const isTest = version.includes('__test'); - + async runMigration(migration: Migration, version: string, description: string, isTest: boolean): Promise { try { if (isTest) { logger.info(`[ТЕСТОВАЯ МИГРАЦИЯ] Запуск миграции ${version} (${description}): ${migration.name}`); @@ -157,7 +155,11 @@ export class MigrationManager { // Предоставляем блокчейн-сервис в миграцию const result = await migration.up({ blockchain: this.blockchainService }); - await this.markMigrationAsApplied(version, migration.name, result, isTest); + + // Отмечаем как примененную только если миграция успешна И не является тестовой + if (result && !isTest) { + await this.markMigrationAsApplied(version, migration.name, true, false); + } if (isTest) { logger.info( @@ -170,7 +172,12 @@ export class MigrationManager { return result; } catch (error) { logger.error(`Ошибка при выполнении миграции ${version} (${description}):`, error); - await this.markMigrationAsApplied(version, migration.name, false, isTest); + + // Отмечаем как неуспешную только если это НЕ тестовая миграция + if (!isTest) { + await this.markMigrationAsApplied(version, migration.name, false, false); + } + return false; } } @@ -230,9 +237,10 @@ export class MigrationManager { } // Запускаем миграцию - const success = await this.runMigration(migration, version, description); + const success = await this.runMigration(migration, version, description, isTest); - if (!success) { + // Останавливаем процесс только при ошибке в НЕ тестовых миграциях + if (!success && !isTest) { logger.error(`Миграция ${version} (${description}) завершена с ошибкой, останавливаем процесс`); break; } diff --git a/components/desktop/src/pages/Cooperative/ListOfDocuments/ListOfDocumentsPage.vue b/components/desktop/src/pages/Cooperative/ListOfDocuments/ListOfDocumentsPage.vue index 79b424ed0cd..22b59ace719 100644 --- a/components/desktop/src/pages/Cooperative/ListOfDocuments/ListOfDocumentsPage.vue +++ b/components/desktop/src/pages/Cooperative/ListOfDocuments/ListOfDocumentsPage.vue @@ -1,24 +1,23 @@ diff --git a/components/desktop/src/shared/ui/BaseDocument/BaseDocument.vue b/components/desktop/src/shared/ui/BaseDocument/BaseDocument.vue index 5d735856805..f05bfe238cc 100644 --- a/components/desktop/src/shared/ui/BaseDocument/BaseDocument.vue +++ b/components/desktop/src/shared/ui/BaseDocument/BaseDocument.vue @@ -172,7 +172,6 @@ const shadowStyles = computed( } th { - background-color: #f4f4f4; width: 30% !important; max-width: 30% !important; word-break: break-word !important; @@ -219,7 +218,6 @@ const shadowStyles = computed( } th { - background-color: #f4f4f4; width: 30% !important; max-width: 30% !important; word-break: break-word !important; diff --git a/components/desktop/src/shared/ui/CardStyles/index.scss b/components/desktop/src/shared/ui/CardStyles/index.scss index ebf1e1a6b43..a1b0e88e795 100644 --- a/components/desktop/src/shared/ui/CardStyles/index.scss +++ b/components/desktop/src/shared/ui/CardStyles/index.scss @@ -4,10 +4,12 @@ width: 100%; border: 1px solid rgba(0, 0, 0, 0.08); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); + background-color: var(--q-surface); .q-dark & { - border: 1px solid rgba(255, 255, 255, 0.12); + border: 1px solid rgba(255, 255, 255, 0.2); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + background-color: rgba(255, 255, 255, 0.05); } } @@ -20,7 +22,7 @@ .q-dark & { background-color: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.15); } &:last-child { @@ -38,7 +40,7 @@ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); .q-dark & { - background-color: rgba(255, 255, 255, 0.08); + background-color: rgba(255, 255, 255, 0.1); } } } @@ -48,10 +50,12 @@ border-radius: 16px; border: 1px solid rgba(0, 0, 0, 0.08); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); + background-color: var(--q-surface); .q-dark & { - border: 1px solid rgba(255, 255, 255, 0.12); + border: 1px solid rgba(255, 255, 255, 0.2); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + background-color: rgba(255, 255, 255, 0.05); } .page-header { @@ -82,10 +86,12 @@ border-radius: 16px; border: 1px solid rgba(0, 0, 0, 0.08); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); + background-color: var(--q-surface); .q-dark & { - border: 1px solid rgba(255, 255, 255, 0.12); + border: 1px solid rgba(255, 255, 255, 0.2); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + background-color: rgba(255, 255, 255, 0.05); } .section-header { @@ -114,7 +120,7 @@ .q-dark & { background-color: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.15); } &:last-child { @@ -127,7 +133,7 @@ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); .q-dark & { - background-color: rgba(255, 255, 255, 0.08); + background-color: rgba(255, 255, 255, 0.1); } } diff --git a/components/desktop/src/shared/ui/MeetCompactCard/ui/MeetCompactCard.vue b/components/desktop/src/shared/ui/MeetCompactCard/ui/MeetCompactCard.vue index e2e02b91495..edf8db48a8f 100644 --- a/components/desktop/src/shared/ui/MeetCompactCard/ui/MeetCompactCard.vue +++ b/components/desktop/src/shared/ui/MeetCompactCard/ui/MeetCompactCard.vue @@ -54,12 +54,19 @@ const meetStatus = useMeetStatus(props.meet); cursor: pointer; transition: all 0.3s ease; + // Дополнительная стилизация для темной темы + .q-dark & { + background-color: rgba(255, 255, 255, 0.07) !important; + border: 1px solid rgba(255, 255, 255, 0.25) !important; + } + &:hover { transform: translateY(-4px); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); .q-dark & { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); + background-color: rgba(255, 255, 255, 0.1) !important; } .balance-card { diff --git a/components/factory/package.json b/components/factory/package.json index ebbdce74fa1..defe37ec3d3 100644 --- a/components/factory/package.json +++ b/components/factory/package.json @@ -43,7 +43,8 @@ "prepublishOnly": "nr build", "release": "bumpp && npm publish", "test": "vitest --dir test --testTimeout=240000 --exclude documents --watch=false", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "setup-indexes": "tsx scripts/setup-indexes.ts" }, "dependencies": { "ajv": "^8.13.0", diff --git a/components/factory/src/Services/Databazor/SearchService.ts b/components/factory/src/Services/Databazor/SearchService.ts index 5e0c4b08989..520641f4e81 100644 --- a/components/factory/src/Services/Databazor/SearchService.ts +++ b/components/factory/src/Services/Databazor/SearchService.ts @@ -17,6 +17,11 @@ export class SearchService { this.storage = storage } + // Экранирование специальных символов регулярных выражений + private escapeRegex(text: string): string { + return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + } + async search(query: string): Promise { if (!query || query.trim().length === 0) { return [] @@ -25,35 +30,45 @@ export class SearchService { const results: ISearchResult[] = [] const trimmedQuery = query.trim() - // Создаем регулярное выражение для поиска без учета регистра - const regex = new RegExp(trimmedQuery, 'i') + console.log(`[SearchService] Поиск по запросу: "${trimmedQuery}"`) + + // Экранируем специальные символы для безопасного использования в regex + const escapedQuery = this.escapeRegex(trimmedQuery) // Разбиваем запрос на слова для поиска по полному ФИО const queryWords = trimmedQuery.split(/\s+/).filter(word => word.length > 0) try { // Поиск в коллекции individuals - const individualResults = await this.searchIndividuals(regex, queryWords) + console.log(`[SearchService] Поиск в individuals...`) + const individualResults = await this.searchIndividuals(escapedQuery, queryWords, trimmedQuery) + console.log(`[SearchService] Найдено individuals: ${individualResults.length}`) results.push(...individualResults) // Поиск в коллекции entrepreneurs - const entrepreneurResults = await this.searchEntrepreneurs(regex, queryWords) + console.log(`[SearchService] Поиск в entrepreneurs...`) + const entrepreneurResults = await this.searchEntrepreneurs(escapedQuery, queryWords, trimmedQuery) + console.log(`[SearchService] Найдено entrepreneurs: ${entrepreneurResults.length}`) results.push(...entrepreneurResults) // Поиск в коллекции organizations - const organizationResults = await this.searchOrganizations(regex) + console.log(`[SearchService] Поиск в organizations...`) + const organizationResults = await this.searchOrganizations(escapedQuery) + console.log(`[SearchService] Найдено organizations: ${organizationResults.length}`) results.push(...organizationResults) + console.log(`[SearchService] Общее количество результатов: ${results.length}`) + // Ограничиваем общее количество результатов до 10 return results.slice(0, 10) } catch (error) { - console.error('Error during search:', error) + console.error('[SearchService] Ошибка при поиске:', error) return [] } } - private async searchIndividuals(regex: RegExp, queryWords: string[]): Promise { + private async searchIndividuals(regexPattern: string, queryWords: string[], originalQuery: string): Promise { const individualModel = new Individual(this.storage) const results: ISearchResult[] = [] @@ -66,16 +81,16 @@ export class SearchService { deleted: false, $or: [ // Поиск по отдельным полям - { first_name: { $regex: regex } }, - { last_name: { $regex: regex } }, - { middle_name: { $regex: regex } }, + { first_name: { $regex: regexPattern, $options: 'i' } }, + { last_name: { $regex: regexPattern, $options: 'i' } }, + { middle_name: { $regex: regexPattern, $options: 'i' } }, // Поиск по полному ФИО (все слова должны быть найдены) { $and: queryWords.map(word => ({ $or: [ - { first_name: { $regex: new RegExp(word, 'i') } }, - { last_name: { $regex: new RegExp(word, 'i') } }, - { middle_name: { $regex: new RegExp(word, 'i') } }, + { first_name: { $regex: this.escapeRegex(word), $options: 'i' } }, + { last_name: { $regex: this.escapeRegex(word), $options: 'i' } }, + { middle_name: { $regex: this.escapeRegex(word), $options: 'i' } }, ], })), }, @@ -87,28 +102,40 @@ export class SearchService { filter = { deleted: false, $or: [ - { first_name: { $regex: regex } }, - { last_name: { $regex: regex } }, - { middle_name: { $regex: regex } }, + { first_name: { $regex: regexPattern, $options: 'i' } }, + { last_name: { $regex: regexPattern, $options: 'i' } }, + { middle_name: { $regex: regexPattern, $options: 'i' } }, ], } } + console.log(`[SearchService] Фильтр для individuals:`, JSON.stringify(filter, null, 2)) + const individuals = await individualModel.getMany(filter) + console.log(`[SearchService] Результаты поиска individuals:`, { + total: individuals.totalResults, + found: individuals.results.length, + query: originalQuery, + names: individuals.results.map(ind => `${ind.last_name} ${ind.first_name} ${ind.middle_name || ''} (deleted: ${ind.deleted || false})`), + }) + for (const individual of individuals.results) { const highlightedFields = [] + // Создаем RegExp для проверки подсветки + const testRegex = new RegExp(regexPattern, 'i') + // Проверяем отдельные поля - if (regex.test(individual.first_name)) + if (individual.first_name && testRegex.test(individual.first_name)) highlightedFields.push('first_name') - if (regex.test(individual.last_name)) + if (individual.last_name && testRegex.test(individual.last_name)) highlightedFields.push('last_name') - if (regex.test(individual.middle_name)) + if (individual.middle_name && testRegex.test(individual.middle_name)) highlightedFields.push('middle_name') // Проверяем полное ФИО - const fullName = `${individual.last_name} ${individual.first_name} ${individual.middle_name}`.trim() + const fullName = `${individual.last_name || ''} ${individual.first_name || ''} ${individual.middle_name || ''}`.trim() if (this.matchesFullName(fullName, queryWords)) { highlightedFields.push('full_name') } @@ -123,7 +150,7 @@ export class SearchService { return results } - private async searchEntrepreneurs(regex: RegExp, queryWords: string[]): Promise { + private async searchEntrepreneurs(regexPattern: string, queryWords: string[], originalQuery: string): Promise { const entrepreneurModel = new Entrepreneur(this.storage) const results: ISearchResult[] = [] @@ -136,18 +163,18 @@ export class SearchService { deleted: false, $or: [ // Поиск по отдельным полям - { first_name: { $regex: regex } }, - { last_name: { $regex: regex } }, - { middle_name: { $regex: regex } }, - { 'details.inn': { $regex: regex } }, - { 'details.ogrn': { $regex: regex } }, + { first_name: { $regex: regexPattern, $options: 'i' } }, + { last_name: { $regex: regexPattern, $options: 'i' } }, + { middle_name: { $regex: regexPattern, $options: 'i' } }, + { 'details.inn': { $regex: regexPattern, $options: 'i' } }, + { 'details.ogrn': { $regex: regexPattern, $options: 'i' } }, // Поиск по полному ФИО { $and: queryWords.map(word => ({ $or: [ - { first_name: { $regex: new RegExp(word, 'i') } }, - { last_name: { $regex: new RegExp(word, 'i') } }, - { middle_name: { $regex: new RegExp(word, 'i') } }, + { first_name: { $regex: this.escapeRegex(word), $options: 'i' } }, + { last_name: { $regex: this.escapeRegex(word), $options: 'i' } }, + { middle_name: { $regex: this.escapeRegex(word), $options: 'i' } }, ], })), }, @@ -159,34 +186,46 @@ export class SearchService { filter = { deleted: false, $or: [ - { first_name: { $regex: regex } }, - { last_name: { $regex: regex } }, - { middle_name: { $regex: regex } }, - { 'details.inn': { $regex: regex } }, - { 'details.ogrn': { $regex: regex } }, + { first_name: { $regex: regexPattern, $options: 'i' } }, + { last_name: { $regex: regexPattern, $options: 'i' } }, + { middle_name: { $regex: regexPattern, $options: 'i' } }, + { 'details.inn': { $regex: regexPattern, $options: 'i' } }, + { 'details.ogrn': { $regex: regexPattern, $options: 'i' } }, ], } } + console.log(`[SearchService] Фильтр для entrepreneurs:`, JSON.stringify(filter, null, 2)) + const entrepreneurs = await entrepreneurModel.getMany(filter) + console.log(`[SearchService] Результаты поиска entrepreneurs:`, { + total: entrepreneurs.totalResults, + found: entrepreneurs.results.length, + query: originalQuery, + names: entrepreneurs.results.map(ent => `${ent.last_name} ${ent.first_name} ${ent.middle_name || ''} (deleted: ${ent.deleted || false})`), + }) + for (const entrepreneur of entrepreneurs.results) { const highlightedFields = [] + // Создаем RegExp для проверки подсветки + const testRegex = new RegExp(regexPattern, 'i') + // Проверяем отдельные поля - if (regex.test(entrepreneur.first_name)) + if (entrepreneur.first_name && testRegex.test(entrepreneur.first_name)) highlightedFields.push('first_name') - if (regex.test(entrepreneur.last_name)) + if (entrepreneur.last_name && testRegex.test(entrepreneur.last_name)) highlightedFields.push('last_name') - if (regex.test(entrepreneur.middle_name)) + if (entrepreneur.middle_name && testRegex.test(entrepreneur.middle_name)) highlightedFields.push('middle_name') - if (entrepreneur.details?.inn && regex.test(entrepreneur.details.inn)) + if (entrepreneur.details?.inn && testRegex.test(entrepreneur.details.inn)) highlightedFields.push('details.inn') - if (entrepreneur.details?.ogrn && regex.test(entrepreneur.details.ogrn)) + if (entrepreneur.details?.ogrn && testRegex.test(entrepreneur.details.ogrn)) highlightedFields.push('details.ogrn') // Проверяем полное ФИО - const fullName = `${entrepreneur.last_name} ${entrepreneur.first_name} ${entrepreneur.middle_name}`.trim() + const fullName = `${entrepreneur.last_name || ''} ${entrepreneur.first_name || ''} ${entrepreneur.middle_name || ''}`.trim() if (this.matchesFullName(fullName, queryWords)) { highlightedFields.push('full_name') } @@ -201,27 +240,30 @@ export class SearchService { return results } - private async searchOrganizations(regex: RegExp): Promise { + private async searchOrganizations(regexPattern: string): Promise { const organizationModel = new Organization(this.storage) const results: ISearchResult[] = [] const organizations = await organizationModel.getMany({ deleted: false, $or: [ - { short_name: { $regex: regex } }, - { 'details.inn': { $regex: regex } }, - { 'details.ogrn': { $regex: regex } }, + { short_name: { $regex: regexPattern, $options: 'i' } }, + { 'details.inn': { $regex: regexPattern, $options: 'i' } }, + { 'details.ogrn': { $regex: regexPattern, $options: 'i' } }, ], }) for (const organization of organizations.results) { const highlightedFields = [] - if (regex.test(organization.short_name)) + // Создаем RegExp для проверки подсветки + const testRegex = new RegExp(regexPattern, 'i') + + if (testRegex.test(organization.short_name)) highlightedFields.push('short_name') - if (organization.details?.inn && regex.test(organization.details.inn)) + if (organization.details?.inn && testRegex.test(organization.details.inn)) highlightedFields.push('details.inn') - if (organization.details?.ogrn && regex.test(organization.details.ogrn)) + if (organization.details?.ogrn && testRegex.test(organization.details.ogrn)) highlightedFields.push('details.ogrn') results.push({ diff --git a/scripts/release-info.md b/scripts/release-info.md index 7e5ca5a82e3..f47c2e49e4e 100644 --- a/scripts/release-info.md +++ b/scripts/release-info.md @@ -1,4 +1,4 @@ VERSION: v2025.6.14 -FROM DATE: 2025-05-14 (10:00 - это для нового релиза, TODO: учесть время) +FROM DATE: 2025-05-14T10:00:00+03:00 COMMENT: Реализован модуль проведения очередных общих собраний пайщиков.