refactor(arch): 落地平台架构演进第一二阶段整改,消除OOM与越权风险并支持高并发缓冲与AI多租户BYOK

- [P0安全底座] 多租户数据闭环:公开留资与雷达足迹自动逆向追溯绑定名片 tenantId 与归属销售 userId
- [P0性能稳定] 消除内存全量加载:公海池分批带条件扫描回收,雷达国家分布改用 SQL 原生 GROUP BY 聚合查询彻底消除 OOM 隐患
- [P0商业变现] 商业化配额门禁切面:在 Cool-Admin modifyBefore 生命周期织入 assertQuotaAvailable 校验,阻断超额名片创建
- [P0环境解耦] 客户端解耦本地硬编码:request.uts 支持 api_base_url 本地覆盖,生产数据库名修正为 crmmpdata
- [P1高并发削峰] 雷达足迹批量缓冲引擎:引入 CrmRadarBufferService 环形缓冲队列与阈值/定时批量 Bulk Insert 落盘
- [P2中枢租户化] AI 智能中枢多租户 BYOK 与 Token 成本风控:支持企业自带 Key 隔离、Token 消耗账本、用量配额门禁与名片人设个性化联动
- [跨端规范] 对齐 Uni-app x 移动微官网原生首屏与底部原生 tabBar 规范
- [测试驱动] 全工程 116 个测试套件,1,175 个单元与集成测试用例全部 100% 绿色通过,管理后台 Vite 打包无错误
这个提交包含在:
wikiboss
2026-09-21 11:14:10 +08:00
父节点 e73c15bd22
当前提交 6c3d5d323a
共有 637 个文件被更改,包括 4731 次插入 和 56919 次删除

查看文件

@@ -6,6 +6,7 @@
import {
AiChatReply,
AiChatRequest,
CardContact,
CardProfile,
CardPortalDetail,
LeadEntity,
@@ -15,6 +16,7 @@ import {
NfcUidParams,
OwnerPageData,
OwnerPageQuery,
QuotaCheckResult,
RadarTrack,
SubmitLeadParams,
RecordRadarTrackParams,
@@ -160,5 +162,52 @@ export function fetchCardQrcodeSvg(
});
}
export function fetchHallContactPage(
query: OwnerPageQuery
): Promise<OwnerPageData<CardContact>> {
return httpPost<OwnerPageData<CardContact>>('/admin/card/contact/hallPage', query);
}
export function collectHallContact(
peerCardId: string | number,
ownerCardId?: string | number
): Promise<CardContact> {
const payload: Record<string, string | number> = { peerCardId };
if (ownerCardId != null) {
payload.ownerCardId = ownerCardId;
}
return httpPost<CardContact>('/admin/card/contact/collect', payload);
}
export function fetchTeamWallPage(
query: OwnerPageQuery
): Promise<OwnerPageData<CardContact>> {
return httpPost<OwnerPageData<CardContact>>('/admin/card/contact/teamWall', query);
}
export function fetchCardQuota(
tenantId?: string | number
): Promise<QuotaCheckResult> {
const payload: Record<string, string | number> = { resourceType: 'cards' };
if (tenantId != null && String(tenantId) !== '') {
payload.tenantId = tenantId;
}
return httpPost<QuotaCheckResult>('/admin/team/billing/quota', payload);
}
export function confirmHallExchange(
exchangeCode: string,
actingCardId?: string | number
): Promise<{ status: string; isDualSynced: boolean }> {
const payload: Record<string, string | number> = { exchangeCode };
if (actingCardId != null) {
payload.actingCardId = actingCardId;
}
return httpPost<{ status: string; isDualSynced: boolean }>(
'/admin/card/exchange/confirm',
payload
);
}
export * from './types.uts';
export * from './request.uts';

查看文件

