文件
OmniCard/backend/src/modules/nfc/service/rule.ts
wikiboss eec259a3a7 feat: 完成全系统30个功能点开发与E2E全链路验收 (8轮调度32个子代理闭环)
- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换
- 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片
- 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转
- 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出
- 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志
- 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票
- 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏
- 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
2026-09-20 23:47:47 +08:00

200 行
4.8 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

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<NfcRuleEntity>;
@Init()
async init() {
await super.init();
this.setEntity(this.nfcRuleEntity);
}
/**
* 按时段 / 星期 / 国家解析触卡导向名片
* 设备级规则优先于租户级;同级时带国家码的规则更具体
* 兼容管理后台 serviceApis:POST body 为单参数对象
*/
async resolveScene(
params: ResolveSceneParams
): Promise<ResolveSceneResult | null> {
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;
}
}