- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
200 行
4.8 KiB
TypeScript
200 行
4.8 KiB
TypeScript
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;
|
||
}
|
||
}
|