Files
makemd/server/src/services/CertificateDatabaseService.ts
wurenzhi 037e412aad feat: 新增多模块功能与服务实现
新增广告计划、用户资产、B2B交易、合规规则等核心模型
实现爬虫工作器、贸易服务、现金流预测等业务服务
添加RBAC权限测试、压力测试等测试用例
完善扩展程序的消息处理与内容脚本功能
重构应用入口与文档生成器
更新项目规则与业务闭环分析文档
2026-03-18 09:38:09 +08:00

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,
};
}
}