@@ -104,7 +104,7 @@ export function parseDeepLink(url?: string): DeepLinkRouteInfo {
*/
export function buildAppPageRoute(info: DeepLinkRouteInfo): string {
if (!info || !info.isValid) {
return '/pages/owner/home';
return '/pages/owner/hall/index';
}
const queryParts: string[] = [];
@@ -116,5 +116,5 @@ export function buildAppPageRoute(info: DeepLinkRouteInfo): string {
if (info.source) queryParts.push(`source=${info.source}`);
const queryString = queryParts.length > 0 ? `?${queryParts.join('&')}` : '';
return `/pages/card/portal${queryString}`;
return `/pages/portal/index${queryString}`;
}

查看文件

@@ -105,7 +105,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'radar.visitor_trace': '访问轨迹',
'radar.lead_score': '意向度评分',
'crm.contacts': '联系人列表',
'crm.leads': '商机线索',
'crm.leads': '客户管理',
'crm.follow_up': '跟进记录',
'portal.unavailable': '微官网暂不可用',
'portal.retry': '重新加载',
@@ -161,7 +161,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'radar.visitor_trace': 'Visitor Trail',
'radar.lead_score': 'Lead Score',
'crm.contacts': 'Contacts',
'crm.leads': 'Leads',
'crm.leads': 'Customers',
'crm.follow_up': 'Follow-up',
'portal.unavailable': 'This page is unavailable',
'portal.retry': 'Reload',
@@ -217,7 +217,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'radar.visitor_trace': 'مسار الزائر',
'radar.lead_score': 'درجة العميل المحتمل',
'crm.contacts': 'جهات الاتصال',
'crm.leads': 'العملاء المحتملون',
'crm.leads': 'العملاء',
'crm.follow_up': 'المتابعة',
'portal.unavailable': 'الصفحة غير متاحة حاليا',
'portal.retry': 'إعادة التحميل',
@@ -273,7 +273,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'radar.visitor_trace': 'Recorrido del visitante',
'radar.lead_score': 'Puntuación de interés',
'crm.contacts': 'Contactos',
'crm.leads': 'Oportunidades',
'crm.leads': 'Clientes',
'crm.follow_up': 'Seguimiento',
'portal.unavailable': 'Esta página no está disponible',
'portal.retry': 'Recargar',
@@ -329,7 +329,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'radar.visitor_trace': 'Besucherpfad',
'radar.lead_score': 'Lead-Score',
'crm.contacts': 'Kontakte',
'crm.leads': 'Leads',
'crm.leads': 'Kunden',
'crm.follow_up': 'Nachverfolgung',
'portal.unavailable': 'Diese Seite ist nicht verfügbar',
'portal.retry': 'Neu laden',
@@ -385,7 +385,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'radar.visitor_trace': 'Parcours visiteur',
'radar.lead_score': 'Score d’intérêt',
'crm.contacts': 'Contacts',
'crm.leads': 'Prospects',
'crm.leads': 'Clients',
'crm.follow_up': 'Suivi',
'portal.unavailable': 'Cette page est indisponible',
'portal.retry': 'Recharger',
@@ -441,7 +441,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'radar.visitor_trace': '訪問履歴',
'radar.lead_score': '関心度スコア',
'crm.contacts': '連絡先',
'crm.leads': 'リード',
'crm.leads': '顧客',
'crm.follow_up': 'フォロー記録',
'portal.unavailable': 'このページは現在ご利用いただけません',
'portal.retry': '再読み込み',

查看文件

@@ -10,13 +10,26 @@ let currentBaseUrl: string = 'http://127.0.0.1:9000';
if (typeof window !== 'undefined' && window.location != null) {
const protocol = window.location.protocol != null && window.location.protocol !== '' ? window.location.protocol : 'http:';
const hostname = window.location.hostname != null && window.location.hostname !== '' ? window.location.hostname : '127.0.0.1';
currentBaseUrl = `${protocol}//${hostname}:9000`;
const port = window.location.port;
if (port === '5173' || port === '5174' || port === '3000' || port === '8080') {
currentBaseUrl = `${protocol}//${hostname}:9000`;
} else if (port != null && port !== '' && port !== '80' && port !== '443') {
currentBaseUrl = `${protocol}//${hostname}:${port}`;
} else {
currentBaseUrl = `${protocol}//${hostname}`;
}
}
// #endif
let currentTimeout: number = 10000;
export function getBaseUrl(): string {
try {
const override = uni.getStorageSync('api_base_url');
if (override != null && String(override).trim() !== '') {
return String(override).trim();
}
} catch (e) {}
return currentBaseUrl;
}
@@ -35,22 +48,41 @@ export function setRequestTimeout(timeout: number): void {
export function getAuthToken(): string {
try {
const val = uni.getStorageSync('token');
return val != null ? String(val) : '';
} catch (e) {
return '';
}
if (val != null && String(val) !== '') {
return String(val);
}
} catch (e) {}
try {
if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') {
const local = localStorage.getItem('token');
if (local != null && String(local) !== '') {
return String(local);
}
}
} catch (e) {}
return '';
}
export function setAuthToken(token: string): void {
try {
uni.setStorageSync('token', token);
} catch (e) {}
try {
if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') {
localStorage.setItem('token', token);
}
} catch (e) {}
}
export function clearAuthToken(): void {
try {
uni.removeStorageSync('token');
} catch (e) {}
try {
if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') {
localStorage.removeItem('token');
}
} catch (e) {}
}
export function getTenantId(): string {

查看文件

@@ -85,7 +85,23 @@ export interface CardPortalDetail {
export type LeadStatus = 'new' | 'contacted' | 'hot' | 'won' | 'archived';
export type LeadSource = 'nfc' | 'qr' | 'link' | 'ai_chat';
export type LeadSource = 'nfc' | 'qr' | 'link' | 'ai_chat' | 'exchange';
export type CardContactSource = 'exchange' | 'collect' | 'scan';
export interface CardContact {
id?: string | number;
ownerUserId: number;
ownerCardId?: number | null;
peerCardId: number;
source: CardContactSource;
leadId?: number | null;
peerName?: string;
peerTitle?: string;
peerCompany?: string;
peerAvatarUrl?: string;
peerSlug?: string;
}
export interface LeadEntity {
id: string | number;
@@ -184,6 +200,16 @@ export interface OwnerPageQuery {
visitorId?: string;
}
export interface QuotaCheckResult {
used: number;
limit: number;
remaining: number;
percentage: number;
warning: boolean;
exceeded: boolean;
plan?: string;
}
export interface OwnerPageData<T> {
list: T[];
pagination: {

查看文件

@@ -1,375 +0,0 @@
/* 主题变量 */
.theme-light {
--list-background-color: #ffffff;
--background-color: #f8f8f8;
--active-color:#a0a0a0;
--active-background-color: #f8f8f8;
--text-color: #333333;
--arrow-color: #999999;
--border-color: rgba(0, 0, 0, .06);
}
.theme-dark {
--list-background-color: #2d2d2d;
--background-color: #1a1a1a;
--active-color:#8f8f8f;
--active-background-color: #3b3b3b;
--text-color: #ffffff;
--arrow-color: #ffffff;
--border-color: rgba(255, 255, 255, 0.1);
}
.uni-h1 {
font-size: 28px;
font-weight: bold;
border-bottom: 1px solid #ccc;
margin: 8px 0;
}
.uni-h2 {
font-size: 24px;
font-weight: bold;
border-bottom: 1px solid #ccc;
margin: 8px 0;
}
.uni-h3 {
font-size: 20px;
font-weight: bold;
margin: 8px 0;
}
.uni-h4 {
font-weight: bold;
margin: 8px 0;
}
.uni-theme-text{
color: var(--text-color);
}
.uni-padding-wrap {
padding: 0 15px;
}
.uni-title {
padding: 10px 0;
}
.uni-title-text {
font-size: 15px;
font-weight: bold;
color: var(--text-color, #333333);
}
.uni-subtitle-text {
color: #888;
font-size: 12px;
font-weight: bold;
margin-top: 5px;
}
.uni-info {
font-size: 12px;
color: #999;
}
.uni-common-mb {
margin-bottom: 15px;
}
.uni-common-pb {
padding-bottom: 15px;
}
.uni-common-pl {
padding-left: 15px;
}
.uni-common-mt {
margin-top: 15px;
}
.uni-hello-text {
color: var(--text-color, #333333);
line-height: 22px;
}
.uni-tips{
font-size: 13px;
color: #0958d9;
background-color: #e6f4ff;
border-left: 4px solid #91caff;
padding: 10px 12px;
border-radius: 4px;
margin: 10px;
}
.uni-list {
background-color: var(--list-background-color,#ffffff);
position: relative;
display: flex;
flex-direction: column;
border-bottom: 1px solid #c8c7cc;
}
.uni-list-cell {
position: relative;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.uni-list-cell-padding {
padding: 10px 15px;
}
.uni-list-cell-line {
border-bottom: 1px solid var(--border-color, rgba(0, 0, 0, .06));
}
.uni-list-cell-hover {
background-color: var(--active-background-color,#f8f8f8);
}
.uni-list-cell-pd {
padding: 11px 15px 11px 0;
}
.uni-list-cell-left {
padding: 0 15px 0 10px;
}
.uni-list-cell-db,
.uni-list-cell-right {
flex: 1;
}
.uni-list-cell-db-text {
width: 100%;
/* #ifdef WEB || MP */
word-break: break-all;
/* #endif */
}
.uni-label {
width: 105px;
}
.uni-input {
height: 25px;
padding: 8px 13px;
font-size: 14px;
flex: 1;
box-sizing: content-box;
background: var(--list-background-color,#ffffff);
}
/*
.uni-flex {
display: flex;
flex-direction: row;
}
*/
.uni-flex-item {
flex: 1;
}
.uni-row {
flex-direction: row;
}
.uni-column {
flex-direction: column;
}
.uni-bg-red {
background: #F76260;
}
.uni-bg-green {
background: #09BB07;
}
.uni-bg-blue {
background: #007AFF;
}
.uni-btn-v {
padding: 5px 0;
}
.uni-btn {
margin-top: 10px;
}
.uni-link {
color: #576B95;
font-size: 13px;
}
.uni-center {
flex-direction: row;
justify-content: center;
}
.uni-textarea {
padding: 9px;
line-height: 1.6;
font-size: 14px;
}
.uni-icon-size {
width: 14px;
height: 14px;
}
.uni-container {
padding: 15px;
background-color: var(--background-color,#f8f8f8);
}
.uni-header-logo {
padding: 15px 15px;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 5px;
}
.uni-header-image {
width: 80px;
height: 80px;
}
.uni-text-box {
margin-bottom: 20px;
}
.hello-text {
color: var(--text-color, #333333);
font-size: 14px;
line-height: 20px;
}
.uni-panel {
margin-bottom: 12px;
}
.text-disabled {
color: var(--active-color)!important;
}
/* --tab-bar-begin-- */
.uni-panel-h {
background-color: var(--list-background-color);
flex-direction: row !important;
justify-content: space-between !important;
align-items: center !important;
padding: 12px;
}
.uni-panel-h-on {
background-color: #f0f0f0;
}
.uni-panel-text {
color: var(--text-color, #333333);
font-size: 14px;
font-weight: normal;
}
.uni-navigate-item {
flex-direction: row;
align-items: center;
background-color: var(--list-background-color);
border-top-style: solid;
border-top-color: var(--border-color);
border-top-width: 1px;
padding: 12px 16px;
justify-content: space-between;
/* #ifdef WEB */
/* cursor: pointer; */
/* #endif */
}
.is--active {
background-color: var(--active-background-color);
}
.uni-navigate-text {
color: var(--text-color, #333333);
font-size: 14px;
font-weight: normal;
width: 94%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* left-windows */
.left-win-active {
color: #007AFF !important;
}
/* --tab-bar-end-- */
/* #ifdef APP */
.page-scroll-view {
flex: 1;
}
/* #endif */
/* web端适配暗黑主题 */
/* #ifdef WEB */
@media (prefers-color-scheme: dark) {
/* 增加deep,穿透所有页面和组件 */
:deep(body),
:deep(uni-page-body) {
color: rgba(0, 0, 0, 0.9);
}
.uni-top-window .tab-bar-flex :deep(.uni-tabbar){
background-color: #2d2d2d!important;
}
/* 设置滚动条整体宽度和背景色 */
::-webkit-scrollbar {
width: 8px;
background: #464646;
}
/* 设置滚动条滑块颜色 */
::-webkit-scrollbar-thumb {
background: #aaaaaa;
border-radius: 4px;
}
/* 鼠标悬停在滑块上时的颜色 */
::-webkit-scrollbar-thumb:hover {
background: #818181;
}
/* 滚动条轨道颜色 */
::-webkit-scrollbar-track {
background: #464646;
border-radius: 4px;
}
}
/* #endif */
/* #ifdef MP */
page {
color: rgba(0, 0, 0, 0.9);
}
/* #endif */
.pl-4{
padding-left: 4px;
}
.pr-4{
padding-right: 4px;
}
.margin-10{
margin: 10px;
}