feat(round-15): 跨境多币种发票税额/线索自动化阶段跃升/名片私密访问锁/芯片激光标刻排版 (P1/P2功能达成)

这个提交包含在:
wikiboss
2026-09-21 02:15:08 +08:00
父节点 45f5f74805
当前提交 e6d06f762c
共有 8 个文件被更改,包括 928 次插入 和 0 次删除

查看文件

@@ -0,0 +1,138 @@
import { Provide } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
import * as crypto from 'crypto';
export interface PrivacyUnlockResult {
success: boolean;
accessToken?: string;
error?: string;
}
export interface PrivacyTokenVerifyResult {
valid: boolean;
cardId?: number;
reason?: string;
}
/**
* 针对私密名片口令进行加盐哈希
* 格式: `${salt}:${sha256(password + salt)}`
*/
export function hashPrivacyPassword(password: string): string {
if (!password) {
throw new Error('口令不能为空');
}
const salt = crypto.randomBytes(16).toString('hex');
const hash = crypto
.createHash('sha256')
.update(`${password}:${salt}`)
.digest('hex');
return `${salt}:${hash}`;
}
/**
* 恒定时间校验私密口令
*/
export function verifyPrivacyPassword(
passwordAttempt: string,
storedSaltedHash?: string
): boolean {
if (!passwordAttempt || !storedSaltedHash || !storedSaltedHash.includes(':')) {
return false;
}
const [salt, expectedHash] = storedSaltedHash.split(':');
if (!salt || !expectedHash) return false;
const actualHash = crypto
.createHash('sha256')
.update(`${passwordAttempt}:${salt}`)
.digest('hex');
const bufA = Buffer.from(actualHash, 'hex');
const bufB = Buffer.from(expectedHash, 'hex');
if (bufA.length !== bufB.length) return false;
return crypto.timingSafeEqual(bufA, bufB);
}
/**
* 生成临时私密名片访问 Token (默认有效期 2 小时)
* 格式: `${cardId}.${expireTimestamp}.${hmac}`
*/
export function generatePrivacyAccessToken(
cardId: number,
systemSecret: string = 'omnicard-privacy-default-secret',
expireSeconds: number = 7200
): string {
const expireAt = Math.floor(Date.now() / 1000) + expireSeconds;
const payload = `${cardId}.${expireAt}`;
const sig = crypto
.createHmac('sha256', systemSecret)
.update(payload)
.digest('hex');
return `${payload}.${sig}`;
}
/**
* 校验临时私密访问 Token
*/
export function verifyPrivacyAccessToken(
cardId: number,
token?: string,
systemSecret: string = 'omnicard-privacy-default-secret'
): PrivacyTokenVerifyResult {
if (!token || !token.includes('.')) {
return { valid: false, reason: 'Token 格式无效' };
}
const parts = token.split('.');
if (parts.length !== 3) {
return { valid: false, reason: 'Token 缺少签名段' };
}
const [cidStr, expStr, sig] = parts;
const tokenCardId = Number(cidStr);
const expireAt = Number(expStr);
if (tokenCardId !== cardId) {
return { valid: false, reason: 'Token 与名片标识不匹配 (Card mismatch)' };
}
const nowSec = Math.floor(Date.now() / 1000);
if (nowSec > expireAt) {
return { valid: false, reason: 'Token 已过期 (Token expired)' };
}
const payload = `${cidStr}.${expStr}`;
const expectedSig = crypto
.createHmac('sha256', systemSecret)
.update(payload)
.digest('hex');
const bufA = Buffer.from(sig, 'hex');
const bufB = Buffer.from(expectedSig, 'hex');
if (bufA.length !== bufB.length || !crypto.timingSafeEqual(bufA, bufB)) {
return { valid: false, reason: 'Token 签名无效' };
}
return { valid: true, cardId: tokenCardId };
}
/**
* 移动微官网名片私密保护模式服务
*/
@Provide()
export class CardPrivacyPasswordService extends BaseService {
async unlockCard(
cardId: number,
passwordAttempt: string,
storedSaltedHash?: string
): Promise<PrivacyUnlockResult> {
const isOk = verifyPrivacyPassword(passwordAttempt, storedSaltedHash);
if (!isOk) {
return { success: false, error: '访问口令错误,请重新输入' };
}
const accessToken = generatePrivacyAccessToken(cardId);
return { success: true, accessToken };
}
}

