migration

This commit is contained in:
Alex Ant
2025-07-01 20:31:21 +05:00
parent ece68df495
commit 9da2fcfbca
9 changed files with 143 additions and 80 deletions
@@ -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<boolean> {
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;
@@ -145,9 +145,7 @@ export class MigrationManager {
});
}
async runMigration(migration: Migration, version: string, description: string): Promise<boolean> {
const isTest = version.includes('__test');
async runMigration(migration: Migration, version: string, description: string, isTest: boolean): Promise<boolean> {
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;
}
@@ -1,24 +1,23 @@
<template lang="pug">
q-page.padding
ListOfDocumentsWidget(
:username="coopname"
:filter="{}"
:showFilter="true"
:initialDocumentType="typeForToggle"
:username='coopname',
:filter='{}',
:showFilter='false',
:initialDocumentType='typeForToggle'
)
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useSystemStore } from 'src/entities/System/model'
import { ListOfDocumentsWidget } from 'src/widgets/Cooperative/Documents/ListOfDocuments/ui'
import type { DocumentType } from 'src/entities/Document/model/types'
import { ref, computed } from 'vue';
import { useSystemStore } from 'src/entities/System/model';
import { ListOfDocumentsWidget } from 'src/widgets/Cooperative/Documents/ListOfDocuments/ui';
import type { DocumentType } from 'src/entities/Document/model/types';
// Получаем системную информацию
const { info } = useSystemStore()
const coopname = computed(() => info.coopname)
const { info } = useSystemStore();
const coopname = computed(() => info.coopname);
// Переменная для отслеживания типа в интерфейсе
const typeForToggle = ref<DocumentType>('newsubmitted')
const typeForToggle = ref<DocumentType>('newresolved');
</script>
@@ -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;
@@ -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);
}
}
@@ -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 {
+2 -1
View File
@@ -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",
@@ -17,6 +17,11 @@ export class SearchService {
this.storage = storage
}
// Экранирование специальных символов регулярных выражений
private escapeRegex(text: string): string {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
async search(query: string): Promise<ISearchResult[]> {
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<ISearchResult[]> {
private async searchIndividuals(regexPattern: string, queryWords: string[], originalQuery: string): Promise<ISearchResult[]> {
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<ISearchResult[]> {
private async searchEntrepreneurs(regexPattern: string, queryWords: string[], originalQuery: string): Promise<ISearchResult[]> {
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<ISearchResult[]> {
private async searchOrganizations(regexPattern: string): Promise<ISearchResult[]> {
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({
+1 -1
View File
@@ -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:
Реализован модуль проведения очередных общих собраний пайщиков.