feat: 完成全系统30个功能点开发与E2E全链路验收 (8轮调度32个子代理闭环)

- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换
- 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片
- 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转
- 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出
- 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志
- 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票
- 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏
- 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
这个提交包含在:
wikiboss
2026-09-20 23:47:47 +08:00
父节点 acd10ffb59
当前提交 eec259a3a7
共有 276 个文件被更改,包括 54906 次插入 和 145 次删除

查看文件

@@ -0,0 +1,31 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { CardBentoEntity } from '../../entity/bento';
import { CardBentoService } from '../../service/bento';
/**
* 后台 Bento 标准 CRUD 配置
*/
export const adminCardBentoCurdOption = {
api: ['add', 'delete', 'update', 'info', 'list', 'page'] as Array<
'add' | 'delete' | 'update' | 'info' | 'list' | 'page'
>,
entity: CardBentoEntity,
service: CardBentoService,
pageQueryOp: {
keyWordLikeFields: ['a.title', 'a.content'],
fieldEq: ['a.cardId', 'a.type', 'a.status'],
addOrderBy: { sortOrder: 'asc' as const },
},
serviceApis: [
{ method: 'getBlocksByCardId', summary: '按名片获取有序 Bento' },
{ method: 'batchSaveLayout', summary: '批量同步画布排版' },
{ method: 'toggleStatus', summary: '切换块启用状态' },
],
};
/**
* 名片模块-后台 Bento 画布
* 路由前缀:/admin/card/bento
*/
@CoolController(adminCardBentoCurdOption)
export class AdminCardBentoController extends BaseController {}

查看文件

@@ -0,0 +1,29 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { CardCasesEntity } from '../../entity/cases';
import { CardCasesService } from '../../service/cases';
/**
* 后台出海案例作品集标准 CRUD 配置
*/
export const adminCardCasesCurdOption = {
api: ['add', 'delete', 'update', 'info', 'list', 'page'] as Array<
'add' | 'delete' | 'update' | 'info' | 'list' | 'page'
>,
entity: CardCasesEntity,
service: CardCasesService,
pageQueryOp: {
keyWordLikeFields: ['a.title', 'a.client', 'a.metrics'],
fieldEq: ['a.cardId'],
addOrderBy: { sortOrder: 'asc' as const },
},
serviceApis: [
{ method: 'getCasesByCardId', summary: '按名片获取案例列表' },
],
};
/**
* 名片模块-后台出海案例作品集管理
* 路由前缀:/admin/card/cases
*/
@CoolController(adminCardCasesCurdOption)
export class AdminCardCasesController extends BaseController {}

查看文件

@@ -0,0 +1,39 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { CardInfoEntity } from '../../entity/info';
import { CardInfoService } from '../../service/info';
/**
* 后台登录态插入参数
*/
interface AdminInsertCtx {
admin?: {
userId?: number;
};
}
/**
* 后台名片标准 CRUD 配置
*/
export const adminCardInfoCurdOption = {
api: ['add', 'delete', 'update', 'info', 'list', 'page'] as Array<
'add' | 'delete' | 'update' | 'info' | 'list' | 'page'
>,
entity: CardInfoEntity,
service: CardInfoService,
insertParam: (ctx: AdminInsertCtx) => ({
userId: ctx.admin?.userId,
}),
infoIgnoreProperty: ['remark', 'internalConfig'],
pageQueryOp: {
keyWordLikeFields: ['a.name', 'a.title', 'a.company', 'a.slug'],
fieldEq: ['a.status', 'a.themeCode'],
addOrderBy: { updateTime: 'desc' as const },
},
};
/**
* 名片模块-后台名片信息
* 路由前缀:/admin/card/info
*/
@CoolController(adminCardInfoCurdOption)
export class AdminCardInfoController extends BaseController {}

查看文件