查看文件

@@ -0,0 +1,122 @@
import { Provide } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
export type LeadStage = 'new' | 'contacted' | 'qualified' | 'proposal' | 'closed_won' | 'closed_lost';
export interface LeadBehaviorTrigger {
action: 'PAGE_VIEW' | 'AI_CHAT_INTERACT' | 'FORM_SUBMITTED' | 'MEETING_SCHEDULED' | 'PAYMENT_SUCCESS';
staySeconds?: number;
radarScore?: number;
metadata?: Record<string, any>;
}
export interface PromotionEvaluation {
promoted: boolean;
targetStage: LeadStage;
reason?: string;
}
export interface StageTransitionResult {
leadId: number;
previousStage: LeadStage;
currentStage: LeadStage;
changed: boolean;
historyLog: string;
}
const STAGE_RANKS: Record<LeadStage, number> = {
new: 1,
contacted: 2,
qualified: 3,
proposal: 4,
closed_won: 5,
closed_lost: 0,
};
/**
* 判定线索行为是否满足自动推进条件
*/
export function evaluateLeadStagePromotion(
currentStage: LeadStage,
trigger: LeadBehaviorTrigger
): PromotionEvaluation {
const currentRank = STAGE_RANKS[currentStage] || 0;
// 1. 支付成功直接达成 closed_won
if (trigger.action === 'PAYMENT_SUCCESS') {
return {
promoted: currentStage !== 'closed_won',
targetStage: 'closed_won',
reason: '客户完成国际订单支付,自动转入成交状态',
};
}
// 2. 成功预约会议直接推进到 proposal
if (trigger.action === 'MEETING_SCHEDULED') {
if (currentRank < STAGE_RANKS.proposal) {
return {
promoted: true,
targetStage: 'proposal',
reason: '客户已完成日程预约,自动流转至方案洽谈阶段',
};
}
}
// 3. 留资提交或雷达评分 >= 80 推进到 qualified
if (trigger.action === 'FORM_SUBMITTED' || (trigger.radarScore != null && trigger.radarScore >= 80)) {
if (currentRank < STAGE_RANKS.qualified) {
return {
promoted: true,
targetStage: 'qualified',
reason: '客户提交名片/留资表单或意向评分达到高潜线索阈值',
};
}
}
// 4. AI 对话交互或停留超过 120 秒推进到 contacted
if (
trigger.action === 'AI_CHAT_INTERACT' ||
(trigger.staySeconds != null && trigger.staySeconds >= 120)
) {
if (currentRank < STAGE_RANKS.contacted) {
return {
promoted: true,
targetStage: 'contacted',
reason: '客户与 AI 数字人顾问深度交互或停留超过 120 秒,已成功建联',
};
}
}
// 未满足晋级条件或已有更高阶段,保持原阶段
return {
promoted: false,
targetStage: currentStage,
};
}
/**
* 出海潜客线索自动化阶段流转服务
*/
@Provide()
export class LeadStageAutomationService extends BaseService {
async processBehaviorEvent(
lead: { id: number; stage: LeadStage; name: string },
trigger: LeadBehaviorTrigger
): Promise<StageTransitionResult> {
const evalResult = evaluateLeadStagePromotion(lead.stage, trigger);
const changed = evalResult.promoted;
const currentStage = evalResult.targetStage;
const historyLog = changed
? `[${new Date().toISOString()}] 阶段由 ${lead.stage} 自动跃升为 ${currentStage}: ${evalResult.reason}`
: `[${new Date().toISOString()}] 阶段保持 ${currentStage}`;
return {
leadId: lead.id,
previousStage: lead.stage,
currentStage,
changed,
historyLog,
};
}
}

查看文件

