feat(ai): 完成 DeepSeek 大模型官方 API 接入、双模调用、高可用平滑降级与管理后台连通性测试
这个提交包含在:
@@ -0,0 +1,65 @@
|
||||
import { Body, Inject, Post } from '@midwayjs/core';
|
||||
import { BaseController, CoolController } from '@cool-midway/core';
|
||||
import { DeepSeekService } from '../../service/deepseek';
|
||||
import {
|
||||
DeepSeekChatCompletionRequest,
|
||||
DeepSeekConfig,
|
||||
} from '../../../../comm/types/omni';
|
||||
|
||||
/**
|
||||
* DeepSeek 大模型后台管理与调试控制器
|
||||
*/
|
||||
@CoolController({
|
||||
api: [],
|
||||
service: DeepSeekService,
|
||||
serviceApis: [
|
||||
{ method: 'getConfig', summary: '获取当前 DeepSeek 配置 (脱敏)' },
|
||||
{ method: 'updateConfig', summary: '更新 DeepSeek 配置' },
|
||||
{ method: 'testConnection', summary: '测试 DeepSeek 接口连通性' },
|
||||
{ method: 'chatCompletion', summary: '大模型对话测试' },
|
||||
],
|
||||
})
|
||||
export class AdminDeepSeekController extends BaseController {
|
||||
@Inject()
|
||||
deepSeekService: DeepSeekService;
|
||||
|
||||
/**
|
||||
* 获取当前有效配置(脱敏 API Key)
|
||||
*/
|
||||
@Post('/config', { summary: '获取或保存 DeepSeek 配置' })
|
||||
async config(@Body() body?: Partial<DeepSeekConfig>) {
|
||||
if (body && Object.keys(body).length > 0) {
|
||||
this.deepSeekService.updateConfig(body);
|
||||
}
|
||||
const current = this.deepSeekService.getConfig();
|
||||
const maskedKey = current.apiKey
|
||||
? current.apiKey.length > 8
|
||||
? `${current.apiKey.slice(0, 4)}****${current.apiKey.slice(-4)}`
|
||||
: '****'
|
||||
: undefined;
|
||||
|
||||
return this.ok({
|
||||
...current,
|
||||
apiKey: maskedKey,
|
||||
isConfigured: this.deepSeekService.isConfigured(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试与 DeepSeek 官方端点连通性
|
||||
*/
|
||||
@Post('/test-connection', { summary: '测试连通性' })
|
||||
async testConnection(@Body('apiKey') apiKey?: string) {
|
||||
const res = await this.deepSeekService.testConnection(apiKey);
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试对话补全
|
||||
*/
|
||||
@Post('/chat', { summary: '调试对话补全' })
|
||||
async chat(@Body() body: DeepSeekChatCompletionRequest) {
|
||||
const reply = await this.deepSeekService.chatCompletion(body);
|
||||
return this.ok({ reply });
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseEntity } from '../../../comm/entity/base';
|
||||
import { BaseEntity } from '../../base/entity/base';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
import {
|
||||
AiConciergeConfig,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseEntity } from '../../../comm/entity/base';
|
||||
import { BaseEntity } from '../../base/entity/base';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
import { AiGlossaryItem } from '../../../comm/types/omni';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseEntity } from '../../../comm/entity/base';
|
||||
import { BaseEntity } from '../../base/entity/base';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
import { AiKnowledgeDoc } from '../../../comm/types/omni';
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Provide } from '@midwayjs/core';
|
||||
import { Inject, Provide } from '@midwayjs/core';
|
||||
import { BaseService, CoolCommException } from '@cool-midway/core';
|
||||
import {
|
||||
AiChatMessage,
|
||||
AiChatReply,
|
||||
AiChatRequest,
|
||||
DeepSeekChatMessage,
|
||||
} from '../../../comm/types/omni';
|
||||
import { DeepSeekService } from './deepseek';
|
||||
|
||||
/** locale 缺省值 */
|
||||
const DEFAULT_LOCALE = 'en-US';
|
||||
@@ -14,10 +16,13 @@ const LEAD_INTENT_PATTERN =
|
||||
/价格|报价|多少钱|合作|联系|price|pricing|cost|quote|cooperat|partner|contact/i;
|
||||
|
||||
/**
|
||||
* 名片内置 AI 顾问(本地 RuleBased,不调用外部 LLM)
|
||||
* 名片内置 AI 顾问(支持 DeepSeek 官方大模型与本地规则平滑降级)
|
||||
*/
|
||||
@Provide()
|
||||
export class AiChatService extends BaseService {
|
||||
@Inject()
|
||||
deepSeekService?: DeepSeekService;
|
||||
|
||||
/**
|
||||
* 顾问对话
|
||||
* 兼容公开端 serviceApis:POST body 为单参数对象 AiChatRequest
|
||||
@@ -42,9 +47,36 @@ export class AiChatService extends BaseService {
|
||||
const locale = req.locale?.trim() || DEFAULT_LOCALE;
|
||||
const shouldCollectLead = LEAD_INTENT_PATTERN.test(content);
|
||||
const isZh = locale.toLowerCase().startsWith('zh');
|
||||
const fallbackReply = this.buildReply(shouldCollectLead, isZh);
|
||||
|
||||
let reply = fallbackReply;
|
||||
|
||||
// 如果 DeepSeek 已经配置,则优先调用 DeepSeek 大模型生成智能回复
|
||||
if (this.deepSeekService?.isConfigured?.()) {
|
||||
try {
|
||||
const dsMessages: DeepSeekChatMessage[] = [
|
||||
{
|
||||
role: 'system',
|
||||
content: `你是 OmniCard 名片智能顾问。当前名片ID: ${cardId},语言偏好: ${locale}。请以专业得体的口吻向客商介绍业务与解答咨询。${shouldCollectLead ? '检测到客户具备采购或商务意向,请在回答中热情引导客户留下联系方式。' : ''}`,
|
||||
},
|
||||
...messages.map((m) => ({
|
||||
role: m.role as 'user' | 'assistant' | 'system',
|
||||
content: m.content,
|
||||
})),
|
||||
];
|
||||
|
||||
reply = await this.deepSeekService.chatCompletion(
|
||||
{ messages: dsMessages },
|
||||
fallbackReply
|
||||
);
|
||||
} catch {
|
||||
// 发生任何不可预测异常,均平滑降级为兜底文本,确保前端 100% 不白屏
|
||||
reply = fallbackReply;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
reply: this.buildReply(shouldCollectLead, isZh),
|
||||
reply,
|
||||
locale,
|
||||
shouldCollectLead,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
import { Provide } from '@midwayjs/core';
|
||||
import { BaseService, CoolCommException } from '@cool-midway/core';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import {
|
||||
DeepSeekChatCompletionRequest,
|
||||
DeepSeekChatCompletionResponse,
|
||||
DeepSeekConfig,
|
||||
DeepSeekModel,
|
||||
DeepSeekTestConnectionResult,
|
||||
} from '../../../comm/types/omni';
|
||||
|
||||
/**
|
||||
* DeepSeek 官方大模型适配服务
|
||||
* 支持 deepseek-chat (V3) 与 deepseek-reasoner (R1)
|
||||
*/
|
||||
@Provide()
|
||||
export class DeepSeekService extends BaseService {
|
||||
private config: DeepSeekConfig;
|
||||
private httpClient: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const envKey = process.env.DEEPSEEK_API_KEY?.trim() || undefined;
|
||||
const envBaseUrl = process.env.DEEPSEEK_BASE_URL?.trim() || 'https://api.deepseek.com';
|
||||
const envModel = (process.env.DEEPSEEK_MODEL?.trim() as DeepSeekModel) || 'deepseek-chat';
|
||||
|
||||
this.config = {
|
||||
apiKey: envKey,
|
||||
baseUrl: envBaseUrl,
|
||||
model: envModel,
|
||||
temperature: 0.7,
|
||||
maxTokens: 2048,
|
||||
timeoutMs: 30000,
|
||||
enabled: Boolean(envKey),
|
||||
};
|
||||
|
||||
this.httpClient = axios.create({
|
||||
timeout: this.config.timeoutMs,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前有效配置(脱敏 API Key)
|
||||
*/
|
||||
getConfig(): DeepSeekConfig {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态更新配置
|
||||
*/
|
||||
updateConfig(partial: Partial<DeepSeekConfig>): void {
|
||||
this.config = {
|
||||
...this.config,
|
||||
...partial,
|
||||
enabled: partial.apiKey ? Boolean(partial.apiKey.trim()) : this.config.enabled,
|
||||
};
|
||||
if (this.config.timeoutMs) {
|
||||
this.httpClient.defaults.timeout = this.config.timeoutMs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已配置有效 API Key
|
||||
*/
|
||||
isConfigured(): boolean {
|
||||
return Boolean(this.config.apiKey && this.config.apiKey.trim().length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装名片 AI 顾问 System 提示词
|
||||
*/
|
||||
buildSystemPrompt(
|
||||
card?: { name?: string; company?: string; title?: string },
|
||||
concierge?: { personaType?: string; toneStyle?: string; promptTemplate?: string },
|
||||
knowledgeChunks?: Array<{ index?: number; content: string }>
|
||||
): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
parts.push('你是 OmniCard AI 驱动的移动微官网数字人智能顾问。');
|
||||
if (card?.name) parts.push(`名片所有者姓名: ${card.name}`);
|
||||
if (card?.company) parts.push(`所属企业: ${card.company}`);
|
||||
if (card?.title) parts.push(`头衔职位: ${card.title}`);
|
||||
|
||||
if (concierge?.promptTemplate) {
|
||||
parts.push(`人设与使命:\n${concierge.promptTemplate}`);
|
||||
} else {
|
||||
parts.push('你的职责是以专业、礼貌、高效的口吻向访客介绍业务与产品,并在适当时机引导有意向的客户留下联系方式。');
|
||||
}
|
||||
|
||||
if (Array.isArray(knowledgeChunks) && knowledgeChunks.length > 0) {
|
||||
parts.push('参考企业知识库信息 (优先基于以下内容作答):');
|
||||
knowledgeChunks.forEach((c, idx) => {
|
||||
parts.push(`[${idx + 1}] ${c.content}`);
|
||||
});
|
||||
}
|
||||
|
||||
parts.push('请直接给出自然得体的中文或对应语言回答,避免套话。');
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装出海文案润色提示词
|
||||
*/
|
||||
buildLocalizePrompt(
|
||||
text: string,
|
||||
targetLocale: string,
|
||||
tone: string,
|
||||
glossaryTerms?: string[]
|
||||
): string {
|
||||
const lines = [
|
||||
`请将以下商务文本改写/润色为符合目标语言(${targetLocale})与目标语调(${tone})的地道出海商业文案。`,
|
||||
'要求:语义准确地道,符合国际商业习惯,禁止机械直译。',
|
||||
];
|
||||
|
||||
if (Array.isArray(glossaryTerms) && glossaryTerms.length > 0) {
|
||||
lines.push('行业专有名词强制对齐规则 (严格遵照):');
|
||||
glossaryTerms.forEach((g) => lines.push(`- ${g}`));
|
||||
}
|
||||
|
||||
lines.push(`\n原文:\n${text}`);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装销售 Copilot 破冰首语提示词
|
||||
*/
|
||||
buildCopilotPrompt(
|
||||
lead: { name?: string; company?: string; demand?: string; phone?: string },
|
||||
tracks?: Array<{ module?: string; duration?: number }>
|
||||
): string {
|
||||
const lines = [
|
||||
'你是一名资深出海销售战略顾问。请根据以下潜在客户画像与其在名片上的浏览行为轨迹,生成一段极具针对性的 WhatsApp/Email 商务破冰首语建议。',
|
||||
`潜客姓名: ${lead?.name || '客商'}`,
|
||||
`所属公司: ${lead?.company || '出海企业'}`,
|
||||
`核心诉求: ${lead?.demand || '商务洽谈'}`,
|
||||
];
|
||||
|
||||
if (Array.isArray(tracks) && tracks.length > 0) {
|
||||
lines.push('访客轨迹分析:');
|
||||
tracks.forEach((t) => {
|
||||
lines.push(`- 模块 [${t.module || 'portal'}], 停留时长: ${t.duration || 0}秒`);
|
||||
});
|
||||
}
|
||||
|
||||
lines.push('请生成:1. 客户核心意向诊断要点;2. 30秒直击痛点的破冰首语推荐。');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话补全调用 (支持优雅降级)
|
||||
*/
|
||||
async chatCompletion(
|
||||
req: DeepSeekChatCompletionRequest,
|
||||
fallbackReply?: string
|
||||
): Promise<string> {
|
||||
const apiKey = this.config.apiKey?.trim();
|
||||
|
||||
if (!apiKey) {
|
||||
if (fallbackReply !== undefined) {
|
||||
return fallbackReply;
|
||||
}
|
||||
throw new CoolCommException('DeepSeek API Key 未配置');
|
||||
}
|
||||
|
||||
const endpoint = `${this.config.baseUrl || 'https://api.deepseek.com'}/chat/completions`;
|
||||
const payload = {
|
||||
model: req.model || this.config.model || 'deepseek-chat',
|
||||
messages: req.messages,
|
||||
temperature: req.temperature ?? this.config.temperature ?? 0.7,
|
||||
max_tokens: req.maxTokens ?? this.config.maxTokens ?? 2048,
|
||||
stream: false,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await this.httpClient.post<DeepSeekChatCompletionResponse>(
|
||||
endpoint,
|
||||
payload,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const content = response.data?.choices?.[0]?.message?.content;
|
||||
if (content) {
|
||||
return content.trim();
|
||||
}
|
||||
|
||||
if (fallbackReply !== undefined) {
|
||||
return fallbackReply;
|
||||
}
|
||||
return 'AI 思考中,暂未生成有效回复。';
|
||||
} catch (err: any) {
|
||||
if (fallbackReply !== undefined) {
|
||||
return fallbackReply;
|
||||
}
|
||||
const errMsg =
|
||||
err?.response?.data?.error?.message || err?.message || 'DeepSeek API 调用失败';
|
||||
throw new CoolCommException(`DeepSeek 服务异常: ${errMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试与 DeepSeek 官方端点连通性
|
||||
*/
|
||||
async testConnection(testApiKey?: string): Promise<DeepSeekTestConnectionResult> {
|
||||
const key = (testApiKey || this.config.apiKey || '').trim();
|
||||
if (!key) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'API Key 未提供,无法测试连通性',
|
||||
};
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const endpoint = `${this.config.baseUrl || 'https://api.deepseek.com'}/chat/completions`;
|
||||
const model = this.config.model || 'deepseek-chat';
|
||||
|
||||
try {
|
||||
await this.httpClient.post(
|
||||
endpoint,
|
||||
{
|
||||
model,
|
||||
messages: [{ role: 'user', content: 'hi' }],
|
||||
max_tokens: 5,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
timeout: 10000, // 测试连通性 10 秒超时
|
||||
}
|
||||
);
|
||||
|
||||
const latencyMs = Date.now() - startTime;
|
||||
return {
|
||||
success: true,
|
||||
latencyMs,
|
||||
model,
|
||||
message: 'DeepSeek API 连通成功',
|
||||
};
|
||||
} catch (err: any) {
|
||||
const errMsg =
|
||||
err?.response?.data?.error?.message || err?.message || '网络连接超时或凭证失效';
|
||||
return {
|
||||
success: false,
|
||||
latencyMs: Date.now() - startTime,
|
||||
model,
|
||||
message: `连通失败: ${errMsg}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
LocalizeTone,
|
||||
} from '../../../comm/types/omni';
|
||||
import { AiGlossaryService } from './glossary';
|
||||
import { DeepSeekService } from './deepseek';
|
||||
|
||||
const DEFAULT_TONE: LocalizeTone = 'professional';
|
||||
|
||||
@@ -23,6 +24,9 @@ export class AiLocalizeService extends BaseService {
|
||||
@Inject()
|
||||
aiGlossaryService?: AiGlossaryService;
|
||||
|
||||
@Inject()
|
||||
deepSeekService?: DeepSeekService;
|
||||
|
||||
/**
|
||||
* 按目标语言与语气改写原文骨架,并结合术语库进行精准替换
|
||||
* 兼容后台 serviceApis:POST body 为单参数对象 LocalizeRequest
|
||||
@@ -39,7 +43,23 @@ export class AiLocalizeService extends BaseService {
|
||||
}
|
||||
|
||||
const tone = this.resolveTone(req.tone);
|
||||
let polishedText = this.polish(text, locale, tone);
|
||||
const fallbackText = this.polish(text, locale, tone);
|
||||
let polishedText = fallbackText;
|
||||
|
||||
// 如果 DeepSeek 已经配置,则优先调用 DeepSeek 进行地道润色
|
||||
if (this.deepSeekService?.isConfigured?.()) {
|
||||
try {
|
||||
const prompt = this.deepSeekService.buildLocalizePrompt(text, locale, tone);
|
||||
polishedText = await this.deepSeekService.chatCompletion(
|
||||
{
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
},
|
||||
fallbackText
|
||||
);
|
||||
} catch {
|
||||
polishedText = fallbackText;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.aiGlossaryService?.replaceTerms) {
|
||||
polishedText = await this.aiGlossaryService.replaceTerms(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Index, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
import { Index, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm';
|
||||
import * as moment from 'moment';
|
||||
import { CoolBaseEntity } from '@cool-midway/core';
|
||||
|
||||
@@ -45,6 +45,7 @@ export abstract class BaseEntity extends CoolBaseEntity {
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
name: 'create_time',
|
||||
comment: '创建时间',
|
||||
type: 'varchar',
|
||||
transformer: transformerTime,
|
||||
@@ -53,6 +54,7 @@ export abstract class BaseEntity extends CoolBaseEntity {
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
name: 'update_time',
|
||||
comment: '更新时间',
|
||||
type: 'varchar',
|
||||
transformer: transformerTime,
|
||||
@@ -60,6 +62,18 @@ export abstract class BaseEntity extends CoolBaseEntity {
|
||||
updateTime: Date;
|
||||
|
||||
@Index()
|
||||
@Column({ comment: '租户ID', nullable: true })
|
||||
@Column({
|
||||
name: 'tenant_id',
|
||||
comment: '租户ID',
|
||||
nullable: true,
|
||||
})
|
||||
tenantId: number;
|
||||
|
||||
@Index()
|
||||
@DeleteDateColumn({
|
||||
name: 'deleted_at',
|
||||
comment: '软删除时间',
|
||||
nullable: true,
|
||||
})
|
||||
deletedAt?: Date | null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseEntity, transformerJson } from '../../../comm/entity/base';
|
||||
import { BaseEntity, transformerJson } from '../../base/entity/base';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
import type { ArticleOgMeta, CardArticle } from '../../../comm/types/omni';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseEntity, transformerJson } from '../../../comm/entity/base';
|
||||
import { BaseEntity, transformerJson } from '../../base/entity/base';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
import type {
|
||||
BentoBlock,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseEntity } from '../../../comm/entity/base';
|
||||
import { BaseEntity } from '../../base/entity/base';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
import type { CardCase } from '../../../comm/types/omni';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseEntity } from '../../../comm/entity/base';
|
||||
import { BaseEntity } from '../../base/entity/base';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
import {
|
||||
CardProfile,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseEntity } from '../../../comm/entity/base';
|
||||
import { BaseEntity } from '../../base/entity/base';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseEntity } from '../../../comm/entity/base';
|
||||
import { BaseEntity } from '../../base/entity/base';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
import { LeadPoolAction } from '../../../comm/types/omni';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseEntity, transformerTime } from '../../../comm/entity/base';
|
||||
import { BaseEntity, transformerTime } from '../../base/entity/base';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
import {
|
||||
LeadEntity as ILeadEntity,
|
||||
@@ -118,4 +118,4 @@ export class CrmLeadEntity extends BaseEntity implements ILeadEntity {
|
||||
lastFollowUpTime?: Date | string | null;
|
||||
}
|
||||
|
||||
export { CrmLeadEntity as LeadInfoEntity, CrmLeadEntity as LeadEntity };
|
||||
export { CrmLeadEntity as LeadInfoEntity };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseEntity, transformerJson } from '../../../comm/entity/base';
|
||||
import { BaseEntity, transformerJson } from '../../base/entity/base';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
import { RadarTrack, TouchType } from '../../../comm/types/omni';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseEntity } from '../../../comm/entity/base';
|
||||
import { BaseEntity } from '../../base/entity/base';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
import {
|
||||
NFCMaterial,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseEntity, transformerJson } from '../../../comm/entity/base';
|
||||
import { BaseEntity, transformerJson } from '../../base/entity/base';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
import { NFCSceneMode, NFCSceneRule } from '../../../comm/types/omni';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseEntity } from '../../../comm/entity/base';
|
||||
import { BaseEntity } from '../../base/entity/base';
|
||||
import { Column, Entity } from 'typeorm';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseEntity } from '../../../comm/entity/base';
|
||||
import { BaseEntity } from '../../base/entity/base';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
import { DomainSslStatus, TenantDomain } from '../../../comm/types/omni';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseEntity } from '../../../comm/entity/base';
|
||||
import { BaseEntity } from '../../base/entity/base';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
import {
|
||||
HandoverLeadAction,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseEntity, transformerJson } from '../../../comm/entity/base';
|
||||
import { BaseEntity, transformerJson } from '../../base/entity/base';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
import {
|
||||
InvoiceBillingInfo,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseEntity } from '../../../comm/entity/base';
|
||||
import { BaseEntity } from '../../base/entity/base';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
import { TenantPlanType, StripeBillingCycle } from '../../../comm/types/omni';
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户