@@ -0,0 +1,29 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { CardLocaleEntity } from '../../entity/locale';
import { CardLocaleService } from '../../service/locale';
/**
* 后台名片多语言标准 CRUD 配置
*/
export const adminCardLocaleCurdOption = {
api: ['add', 'delete', 'update', 'info', 'list', 'page'] as Array<
'add' | 'delete' | 'update' | 'info' | 'list' | 'page'
>,
entity: CardLocaleEntity,
service: CardLocaleService,
pageQueryOp: {
fieldEq: ['a.cardId', 'a.locale'],
},
serviceApis: [
{ method: 'upsert', summary: '按名片语言覆盖写入' },
{ method: 'listByCardId', summary: '按名片列出多语分支' },
{ method: 'getByCardIdAndLocale', summary: '按名片与语言获取' },
],
};
/**
* 名片模块-后台内容级多语言
* 路由前缀:/admin/card/locale
*/
@CoolController(adminCardLocaleCurdOption)
export class AdminCardLocaleController extends BaseController {}

查看文件

@@ -6,6 +6,38 @@ import {
TagTypes,
} from '@cool-midway/core';
import { CardPortalService } from '../../service/portal';
import { CardCalendarService } from '../../service/calendar';
import { CardSeoService } from '../../service/seo';
import {
GeoHeaderMap,
resolveSuggestedLocale,
} from '../../../../comm/i18n/geo';
export interface PortalRequestContext {
headers?: GeoHeaderMap;
set?: (key: string, value: string) => void;
status?: number;
body?: unknown;
get?: (field: string) => string | undefined;
}
export interface MeetingIcsQuery {
summary?: string;
title?: string;
startTime?: string;
endTime?: string;
start?: string;
end?: string;
description?: string;
location?: string;
organizerName?: string;
organizerEmail?: string;
organizer?: string;
uid?: string;
url?: string;
slug?: string;
id?: string | number;
}
/**
* 访客端公开微官网与短链路由控制器 (免登录触达)
@@ -17,6 +49,15 @@ export class OpenCardPortalController extends BaseController {
@Inject()
cardPortalService: CardPortalService;
@Inject()
cardCalendarService: CardCalendarService;
@Inject()
cardSeoService: CardSeoService;
@Inject()
ctx: PortalRequestContext;
/**
* 获取公开名片微官网详情 (Query 方式)
* 接口地址: GET /open/card/portal/detail?id=123 或 ?slug=alex-zheng
@@ -26,7 +67,8 @@ export class OpenCardPortalController extends BaseController {
async detail(
@Query('id') id?: string | number,
@Query('slug') slug?: string,
@Query('slugOrId') slugOrId?: string
@Query('slugOrId') slugOrId?: string,
@Query('lang') lang?: string
) {
const target = slug || id || slugOrId;
if (!target) {
@@ -34,7 +76,10 @@ export class OpenCardPortalController extends BaseController {
}
try {
const data = await this.cardPortalService.getPortalDetail(target);
const data = await this.cardPortalService.getPortalDetail(
target,
this.detectSuggestedLocale(lang)
);
return this.ok(data);
} catch (err: any) {
return this.fail(
@@ -44,25 +89,234 @@ export class OpenCardPortalController extends BaseController {
}
}
/**
* 导出国际商务日程预约排期 ICS 日历订阅文件
* 接口地址: GET /open/card/portal/meeting-ics
*/
@CoolTag(TagTypes.IGNORE_TOKEN)
@Get('/meeting-ics', { summary: '导出会议排期 ICS 日历订阅文件' })
async meetingIcs(
@Query() query?: MeetingIcsQuery | string,
@Query('summary') summaryParam?: string,
@Query('startTime') startTimeParam?: string,
@Query('endTime') endTimeParam?: string,
@Query('description') descriptionParam?: string,
@Query('location') locationParam?: string,
@Query('organizerName') organizerNameParam?: string,
@Query('organizerEmail') organizerEmailParam?: string,
@Query('uid') uidParam?: string
) {
let summary: string | undefined;
let startTime: string | undefined;
let endTime: string | undefined;
let description: string | undefined;
let location: string | undefined;
let organizerName: string | undefined;
let organizerEmail: string | undefined;
let uid: string | undefined;
let url: string | undefined;
let organizer: string | undefined;
if (typeof query === 'object' && query !== null) {
summary = query.summary || query.title;
startTime = query.startTime || query.start;
endTime = query.endTime || query.end;
description = query.description;
location = query.location;
organizerName = query.organizerName;
organizerEmail = query.organizerEmail;
organizer = query.organizer;
uid = query.uid;
url = query.url;
} else if (typeof query === 'string') {
summary = query;
if (
summaryParam &&
(summaryParam.includes('-') ||
summaryParam.includes('T') ||
!isNaN(Number(summaryParam)))
) {
startTime = summaryParam;
endTime = startTimeParam;
description = endTimeParam;
location = descriptionParam;
} else {
summary = query || summaryParam;
startTime = startTimeParam;
endTime = endTimeParam;
description = descriptionParam;
location = locationParam;
}
organizerName = organizerNameParam;
organizerEmail = organizerEmailParam;
uid = uidParam;
}
if (!summary || !startTime || !endTime) {
return this.fail('缺少会议标题或排期时间参数~', 1001);
}
try {
const ics = this.cardCalendarService.generateIcsEvent({
summary,
startTime,
endTime,
description,
location,
organizer,
organizerName,
organizerEmail,
uid,
url,
});
if (this.ctx && typeof this.ctx.set === 'function') {
this.ctx.set('Content-Type', 'text/calendar; charset=utf-8');
const safeFilename = encodeURIComponent(
summary.slice(0, 30).replace(/[/\\?%*:|"<>]/g, '_')
);
this.ctx.set(
'Content-Disposition',
`attachment; filename="meeting-${safeFilename || 'schedule'}.ics"`
);
this.ctx.body = ics;
}
return ics;
} catch (err: unknown) {
const message =
err instanceof Error ? err.message : '生成日历预约文件失败';
return this.fail(message, 1001);
}
}
/**
* 社交爬虫与网页预览 HTML 渲染 (免登录服务端渲染短链预览)
* 接口地址: GET /open/card/portal/preview-html/:slug
*/
@CoolTag(TagTypes.IGNORE_TOKEN)
@Get('/preview-html/:slug', { summary: '社交爬虫与网页预览 HTML 渲染' })
async previewHtml(@Param('slug') slug: string, @Query('lang') lang?: string) {
if (!slug) {
if (this.ctx && typeof this.ctx.set === 'function') {
this.ctx.set('Content-Type', 'text/html; charset=utf-8');
}
if (this.ctx) this.ctx.status = 400;
return this.cardSeoService.renderNotFoundHtml('缺少名片标识参数~');
}
try {
const data = await this.cardPortalService.getPortalDetail(
slug,
this.detectSuggestedLocale(lang)
);
const userAgent = this.getRequestHeader('user-agent');
const isCrawler = this.cardSeoService.isCrawler(userAgent);
const crawlerName = this.cardSeoService.detectCrawler(userAgent);
const html = this.cardSeoService.renderCardHtml(data, {
isCrawler,
crawlerName,
});
if (this.ctx && typeof this.ctx.set === 'function') {
this.ctx.set('Content-Type', 'text/html; charset=utf-8');
}
if (this.ctx) {
this.ctx.status = 200;
this.ctx.body = html;
}
return html;
} catch (err: unknown) {
const message = err instanceof Error ? err.message : '名片不存在或已下线';
const status =
typeof err === 'object' && err !== null && 'status' in err
? Number((err as { status?: number }).status) || 404
: 404;
if (this.ctx && typeof this.ctx.set === 'function') {
this.ctx.set('Content-Type', 'text/html; charset=utf-8');
}
if (this.ctx) {
this.ctx.status = status;
this.ctx.body = this.cardSeoService.renderNotFoundHtml(message);
}
return this.cardSeoService.renderNotFoundHtml(message);
}
}
/**
* 根据自定义短链或 ID 获取微官网详情 (Path 参数方式)
* 接口地址: GET /open/card/portal/:slugOrId (如 /open/card/portal/alex-zheng)
*/
@CoolTag(TagTypes.IGNORE_TOKEN)
@Get('/:slugOrId', { summary: '公开微官网详情 (Path 短链/ID)' })
async getBySlugOrId(@Param('slugOrId') slugOrId: string) {
if (!slugOrId || slugOrId === 'detail') {
async getBySlugOrId(
@Param('slugOrId') slugOrId: string,
@Query('lang') lang?: string
) {
if (
!slugOrId ||
slugOrId === 'detail' ||
slugOrId === 'meeting-ics' ||
slugOrId === 'preview-html'
) {
return this.fail('缺少名片标识参数~', 1001);
}
// 社交爬虫探测:若检测到社交机器人 UA,自动透明转为返回服务端渲染 HTML
const userAgent = this.getRequestHeader('user-agent');
if (this.cardSeoService && this.cardSeoService.isCrawler(userAgent)) {
return (await this.previewHtml(slugOrId, lang)) as unknown as ReturnType<
BaseController['ok']
>;
}
try {
const data = await this.cardPortalService.getPortalDetail(slugOrId);
return this.ok(data);
} catch (err: any) {
return this.fail(
err?.message || '获取名片微官网失败',
err?.status || 1001
const data = await this.cardPortalService.getPortalDetail(
slugOrId,
this.detectSuggestedLocale(lang)
);
return this.ok(data);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : '获取名片微官网失败';
const status =
typeof err === 'object' && err !== null && 'status' in err
? Number((err as { status?: number }).status) || 1001
: 1001;
return this.fail(message, status);
}
}
/**
* 从 query.lang 与请求头探测建议语言
*/
private detectSuggestedLocale(lang?: string): string {
return resolveSuggestedLocale({
lang,
headers: this.ctx?.headers,
});
}
/**
* 安全获取请求头字段
*/
private getRequestHeader(name: string): string | undefined {
if (!this.ctx) return undefined;
if (typeof this.ctx.get === 'function') {
const val = this.ctx.get(name);
if (val) return val;
}
const headers = this.ctx.headers;
if (!headers) return undefined;
const target = name.toLowerCase();
for (const [key, value] of Object.entries(headers)) {
if (key.toLowerCase() === target) {
if (Array.isArray(value)) return value[0];
return value;
}
}
return undefined;
}
}

查看文件

@@ -0,0 +1,41 @@
import { Inject, Provide, Get, Query } from '@midwayjs/core';
import {
CoolController,
BaseController,
CoolTag,
TagTypes,
} from '@cool-midway/core';
import { CardQrcodeService } from '../../service/qrcode';
/**
* 访客端公开动态短链二维码下载(免登录)
* 路由前缀: /open/card/qrcode
*/
@Provide()
@CoolController()
export class OpenCardQrcodeController extends BaseController {
@Inject()
cardQrcodeService: CardQrcodeService;
/**
* 下载 SVG 二维码
* 接口地址: GET /open/card/qrcode/svg?slugOrId=alex-zheng
*/
@CoolTag(TagTypes.IGNORE_TOKEN)
@Get('/svg', { summary: '公开名片二维码 SVG' })
async svg(@Query('slugOrId') slugOrId?: string) {
const data = await this.cardQrcodeService.generateSvg(slugOrId);
return this.ok(data);
}
/**
* 下载 PNG 二维码(dataURL)
* 接口地址: GET /open/card/qrcode/png?slugOrId=alex-zheng
*/
@CoolTag(TagTypes.IGNORE_TOKEN)
@Get('/png', { summary: '公开名片二维码 PNG' })
async png(@Query('slugOrId') slugOrId?: string) {
const data = await this.cardQrcodeService.generatePng(slugOrId);
return this.ok(data);
}
}