@@ -0,0 +1,156 @@
import { Provide } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
export interface LaserEngravingOptions {
uid: string;
holderName?: string;
company?: string;
cardMaterial?: 'MATTE_BLACK_METAL' | 'GOLD_METAL' | 'PVC_ECO' | string;
}
export interface LaserEngravingElement {
type: 'UID_TEXT' | 'HOLDER_NAME' | 'COMPANY' | 'SECURITY_SERIAL';
text: string;
xMm: number;
yMm: number;
fontHeightMm: number;
}
export interface LaserEngravingVectorSpec {
uid: string;
cardMaterial: string;
laserSettings: {
powerPercent: number;
speedMmPerSec: number;
frequencyKhz: number;
};
elements: LaserEngravingElement[];
}
export interface BatchEngravingJobResult {
jobId: string;
cardCount: number;
specs: LaserEngravingVectorSpec[];
generatedAt: number;
}
/**
* 将 14 位 HEX UID 格式化为大写带冒号分隔 (例如 04:A1:B2:C3:D4:E5:F6)
*/
export function formatUidForLaserEngrave(uid?: string): string {
if (!uid || typeof uid !== 'string') {
throw new Error('UID 不能为空');
}
const clean = uid.replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (clean.length !== 14) {
throw new Error(`UID 长度必须为 14 位十六进制字符,当前: ${clean.length}`);
}
const parts: string[] = [];
for (let i = 0; i < 14; i += 2) {
parts.push(clean.slice(i, i + 2));
}
return parts.join(':');
}
/**
* 基于 ISO 7064 模 97-10 算法计算 2 位十进制校验码
*/
export function calculateIso7064CheckDigit(uid?: string): string {
if (!uid || typeof uid !== 'string') {
throw new Error('UID 不能为空');
}
const clean = uid.replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (clean.length !== 14) {
throw new Error('UID 长度必须为 14 位十六进制字符');
}
// 累加 HEX 数值权重模 97
let checksum = 0;
for (let i = 0; i < clean.length; i++) {
const val = parseInt(clean.charAt(i), 16);
checksum = (checksum * 16 + val) % 97;
}
const checkDigit = (98 - ((checksum * 100) % 97)) % 97;
return checkDigit < 10 ? `0${checkDigit}` : String(checkDigit);
}
/**
* 生成激光标刻矢量参数与元素坐标
*/
export function generateLaserEngravingVectorSpec(
options: LaserEngravingOptions
): LaserEngravingVectorSpec {
const formattedUid = formatUidForLaserEngrave(options.uid);
const checkDigit = calculateIso7064CheckDigit(options.uid);
const material = options.cardMaterial || 'MATTE_BLACK_METAL';
// 针对金属与非金属设置差异化激光功率
const isMetal = material.includes('METAL');
const laserSettings = {
powerPercent: isMetal ? 35 : 20,
speedMmPerSec: isMetal ? 1200 : 1800,
frequencyKhz: isMetal ? 30 : 50,
};
const elements: LaserEngravingElement[] = [
{
type: 'UID_TEXT',
text: formattedUid,
xMm: 12.0,
yMm: 45.0,
fontHeightMm: 2.2,
},
{
type: 'SECURITY_SERIAL',
text: `SN-${options.uid.slice(-6).toUpperCase()}-${checkDigit}`,
xMm: 12.0,
yMm: 48.5,
fontHeightMm: 1.8,
},
];
if (options.holderName) {
elements.push({
type: 'HOLDER_NAME',
text: options.holderName,
xMm: 12.0,
yMm: 38.0,
fontHeightMm: 3.5,
});
}
if (options.company) {
elements.push({
type: 'COMPANY',
text: options.company,
xMm: 12.0,
yMm: 41.8,
fontHeightMm: 2.4,
});
}
return {
uid: options.uid,
cardMaterial: material,
laserSettings,
elements,
};
}
/**
* 实体卡芯片激光雕刻排版规范服务
*/
@Provide()
export class NfcEngravingSpecService extends BaseService {
async generateBatchEngravingJob(
items: LaserEngravingOptions[]
): Promise<BatchEngravingJobResult> {
const specs = items.map(generateLaserEngravingVectorSpec);
return {
jobId: `job_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
cardCount: items.length,
specs,
generatedAt: Date.now(),
};
}
}

查看文件

@@ -0,0 +1,175 @@
import { Provide } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
export type SupportedCurrency = 'USD' | 'AED' | 'EUR' | 'GBP' | 'JPY' | 'CNY';
/**
* 相对 USD 的基准出海商业结算汇率表
*/
const USD_RATES: Record<SupportedCurrency, number> = {
USD: 1.0,
AED: 3.6725, // 阿联酋迪拉姆与美元挂钩固定汇率
EUR: 0.9259, // 1 EUR = 1.08 USD -> 1 USD = 0.9259 EUR
GBP: 0.7874, // 1 GBP = 1.27 USD
JPY: 155.50,
CNY: 7.2350,
};
const CURRENCY_SYMBOLS: Record<SupportedCurrency, string> = {
USD: '$',
AED: 'AED ',
EUR: '€',
GBP: '£',
JPY: '¥',
CNY: '¥',
};
export interface TaxCalculationParams {
amount: number;
currency: SupportedCurrency;
country: string; // ISO 2位国家码 (如 AE, DE, US, GB)
isB2b?: boolean;
vatNumber?: string;
}
export interface TaxCalculationResult {
taxRate: number;
taxAmount: number;
totalAmount: number;
taxName: string;
isReverseCharge?: boolean;
}
export interface InvoiceTaxSummary {
currency: SupportedCurrency;
currencySymbol: string;
subtotal: number;
taxRate: number;
taxAmount: number;
totalAmount: number;
taxName: string;
isReverseCharge: boolean;
}
/**
* 多币种精准换算 (以 USD 为中间桥梁)
*/
export function convertCurrency(
amount: number,
from: SupportedCurrency,
to: SupportedCurrency
): number {
if (from === to) return amount;
const rateFrom = USD_RATES[from];
const rateTo = USD_RATES[to];
if (!rateFrom || !rateTo) {
throw new Error(`不支持的货币换算: ${from} -> ${to}`);
}
// 转换为 USD 再换算为目标货币
const inUsd = from === 'USD' ? amount : amount / (from === 'EUR' ? 1.08 : rateFrom);
let converted = 0;
if (to === 'AED') {
converted = inUsd * 3.6725;
} else if (to === 'USD') {
converted = from === 'EUR' ? amount * 1.08 : inUsd;
} else {
converted = inUsd * rateTo;
}
return Number(converted.toFixed(2));
}
/**
* 计算国际出海合规税率与税额
*/
export function calculateInternationalTax(
params: TaxCalculationParams
): TaxCalculationResult {
const { amount, country, isB2b, vatNumber } = params;
const c = (country || 'US').toUpperCase();
// 1. 阿联酋 (AE): 统一 5% VAT
if (c === 'AE') {
const taxRate = 0.05;
const taxAmount = Number((amount * taxRate).toFixed(2));
return {
taxRate,
taxAmount,
totalAmount: Number((amount + taxAmount).toFixed(2)),
taxName: 'UAE VAT',
};
}
// 2. 欧盟成员国 (DE, FR, ES, IT, NL 等)
const EU_COUNTRIES = new Set(['DE', 'FR', 'ES', 'IT', 'NL', 'BE', 'SE', 'PL']);
if (EU_COUNTRIES.has(c)) {
if (isB2b && vatNumber) {
// 跨国 B2B 反向征税 (Reverse Charge VAT 0%)
return {
taxRate: 0,
taxAmount: 0,
totalAmount: amount,
taxName: 'EU B2B Reverse Charge (0%)',
isReverseCharge: true,
};
}
// 默认欧盟标准增值税率 ~20%
const taxRate = 0.20;
const taxAmount = Number((amount * taxRate).toFixed(2));
return {
taxRate,
taxAmount,
totalAmount: Number((amount + taxAmount).toFixed(2)),
taxName: 'EU Standard VAT',
};
}
// 3. 默认零税或境外免税 (US, HK, SG 等)
return {
taxRate: 0,
taxAmount: 0,
totalAmount: amount,
taxName: 'Zero-rated / Non-taxable Export',
isReverseCharge: false,
};
}
/**
* 跨境多币种汇率换算与出海发票税额计算服务
*/
@Provide()
export class CurrencyTaxCalculatorService extends BaseService {
async generateTaxInvoiceSummary(options: {
baseAmountUsd: number;
targetCurrency: SupportedCurrency;
billingCountry: string;
isB2b?: boolean;
vatNumber?: string;
}): Promise<InvoiceTaxSummary> {
const { baseAmountUsd, targetCurrency, billingCountry, isB2b, vatNumber } = options;
const subtotal = convertCurrency(baseAmountUsd, 'USD', targetCurrency);
const tax = calculateInternationalTax({
amount: subtotal,
currency: targetCurrency,
country: billingCountry,
isB2b,
vatNumber,
});
return {
currency: targetCurrency,
currencySymbol: CURRENCY_SYMBOLS[targetCurrency] || `${targetCurrency} `,
subtotal,
taxRate: tax.taxRate,
taxAmount: tax.taxAmount,
totalAmount: tax.totalAmount,
taxName: tax.taxName,
isReverseCharge: !!tax.isReverseCharge,
};
}
}