新增广告计划、用户资产、B2B交易、合规规则等核心模型 实现爬虫工作器、贸易服务、现金流预测等业务服务 添加RBAC权限测试、压力测试等测试用例 完善扩展程序的消息处理与内容脚本功能 重构应用入口与文档生成器 更新项目规则与业务闭环分析文档
515 lines
17 KiB
TypeScript
515 lines
17 KiB
TypeScript
import db from '../config/database';
|
|
import { logger } from '../utils/logger';
|
|
import {
|
|
Certificate,
|
|
CertificateCreateInput,
|
|
CertificateUpdateInput,
|
|
CertificateQueryOptions,
|
|
CertificateType,
|
|
CertificateStatus,
|
|
} from '../models/Certificate';
|
|
|
|
export interface CertificateDatabaseRecord {
|
|
id: string;
|
|
tenantId: string;
|
|
shopId: string;
|
|
taskId?: string;
|
|
traceId: string;
|
|
businessType: 'TOC' | 'TOB';
|
|
certificateName: string;
|
|
certificateType: CertificateType;
|
|
certificateNo: string;
|
|
productId?: string;
|
|
productName?: string;
|
|
issuer: string;
|
|
issueDate: Date;
|
|
expiryDate: Date;
|
|
status: CertificateStatus;
|
|
fileUrl?: string;
|
|
fileHash?: string;
|
|
remarks?: string;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
}
|
|
|
|
export interface CertificateBackupData {
|
|
version: string;
|
|
exportedAt: Date;
|
|
tenantId: string;
|
|
totalRecords: number;
|
|
certificates: CertificateDatabaseRecord[];
|
|
}
|
|
|
|
export interface CertificateMigrationResult {
|
|
success: boolean;
|
|
totalMigrated: number;
|
|
failedRecords: string[];
|
|
errors: string[];
|
|
traceId: string;
|
|
}
|
|
|
|
export interface CertificateStatistics {
|
|
totalCertificates: number;
|
|
byStatus: Record<CertificateStatus, number>;
|
|
byType: Record<CertificateType, number>;
|
|
expiringWithin30Days: number;
|
|
expired: number;
|
|
byPlatform: Record<string, number>;
|
|
}
|
|
|
|
export interface BulkImportResult {
|
|
success: number;
|
|
failed: number;
|
|
errors: Array<{ row: number; error: string }>;
|
|
}
|
|
|
|
export class CertificateDatabaseService {
|
|
private static readonly TABLE_NAME = 'cf_certificate';
|
|
private static readonly BACKUP_TABLE = 'cf_certificate_backup';
|
|
private static readonly VERSION = '1.0.0';
|
|
|
|
static async initTable(): Promise<void> {
|
|
const hasTable = await db.schema.hasTable(this.TABLE_NAME);
|
|
if (!hasTable) {
|
|
logger.info(`[CertificateDatabaseService] Creating ${this.TABLE_NAME} table...`);
|
|
await db.schema.createTable(this.TABLE_NAME, (table) => {
|
|
table.string('id', 36).primary();
|
|
table.string('tenant_id', 64).notNullable().index();
|
|
table.string('shop_id', 64).notNullable().index();
|
|
table.string('task_id', 36);
|
|
table.string('trace_id', 64).notNullable();
|
|
table.enum('business_type', ['TOC', 'TOB']).notNullable();
|
|
table.string('certificate_name', 255).notNullable();
|
|
table.enum('certificate_type', [
|
|
'CE', 'FCC', 'ROHS', 'ISO9001', 'ISO14001',
|
|
'FDA', 'UL', 'CCC', 'EXPORT_LICENSE', 'ORIGIN', 'OTHER'
|
|
]).notNullable();
|
|
table.string('certificate_no', 128).notNullable();
|
|
table.string('product_id', 64);
|
|
table.string('product_name', 255);
|
|
table.string('issuer', 255).notNullable();
|
|
table.date('issue_date').notNullable();
|
|
table.date('expiry_date').notNullable().index();
|
|
table.enum('status', [
|
|
'PENDING_REVIEW', 'APPROVED', 'REJECTED', 'EXPIRED', 'REVOKED'
|
|
]).defaultTo('PENDING_REVIEW').notNullable();
|
|
table.string('file_url', 512);
|
|
table.string('file_hash', 128);
|
|
table.text('remarks');
|
|
table.timestamp('created_at').defaultTo(db.fn.now());
|
|
table.timestamp('updated_at').defaultTo(db.fn.now());
|
|
|
|
table.unique(['tenant_id', 'certificate_no'], 'uk_cert_no');
|
|
table.index(['certificate_type'], 'idx_cert_type');
|
|
table.index(['status'], 'idx_cert_status');
|
|
table.index(['product_id'], 'idx_cert_product');
|
|
});
|
|
logger.info(`[CertificateDatabaseService] Table ${this.TABLE_NAME} created`);
|
|
}
|
|
|
|
const hasBackupTable = await db.schema.hasTable(this.BACKUP_TABLE);
|
|
if (!hasBackupTable) {
|
|
await db.schema.createTable(this.BACKUP_TABLE, (table) => {
|
|
table.string('id', 36).primary();
|
|
table.string('tenant_id', 64).notNullable().index();
|
|
table.json('backup_data').notNullable();
|
|
table.string('version', 32).notNullable();
|
|
table.timestamp('created_at').defaultTo(db.fn.now());
|
|
});
|
|
logger.info(`[CertificateDatabaseService] Backup table ${this.BACKUP_TABLE} created`);
|
|
}
|
|
}
|
|
|
|
static async insert(record: CertificateDatabaseRecord): Promise<string> {
|
|
const id = record.id || this.generateId();
|
|
|
|
await db(this.TABLE_NAME).insert({
|
|
id,
|
|
tenant_id: record.tenantId,
|
|
shop_id: record.shopId,
|
|
task_id: record.taskId || null,
|
|
trace_id: record.traceId,
|
|
business_type: record.businessType,
|
|
certificate_name: record.certificateName,
|
|
certificate_type: record.certificateType,
|
|
certificate_no: record.certificateNo,
|
|
product_id: record.productId || null,
|
|
product_name: record.productName || null,
|
|
issuer: record.issuer,
|
|
issue_date: record.issueDate,
|
|
expiry_date: record.expiryDate,
|
|
status: record.status,
|
|
file_url: record.fileUrl || null,
|
|
file_hash: record.fileHash || null,
|
|
remarks: record.remarks || null,
|
|
});
|
|
|
|
logger.info(`[CertificateDatabaseService] Certificate inserted: id=${id}, tenantId=${record.tenantId}, traceId=${record.traceId}`);
|
|
return id;
|
|
}
|
|
|
|
static async update(
|
|
tenantId: string,
|
|
id: string,
|
|
updates: Partial<CertificateDatabaseRecord>,
|
|
traceId: string
|
|
): Promise<boolean> {
|
|
const updateData: Record<string, any> = {
|
|
updated_at: new Date(),
|
|
};
|
|
|
|
if (updates.certificateName) updateData.certificate_name = updates.certificateName;
|
|
if (updates.certificateType) updateData.certificate_type = updates.certificateType;
|
|
if (updates.certificateNo) updateData.certificate_no = updates.certificateNo;
|
|
if (updates.productId !== undefined) updateData.product_id = updates.productId || null;
|
|
if (updates.productName !== undefined) updateData.product_name = updates.productName || null;
|
|
if (updates.issuer) updateData.issuer = updates.issuer;
|
|
if (updates.issueDate) updateData.issue_date = updates.issueDate;
|
|
if (updates.expiryDate) updateData.expiry_date = updates.expiryDate;
|
|
if (updates.status) updateData.status = updates.status;
|
|
if (updates.fileUrl !== undefined) updateData.file_url = updates.fileUrl || null;
|
|
if (updates.fileHash !== undefined) updateData.file_hash = updates.fileHash || null;
|
|
if (updates.remarks !== undefined) updateData.remarks = updates.remarks || null;
|
|
|
|
const result = await db(this.TABLE_NAME)
|
|
.where({ id, tenant_id: tenantId })
|
|
.update(updateData);
|
|
|
|
logger.info(`[CertificateDatabaseService] Certificate updated: id=${id}, tenantId=${tenantId}, traceId=${traceId}`);
|
|
return result > 0;
|
|
}
|
|
|
|
static async delete(tenantId: string, id: string, traceId: string): Promise<boolean> {
|
|
const result = await db(this.TABLE_NAME)
|
|
.where({ id, tenant_id: tenantId })
|
|
.delete();
|
|
|
|
logger.info(`[CertificateDatabaseService] Certificate deleted: id=${id}, tenantId=${tenantId}, traceId=${traceId}`);
|
|
return result > 0;
|
|
}
|
|
|
|
static async getById(tenantId: string, id: string): Promise<CertificateDatabaseRecord | null> {
|
|
const row = await db(this.TABLE_NAME)
|
|
.where({ id, tenant_id: tenantId })
|
|
.first();
|
|
|
|
return row ? this.mapRowToRecord(row) : null;
|
|
}
|
|
|
|
static async query(options: CertificateQueryOptions): Promise<CertificateDatabaseRecord[]> {
|
|
let query = db(this.TABLE_NAME).where('tenant_id', options.tenantId);
|
|
|
|
if (options.shopId) {
|
|
query = query.andWhere('shop_id', options.shopId);
|
|
}
|
|
if (options.certificateType) {
|
|
query = query.andWhere('certificate_type', options.certificateType);
|
|
}
|
|
if (options.status) {
|
|
query = query.andWhere('status', options.status);
|
|
}
|
|
if (options.productId) {
|
|
query = query.andWhere('product_id', options.productId);
|
|
}
|
|
if (options.searchKeyword) {
|
|
query = query.andWhere(function() {
|
|
this.where('certificate_name', 'like', `%${options.searchKeyword}%`)
|
|
.orWhere('certificate_no', 'like', `%${options.searchKeyword}%`)
|
|
.orWhere('product_name', 'like', `%${options.searchKeyword}%`);
|
|
});
|
|
}
|
|
if (options.expiryDateFrom) {
|
|
query = query.andWhere('expiry_date', '>=', options.expiryDateFrom);
|
|
}
|
|
if (options.expiryDateTo) {
|
|
query = query.andWhere('expiry_date', '<=', options.expiryDateTo);
|
|
}
|
|
|
|
const page = options.page || 1;
|
|
const pageSize = options.pageSize || 20;
|
|
const offset = (page - 1) * pageSize;
|
|
|
|
query = query.orderBy('created_at', 'desc').limit(pageSize).offset(offset);
|
|
|
|
const rows = await query;
|
|
return rows.map(this.mapRowToRecord);
|
|
}
|
|
|
|
static async count(options: CertificateQueryOptions): Promise<number> {
|
|
let query = db(this.TABLE_NAME).where('tenant_id', options.tenantId);
|
|
|
|
if (options.shopId) {
|
|
query = query.andWhere('shop_id', options.shopId);
|
|
}
|
|
if (options.certificateType) {
|
|
query = query.andWhere('certificate_type', options.certificateType);
|
|
}
|
|
if (options.status) {
|
|
query = query.andWhere('status', options.status);
|
|
}
|
|
if (options.productId) {
|
|
query = query.andWhere('product_id', options.productId);
|
|
}
|
|
|
|
const result = await query.count('* as count').first();
|
|
return Number(result?.count || 0);
|
|
}
|
|
|
|
static async getStatistics(tenantId: string, shopId?: string): Promise<CertificateStatistics> {
|
|
let query = db(this.TABLE_NAME).where('tenant_id', tenantId);
|
|
if (shopId) {
|
|
query = query.andWhere('shop_id', shopId);
|
|
}
|
|
|
|
const records = await query;
|
|
|
|
const stats: CertificateStatistics = {
|
|
totalCertificates: records.length,
|
|
byStatus: {
|
|
PENDING_REVIEW: 0,
|
|
APPROVED: 0,
|
|
REJECTED: 0,
|
|
EXPIRED: 0,
|
|
REVOKED: 0,
|
|
},
|
|
byType: {
|
|
CE: 0,
|
|
FCC: 0,
|
|
ROHS: 0,
|
|
ISO9001: 0,
|
|
ISO14001: 0,
|
|
FDA: 0,
|
|
UL: 0,
|
|
CCC: 0,
|
|
EXPORT_LICENSE: 0,
|
|
ORIGIN: 0,
|
|
OTHER: 0,
|
|
},
|
|
expiringWithin30Days: 0,
|
|
expired: 0,
|
|
byPlatform: {},
|
|
};
|
|
|
|
const now = new Date();
|
|
const thirtyDaysLater = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
|
|
|
|
for (const record of records) {
|
|
stats.byStatus[record.status as CertificateStatus]++;
|
|
stats.byType[record.certificate_type as CertificateType]++;
|
|
|
|
const expiryDate = new Date(record.expiry_date);
|
|
if (expiryDate < now) {
|
|
stats.expired++;
|
|
} else if (expiryDate <= thirtyDaysLater) {
|
|
stats.expiringWithin30Days++;
|
|
}
|
|
}
|
|
|
|
return stats;
|
|
}
|
|
|
|
static async backup(tenantId: string, traceId: string): Promise<string> {
|
|
const records = await db(this.TABLE_NAME).where('tenant_id', tenantId);
|
|
|
|
const backupData: CertificateBackupData = {
|
|
version: this.VERSION,
|
|
exportedAt: new Date(),
|
|
tenantId,
|
|
totalRecords: records.length,
|
|
certificates: records.map(this.mapRowToRecord),
|
|
};
|
|
|
|
const backupId = this.generateBackupId();
|
|
|
|
await db(this.BACKUP_TABLE).insert({
|
|
id: backupId,
|
|
tenant_id: tenantId,
|
|
backup_data: JSON.stringify(backupData),
|
|
version: this.VERSION,
|
|
});
|
|
|
|
logger.info(`[CertificateDatabaseService] Backup created: backupId=${backupId}, tenantId=${tenantId}, records=${records.length}, traceId=${traceId}`);
|
|
return backupId;
|
|
}
|
|
|
|
static async restore(
|
|
tenantId: string,
|
|
backupId: string,
|
|
traceId: string
|
|
): Promise<CertificateMigrationResult> {
|
|
const result: CertificateMigrationResult = {
|
|
success: false,
|
|
totalMigrated: 0,
|
|
failedRecords: [],
|
|
errors: [],
|
|
traceId,
|
|
};
|
|
|
|
try {
|
|
const backup = await db(this.BACKUP_TABLE)
|
|
.where({ id: backupId, tenant_id: tenantId })
|
|
.first();
|
|
|
|
if (!backup) {
|
|
result.errors.push('Backup not found');
|
|
return result;
|
|
}
|
|
|
|
const backupData: CertificateBackupData = JSON.parse(backup.backup_data);
|
|
|
|
for (const cert of backupData.certificates) {
|
|
try {
|
|
const existing = await this.getById(tenantId, cert.id);
|
|
|
|
if (existing) {
|
|
await this.update(tenantId, cert.id, cert, traceId);
|
|
} else {
|
|
await this.insert(cert);
|
|
}
|
|
|
|
result.totalMigrated++;
|
|
} catch (error: any) {
|
|
result.failedRecords.push(cert.id);
|
|
result.errors.push(`Failed to restore certificate ${cert.id}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
result.success = result.failedRecords.length === 0;
|
|
logger.info(`[CertificateDatabaseService] Restore completed: backupId=${backupId}, migrated=${result.totalMigrated}, traceId=${traceId}`);
|
|
} catch (error: any) {
|
|
result.errors.push(`Restore failed: ${error.message}`);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static async exportToJson(tenantId: string, traceId: string): Promise<CertificateBackupData> {
|
|
const records = await db(this.TABLE_NAME).where('tenant_id', tenantId);
|
|
|
|
const exportData: CertificateBackupData = {
|
|
version: this.VERSION,
|
|
exportedAt: new Date(),
|
|
tenantId,
|
|
totalRecords: records.length,
|
|
certificates: records.map(this.mapRowToRecord),
|
|
};
|
|
|
|
logger.info(`[CertificateDatabaseService] Export completed: tenantId=${tenantId}, records=${records.length}, traceId=${traceId}`);
|
|
return exportData;
|
|
}
|
|
|
|
static async bulkImport(
|
|
tenantId: string,
|
|
certificates: CertificateDatabaseRecord[],
|
|
traceId: string
|
|
): Promise<BulkImportResult> {
|
|
const result: BulkImportResult = {
|
|
success: 0,
|
|
failed: 0,
|
|
errors: [],
|
|
};
|
|
|
|
for (let i = 0; i < certificates.length; i++) {
|
|
const cert = certificates[i];
|
|
try {
|
|
cert.tenantId = tenantId;
|
|
await this.insert(cert);
|
|
result.success++;
|
|
} catch (error: any) {
|
|
result.failed++;
|
|
result.errors.push({ row: i + 1, error: error.message });
|
|
}
|
|
}
|
|
|
|
logger.info(`[CertificateDatabaseService] Bulk import completed: success=${result.success}, failed=${result.failed}, traceId=${traceId}`);
|
|
return result;
|
|
}
|
|
|
|
static async getExpiringCertificates(
|
|
tenantId: string,
|
|
days: number,
|
|
traceId: string
|
|
): Promise<CertificateDatabaseRecord[]> {
|
|
const now = new Date();
|
|
const futureDate = new Date(now.getTime() + days * 24 * 60 * 60 * 1000);
|
|
|
|
const records = await db(this.TABLE_NAME)
|
|
.where('tenant_id', tenantId)
|
|
.where('status', 'APPROVED')
|
|
.where('expiry_date', '>=', now)
|
|
.where('expiry_date', '<=', futureDate)
|
|
.orderBy('expiry_date', 'asc');
|
|
|
|
logger.info(`[CertificateDatabaseService] Found ${records.length} expiring certificates within ${days} days, traceId=${traceId}`);
|
|
return records.map(this.mapRowToRecord);
|
|
}
|
|
|
|
static async updateExpiredStatus(tenantId: string, traceId: string): Promise<number> {
|
|
const now = new Date();
|
|
|
|
const result = await db(this.TABLE_NAME)
|
|
.where('tenant_id', tenantId)
|
|
.where('status', 'APPROVED')
|
|
.where('expiry_date', '<', now)
|
|
.update({ status: 'EXPIRED', updated_at: now });
|
|
|
|
logger.info(`[CertificateDatabaseService] Updated ${result} certificates to EXPIRED status, tenantId=${tenantId}, traceId=${traceId}`);
|
|
return result;
|
|
}
|
|
|
|
static async getBackups(tenantId: string): Promise<Array<{ id: string; version: string; createdAt: Date }>> {
|
|
const backups = await db(this.BACKUP_TABLE)
|
|
.where('tenant_id', tenantId)
|
|
.orderBy('created_at', 'desc')
|
|
.select('id', 'version', 'created_at');
|
|
|
|
return backups.map(b => ({
|
|
id: b.id,
|
|
version: b.version,
|
|
createdAt: b.created_at,
|
|
}));
|
|
}
|
|
|
|
static async deleteBackup(tenantId: string, backupId: string, traceId: string): Promise<boolean> {
|
|
const result = await db(this.BACKUP_TABLE)
|
|
.where({ id: backupId, tenant_id: tenantId })
|
|
.delete();
|
|
|
|
logger.info(`[CertificateDatabaseService] Backup deleted: backupId=${backupId}, tenantId=${tenantId}, traceId=${traceId}`);
|
|
return result > 0;
|
|
}
|
|
|
|
private static generateId(): string {
|
|
return `cert_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
}
|
|
|
|
private static generateBackupId(): string {
|
|
return `backup_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
}
|
|
|
|
private static mapRowToRecord(row: Record<string, any>): CertificateDatabaseRecord {
|
|
return {
|
|
id: row.id,
|
|
tenantId: row.tenant_id,
|
|
shopId: row.shop_id,
|
|
taskId: row.task_id,
|
|
traceId: row.trace_id,
|
|
businessType: row.business_type,
|
|
certificateName: row.certificate_name,
|
|
certificateType: row.certificate_type,
|
|
certificateNo: row.certificate_no,
|
|
productId: row.product_id,
|
|
productName: row.product_name,
|
|
issuer: row.issuer,
|
|
issueDate: row.issue_date,
|
|
expiryDate: row.expiry_date,
|
|
status: row.status,
|
|
fileUrl: row.file_url,
|
|
fileHash: row.file_hash,
|
|
remarks: row.remarks,
|
|
createdAt: row.created_at,
|
|
updatedAt: row.updated_at,
|
|
};
|
|
}
|
|
}
|