import { Init, Provide } from '@midwayjs/core'; import { BaseService } from '@cool-midway/core'; import { InjectEntityModel } from '@midwayjs/typeorm'; import { Repository } from 'typeorm'; import { NFCSceneMode } from '../../../comm/types/omni'; import { NfcRuleEntity } from '../entity/rule'; /** * 场景路由解析入参(兼容管理后台 serviceApis 单对象) */ export interface ResolveSceneParams { deviceId?: string | number | null; tenantId: string | number; now?: Date | string; countryCode?: string | null; } /** * 场景路由解析结果 */ export interface ResolveSceneResult { targetCardId: string | number; sceneMode: NFCSceneMode; } /** * NFC 场景路由规则服务 */ @Provide() export class NfcRuleService extends BaseService { @InjectEntityModel(NfcRuleEntity) nfcRuleEntity: Repository; @Init() async init() { await super.init(); this.setEntity(this.nfcRuleEntity); } /** * 按时段 / 星期 / 国家解析触卡导向名片 * 设备级规则优先于租户级;同级时带国家码的规则更具体 * 兼容管理后台 serviceApis:POST body 为单参数对象 */ async resolveScene( params: ResolveSceneParams ): Promise { const tenantId = Number(params?.tenantId); if (!tenantId) { return null; } const now = this.toDate(params.now); const rules = await this.nfcRuleEntity.find({ where: { tenantId, enabled: true }, }); const matched = rules.filter(rule => this.matchesRule(rule, params.deviceId, now, params.countryCode) ); if (matched.length === 0) { return null; } matched.sort( (a, b) => this.ruleScore(b, params.deviceId) - this.ruleScore(a, params.deviceId) ); const winner = matched[0]; return { targetCardId: winner.targetCardId, sceneMode: winner.sceneMode, }; } private toDate(now?: Date | string): Date { if (now instanceof Date) { return now; } if (typeof now === 'string' && now.trim()) { const parsed = new Date(now); if (!Number.isNaN(parsed.getTime())) { return parsed; } } return new Date(); } private matchesRule( rule: NfcRuleEntity, deviceId: string | number | null | undefined, now: Date, countryCode?: string | null ): boolean { if (rule.enabled !== true) { return false; } if (!this.matchesDevice(rule.deviceId, deviceId)) { return false; } if (!this.matchesWeekdays(rule.weekdays, now)) { return false; } if (!this.matchesTime(rule.startTime, rule.endTime, now)) { return false; } return this.matchesCountry(rule.countryCode, countryCode); } private isBlank(value: string | number | null | undefined): boolean { return value === null || value === undefined || value === ''; } private matchesDevice( ruleDeviceId: string | number | null | undefined, deviceId: string | number | null | undefined ): boolean { if (this.isBlank(ruleDeviceId)) { return true; } if (this.isBlank(deviceId)) { return false; } return String(ruleDeviceId) === String(deviceId); } private matchesWeekdays( weekdays: number[] | null | undefined, now: Date ): boolean { if (!weekdays || weekdays.length === 0) { return true; } return weekdays.includes(now.getDay()); } private matchesTime( startTime: string | null | undefined, endTime: string | null | undefined, now: Date ): boolean { const start = (startTime || '').trim(); const end = (endTime || '').trim(); if (!start && !end) { return true; } const current = this.formatHm(now); if (start && end) { if (start <= end) { return current >= start && current <= end; } return current >= start || current <= end; } if (start) { return current >= start; } return current <= end; } private formatHm(now: Date): string { const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); return `${hours}:${minutes}`; } private matchesCountry( ruleCountry: string | null | undefined, countryCode?: string | null ): boolean { const expected = (ruleCountry || '').trim(); if (!expected) { return true; } const actual = (countryCode || '').trim(); if (!actual) { return false; } return expected.toLowerCase() === actual.toLowerCase(); } private ruleScore( rule: NfcRuleEntity, deviceId: string | number | null | undefined ): number { let score = 0; if ( !this.isBlank(rule.deviceId) && !this.isBlank(deviceId) && String(rule.deviceId) === String(deviceId) ) { score += 2; } if ((rule.countryCode || '').trim()) { score += 1; } return score; } }