mirror of
https://github.com/vastxie/99AI.git
synced 2026-04-23 18:54:25 +08:00
v4.3.0
This commit is contained in:
867
service/src/modules/aiTool/chat/chat.service.ts
Normal file
867
service/src/modules/aiTool/chat/chat.service.ts
Normal file
@@ -0,0 +1,867 @@
|
||||
import { handleError } from '@/common/utils';
|
||||
import { correctApiBaseUrl } from '@/common/utils/correctApiBaseUrl';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import OpenAI from 'openai';
|
||||
import { GlobalConfigService } from '../../globalConfig/globalConfig.service';
|
||||
import { NetSearchService } from '../search/netSearch.service';
|
||||
// 引入其他需要的模块或服务
|
||||
|
||||
@Injectable()
|
||||
export class OpenAIChatService {
|
||||
constructor(
|
||||
private readonly globalConfigService: GlobalConfigService,
|
||||
private readonly netSearchService: NetSearchService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 处理深度思考逻辑
|
||||
* @param messagesHistory 消息历史
|
||||
* @param inputs 输入参数
|
||||
* @param result 结果对象
|
||||
* @returns 是否应该终止请求
|
||||
*/
|
||||
private async handleDeepThinking(
|
||||
messagesHistory: any,
|
||||
inputs: {
|
||||
apiKey: any;
|
||||
model: any;
|
||||
proxyUrl: any;
|
||||
timeout: any;
|
||||
usingDeepThinking?: boolean;
|
||||
deepThinkingModel?: string;
|
||||
deepThinkingUrl?: string;
|
||||
deepThinkingKey?: string;
|
||||
searchResults?: any[];
|
||||
deepThinkingType?: any;
|
||||
abortController: AbortController;
|
||||
onProgress?: (data: any) => void;
|
||||
},
|
||||
result: any,
|
||||
): Promise<boolean> {
|
||||
const {
|
||||
apiKey,
|
||||
model,
|
||||
proxyUrl,
|
||||
timeout,
|
||||
usingDeepThinking,
|
||||
searchResults,
|
||||
abortController,
|
||||
deepThinkingType,
|
||||
onProgress,
|
||||
} = inputs;
|
||||
|
||||
const {
|
||||
openaiBaseUrl,
|
||||
openaiBaseKey,
|
||||
openaiBaseModel,
|
||||
deepThinkingUrl,
|
||||
deepThinkingKey,
|
||||
deepThinkingModel,
|
||||
} = await this.globalConfigService.getConfigs([
|
||||
'openaiBaseUrl',
|
||||
'openaiBaseKey',
|
||||
'openaiBaseModel',
|
||||
'deepThinkingUrl',
|
||||
'deepThinkingKey',
|
||||
'deepThinkingModel',
|
||||
]);
|
||||
|
||||
// 如果不使用深度思考且不是DeepSeek模型,直接返回
|
||||
if (!usingDeepThinking && deepThinkingType !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const deepUrl = deepThinkingType === 2 ? proxyUrl : deepThinkingUrl || openaiBaseUrl;
|
||||
const deepKey = deepThinkingType === 2 ? apiKey : deepThinkingKey || openaiBaseKey;
|
||||
const deepModel = deepThinkingType === 2 ? model : deepThinkingModel || openaiBaseModel;
|
||||
|
||||
let shouldEndThinkStream = false;
|
||||
let thinkingSourceType = null; // 'reasoning_content' 或 'think_tag'
|
||||
|
||||
// 处理所有消息中的imageUrl类型
|
||||
const processedMessages = JSON.parse(JSON.stringify(messagesHistory)).map((message: any) => {
|
||||
if (message.role === 'user' && Array.isArray(message.content)) {
|
||||
// 将带有image_url类型的内容转换为普通文本
|
||||
message.content = message.content
|
||||
.filter((item: any) => item.type !== 'image_url')
|
||||
.map((item: any) => item.text || item)
|
||||
.join('');
|
||||
}
|
||||
return message;
|
||||
});
|
||||
|
||||
// 添加文件向量搜索、图片描述和MCP工具结果到system消息
|
||||
const systemMessageIndex = processedMessages.findIndex((msg: any) => msg.role === 'system');
|
||||
let additionalContent = '';
|
||||
|
||||
// 如果有网络搜索结果,添加到system消息中
|
||||
if (searchResults && searchResults.length > 0) {
|
||||
// 将 searchResult 转换为 JSON 字符串
|
||||
let searchPrompt = JSON.stringify(searchResults, null, 2);
|
||||
|
||||
additionalContent += `\n\n以下是网络搜索结果(请基于这些信息回答用户问题,这些信息比你的训练数据更新):\n${searchPrompt}`;
|
||||
}
|
||||
|
||||
// 将额外内容添加到system消息中
|
||||
if (systemMessageIndex !== -1) {
|
||||
processedMessages[systemMessageIndex].content += additionalContent;
|
||||
} else if (additionalContent) {
|
||||
processedMessages.unshift({
|
||||
role: 'system',
|
||||
content: additionalContent,
|
||||
});
|
||||
}
|
||||
|
||||
const correctedDeepUrl = await correctApiBaseUrl(deepUrl);
|
||||
const thinkOpenai = new OpenAI({
|
||||
apiKey: deepKey,
|
||||
baseURL: correctedDeepUrl,
|
||||
timeout: timeout * 5,
|
||||
});
|
||||
|
||||
Logger.debug(
|
||||
`思考流请求 - Messages: ${JSON.stringify(processedMessages)}`,
|
||||
'OpenAIChatService',
|
||||
);
|
||||
|
||||
// 构建请求配置
|
||||
const requestConfig: any = {
|
||||
model: deepModel,
|
||||
messages: processedMessages,
|
||||
stream: true,
|
||||
};
|
||||
|
||||
// 如果是 grok-3-mini-latest 模型,添加 reasoning_effort 参数
|
||||
// if (deepModel === 'grok-3-mini-latest') {
|
||||
// requestConfig.reasoning_effort = 'high';
|
||||
// Logger.debug('为grok-3-mini-latest模型添加reasoning_effort=high参数', 'OpenAIChatService');
|
||||
// }
|
||||
|
||||
const stream = await thinkOpenai.chat.completions.create(requestConfig, {
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
// @ts-ignore - 忽略TypeScript错误,因为我们知道stream是可迭代的
|
||||
for await (const chunk of stream) {
|
||||
if (abortController.signal.aborted || shouldEndThinkStream) {
|
||||
break;
|
||||
}
|
||||
const delta = chunk.choices[0]?.delta;
|
||||
Logger.debug(`思考流delta: ${JSON.stringify(delta)}`, 'OpenAIChatService');
|
||||
const content = delta?.content;
|
||||
const reasoning_content = (delta as any)?.reasoning_content || '';
|
||||
|
||||
// 根据已确定的思考流来源类型处理数据
|
||||
if (thinkingSourceType === 'reasoning_content') {
|
||||
// 已确定使用reasoning_content字段
|
||||
if (reasoning_content) {
|
||||
Logger.debug(
|
||||
`继续接收reasoning_content思考流: ${reasoning_content}`,
|
||||
'OpenAIChatService',
|
||||
);
|
||||
result.reasoning_content = [
|
||||
{
|
||||
type: 'text',
|
||||
text: reasoning_content,
|
||||
},
|
||||
];
|
||||
result.full_reasoning_content += reasoning_content;
|
||||
onProgress?.({
|
||||
reasoning_content: result.reasoning_content,
|
||||
});
|
||||
} else if (content && !content.includes('<think>')) {
|
||||
// 如果出现普通content,对于非DeepSeek模型终止思考流
|
||||
// 对于DeepSeek模型,将内容作为正常响应处理
|
||||
Logger.debug(`reasoning_content模式下收到普通content: ${content}`, 'OpenAIChatService');
|
||||
if (deepThinkingType === 2) {
|
||||
result.content = [
|
||||
{
|
||||
type: 'text',
|
||||
text: content,
|
||||
},
|
||||
];
|
||||
result.full_content += content;
|
||||
onProgress?.({
|
||||
content: result.content,
|
||||
});
|
||||
} else {
|
||||
shouldEndThinkStream = true;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
} else if (thinkingSourceType === 'think_tag') {
|
||||
// 已确定使用think标签
|
||||
if (content) {
|
||||
if (content.includes('</think>')) {
|
||||
// 如果包含结束标签,提取剩余思考内容
|
||||
Logger.debug(`检测到</think>标签,思考流结束`, 'OpenAIChatService');
|
||||
const regex = /([\s\S]*?)<\/think>([\s\S]*)/;
|
||||
const matches = content.match(regex);
|
||||
|
||||
if (matches) {
|
||||
const thinkContent = matches[1] || '';
|
||||
const remainingContent = matches[2] || '';
|
||||
|
||||
if (thinkContent) {
|
||||
result.reasoning_content = [
|
||||
{
|
||||
type: 'text',
|
||||
text: thinkContent,
|
||||
},
|
||||
];
|
||||
result.full_reasoning_content += thinkContent;
|
||||
onProgress?.({
|
||||
reasoning_content: result.reasoning_content,
|
||||
});
|
||||
}
|
||||
|
||||
// 对于DeepSeek模型,如果有剩余内容,作为正常响应处理
|
||||
if (deepThinkingType === 2 && remainingContent) {
|
||||
result.content = [
|
||||
{
|
||||
type: 'text',
|
||||
text: remainingContent,
|
||||
},
|
||||
];
|
||||
result.full_content += remainingContent;
|
||||
onProgress?.({
|
||||
content: result.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 对于非DeepSeek模型,终止思考流
|
||||
// 对于DeepSeek模型,只标记思考流结束,但继续处理后续内容
|
||||
if (deepThinkingType !== 2) {
|
||||
shouldEndThinkStream = true;
|
||||
} else {
|
||||
thinkingSourceType = 'normal_content';
|
||||
}
|
||||
} else {
|
||||
// 继续接收think标签内的思考内容
|
||||
Logger.debug(`继续接收think标签思考流: ${content}`, 'OpenAIChatService');
|
||||
result.reasoning_content = [
|
||||
{
|
||||
type: 'text',
|
||||
text: content,
|
||||
},
|
||||
];
|
||||
result.full_reasoning_content += content;
|
||||
onProgress?.({
|
||||
reasoning_content: result.reasoning_content,
|
||||
});
|
||||
}
|
||||
}
|
||||
continue;
|
||||
} else if (thinkingSourceType === 'normal_content' && deepThinkingType === 2) {
|
||||
// DeepSeek模型在思考流结束后的正常内容处理
|
||||
if (content) {
|
||||
result.content = [
|
||||
{
|
||||
type: 'text',
|
||||
text: content,
|
||||
},
|
||||
];
|
||||
result.full_content += content;
|
||||
onProgress?.({
|
||||
content: result.content,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 尚未确定思考流来源类型,进行检测
|
||||
if (reasoning_content) {
|
||||
// 确定使用reasoning_content字段作为思考流
|
||||
Logger.debug(
|
||||
`首次检测到reasoning_content,确定使用reasoning_content思考流方式: ${reasoning_content}`,
|
||||
'OpenAIChatService',
|
||||
);
|
||||
thinkingSourceType = 'reasoning_content';
|
||||
result.reasoning_content = [
|
||||
{
|
||||
type: 'text',
|
||||
text: reasoning_content,
|
||||
},
|
||||
];
|
||||
result.full_reasoning_content += reasoning_content;
|
||||
onProgress?.({
|
||||
reasoning_content: result.reasoning_content,
|
||||
});
|
||||
} else if (content) {
|
||||
if (content.includes('<think>')) {
|
||||
// 确定使用think标签作为思考流
|
||||
Logger.debug(`首次检测到<think>标签,确定使用think标签思考流方式`, 'OpenAIChatService');
|
||||
thinkingSourceType = 'think_tag';
|
||||
|
||||
// 提取第一个块中的内容
|
||||
const thinkContent = content.replace(/<think>/, '');
|
||||
if (thinkContent) {
|
||||
Logger.debug(`从<think>标签中提取的初始思考内容: ${thinkContent}`, 'OpenAIChatService');
|
||||
result.reasoning_content = [
|
||||
{
|
||||
type: 'text',
|
||||
text: thinkContent,
|
||||
},
|
||||
];
|
||||
result.full_reasoning_content += thinkContent;
|
||||
onProgress?.({
|
||||
reasoning_content: result.reasoning_content,
|
||||
});
|
||||
|
||||
// 如果已经包含了</think>标签,提取思考内容和剩余内容
|
||||
if (content.includes('</think>')) {
|
||||
Logger.debug('在首个块中检测到</think>标签', 'OpenAIChatService');
|
||||
|
||||
const regex = /<think>([\s\S]*?)<\/think>([\s\S]*)/;
|
||||
const matches = content.match(regex);
|
||||
|
||||
if (matches) {
|
||||
const fullThinkContent = matches[1] || '';
|
||||
const remainingContent = matches[2] || '';
|
||||
|
||||
// 更新思考内容
|
||||
result.reasoning_content = [
|
||||
{
|
||||
type: 'text',
|
||||
text: fullThinkContent,
|
||||
},
|
||||
];
|
||||
result.full_reasoning_content = fullThinkContent;
|
||||
onProgress?.({
|
||||
reasoning_content: result.reasoning_content,
|
||||
});
|
||||
|
||||
// 对于DeepSeek模型,如果有剩余内容,作为正常响应处理
|
||||
if (deepThinkingType === 2 && remainingContent) {
|
||||
result.content = [
|
||||
{
|
||||
type: 'text',
|
||||
text: remainingContent,
|
||||
},
|
||||
];
|
||||
result.full_content += remainingContent;
|
||||
onProgress?.({
|
||||
content: result.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 对于非DeepSeek模型,终止思考流
|
||||
// 对于DeepSeek模型,只标记思考流结束,继续处理后续内容
|
||||
if (deepThinkingType !== 2) {
|
||||
shouldEndThinkStream = true;
|
||||
} else {
|
||||
thinkingSourceType = 'normal_content';
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 没有任何思考流标记,不同模型有不同处理
|
||||
Logger.debug(`没有检测到思考流标记,处理普通内容: ${content}`, 'OpenAIChatService');
|
||||
|
||||
if (deepThinkingType === 2) {
|
||||
// DeepSeek模型直接处理为正常内容
|
||||
thinkingSourceType = 'normal_content';
|
||||
result.content = [
|
||||
{
|
||||
type: 'text',
|
||||
text: content,
|
||||
},
|
||||
];
|
||||
result.full_content += content;
|
||||
onProgress?.({
|
||||
content: result.content,
|
||||
});
|
||||
} else {
|
||||
// 非DeepSeek模型终止思考流
|
||||
shouldEndThinkStream = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.debug('思考流处理完成', 'OpenAIChatService');
|
||||
|
||||
// 如果是DeepSeek模型并且有内容,直接返回true表示应该终止请求
|
||||
return deepThinkingType === 2 && result.full_content.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理常规响应逻辑
|
||||
* @param messagesHistory 消息历史
|
||||
* @param inputs 输入参数
|
||||
* @param result 结果对象
|
||||
*/
|
||||
private async handleRegularResponse(
|
||||
messagesHistory: any,
|
||||
inputs: {
|
||||
apiKey: any;
|
||||
model: any;
|
||||
proxyUrl: any;
|
||||
timeout: any;
|
||||
temperature: any;
|
||||
max_tokens?: any;
|
||||
extraParam?: any;
|
||||
searchResults?: any[];
|
||||
images?: string[];
|
||||
abortController: AbortController;
|
||||
onProgress?: (data: any) => void;
|
||||
},
|
||||
result: any,
|
||||
): Promise<void> {
|
||||
const {
|
||||
apiKey,
|
||||
model,
|
||||
proxyUrl,
|
||||
timeout,
|
||||
temperature,
|
||||
max_tokens,
|
||||
searchResults,
|
||||
images,
|
||||
abortController,
|
||||
onProgress,
|
||||
} = inputs;
|
||||
|
||||
// 步骤1: 准备和增强系统消息
|
||||
const processedMessages = this.prepareSystemMessage(
|
||||
messagesHistory,
|
||||
{
|
||||
searchResults,
|
||||
images,
|
||||
},
|
||||
result,
|
||||
);
|
||||
|
||||
// 步骤2: 处理OpenAI聊天API调用
|
||||
await this.handleOpenAIChat(
|
||||
processedMessages,
|
||||
{
|
||||
apiKey,
|
||||
model,
|
||||
proxyUrl,
|
||||
timeout,
|
||||
temperature,
|
||||
max_tokens,
|
||||
abortController,
|
||||
onProgress,
|
||||
},
|
||||
result,
|
||||
);
|
||||
}
|
||||
|
||||
async chat(
|
||||
messagesHistory: any,
|
||||
inputs: {
|
||||
chatId: any;
|
||||
maxModelTokens?: any;
|
||||
max_tokens?: any;
|
||||
apiKey: any;
|
||||
model: any;
|
||||
modelName: any;
|
||||
temperature: any;
|
||||
modelType?: any;
|
||||
prompt?: any;
|
||||
imageUrl?: any;
|
||||
isFileUpload: any;
|
||||
isImageUpload?: any;
|
||||
fileUrl?: any;
|
||||
usingNetwork?: boolean;
|
||||
timeout: any;
|
||||
proxyUrl: any;
|
||||
modelAvatar?: any;
|
||||
usingDeepThinking?: boolean;
|
||||
usingMcpTool?: boolean;
|
||||
isMcpTool?: boolean;
|
||||
extraParam?: any;
|
||||
deepThinkingType?: any;
|
||||
onProgress?: (data: {
|
||||
text?: string;
|
||||
content?: [];
|
||||
reasoning_content?: [];
|
||||
tool_calls?: string;
|
||||
networkSearchResult?: string;
|
||||
finishReason?: string;
|
||||
// full_json?: string; // 编辑模式相关,已注释
|
||||
}) => void;
|
||||
onFailure?: (error: any) => void;
|
||||
onDatabase?: (data: any) => void;
|
||||
abortController: AbortController;
|
||||
},
|
||||
) {
|
||||
const {
|
||||
chatId,
|
||||
maxModelTokens,
|
||||
max_tokens,
|
||||
apiKey,
|
||||
model,
|
||||
modelName,
|
||||
temperature,
|
||||
prompt,
|
||||
timeout,
|
||||
proxyUrl,
|
||||
modelAvatar,
|
||||
usingDeepThinking,
|
||||
usingNetwork,
|
||||
extraParam,
|
||||
deepThinkingType,
|
||||
onProgress,
|
||||
onFailure,
|
||||
onDatabase,
|
||||
abortController,
|
||||
} = inputs;
|
||||
|
||||
// 创建原始消息历史的副本
|
||||
const originalMessagesHistory = JSON.parse(JSON.stringify(messagesHistory));
|
||||
|
||||
const result: any = {
|
||||
chatId,
|
||||
modelName,
|
||||
modelAvatar,
|
||||
model,
|
||||
status: 2,
|
||||
full_content: '',
|
||||
full_reasoning_content: '',
|
||||
networkSearchResult: '',
|
||||
fileVectorResult: '',
|
||||
finishReason: null,
|
||||
};
|
||||
|
||||
try {
|
||||
// 步骤1: 处理网络搜索 - 使用NetSearchService
|
||||
const { searchResults, images } = await this.netSearchService.processNetSearch(
|
||||
prompt || '',
|
||||
{
|
||||
usingNetwork,
|
||||
onProgress,
|
||||
onDatabase,
|
||||
},
|
||||
result,
|
||||
);
|
||||
|
||||
// 步骤5: 处理深度思考
|
||||
const shouldEndRequest = await this.handleDeepThinking(
|
||||
messagesHistory,
|
||||
{
|
||||
apiKey,
|
||||
model,
|
||||
proxyUrl,
|
||||
timeout,
|
||||
usingDeepThinking,
|
||||
searchResults,
|
||||
abortController,
|
||||
deepThinkingType,
|
||||
onProgress,
|
||||
},
|
||||
result,
|
||||
);
|
||||
|
||||
// 如果深度思考处理后应该终止请求,则直接返回结果
|
||||
if (shouldEndRequest) {
|
||||
result.content = '';
|
||||
result.reasoning_content = '';
|
||||
result.finishReason = 'stop';
|
||||
return result;
|
||||
}
|
||||
|
||||
// 步骤6: 处理常规响应
|
||||
await this.handleRegularResponse(
|
||||
originalMessagesHistory,
|
||||
{
|
||||
apiKey,
|
||||
model,
|
||||
proxyUrl,
|
||||
timeout,
|
||||
temperature,
|
||||
max_tokens,
|
||||
extraParam,
|
||||
searchResults,
|
||||
images,
|
||||
abortController,
|
||||
onProgress,
|
||||
},
|
||||
result,
|
||||
);
|
||||
|
||||
result.content = [
|
||||
{
|
||||
type: 'text',
|
||||
text: '',
|
||||
},
|
||||
];
|
||||
result.reasoning_content = [
|
||||
{
|
||||
type: 'text',
|
||||
text: '',
|
||||
},
|
||||
];
|
||||
result.finishReason = 'stop';
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage = handleError(error);
|
||||
Logger.error(`对话请求失败: ${errorMessage}`, 'OpenAIChatService');
|
||||
result.errMsg = errorMessage;
|
||||
onFailure?.(result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
async chatFree(prompt: string, systemMessage?: string, messagesHistory?: any[], imageUrl?: any) {
|
||||
const {
|
||||
openaiBaseUrl = '',
|
||||
openaiBaseKey = '',
|
||||
openaiBaseModel,
|
||||
} = await this.globalConfigService.getConfigs([
|
||||
'openaiBaseKey',
|
||||
'openaiBaseUrl',
|
||||
'openaiBaseModel',
|
||||
]);
|
||||
|
||||
const key = openaiBaseKey;
|
||||
const proxyUrl = openaiBaseUrl;
|
||||
|
||||
let requestData = [];
|
||||
|
||||
if (systemMessage) {
|
||||
requestData.push({
|
||||
role: 'system',
|
||||
content: systemMessage,
|
||||
});
|
||||
}
|
||||
|
||||
if (messagesHistory && messagesHistory.length > 0) {
|
||||
requestData = requestData.concat(messagesHistory);
|
||||
} else {
|
||||
if (imageUrl) {
|
||||
requestData.push({
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: prompt,
|
||||
},
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: imageUrl,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
requestData.push({
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const openai = new OpenAI({
|
||||
apiKey: key,
|
||||
baseURL: await correctApiBaseUrl(proxyUrl),
|
||||
});
|
||||
|
||||
const response = await openai.chat.completions.create(
|
||||
{
|
||||
model: openaiBaseModel || 'gpt-4o-mini',
|
||||
messages: requestData,
|
||||
},
|
||||
{
|
||||
timeout: 30000,
|
||||
},
|
||||
);
|
||||
|
||||
return response.choices[0].message.content;
|
||||
} catch (error) {
|
||||
const errorMessage = handleError(error);
|
||||
Logger.error(`全局模型调用失败: ${errorMessage}`, 'OpenAIChatService');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备和增强系统消息
|
||||
* @param messagesHistory 消息历史
|
||||
* @param inputs 输入参数
|
||||
* @param result 结果对象
|
||||
* @returns 处理后的消息历史
|
||||
*/
|
||||
private prepareSystemMessage(
|
||||
messagesHistory: any,
|
||||
inputs: {
|
||||
searchResults?: any[];
|
||||
images?: string[];
|
||||
},
|
||||
result: any,
|
||||
): any {
|
||||
const { searchResults, images } = inputs;
|
||||
|
||||
// 创建消息历史的副本
|
||||
const processedMessages = JSON.parse(JSON.stringify(messagesHistory));
|
||||
|
||||
// 查找系统消息
|
||||
const systemMessage = processedMessages?.find((message: any) => message.role === 'system');
|
||||
|
||||
if (systemMessage) {
|
||||
const imageUrlMessages =
|
||||
processedMessages?.filter((message: any) => message.type === 'image_url') || [];
|
||||
|
||||
let updatedContent = '';
|
||||
|
||||
// 添加推理思考内容
|
||||
if (result.full_reasoning_content) {
|
||||
updatedContent = `\n\n以下是针对这个问题的思考推理思路(思路不一定完全正确,仅供参考):\n${result.full_reasoning_content}`;
|
||||
}
|
||||
|
||||
// 添加网络搜索结果
|
||||
if (searchResults && searchResults.length > 0) {
|
||||
// 将 searchResult 转换为 JSON 字符串
|
||||
let searchPrompt = JSON.stringify(searchResults, null, 2); // 格式化为漂亮的 JSON 字符串
|
||||
|
||||
// 处理图片数据
|
||||
let imagesPrompt = '';
|
||||
if (images && images.length > 0) {
|
||||
imagesPrompt = `\n\n以下是搜索到的相关图片链接:\n${images.join('\n')}`;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const options = {
|
||||
timeZone: 'Asia/Shanghai', // 设置时区为 'Asia/Shanghai'(北京时间)
|
||||
year: 'numeric' as const,
|
||||
month: '2-digit' as const,
|
||||
day: '2-digit' as const,
|
||||
hour: '2-digit' as const,
|
||||
minute: '2-digit' as const,
|
||||
hour12: false, // 使用24小时制
|
||||
};
|
||||
|
||||
const currentDate = new Intl.DateTimeFormat('zh-CN', options).format(now);
|
||||
|
||||
updatedContent += `
|
||||
\n\n你的任务是根据用户的问题,通过下面的搜索结果提供更精确、详细、具体的回答。
|
||||
请在适当的情况下在对应部分句子末尾标注引用的链接,使用[[序号](链接地址)]格式,同时使用多个链接可连续使用比如[[2](链接地址)][[5](链接地址)],以下是搜索结果:
|
||||
${searchPrompt}${imagesPrompt}
|
||||
在回答时,请注意以下几点:
|
||||
- 现在时间是: ${currentDate}。
|
||||
- 如果结果中包含图片链接,可在适当位置使用MarkDown格式插入至少一张图片,让回答图文并茂。
|
||||
- 并非搜索结果的所有内容都与用户的问题密切相关,你需要结合问题,对搜索结果进行甄别、筛选。
|
||||
- 对于列举类的问题(如列举所有航班信息),尽量将答案控制在10个要点以内,并告诉用户可以查看搜索来源、获得完整信息。优先提供信息完整、最相关的列举项;如非必要,不要主动告诉用户搜索结果未提供的内容。
|
||||
- 对于创作类的问题(如写论文),请务必在正文的段落中引用对应的参考编号。你需要解读并概括用户的题目要求,选择合适的格式,充分利用搜索结果并抽取重要信息,生成符合用户要求、极具思想深度、富有创造力与专业性的答案。你的创作篇幅需要尽可能延长,对于每一个要点的论述要推测用户的意图,给出尽可能多角度的回答要点,且务必信息量大、论述详尽。
|
||||
- 如果回答很长,请尽量结构化、分段落总结。如果需要分点作答,尽量控制在5个点以内,并合并相关的内容。
|
||||
- 对于客观类的问答,如果问题的答案非常简短,可以适当补充一到两句相关信息,以丰富内容。
|
||||
- 你需要根据用户要求和回答内容选择合适、美观的回答格式,确保可读性强。
|
||||
- 你的回答应该综合多个相关网页来回答,不能只重复引用一个网页。
|
||||
- 除非用户要求,否则你回答的语言需要和用户提问的语言保持一致。
|
||||
`;
|
||||
}
|
||||
|
||||
// 添加图片URL消息
|
||||
if (imageUrlMessages && imageUrlMessages.length > 0) {
|
||||
imageUrlMessages.forEach((imageMessage: any) => {
|
||||
updatedContent = `${updatedContent}\n${JSON.stringify(imageMessage)}`;
|
||||
});
|
||||
}
|
||||
|
||||
systemMessage.content += updatedContent;
|
||||
}
|
||||
|
||||
return processedMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理OpenAI聊天API调用和流式响应
|
||||
* @param messagesHistory 处理后的消息历史
|
||||
* @param inputs 输入参数
|
||||
* @param result 结果对象
|
||||
*/
|
||||
private async handleOpenAIChat(
|
||||
messagesHistory: any,
|
||||
inputs: {
|
||||
apiKey: any;
|
||||
model: any;
|
||||
proxyUrl: any;
|
||||
timeout: any;
|
||||
temperature: any;
|
||||
max_tokens?: any;
|
||||
abortController: AbortController;
|
||||
onProgress?: (data: any) => void;
|
||||
},
|
||||
result: any,
|
||||
): Promise<void> {
|
||||
const {
|
||||
apiKey,
|
||||
model,
|
||||
proxyUrl,
|
||||
timeout,
|
||||
temperature,
|
||||
max_tokens,
|
||||
abortController,
|
||||
onProgress,
|
||||
} = inputs;
|
||||
|
||||
// 准备请求数据
|
||||
const streamData = {
|
||||
model,
|
||||
messages: messagesHistory,
|
||||
stream: true,
|
||||
temperature,
|
||||
};
|
||||
|
||||
// 创建OpenAI实例
|
||||
const openai = new OpenAI({
|
||||
apiKey: apiKey,
|
||||
baseURL: await correctApiBaseUrl(proxyUrl),
|
||||
timeout: timeout,
|
||||
});
|
||||
|
||||
try {
|
||||
Logger.debug(
|
||||
`对话请求 - Messages: ${JSON.stringify(streamData.messages)}`,
|
||||
'OpenAIChatService',
|
||||
);
|
||||
|
||||
// 发送流式请求
|
||||
const stream = await openai.chat.completions.create(
|
||||
{
|
||||
model: streamData.model,
|
||||
messages: streamData.messages,
|
||||
stream: true,
|
||||
max_tokens: max_tokens,
|
||||
temperature: streamData.temperature,
|
||||
},
|
||||
{
|
||||
signal: abortController.signal,
|
||||
},
|
||||
);
|
||||
|
||||
// 处理流式响应
|
||||
for await (const chunk of stream) {
|
||||
if (abortController.signal.aborted) {
|
||||
break;
|
||||
}
|
||||
|
||||
const content = chunk.choices[0]?.delta?.content || '';
|
||||
|
||||
if (content) {
|
||||
// 处理流式内容
|
||||
result.content = [
|
||||
{
|
||||
type: 'text',
|
||||
text: content,
|
||||
},
|
||||
];
|
||||
|
||||
result.full_content += content;
|
||||
onProgress?.({
|
||||
content: result.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`OpenAI请求失败: ${handleError(error)}`, 'OpenAIChatService');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
237
service/src/modules/aiTool/search/netSearch.service.ts
Normal file
237
service/src/modules/aiTool/search/netSearch.service.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { handleError } from '@/common/utils';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { GlobalConfigService } from '../../globalConfig/globalConfig.service';
|
||||
|
||||
@Injectable()
|
||||
export class NetSearchService {
|
||||
constructor(private readonly globalConfigService: GlobalConfigService) {}
|
||||
|
||||
/**
|
||||
* 处理网络搜索流程
|
||||
* @param prompt 搜索关键词
|
||||
* @param inputs 输入参数
|
||||
* @param result 结果对象
|
||||
* @returns 搜索结果对象
|
||||
*/
|
||||
async processNetSearch(
|
||||
prompt: string,
|
||||
inputs: {
|
||||
usingNetwork?: boolean;
|
||||
onProgress?: (data: any) => void;
|
||||
onDatabase?: (data: any) => void;
|
||||
},
|
||||
result: any,
|
||||
): Promise<{ searchResults: any[]; images: string[] }> {
|
||||
const { usingNetwork, onProgress, onDatabase } = inputs;
|
||||
let searchResults: any[] = [];
|
||||
let images: string[] = [];
|
||||
|
||||
// 如果不使用网络搜索,直接返回空结果
|
||||
if (!usingNetwork) {
|
||||
return { searchResults, images };
|
||||
}
|
||||
|
||||
try {
|
||||
Logger.log(`[网络搜索] 开始搜索: ${prompt}`, 'NetSearchService');
|
||||
|
||||
// 调用网络搜索服务
|
||||
const searchResponse = await this.webSearchPro(prompt);
|
||||
searchResults = searchResponse.searchResults;
|
||||
images = searchResponse.images;
|
||||
|
||||
Logger.log(
|
||||
`[网络搜索] 完成,获取到 ${searchResults.length} 条结果和 ${images.length} 张图片`,
|
||||
'NetSearchService',
|
||||
);
|
||||
|
||||
// 更新结果对象
|
||||
result.networkSearchResult = JSON.stringify(searchResults);
|
||||
onProgress?.({
|
||||
networkSearchResult: result.networkSearchResult,
|
||||
});
|
||||
|
||||
// 存储数据到数据库
|
||||
onDatabase?.({
|
||||
networkSearchResult: JSON.stringify(
|
||||
searchResults.map((item: { [x: string]: any; content: any }) => {
|
||||
const { content, ...rest } = item; // 删除 content 部分
|
||||
return rest; // 返回剩余部分
|
||||
}),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
});
|
||||
|
||||
return { searchResults, images };
|
||||
} catch (error) {
|
||||
Logger.error(`[网络搜索] 失败: ${handleError(error)}`, 'NetSearchService');
|
||||
|
||||
// 即时存储错误信息
|
||||
onDatabase?.({
|
||||
network_search_error: {
|
||||
error: handleError(error),
|
||||
query: prompt,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return { searchResults: [], images: [] };
|
||||
}
|
||||
}
|
||||
|
||||
async webSearchPro(prompt: string) {
|
||||
try {
|
||||
const { pluginUrl, pluginKey } = await this.globalConfigService.getConfigs([
|
||||
'pluginUrl',
|
||||
'pluginKey',
|
||||
]);
|
||||
|
||||
if (!pluginUrl || !pluginKey) {
|
||||
Logger.warn('搜索插件配置缺失');
|
||||
return { searchResults: [], images: [] };
|
||||
}
|
||||
|
||||
// 如果有多个 key,随机选择一个
|
||||
const keys = pluginKey.split(',').filter(key => key.trim());
|
||||
const selectedKey = keys[Math.floor(Math.random() * keys.length)];
|
||||
|
||||
const isBochaiApi = pluginUrl.includes('bochaai.com');
|
||||
const isBigModelApi = pluginUrl.includes('bigmodel.cn');
|
||||
const isTavilyApi = pluginUrl.includes('tavily.com');
|
||||
|
||||
Logger.log(
|
||||
`[搜索] API类型: ${
|
||||
isBochaiApi ? 'Bochai' : isBigModelApi ? 'BigModel' : isTavilyApi ? 'Tavily' : '未知'
|
||||
}`,
|
||||
);
|
||||
Logger.log(`[搜索] 请求URL: ${pluginUrl}`);
|
||||
Logger.log(`[搜索] 搜索关键词: ${prompt}`);
|
||||
|
||||
const requestBody = isBochaiApi
|
||||
? {
|
||||
query: prompt,
|
||||
// freshness: 'oneWeek',
|
||||
summary: true,
|
||||
count: 20,
|
||||
}
|
||||
: isTavilyApi
|
||||
? {
|
||||
query: prompt,
|
||||
search_depth: 'basic',
|
||||
// search_depth: 'advanced',
|
||||
include_answer: false,
|
||||
// include_raw_content: true,
|
||||
include_images: true,
|
||||
max_results: 10,
|
||||
}
|
||||
: {
|
||||
tool: 'web-search-pro',
|
||||
stream: false,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
};
|
||||
|
||||
Logger.log(`[搜索] 请求参数: ${JSON.stringify(requestBody, null, 2)}`);
|
||||
|
||||
const response = await fetch(pluginUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: selectedKey,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
Logger.error(`[搜索] 接口返回错误: ${response.status}`);
|
||||
return { searchResults: [], images: [] };
|
||||
}
|
||||
|
||||
const apiResult = await response.json();
|
||||
Logger.log(`[搜索] 原始返回数据: ${JSON.stringify(apiResult, null, 2)}`);
|
||||
|
||||
let searchResults: any[] = [];
|
||||
|
||||
if (isBochaiApi) {
|
||||
if (apiResult?.code === 200 && apiResult?.data?.webPages?.value) {
|
||||
searchResults = apiResult.data.webPages.value.map((item: any) => ({
|
||||
title: item?.name || '',
|
||||
link: item?.url || '',
|
||||
content: item?.summary || '',
|
||||
icon: item?.siteIcon || '',
|
||||
media: item?.siteName || '',
|
||||
}));
|
||||
}
|
||||
} else if (isBigModelApi) {
|
||||
if (apiResult?.choices?.[0]?.message?.tool_calls?.length > 0) {
|
||||
for (const toolCall of apiResult.choices[0].message.tool_calls) {
|
||||
if (Array.isArray(toolCall.search_result)) {
|
||||
searchResults = toolCall.search_result.map((item: any) => ({
|
||||
title: item?.title || '',
|
||||
link: item?.link || '',
|
||||
content: item?.content || '',
|
||||
icon: item?.icon || '',
|
||||
media: item?.media || '',
|
||||
}));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isTavilyApi) {
|
||||
if (Array.isArray(apiResult?.results)) {
|
||||
searchResults = apiResult.results.map((item: any) => ({
|
||||
title: item?.title || '',
|
||||
link: item?.url || '',
|
||||
content: item?.raw_content || item?.content || '',
|
||||
icon: '',
|
||||
media: '',
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
const formattedResult = searchResults.map((item, index) => ({
|
||||
resultIndex: index + 1,
|
||||
...item,
|
||||
}));
|
||||
|
||||
// 提取 Tavily API 返回的图片
|
||||
let images: string[] = [];
|
||||
if (isTavilyApi && Array.isArray(apiResult?.images)) {
|
||||
images = apiResult.images;
|
||||
}
|
||||
|
||||
// 处理博查API返回的图片
|
||||
if (isBochaiApi) {
|
||||
// 博查API的图片可能在两个不同的路径
|
||||
if (apiResult?.data?.images?.value && Array.isArray(apiResult.data.images.value)) {
|
||||
// 从博查API的图片结构中提取contentUrl
|
||||
images = apiResult.data.images.value
|
||||
.filter(img => img.contentUrl)
|
||||
.map(img => img.contentUrl);
|
||||
}
|
||||
// else if (
|
||||
// apiResult?.images?.value &&
|
||||
// Array.isArray(apiResult.images.value)
|
||||
// ) {
|
||||
// // 备选路径
|
||||
// images = apiResult.images.value
|
||||
// .filter((img) => img.contentUrl)
|
||||
// .map((img) => img.contentUrl);
|
||||
// }
|
||||
}
|
||||
|
||||
Logger.log(`[搜索] 格式化后的结果: ${JSON.stringify(formattedResult, null, 2)}`);
|
||||
|
||||
// 同时返回搜索结果和图片数组
|
||||
return {
|
||||
searchResults: formattedResult,
|
||||
images: images,
|
||||
};
|
||||
} catch (fetchError) {
|
||||
Logger.error('[搜索] 调用接口出错:', fetchError);
|
||||
return {
|
||||
searchResults: [],
|
||||
images: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
136
service/src/modules/app/app.controller.ts
Normal file
136
service/src/modules/app/app.controller.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { AdminAuthGuard } from '@/common/auth/adminAuth.guard';
|
||||
import { JwtAuthGuard } from '@/common/auth/jwtAuth.guard';
|
||||
import { SuperAuthGuard } from '@/common/auth/superAuth.guard';
|
||||
import { Body, Controller, Get, Post, Query, Req, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { Request } from 'express';
|
||||
import { AppService } from './app.service';
|
||||
import { CollectAppDto } from './dto/collectApp.dto';
|
||||
import { CreateAppDto } from './dto/createApp.dto';
|
||||
import { CreateCatsDto } from './dto/createCats.dto';
|
||||
import { OperateAppDto } from './dto/deleteApp.dto';
|
||||
import { DeleteCatsDto } from './dto/deleteCats.dto';
|
||||
import { QuerAppDto } from './dto/queryApp.dto';
|
||||
import { QuerCatsDto } from './dto/queryCats.dto';
|
||||
import { UpdateAppDto } from './dto/updateApp.dto';
|
||||
import { UpdateCatsDto } from './dto/updateCats.dto';
|
||||
|
||||
@ApiTags('app')
|
||||
@Controller('app')
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get('queryAppCats')
|
||||
@ApiOperation({ summary: '获取App分类列表' })
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
appCatsList(@Query() query: QuerCatsDto, @Req() req: Request) {
|
||||
return this.appService.appCatsList(query, req);
|
||||
}
|
||||
|
||||
@Get('queryCats')
|
||||
@ApiOperation({ summary: '用户端获取App分类列表' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
catsList(@Req() req: Request) {
|
||||
const params: QuerCatsDto = { status: 1, page: 1, size: 1000, name: '' };
|
||||
return this.appService.appCatsList(params, req);
|
||||
}
|
||||
|
||||
@Get('queryOneCat')
|
||||
@ApiOperation({ summary: '用户端获取App详情' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
queryOneCats(@Query() query, @Req() req: Request) {
|
||||
return this.appService.queryOneCat(query, req);
|
||||
}
|
||||
|
||||
@Post('createAppCats')
|
||||
@ApiOperation({ summary: '添加App分类' })
|
||||
@UseGuards(SuperAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
createAppCat(@Body() body: CreateCatsDto) {
|
||||
return this.appService.createAppCat(body);
|
||||
}
|
||||
|
||||
@Post('updateAppCats')
|
||||
@ApiOperation({ summary: '修改App分类' })
|
||||
@UseGuards(SuperAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
updateAppCats(@Body() body: UpdateCatsDto) {
|
||||
return this.appService.updateAppCats(body);
|
||||
}
|
||||
|
||||
@Post('delAppCats')
|
||||
@ApiOperation({ summary: '删除App分类' })
|
||||
@UseGuards(SuperAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
delAppCat(@Body() body: DeleteCatsDto) {
|
||||
return this.appService.delAppCat(body);
|
||||
}
|
||||
|
||||
@Get('queryApp')
|
||||
@ApiOperation({ summary: '获取App列表' })
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
appList(@Req() req: Request, @Query() query: QuerAppDto) {
|
||||
return this.appService.appList(req, query);
|
||||
}
|
||||
|
||||
@Get('list')
|
||||
@ApiOperation({ summary: '客户端获取App' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
list(@Req() req: Request, @Query() query: QuerAppDto) {
|
||||
return this.appService.frontAppList(req, query);
|
||||
}
|
||||
|
||||
@Post('searchList')
|
||||
@ApiOperation({ summary: '客户端获取App' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
async searchList(@Body() body: any, @Req() req: Request) {
|
||||
body.userId = req.user.id;
|
||||
return this.appService.searchAppList(body);
|
||||
}
|
||||
|
||||
@Post('createApp')
|
||||
@ApiOperation({ summary: '添加App' })
|
||||
@UseGuards(SuperAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
createApp(@Body() body: CreateAppDto) {
|
||||
return this.appService.createApp(body);
|
||||
}
|
||||
|
||||
@Post('updateApp')
|
||||
@ApiOperation({ summary: '修改App' })
|
||||
@UseGuards(SuperAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
updateApp(@Body() body: UpdateAppDto) {
|
||||
return this.appService.updateApp(body);
|
||||
}
|
||||
|
||||
@Post('delApp')
|
||||
@ApiOperation({ summary: '删除App' })
|
||||
@UseGuards(SuperAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
delApp(@Body() body: OperateAppDto) {
|
||||
return this.appService.delApp(body);
|
||||
}
|
||||
|
||||
@Post('collect')
|
||||
@ApiOperation({ summary: '收藏/取消收藏App' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
collect(@Body() body: CollectAppDto, @Req() req: Request) {
|
||||
return this.appService.collect(body, req);
|
||||
}
|
||||
|
||||
@Get('mineApps')
|
||||
@ApiOperation({ summary: '我的收藏' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
mineApps(@Req() req: Request) {
|
||||
return this.appService.mineApps(req);
|
||||
}
|
||||
}
|
||||
68
service/src/modules/app/app.entity.ts
Normal file
68
service/src/modules/app/app.entity.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { BaseEntity } from 'src/common/entity/baseEntity';
|
||||
import { Column, Entity } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'app' })
|
||||
export class AppEntity extends BaseEntity {
|
||||
@Column({ unique: true, comment: 'App应用名称' })
|
||||
name: string;
|
||||
|
||||
@Column({ comment: 'App分类Id列表,多个分类Id以逗号分隔', type: 'text' })
|
||||
catId: string;
|
||||
|
||||
@Column({ comment: 'App应用描述信息' })
|
||||
des: string;
|
||||
|
||||
@Column({ comment: 'App应用预设场景信息', type: 'text' })
|
||||
preset: string;
|
||||
|
||||
@Column({ comment: 'App应用封面图片', nullable: true, type: 'text' })
|
||||
coverImg: string;
|
||||
|
||||
@Column({ comment: 'App应用排序、数字越大越靠前', default: 100 })
|
||||
order: number;
|
||||
|
||||
@Column({ comment: 'App应用是否启用中 0:禁用 1:启用', default: 1 })
|
||||
status: number;
|
||||
|
||||
@Column({ comment: 'App示例数据', nullable: true, type: 'text' })
|
||||
demoData: string;
|
||||
|
||||
@Column({ comment: 'App应用角色 system user', default: 'system' })
|
||||
role: string;
|
||||
|
||||
@Column({ comment: 'App应用是否是GPTs', default: '0' })
|
||||
isGPTs: number;
|
||||
|
||||
@Column({ comment: 'App应用是否是固定使用模型', default: '0' })
|
||||
isFixedModel: number;
|
||||
|
||||
@Column({ comment: 'App应用使用的模型', type: 'text' })
|
||||
appModel: string;
|
||||
|
||||
@Column({ comment: 'GPTs 的调用ID', default: '' })
|
||||
gizmoID: string;
|
||||
|
||||
@Column({ comment: 'App是否共享到应用广场', default: false })
|
||||
public: boolean;
|
||||
|
||||
@Column({ comment: '用户Id', nullable: true })
|
||||
userId: number;
|
||||
|
||||
@Column({ comment: '是否使用flowith模型', default: 0 })
|
||||
isFlowith: number;
|
||||
|
||||
@Column({ comment: 'flowith模型ID', nullable: true })
|
||||
flowithId: string;
|
||||
|
||||
@Column({ comment: 'flowith模型名称', nullable: true })
|
||||
flowithName: string;
|
||||
|
||||
@Column({ comment: 'flowith模型Key', nullable: true })
|
||||
flowithKey: string;
|
||||
|
||||
@Column({ comment: 'App背景图', nullable: true, type: 'text' })
|
||||
backgroundImg: string;
|
||||
|
||||
@Column({ comment: 'App提问模版', nullable: true, type: 'text' })
|
||||
prompt: string;
|
||||
}
|
||||
15
service/src/modules/app/app.module.ts
Normal file
15
service/src/modules/app/app.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UserBalanceService } from '../userBalance/userBalance.service';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppEntity } from './app.entity';
|
||||
import { AppService } from './app.service';
|
||||
import { AppCatsEntity } from './appCats.entity';
|
||||
import { UserAppsEntity } from './userApps.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([AppCatsEntity, AppEntity, UserAppsEntity])],
|
||||
controllers: [AppController],
|
||||
providers: [AppService, UserBalanceService],
|
||||
})
|
||||
export class AppModule {}
|
||||
716
service/src/modules/app/app.service.ts
Normal file
716
service/src/modules/app/app.service.ts
Normal file
@@ -0,0 +1,716 @@
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Request } from 'express';
|
||||
import { In, IsNull, Like, MoreThan, Not, Repository } from 'typeorm';
|
||||
import { UserBalanceService } from '../userBalance/userBalance.service';
|
||||
import { AppEntity } from './app.entity';
|
||||
import { AppCatsEntity } from './appCats.entity';
|
||||
import { CollectAppDto } from './dto/collectApp.dto';
|
||||
import { CreateAppDto } from './dto/createApp.dto';
|
||||
import { CreateCatsDto } from './dto/createCats.dto';
|
||||
import { OperateAppDto } from './dto/deleteApp.dto';
|
||||
import { DeleteCatsDto } from './dto/deleteCats.dto';
|
||||
import { QuerAppDto } from './dto/queryApp.dto';
|
||||
import { QuerCatsDto } from './dto/queryCats.dto';
|
||||
import { UpdateAppDto } from './dto/updateApp.dto';
|
||||
import { UpdateCatsDto } from './dto/updateCats.dto';
|
||||
import { UserAppsEntity } from './userApps.entity';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
constructor(
|
||||
@InjectRepository(AppCatsEntity)
|
||||
private readonly appCatsEntity: Repository<AppCatsEntity>,
|
||||
@InjectRepository(AppEntity)
|
||||
private readonly appEntity: Repository<AppEntity>,
|
||||
@InjectRepository(UserAppsEntity)
|
||||
private readonly userAppsEntity: Repository<UserAppsEntity>,
|
||||
private readonly userBalanceService: UserBalanceService,
|
||||
) {}
|
||||
|
||||
async createAppCat(body: CreateCatsDto) {
|
||||
const { name } = body;
|
||||
const c = await this.appCatsEntity.findOne({ where: { name } });
|
||||
if (c) {
|
||||
throw new HttpException('该分类名称已存在!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
return await this.appCatsEntity.save(body);
|
||||
}
|
||||
|
||||
async delAppCat(body: DeleteCatsDto) {
|
||||
const { id } = body;
|
||||
const c = await this.appCatsEntity.findOne({ where: { id } });
|
||||
if (!c) {
|
||||
throw new HttpException('该分类不存在!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
// 查找所有包含该分类ID的App
|
||||
const apps = await this.appEntity.find();
|
||||
const appsWithThisCat = apps.filter(app => {
|
||||
const catIds = app.catId.split(',');
|
||||
return catIds.includes(id.toString());
|
||||
});
|
||||
|
||||
if (appsWithThisCat.length > 0) {
|
||||
throw new HttpException('该分类下存在App,不可删除!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
const res = await this.appCatsEntity.delete(id);
|
||||
if (res.affected > 0) return '删除成功';
|
||||
throw new HttpException('删除失败!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
async updateAppCats(body: UpdateCatsDto) {
|
||||
const { id, name } = body;
|
||||
const c = await this.appCatsEntity.findOne({
|
||||
where: { name, id: Not(id) },
|
||||
});
|
||||
if (c) {
|
||||
throw new HttpException('该分类名称已存在!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
const res = await this.appCatsEntity.update({ id }, body);
|
||||
if (res.affected > 0) return '修改成功';
|
||||
throw new HttpException('修改失败!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
async queryOneCat(params, req?: Request) {
|
||||
const { id } = params;
|
||||
if (!id) {
|
||||
throw new HttpException('缺失必要参数!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
const app = await this.appEntity.findOne({ where: { id } });
|
||||
if (!app) {
|
||||
throw new HttpException('应用不存在!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const appData = app as any;
|
||||
return {
|
||||
demoData: appData.demoData ? appData.demoData.split('\n') : [],
|
||||
coverImg: appData.coverImg,
|
||||
des: appData.des,
|
||||
name: appData.name,
|
||||
isGPTs: appData.isGPTs,
|
||||
isFlowith: appData.isFlowith,
|
||||
flowithId: appData.flowithId,
|
||||
flowithName: appData.flowithName,
|
||||
|
||||
isFixedModel: appData.isFixedModel,
|
||||
appModel: appData.appModel,
|
||||
backgroundImg: appData.backgroundImg,
|
||||
prompt: appData.prompt,
|
||||
};
|
||||
}
|
||||
|
||||
async appCatsList(query: QuerCatsDto, req?: Request) {
|
||||
const { page = 1, size = 10, name, status } = query;
|
||||
const where: any = {};
|
||||
name && (where.name = Like(`%${name}%`));
|
||||
[0, 1, '0', '1'].includes(status) && (where.status = status);
|
||||
|
||||
const [rows, count] = await this.appCatsEntity.findAndCount({
|
||||
where,
|
||||
order: { order: 'DESC' },
|
||||
skip: (page - 1) * size,
|
||||
take: size,
|
||||
});
|
||||
|
||||
// 如果是超级管理员,跳过过滤逻辑
|
||||
let filteredRows = [...rows];
|
||||
if (req?.user?.role !== 'super') {
|
||||
// 获取用户的分类ID列表
|
||||
const userCatIds = await this.userBalanceService.getUserApps(Number(req.user.id));
|
||||
const userCatIdsSet = new Set(userCatIds);
|
||||
|
||||
// 过滤分类:如果分类ID在用户的分类ID列表中则保留,否则检查是否需要隐藏
|
||||
filteredRows = rows.filter(cat => {
|
||||
// 如果分类ID在用户的分类ID列表中,保留它
|
||||
if (userCatIdsSet.has(cat.id.toString())) {
|
||||
return true;
|
||||
}
|
||||
// 只过滤掉设置了hideFromNonMember的分类,不考虑isMember属性
|
||||
return cat.hideFromNonMember !== 1;
|
||||
});
|
||||
}
|
||||
|
||||
// 查出所有分类下对应的App数量
|
||||
const catIds = filteredRows.map(item => item.id);
|
||||
const apps = await this.appEntity.find();
|
||||
const appCountMap = {};
|
||||
|
||||
// 初始化每个分类的App计数为0
|
||||
catIds.forEach(id => {
|
||||
appCountMap[id] = 0;
|
||||
});
|
||||
|
||||
// 统计每个分类下的App数量
|
||||
apps.forEach(item => {
|
||||
const appCatIds = item.catId.split(',');
|
||||
appCatIds.forEach(catId => {
|
||||
const catIdNum = Number(catId);
|
||||
if (catIds.includes(catIdNum)) {
|
||||
appCountMap[catIdNum] = (appCountMap[catIdNum] || 0) + 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
filteredRows.forEach((item: any) => (item.appCount = appCountMap[item.id] || 0));
|
||||
|
||||
return { rows: filteredRows, count: filteredRows.length };
|
||||
}
|
||||
|
||||
async appList(req: Request, query: QuerAppDto, orderKey = 'id') {
|
||||
const { page = 1, size = 10, name, status, catId, role } = query;
|
||||
const where: any = {};
|
||||
name && (where.name = Like(`%${name}%`));
|
||||
// 如果指定了分类ID,则查找包含该分类ID的App
|
||||
let filteredByCategory = null;
|
||||
if (catId) {
|
||||
const apps = await this.appEntity.find();
|
||||
filteredByCategory = apps
|
||||
.filter(app => {
|
||||
const appCatIds = app.catId.split(',');
|
||||
return appCatIds.includes(catId.toString());
|
||||
})
|
||||
.map(app => app.id);
|
||||
|
||||
if (filteredByCategory.length === 0) {
|
||||
return { rows: [], count: 0 };
|
||||
}
|
||||
where.id = In(filteredByCategory);
|
||||
}
|
||||
|
||||
role && (where.role = role);
|
||||
status && (where.status = status);
|
||||
const [rows, count] = await this.appEntity.findAndCount({
|
||||
where,
|
||||
order: { [orderKey]: 'DESC' },
|
||||
skip: (page - 1) * size,
|
||||
take: size,
|
||||
});
|
||||
|
||||
// 获取所有分类信息
|
||||
const allCats = await this.appCatsEntity.find();
|
||||
const catsMap = {};
|
||||
allCats.forEach(cat => {
|
||||
catsMap[cat.id] = cat;
|
||||
});
|
||||
|
||||
// 为每个App添加分类名称
|
||||
rows.forEach((item: any) => {
|
||||
const catIds = item.catId.split(',');
|
||||
const catNames = catIds
|
||||
.map(id => {
|
||||
const cat = catsMap[Number(id)];
|
||||
return cat ? cat.name : '';
|
||||
})
|
||||
.filter(name => name);
|
||||
|
||||
item.catName = catNames.join(', ');
|
||||
item.backgroundImg = item.backgroundImg;
|
||||
item.prompt = item.prompt;
|
||||
});
|
||||
|
||||
if (req?.user?.role !== 'super') {
|
||||
rows.forEach((item: any) => {
|
||||
delete item.preset;
|
||||
});
|
||||
}
|
||||
return { rows, count };
|
||||
}
|
||||
|
||||
async frontAppList(req: Request, query: QuerAppDto, orderKey = 'id') {
|
||||
const { page = 1, size = 1000, catId } = query;
|
||||
const where: any = [
|
||||
{
|
||||
status: In([1, 4]),
|
||||
userId: IsNull(),
|
||||
public: false,
|
||||
},
|
||||
{ userId: MoreThan(0), public: true },
|
||||
];
|
||||
|
||||
const userCatIds = await this.userBalanceService.getUserApps(Number(req.user.id));
|
||||
const userCatIdsSet = new Set(userCatIds);
|
||||
|
||||
// 如果指定了分类ID,则过滤包含该分类ID的App
|
||||
if (catId) {
|
||||
const apps = await this.appEntity.find();
|
||||
const filteredByCategory = apps
|
||||
.filter(app => {
|
||||
const appCatIds = app.catId.split(',');
|
||||
return appCatIds.includes(catId.toString());
|
||||
})
|
||||
.map(app => app.id);
|
||||
|
||||
if (filteredByCategory.length === 0) {
|
||||
return { rows: [], count: 0 };
|
||||
}
|
||||
|
||||
// 修改查询条件,只查询包含指定分类ID的App
|
||||
where[0].id = In(filteredByCategory);
|
||||
where[1].id = In(filteredByCategory);
|
||||
}
|
||||
|
||||
const [rows, count] = await this.appEntity.findAndCount({
|
||||
where,
|
||||
order: { order: 'DESC' },
|
||||
skip: (page - 1) * size,
|
||||
take: size,
|
||||
});
|
||||
|
||||
// 获取所有分类信息
|
||||
const allCats = await this.appCatsEntity.find();
|
||||
const catsMap = {};
|
||||
allCats.forEach(cat => {
|
||||
catsMap[cat.id] = cat;
|
||||
});
|
||||
|
||||
// 如果是超级管理员,跳过过滤逻辑
|
||||
let filteredRows = [...rows];
|
||||
if (req?.user?.role !== 'super') {
|
||||
// 过滤应用:如果应用的分类ID在用户的 userCatIds 中则保留,否则检查是否需要隐藏
|
||||
filteredRows = rows.filter(app => {
|
||||
// 获取应用所属的所有分类
|
||||
const appCatIds = app.catId.split(',').map(id => Number(id));
|
||||
|
||||
// 检查应用是否属于用户拥有的任何分类
|
||||
for (const catId of appCatIds) {
|
||||
if (userCatIdsSet.has(catId.toString())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查应用的分类是否有会员专属且对非会员隐藏的
|
||||
for (const catId of appCatIds) {
|
||||
const cat = catsMap[catId];
|
||||
if (cat && cat.isMember === 1 && cat.hideFromNonMember === 1) {
|
||||
return false; // 过滤掉这个应用
|
||||
}
|
||||
}
|
||||
return true; // 保留这个应用
|
||||
});
|
||||
}
|
||||
|
||||
// 为每个App添加分类名称
|
||||
filteredRows.forEach((item: any) => {
|
||||
const appCatIds = item.catId.split(',');
|
||||
const catNames = appCatIds
|
||||
.map(id => {
|
||||
const cat = catsMap[Number(id)];
|
||||
return cat ? cat.name : '';
|
||||
})
|
||||
.filter(name => name);
|
||||
|
||||
item.catName = catNames.join(',');
|
||||
item.backgroundImg = item.backgroundImg;
|
||||
});
|
||||
|
||||
// 只有非超级管理员需要删除 preset
|
||||
if (req?.user?.role !== 'super') {
|
||||
filteredRows.forEach((item: any) => {
|
||||
delete item.preset;
|
||||
});
|
||||
}
|
||||
|
||||
return { rows: filteredRows, count: filteredRows.length };
|
||||
}
|
||||
|
||||
async searchAppList(body: any) {
|
||||
const { page = 1, size = 1000, keyword, catId, userId, role } = body;
|
||||
|
||||
// 基础查询条件
|
||||
let baseWhere: any = [
|
||||
{
|
||||
status: In([1, 4]),
|
||||
userId: IsNull(),
|
||||
public: false,
|
||||
},
|
||||
{ userId: MoreThan(0), public: true },
|
||||
];
|
||||
|
||||
// 如果存在关键字,修改查询条件以搜索 name
|
||||
if (keyword) {
|
||||
baseWhere = baseWhere.map(condition => ({
|
||||
...condition,
|
||||
name: Like(`%${keyword}%`),
|
||||
}));
|
||||
}
|
||||
|
||||
// 如果指定了分类ID,则过滤包含该分类ID的App
|
||||
if (catId && !isNaN(Number(catId))) {
|
||||
const apps = await this.appEntity.find();
|
||||
const filteredByCategory = apps
|
||||
.filter(app => {
|
||||
const appCatIds = app.catId.split(',');
|
||||
return appCatIds.includes(catId.toString());
|
||||
})
|
||||
.map(app => app.id);
|
||||
|
||||
if (filteredByCategory.length === 0) {
|
||||
return { rows: [], count: 0 };
|
||||
}
|
||||
|
||||
baseWhere = baseWhere.map(condition => ({
|
||||
...condition,
|
||||
id: In(filteredByCategory),
|
||||
}));
|
||||
}
|
||||
|
||||
try {
|
||||
// 确保 userId 是有效数字
|
||||
const userIdNum = isNaN(Number(userId)) ? 0 : Number(userId);
|
||||
|
||||
// 获取用户的分类ID列表
|
||||
const userCatIds = await this.userBalanceService.getUserApps(userIdNum);
|
||||
const userCatIdsSet = new Set(userCatIds);
|
||||
|
||||
const [rows, count] = await this.appEntity.findAndCount({
|
||||
where: baseWhere,
|
||||
skip: (page - 1) * size,
|
||||
take: size,
|
||||
});
|
||||
|
||||
// 获取所有分类信息
|
||||
const allCats = await this.appCatsEntity.find();
|
||||
const catsMap = {};
|
||||
allCats.forEach(cat => {
|
||||
catsMap[cat.id] = cat;
|
||||
});
|
||||
|
||||
// 如果是超级管理员,跳过过滤逻辑
|
||||
let filteredRows = [...rows];
|
||||
if (role !== 'super') {
|
||||
// 过滤应用:如果应用的分类在用户的分类ID列表中则保留,否则检查是否需要隐藏
|
||||
filteredRows = rows.filter(app => {
|
||||
// 获取应用所属的所有分类
|
||||
const appCatIds = app.catId.split(',').map(id => Number(id));
|
||||
|
||||
// 检查应用是否属于用户拥有的任何分类
|
||||
for (const catId of appCatIds) {
|
||||
if (userCatIdsSet.has(catId.toString())) {
|
||||
return true; // 保留这个应用
|
||||
}
|
||||
}
|
||||
|
||||
// 检查应用的分类是否有会员专属且对非会员隐藏的
|
||||
for (const catId of appCatIds) {
|
||||
const cat = catsMap[catId];
|
||||
if (cat && cat.isMember === 1 && cat.hideFromNonMember === 1) {
|
||||
return false; // 过滤掉这个应用
|
||||
}
|
||||
}
|
||||
return true; // 保留这个应用
|
||||
});
|
||||
}
|
||||
|
||||
// 为每个App添加分类名称
|
||||
filteredRows.forEach((item: any) => {
|
||||
const appCatIds = item.catId.split(',');
|
||||
const catNames = appCatIds
|
||||
.map(id => {
|
||||
const cat = catsMap[Number(id)];
|
||||
return cat ? cat.name : '';
|
||||
})
|
||||
.filter(name => name);
|
||||
|
||||
item.catName = catNames.join(', ');
|
||||
item.backgroundImg = item.backgroundImg;
|
||||
item.prompt = item.prompt;
|
||||
// 只有非超级管理员需要删除 preset
|
||||
if (role !== 'super') {
|
||||
delete item.preset;
|
||||
}
|
||||
});
|
||||
|
||||
return { rows: filteredRows, count: filteredRows.length };
|
||||
} catch (error) {
|
||||
throw new HttpException('查询应用列表失败', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
async createApp(body: CreateAppDto) {
|
||||
const { name, catId } = body;
|
||||
body.role = 'system';
|
||||
|
||||
// 检查应用名称是否已存在
|
||||
const a = await this.appEntity.findOne({ where: { name } });
|
||||
if (a) {
|
||||
throw new HttpException('该应用名称已存在!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// 验证所有分类ID是否存在
|
||||
if (!catId) {
|
||||
throw new HttpException('缺少分类ID!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const catIds = catId.split(',');
|
||||
for (const id of catIds) {
|
||||
const numId = Number(id);
|
||||
if (isNaN(numId)) {
|
||||
throw new HttpException(`分类ID ${id} 不是有效的数字!`, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const c = await this.appCatsEntity.findOne({ where: { id: numId } });
|
||||
if (!c) {
|
||||
throw new HttpException(`分类ID ${id} 不存在!`, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 添加必要的默认字段
|
||||
const saveData: any = { ...body };
|
||||
|
||||
// 检查ID是否有效,如果无效则删除
|
||||
if (!saveData.id || isNaN(Number(saveData.id))) {
|
||||
delete saveData.id;
|
||||
}
|
||||
|
||||
saveData.public = false;
|
||||
|
||||
// 设置默认值
|
||||
saveData.appModel = saveData.appModel || '';
|
||||
saveData.order = isNaN(Number(saveData.order)) ? 100 : saveData.order;
|
||||
saveData.status = isNaN(Number(saveData.status)) ? 1 : saveData.status;
|
||||
saveData.isGPTs = isNaN(Number(saveData.isGPTs)) ? 0 : saveData.isGPTs;
|
||||
saveData.isFlowith = isNaN(Number(saveData.isFlowith)) ? 0 : saveData.isFlowith;
|
||||
saveData.flowithId = saveData.flowithId || '';
|
||||
saveData.flowithName = saveData.flowithName || '';
|
||||
saveData.flowithKey = saveData.flowithKey || '';
|
||||
saveData.isFixedModel = isNaN(Number(saveData.isFixedModel)) ? 0 : saveData.isFixedModel;
|
||||
saveData.backgroundImg = saveData.backgroundImg || '';
|
||||
saveData.prompt = saveData.prompt || '';
|
||||
|
||||
// 保存应用
|
||||
return await this.appEntity.save(saveData);
|
||||
} catch (error) {
|
||||
throw new HttpException(`保存应用失败`, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
async updateApp(body: UpdateAppDto) {
|
||||
const { id, name, catId, status } = body;
|
||||
|
||||
// 验证ID是否有效
|
||||
if (id === undefined || id === null || isNaN(Number(id))) {
|
||||
throw new HttpException('无效的应用ID!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const a = await this.appEntity.findOne({ where: { name, id: Not(id) } });
|
||||
if (a) {
|
||||
throw new HttpException('该应用名称已存在!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// 验证所有分类ID是否存在
|
||||
const catIds = catId.split(',');
|
||||
for (const id of catIds) {
|
||||
const c = await this.appCatsEntity.findOne({ where: { id: Number(id) } });
|
||||
if (!c) {
|
||||
throw new HttpException(`分类ID ${id} 不存在!`, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建更新数据对象
|
||||
const updateData = { ...body } as any;
|
||||
const curApp = await this.appEntity.findOne({ where: { id } });
|
||||
const curAppData = curApp as any;
|
||||
|
||||
// 设置默认值
|
||||
updateData.appModel = updateData.appModel ?? (curAppData.appModel || '');
|
||||
updateData.order = isNaN(Number(updateData.order)) ? 100 : updateData.order;
|
||||
updateData.status = isNaN(Number(updateData.status)) ? 1 : updateData.status;
|
||||
updateData.isGPTs = isNaN(Number(updateData.isGPTs)) ? 0 : updateData.isGPTs;
|
||||
updateData.isFlowith = isNaN(Number(updateData.isFlowith)) ? 0 : updateData.isFlowith;
|
||||
updateData.flowithId = updateData.flowithId ?? (curAppData.flowithId || '');
|
||||
updateData.flowithName = updateData.flowithName ?? (curAppData.flowithName || '');
|
||||
updateData.isFixedModel = isNaN(Number(updateData.isFixedModel)) ? 0 : updateData.isFixedModel;
|
||||
updateData.backgroundImg = updateData.backgroundImg ?? (curAppData.backgroundImg || '');
|
||||
updateData.prompt = updateData.prompt ?? (curAppData.prompt || '');
|
||||
|
||||
if (curAppData.status !== updateData.status) {
|
||||
await this.userAppsEntity.update({ appId: id }, { status: updateData.status });
|
||||
}
|
||||
const res = await this.appEntity.update({ id }, updateData);
|
||||
if (res.affected > 0) return '修改App信息成功';
|
||||
throw new HttpException('修改App信息失败!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
async delApp(body: OperateAppDto) {
|
||||
const { id } = body;
|
||||
const a = await this.appEntity.findOne({ where: { id } });
|
||||
if (!a) {
|
||||
throw new HttpException('该应用不存在!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
const res = await this.appEntity.delete(id);
|
||||
if (res.affected > 0) return '删除App成功';
|
||||
throw new HttpException('删除App失败!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
async collect(body: CollectAppDto, req: Request) {
|
||||
const { appId } = body;
|
||||
const { id: userId } = req.user;
|
||||
|
||||
// 验证参数
|
||||
if (appId === undefined || appId === null || isNaN(Number(appId))) {
|
||||
throw new HttpException('无效的应用ID!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (userId === undefined || userId === null || isNaN(Number(userId))) {
|
||||
throw new HttpException('无效的用户ID!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const historyApp = await this.userAppsEntity.findOne({
|
||||
where: { appId, userId },
|
||||
});
|
||||
if (historyApp) {
|
||||
const r = await this.userAppsEntity.delete({ appId, userId });
|
||||
if (r.affected > 0) {
|
||||
return '取消收藏成功!';
|
||||
} else {
|
||||
throw new HttpException('取消收藏失败!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
const app = await this.appEntity.findOne({ where: { id: appId } });
|
||||
if (!app) {
|
||||
throw new HttpException('应用不存在!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const { id, role: appRole, catId } = app;
|
||||
const collectInfo = {
|
||||
userId,
|
||||
appId: id,
|
||||
catId,
|
||||
appRole,
|
||||
public: true,
|
||||
status: 1,
|
||||
};
|
||||
await this.userAppsEntity.save(collectInfo);
|
||||
return '已将应用加入到我的收藏!';
|
||||
}
|
||||
|
||||
async mineApps(req: Request, query = { page: 1, size: 30 }) {
|
||||
const { id } = req.user;
|
||||
const { page = 1, size = 30 } = query;
|
||||
let filteredRows = [];
|
||||
|
||||
try {
|
||||
// 获取用户的分类ID列表
|
||||
const userCatIds = await this.userBalanceService.getUserApps(Number(id));
|
||||
const userCatIdsSet = new Set(userCatIds);
|
||||
|
||||
const [rows, count] = await this.userAppsEntity.findAndCount({
|
||||
where: { userId: id, status: In([1, 3, 4, 5]) },
|
||||
order: { id: 'DESC' },
|
||||
skip: (page - 1) * size,
|
||||
take: size,
|
||||
});
|
||||
|
||||
const appIds = rows.map(item => item.appId);
|
||||
const appsInfo = await this.appEntity.find({ where: { id: In(appIds) } });
|
||||
|
||||
// 获取所有分类信息
|
||||
const allCats = await this.appCatsEntity.find();
|
||||
const catsMap = {};
|
||||
allCats.forEach(cat => {
|
||||
catsMap[cat.id] = cat;
|
||||
});
|
||||
|
||||
// 如果是超级管理员,跳过过滤逻辑
|
||||
filteredRows = [...rows];
|
||||
if (req?.user?.role !== 'super') {
|
||||
filteredRows = rows.filter(item => {
|
||||
const app = appsInfo.find(c => c.id === item.appId);
|
||||
if (!app) return false;
|
||||
|
||||
// 获取应用所属的所有分类
|
||||
const appCatIds = app.catId.split(',').map(id => Number(id));
|
||||
|
||||
// 检查应用是否属于用户拥有的任何分类
|
||||
for (const catId of appCatIds) {
|
||||
if (userCatIdsSet.has(catId.toString())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查应用的分类是否有会员专属且对非会员隐藏的
|
||||
for (const catId of appCatIds) {
|
||||
const cat = catsMap[catId];
|
||||
if (cat && cat.isMember === 1 && cat.hideFromNonMember === 1) {
|
||||
return false; // 过滤掉这个应用
|
||||
}
|
||||
}
|
||||
return true; // 保留这个应用
|
||||
});
|
||||
}
|
||||
|
||||
// 为每个应用添加详细信息
|
||||
filteredRows.forEach((item: any) => {
|
||||
const app = appsInfo.find(c => c.id === item.appId);
|
||||
if (!app) return;
|
||||
|
||||
item.appName = app.name || '';
|
||||
item.appRole = app.role || '';
|
||||
item.appDes = app.des || '';
|
||||
item.coverImg = app.coverImg || '';
|
||||
item.demoData = app.demoData || '';
|
||||
item.backgroundImg = app.backgroundImg || '';
|
||||
|
||||
// 添加分类名称
|
||||
const appCatIds = app.catId.split(',');
|
||||
const catNames = appCatIds
|
||||
.map(id => {
|
||||
const cat = catsMap[Number(id)];
|
||||
return cat ? cat.name : '';
|
||||
})
|
||||
.filter(name => name);
|
||||
item.catName = catNames.join(',');
|
||||
|
||||
// 处理 preset 字段
|
||||
item.preset = app.userId === id ? app.preset : '******';
|
||||
item.prompt = app.prompt || '';
|
||||
});
|
||||
} catch (error) {
|
||||
throw new HttpException('获取用户应用列表失败', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
return { rows: filteredRows, count: filteredRows.length };
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查应用是否是会员专属
|
||||
* @param appId 应用ID
|
||||
* @returns 返回应用是否是会员专属的布尔值
|
||||
*/
|
||||
async checkAppIsMemberOnly(appId: number): Promise<boolean> {
|
||||
try {
|
||||
// 查询应用信息
|
||||
const appInfo = await this.appEntity.findOne({
|
||||
where: { id: appId },
|
||||
select: ['catId'],
|
||||
});
|
||||
|
||||
if (!appInfo || !appInfo.catId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 解析分类ID列表
|
||||
const catIds = appInfo.catId
|
||||
.split(',')
|
||||
.map(id => Number(id.trim()))
|
||||
.filter(id => id > 0);
|
||||
|
||||
if (catIds.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 查询这些分类是否有会员专属的
|
||||
const cats = await this.appCatsEntity.find({
|
||||
where: { id: In(catIds) },
|
||||
select: ['id', 'isMember'],
|
||||
});
|
||||
|
||||
// 检查是否有任何一个分类是会员专属的
|
||||
return cats.some(cat => cat.isMember === 1);
|
||||
} catch (error) {
|
||||
return false; // 出错时默认返回非会员专属
|
||||
}
|
||||
}
|
||||
}
|
||||
20
service/src/modules/app/appCats.entity.ts
Normal file
20
service/src/modules/app/appCats.entity.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { BaseEntity } from 'src/common/entity/baseEntity';
|
||||
import { Column, Entity } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'app_cats' })
|
||||
export class AppCatsEntity extends BaseEntity {
|
||||
@Column({ unique: true, comment: 'App分类名称' })
|
||||
name: string;
|
||||
|
||||
@Column({ comment: 'App分类排序、数字越大越靠前', default: 100 })
|
||||
order: number;
|
||||
|
||||
@Column({ comment: 'App分类是否启用中 0:禁用 1:启用', default: 1 })
|
||||
status: number;
|
||||
|
||||
@Column({ comment: 'App分类是否为会员专属 0:否 1:是', default: 0 })
|
||||
isMember: number;
|
||||
|
||||
@Column({ comment: '非会员是否隐藏 0:否 1:是', default: 0 })
|
||||
hideFromNonMember: number;
|
||||
}
|
||||
8
service/src/modules/app/dto/collectApp.dto.ts
Normal file
8
service/src/modules/app/dto/collectApp.dto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumber } from 'class-validator';
|
||||
|
||||
export class CollectAppDto {
|
||||
@ApiProperty({ example: 1, description: '要收藏的appId', required: true })
|
||||
@IsNumber({}, { message: 'ID必须是Number' })
|
||||
appId: number;
|
||||
}
|
||||
111
service/src/modules/app/dto/createApp.dto.ts
Normal file
111
service/src/modules/app/dto/createApp.dto.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsDefined, IsIn, IsNumber, IsOptional } from 'class-validator';
|
||||
|
||||
export class CreateAppDto {
|
||||
@ApiProperty({ example: '前端助手', description: 'app名称', required: true })
|
||||
@IsDefined({ message: 'app名称是必传参数' })
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '1,2,3',
|
||||
description: 'app分类Id列表,多个分类Id以逗号分隔',
|
||||
required: true,
|
||||
})
|
||||
@IsDefined({ message: 'app分类Id必传参数' })
|
||||
catId: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '适用于编程编码、期望成为您的编程助手',
|
||||
description: 'app名称详情描述',
|
||||
required: false,
|
||||
})
|
||||
@IsDefined({ message: 'app名称描述是必传参数' })
|
||||
des: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '你现在是一个翻译官。接下来我说的所有话帮我翻译成中文',
|
||||
description: '预设的prompt',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
preset: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'GPTs 的调用ID',
|
||||
description: 'GPTs 使用的 ID',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
gizmoID: string;
|
||||
|
||||
@ApiProperty({ description: '是否GPTs', required: false })
|
||||
@IsOptional()
|
||||
isGPTs: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'https://xxxx.png',
|
||||
description: '套餐封面图片',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
coverImg: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 100,
|
||||
description: '套餐排序、数字越大越靠前',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
order: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 1,
|
||||
description: '套餐状态 0:禁用 1:启用',
|
||||
required: true,
|
||||
})
|
||||
@IsNumber({}, { message: '套餐状态必须是Number' })
|
||||
@IsIn([0, 1, 3, 4, 5], { message: '套餐状态错误' })
|
||||
status: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: '这是一句示例数据',
|
||||
description: 'app示例数据',
|
||||
required: false,
|
||||
})
|
||||
demoData: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'system',
|
||||
description: '创建的角色',
|
||||
required: false,
|
||||
})
|
||||
role: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 0,
|
||||
description: '是否使用flowith模型',
|
||||
required: false,
|
||||
})
|
||||
isFlowith: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'flowith模型ID',
|
||||
description: 'flowith模型ID',
|
||||
required: false,
|
||||
})
|
||||
flowithId: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'flowith模型名称',
|
||||
description: 'flowith模型名称',
|
||||
required: false,
|
||||
})
|
||||
flowithName: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'flowith模型Key',
|
||||
description: 'flowith模型Key',
|
||||
required: false,
|
||||
})
|
||||
flowithKey: string;
|
||||
}
|
||||
47
service/src/modules/app/dto/createCats.dto.ts
Normal file
47
service/src/modules/app/dto/createCats.dto.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsDefined, IsIn, IsNumber, IsOptional } from 'class-validator';
|
||||
|
||||
export class CreateCatsDto {
|
||||
@ApiProperty({
|
||||
example: '编程助手',
|
||||
description: 'app分类名称',
|
||||
required: true,
|
||||
})
|
||||
@IsDefined({ message: 'app分类名称是必传参数' })
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 100,
|
||||
description: '分类排序、数字越大越靠前',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
order: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 1,
|
||||
description: '分类状态 0:禁用 1:启用',
|
||||
required: true,
|
||||
})
|
||||
@IsNumber({}, { message: '状态必须是Number' })
|
||||
@IsIn([0, 1, 3, 4, 5], { message: '套餐状态错误' })
|
||||
status: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 0,
|
||||
description: '分类是否为会员专属 0:否 1:是',
|
||||
required: true,
|
||||
})
|
||||
@IsNumber({}, { message: '分类是否为会员专属必须是Number' })
|
||||
@IsIn([0, 1], { message: '分类是否为会员专属错误' })
|
||||
isMember: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 0,
|
||||
description: '非会员是否隐藏 0:否 1:是',
|
||||
required: true,
|
||||
})
|
||||
@IsNumber({}, { message: '非会员是否隐藏必须是Number' })
|
||||
@IsIn([0, 1], { message: '非会员是否隐藏状态错误' })
|
||||
hideFromNonMember: number;
|
||||
}
|
||||
50
service/src/modules/app/dto/custonApp.dto.ts
Normal file
50
service/src/modules/app/dto/custonApp.dto.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { IsOptional, IsDefined } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CustomAppDto {
|
||||
@ApiProperty({ example: '前端助手', description: 'app名称', required: true })
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ example: 1, description: 'app分类Id', required: true })
|
||||
catId: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: '适用于编程编码、期望成为您的编程助手',
|
||||
description: 'app名称详情描述',
|
||||
required: false,
|
||||
})
|
||||
@IsDefined({ message: 'app名称描述是必传参数' })
|
||||
des: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '你现在是一个翻译官。接下来我说的所有话帮我翻译成中文',
|
||||
description: '预设的prompt',
|
||||
required: true,
|
||||
})
|
||||
preset: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'https://xxxx.png',
|
||||
description: '套餐封面图片',
|
||||
required: false,
|
||||
})
|
||||
coverImg: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '这是一句示例数据',
|
||||
description: 'app示例数据',
|
||||
required: false,
|
||||
})
|
||||
demoData: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: false,
|
||||
description: '是否共享到所有人',
|
||||
required: false,
|
||||
})
|
||||
public: boolean;
|
||||
|
||||
@ApiProperty({ example: 1, description: '应用ID', required: false })
|
||||
@IsOptional()
|
||||
appId: number;
|
||||
}
|
||||
8
service/src/modules/app/dto/deleteApp.dto.ts
Normal file
8
service/src/modules/app/dto/deleteApp.dto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumber } from 'class-validator';
|
||||
|
||||
export class OperateAppDto {
|
||||
@ApiProperty({ example: 1, description: '要删除的appId', required: true })
|
||||
@IsNumber({}, { message: 'ID必须是Number' })
|
||||
id: number;
|
||||
}
|
||||
8
service/src/modules/app/dto/deleteCats.dto.ts
Normal file
8
service/src/modules/app/dto/deleteCats.dto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumber } from 'class-validator';
|
||||
|
||||
export class DeleteCatsDto {
|
||||
@ApiProperty({ example: 1, description: '要删除app分类Id', required: true })
|
||||
@IsNumber({}, { message: 'ID必须是Number' })
|
||||
id: number;
|
||||
}
|
||||
40
service/src/modules/app/dto/queryApp.dto.ts
Normal file
40
service/src/modules/app/dto/queryApp.dto.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsOptional } from 'class-validator';
|
||||
|
||||
export class QuerAppDto {
|
||||
@ApiProperty({ example: 1, description: '查询页数', required: false })
|
||||
@IsOptional()
|
||||
page: number;
|
||||
|
||||
@ApiProperty({ example: 10, description: '每页数量', required: false })
|
||||
@IsOptional()
|
||||
size: number;
|
||||
|
||||
@ApiProperty({ example: 'name', description: 'app名称', required: false })
|
||||
@IsOptional()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 1,
|
||||
description: 'app状态 0:禁用 1:启用 3:审核加入广场中 4:已拒绝加入广场',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
status: number;
|
||||
|
||||
@ApiProperty({ example: 2, description: 'app分类Id', required: false })
|
||||
@IsOptional()
|
||||
catId: number;
|
||||
|
||||
@ApiProperty({ example: 'role', description: 'app角色', required: false })
|
||||
@IsOptional()
|
||||
role: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '关键词',
|
||||
description: '搜索关键词',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
keyword: string;
|
||||
}
|
||||
24
service/src/modules/app/dto/queryCats.dto.ts
Normal file
24
service/src/modules/app/dto/queryCats.dto.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { IsOptional } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class QuerCatsDto {
|
||||
@ApiProperty({ example: 1, description: '查询页数', required: false })
|
||||
@IsOptional()
|
||||
page: number;
|
||||
|
||||
@ApiProperty({ example: 10, description: '每页数量', required: false })
|
||||
@IsOptional()
|
||||
size: number;
|
||||
|
||||
@ApiProperty({ example: 'name', description: '分类名称', required: false })
|
||||
@IsOptional()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 1,
|
||||
description: '分类状态 0:禁用 1:启用',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
status: number;
|
||||
}
|
||||
9
service/src/modules/app/dto/updateApp.dto.ts
Normal file
9
service/src/modules/app/dto/updateApp.dto.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { IsNumber } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { CreateAppDto } from './createApp.dto';
|
||||
|
||||
export class UpdateAppDto extends CreateAppDto {
|
||||
@ApiProperty({ example: 1, description: '要修改的分类Id', required: true })
|
||||
@IsNumber({}, { message: '分类ID必须是Number' })
|
||||
id: number;
|
||||
}
|
||||
9
service/src/modules/app/dto/updateCats.dto.ts
Normal file
9
service/src/modules/app/dto/updateCats.dto.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { IsNumber } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { CreateCatsDto } from './createCats.dto';
|
||||
|
||||
export class UpdateCatsDto extends CreateCatsDto {
|
||||
@ApiProperty({ example: 1, description: '要修改的分类Id', required: true })
|
||||
@IsNumber({}, { message: '分类ID必须是Number' })
|
||||
id: number;
|
||||
}
|
||||
23
service/src/modules/app/userApps.entity.ts
Normal file
23
service/src/modules/app/userApps.entity.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { BaseEntity } from 'src/common/entity/baseEntity';
|
||||
import { Column, Entity } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'user_apps' })
|
||||
export class UserAppsEntity extends BaseEntity {
|
||||
@Column({ comment: '用户ID' })
|
||||
userId: number;
|
||||
|
||||
@Column({ comment: '应用ID' })
|
||||
appId: number;
|
||||
|
||||
@Column({ comment: 'app类型 system/user', default: 'user' })
|
||||
appType: string;
|
||||
|
||||
@Column({ comment: '是否公开到公告菜单', default: false })
|
||||
public: boolean;
|
||||
|
||||
@Column({ comment: 'app状态 1正常 2审核 3违规', default: 1 })
|
||||
status: number;
|
||||
|
||||
@Column({ comment: 'App应用排序、数字越大越靠前', default: 100 })
|
||||
order: number;
|
||||
}
|
||||
80
service/src/modules/auth/auth.controller.ts
Normal file
80
service/src/modules/auth/auth.controller.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { JwtAuthGuard } from '@/common/auth/jwtAuth.guard';
|
||||
import { SuperAuthGuard } from '@/common/auth/superAuth.guard';
|
||||
import { Body, Controller, Get, Logger, Post, Req, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { Request } from 'express';
|
||||
import { AuthService } from './auth.service';
|
||||
import { UserLoginDto } from './dto/authLogin.dto';
|
||||
import { UpdatePassByOtherDto } from './dto/updatePassByOther.dto';
|
||||
import { UpdatePasswordDto } from './dto/updatePassword.dto';
|
||||
|
||||
@ApiTags('auth')
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
@Post('login')
|
||||
@ApiOperation({ summary: '用户登录' })
|
||||
async login(@Body() body: UserLoginDto, @Req() req: Request) {
|
||||
return this.authService.login(body, req);
|
||||
}
|
||||
|
||||
@Post('updatePassword')
|
||||
@ApiOperation({ summary: '用户更改密码' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
async updatePassword(@Req() req: Request, @Body() body: UpdatePasswordDto) {
|
||||
return this.authService.updatePassword(req, body);
|
||||
}
|
||||
|
||||
@Post('updatePassByOther')
|
||||
@ApiOperation({ summary: '管理员更改用户密码' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(SuperAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
async updatePassByOther(@Req() req: Request, @Body() body: UpdatePassByOtherDto) {
|
||||
return this.authService.updatePassByOther(req, body);
|
||||
}
|
||||
|
||||
@Get('getInfo')
|
||||
@ApiOperation({ summary: '获取用户个人信息' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
async getInfo(@Req() req: Request) {
|
||||
const { id, role } = req.user || {};
|
||||
const fingerprint = req.headers.fingerprint;
|
||||
Logger.debug(
|
||||
`用户信息请求 | ID: ${id} | 角色: ${role} | 指纹: ${fingerprint}`,
|
||||
'AuthController',
|
||||
);
|
||||
return this.authService.getInfo(req);
|
||||
}
|
||||
|
||||
@Post('sendCode')
|
||||
@ApiOperation({ summary: '发送验证码' })
|
||||
async sendCode(@Body() parmas: any) {
|
||||
return this.authService.sendCode(parmas);
|
||||
}
|
||||
|
||||
@Post('sendPhoneCode')
|
||||
@ApiOperation({ summary: '发送手机验证码' })
|
||||
async sendPhoneCode(@Body() parmas: any) {
|
||||
return this.authService.sendPhoneCode(parmas);
|
||||
}
|
||||
|
||||
@Post('verifyIdentity')
|
||||
@ApiOperation({ summary: '验证身份' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
async verifyIdentity(@Req() req: Request, @Body() body: any) {
|
||||
return this.authService.verifyIdentity(req, body);
|
||||
}
|
||||
|
||||
@Post('verifyPhoneIdentity')
|
||||
@ApiOperation({ summary: '验证手机号' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
async verifyPhoneIdentity(@Req() req: Request, @Body() body: any) {
|
||||
return this.authService.verifyPhoneIdentity(req, body);
|
||||
}
|
||||
}
|
||||
64
service/src/modules/auth/auth.module.ts
Normal file
64
service/src/modules/auth/auth.module.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { JwtStrategy } from '@/common/auth/jwt.strategy';
|
||||
import { JwtAuthGuard } from '@/common/auth/jwtAuth.guard';
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ChatGroupEntity } from '../chatGroup/chatGroup.entity';
|
||||
import { ChatLogEntity } from '../chatLog/chatLog.entity';
|
||||
import { CramiPackageEntity } from '../crami/cramiPackage.entity';
|
||||
import { ConfigEntity } from '../globalConfig/config.entity';
|
||||
import { MailerService } from '../mailer/mailer.service';
|
||||
import { RedisCacheModule } from '../redisCache/redisCache.module';
|
||||
import { RedisCacheService } from '../redisCache/redisCache.service';
|
||||
import { UserEntity } from '../user/user.entity';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { AccountLogEntity } from '../userBalance/accountLog.entity';
|
||||
import { BalanceEntity } from '../userBalance/balance.entity';
|
||||
import { FingerprintLogEntity } from '../userBalance/fingerprint.entity';
|
||||
import { UserBalanceEntity } from '../userBalance/userBalance.entity';
|
||||
import { UserBalanceService } from '../userBalance/userBalance.service';
|
||||
import { VerificationEntity } from './../verification/verification.entity';
|
||||
import { VerificationService } from './../verification/verification.service';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
UserModule,
|
||||
RedisCacheModule,
|
||||
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||
JwtModule.registerAsync({
|
||||
inject: [RedisCacheService],
|
||||
useFactory: async (redisService: RedisCacheService) => ({
|
||||
secret: await redisService.getJwtSecret(),
|
||||
signOptions: { expiresIn: '7d' },
|
||||
}),
|
||||
}),
|
||||
TypeOrmModule.forFeature([
|
||||
VerificationEntity,
|
||||
BalanceEntity,
|
||||
AccountLogEntity,
|
||||
ConfigEntity,
|
||||
CramiPackageEntity,
|
||||
UserBalanceEntity,
|
||||
UserEntity,
|
||||
FingerprintLogEntity,
|
||||
ChatLogEntity,
|
||||
ChatGroupEntity,
|
||||
]),
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [
|
||||
AuthService,
|
||||
JwtStrategy,
|
||||
JwtAuthGuard,
|
||||
MailerService,
|
||||
VerificationService,
|
||||
UserBalanceService,
|
||||
RedisCacheService,
|
||||
],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
491
service/src/modules/auth/auth.service.ts
Normal file
491
service/src/modules/auth/auth.service.ts
Normal file
@@ -0,0 +1,491 @@
|
||||
import { UserStatusEnum, UserStatusErrMsg } from '@/common/constants/user.constant';
|
||||
import { createRandomCode, createRandomUid, getClientIp } from '@/common/utils';
|
||||
import { GlobalConfigService } from '@/modules/globalConfig/globalConfig.service';
|
||||
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { Request } from 'express';
|
||||
import * as os from 'os';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ConfigEntity } from '../globalConfig/config.entity';
|
||||
import { MailerService } from '../mailer/mailer.service';
|
||||
import { RedisCacheService } from '../redisCache/redisCache.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { UserBalanceService } from '../userBalance/userBalance.service';
|
||||
import { UserEntity } from './../user/user.entity';
|
||||
import { VerificationService } from './../verification/verification.service';
|
||||
import { UserLoginDto } from './dto/authLogin.dto';
|
||||
import { UpdatePassByOtherDto } from './dto/updatePassByOther.dto';
|
||||
import { UpdatePasswordDto } from './dto/updatePassword.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private ipAddress: string;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ConfigEntity)
|
||||
private readonly configEntity: Repository<ConfigEntity>,
|
||||
private userService: UserService,
|
||||
private jwtService: JwtService,
|
||||
private mailerService: MailerService,
|
||||
private readonly verificationService: VerificationService,
|
||||
private readonly userBalanceService: UserBalanceService,
|
||||
private readonly redisCacheService: RedisCacheService,
|
||||
private readonly globalConfigService: GlobalConfigService, // private readonly userEntity: Repository<UserEntity>
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
this.getIp();
|
||||
}
|
||||
|
||||
async login(user: UserLoginDto, req: Request): Promise<string> {
|
||||
Logger.debug(`用户登录尝试,用户名: ${user.username}`, 'authService');
|
||||
|
||||
// 检查是否是验证码登录
|
||||
if (user.captchaId) {
|
||||
Logger.debug(`检测到验证码登录,联系方式: ${user.username}`, 'authService');
|
||||
return await this.loginWithCaptcha({ contact: user.username, code: user.captchaId }, req);
|
||||
}
|
||||
|
||||
// 密码登录流程
|
||||
const u: UserEntity = await this.userService.verifyUserCredentials(user);
|
||||
if (!u) {
|
||||
Logger.error(`登录失败: 用户凭证无效 - 用户名: ${user.username}`, 'authService');
|
||||
throw new HttpException('登录失败,用户凭证无效。', HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
const { username, id, email, role, openId, client, phone } = u;
|
||||
Logger.debug(`用户${username}(ID: ${id})登录成功`, 'authService');
|
||||
|
||||
// 保存登录IP
|
||||
const ip = getClientIp(req);
|
||||
await this.userService.savaLoginIp(id, ip);
|
||||
|
||||
// 生成JWT令牌
|
||||
const token = await this.jwtService.sign({
|
||||
username,
|
||||
id,
|
||||
email,
|
||||
role,
|
||||
openId,
|
||||
client,
|
||||
phone,
|
||||
});
|
||||
|
||||
// 保存令牌到Redis
|
||||
await this.redisCacheService.saveToken(id, token);
|
||||
Logger.debug(`用户${username}(ID: ${id})登录完成,IP: ${ip}`, 'authService');
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
async loginWithCaptcha(body: any, req: Request): Promise<string> {
|
||||
const { contact, code } = body;
|
||||
let email = '',
|
||||
phone = '';
|
||||
|
||||
// 判断 contact 是邮箱还是手机号
|
||||
const isEmail = /\S+@\S+\.\S+/.test(contact);
|
||||
const isPhone = /^\d{10,}$/.test(contact); // 根据实际需求调整正则表达式
|
||||
Logger.debug(`验证码登录 | 联系方式: ${contact}`, 'authService');
|
||||
|
||||
if (isEmail) {
|
||||
email = contact;
|
||||
} else if (isPhone) {
|
||||
phone = contact;
|
||||
} else {
|
||||
throw new HttpException('请提供有效的邮箱地址或手机号码。', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// 验证短信或邮箱验证码
|
||||
const nameSpace = await this.globalConfigService.getNamespace();
|
||||
const codeKey = `${nameSpace}:CODE:${contact}`;
|
||||
|
||||
// 获取验证码
|
||||
const savedCode = await this.redisCacheService.get({ key: codeKey });
|
||||
|
||||
if (savedCode) {
|
||||
// 验证码存在,检查是否匹配
|
||||
if (savedCode !== code) {
|
||||
Logger.log(`验证码错误 | 联系方式: ${contact}`, 'authService');
|
||||
throw new HttpException('验证码错误', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
Logger.debug(`验证码验证成功`);
|
||||
|
||||
// 验证码验证成功后,立即删除缓存中的验证码,避免重复使用
|
||||
await this.redisCacheService.del({ key: codeKey });
|
||||
|
||||
// 处理用户登录
|
||||
return await this.processUserLogin(email, phone, contact, req);
|
||||
} else {
|
||||
Logger.log(`验证码不存在或已过期 | 联系方式: ${contact}`, 'authService');
|
||||
throw new HttpException('验证码不存在或已过期,请重新获取', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
// 抽取用户登录处理逻辑为独立方法
|
||||
private async processUserLogin(
|
||||
email: string,
|
||||
phone: string,
|
||||
contact: string,
|
||||
req: Request,
|
||||
): Promise<string> {
|
||||
// 检查用户是否存在
|
||||
let u = await this.userService.getUserByContact({ email, phone });
|
||||
|
||||
// 如果用户不存在,创建新用户
|
||||
if (!u) {
|
||||
Logger.log(`创建新用户 | 联系方式: ${contact}`, 'authService');
|
||||
|
||||
// 创建随机用户名
|
||||
let username = createRandomUid();
|
||||
while (true) {
|
||||
const usernameTaken = await this.userService.verifyUserRegister({
|
||||
username,
|
||||
});
|
||||
if (usernameTaken) {
|
||||
break;
|
||||
}
|
||||
username = createRandomUid();
|
||||
}
|
||||
|
||||
// 创建新用户对象
|
||||
let newUser: any = {
|
||||
username,
|
||||
status: UserStatusEnum.ACTIVE,
|
||||
};
|
||||
|
||||
// 根据联系方式类型添加相应字段
|
||||
const isEmail = /\S+@\S+\.\S+/.test(contact);
|
||||
if (isEmail) {
|
||||
newUser.email = contact;
|
||||
} else {
|
||||
// 为手机用户创建一个随机邮箱
|
||||
newUser.email = `${createRandomUid()}@aiweb.com`;
|
||||
newUser.phone = contact;
|
||||
}
|
||||
|
||||
// 创建随机密码并加密
|
||||
const randomPassword = createRandomUid().substring(0, 8);
|
||||
const hashedPassword = bcrypt.hashSync(randomPassword, 10);
|
||||
newUser.password = hashedPassword;
|
||||
|
||||
// 保存新用户到数据库
|
||||
u = await this.userService.createUser(newUser);
|
||||
Logger.log(`用户创建成功 | 用户ID: ${u.id}`, 'authService');
|
||||
|
||||
// 为新用户添加初始余额
|
||||
await this.userBalanceService.addBalanceToNewUser(u.id);
|
||||
}
|
||||
|
||||
if (!u) {
|
||||
throw new HttpException('登录失败,用户创建失败。', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
const { username, id, role, openId, client } = u;
|
||||
|
||||
// 保存登录IP
|
||||
const ip = getClientIp(req);
|
||||
await this.userService.savaLoginIp(id, ip);
|
||||
|
||||
// 生成JWT令牌
|
||||
const token = await this.jwtService.sign({
|
||||
username,
|
||||
id,
|
||||
email,
|
||||
role,
|
||||
openId,
|
||||
client,
|
||||
phone,
|
||||
});
|
||||
|
||||
// 保存令牌到Redis
|
||||
await this.redisCacheService.saveToken(id, token);
|
||||
Logger.log(`用户登录成功 | 用户ID: ${id} | 联系方式: ${contact}`, 'authService');
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
async loginByOpenId(user: UserEntity, req: Request): Promise<string> {
|
||||
const { status } = user;
|
||||
if (status !== UserStatusEnum.ACTIVE) {
|
||||
throw new HttpException(UserStatusErrMsg[status], HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
const { username, id, email, role, openId, client } = user;
|
||||
const ip = getClientIp(req);
|
||||
await this.userService.savaLoginIp(id, ip);
|
||||
const token = await this.jwtService.sign({
|
||||
username,
|
||||
id,
|
||||
email,
|
||||
role,
|
||||
openId,
|
||||
client,
|
||||
});
|
||||
await this.redisCacheService.saveToken(id, token);
|
||||
return token;
|
||||
}
|
||||
|
||||
async getInfo(req: Request) {
|
||||
const { id, role } = req.user;
|
||||
Logger.debug(`获取用户信息 | 用户ID: ${id} | 角色: ${role}`, 'AuthService-getInfo');
|
||||
|
||||
// 记录请求头中的指纹
|
||||
if (req.headers.fingerprint) {
|
||||
Logger.debug(`请求包含指纹头: ${req.headers.fingerprint}`, 'AuthService-getInfo');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.userService.getUserInfo(id);
|
||||
Logger.debug(`成功获取用户信息 | 用户ID: ${id}`, 'AuthService-getInfo');
|
||||
return result;
|
||||
} catch (error) {
|
||||
Logger.error(`获取用户信息失败: ${error.message}`, 'AuthService-getInfo');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async updatePassword(req: Request, body: UpdatePasswordDto) {
|
||||
const { id, client, role } = req.user;
|
||||
if (client && Number(client) > 0) {
|
||||
throw new HttpException('无权此操作、请联系管理员!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
if (role === 'admin') {
|
||||
throw new HttpException('非法操作、请联系管理员!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
// const bool = await this.userService.verifyUserPassword(id, body.oldPassword);
|
||||
// if (!bool) {
|
||||
// throw new HttpException('旧密码错误、请检查提交', HttpStatus.BAD_REQUEST);
|
||||
// }
|
||||
this.userService.updateUserPassword(id, body.password);
|
||||
return '密码修改成功';
|
||||
}
|
||||
|
||||
async updatePassByOther(req: Request, body: UpdatePassByOtherDto) {
|
||||
const { id, client } = req.user;
|
||||
if (!client) {
|
||||
throw new HttpException('无权此操作!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
this.userService.updateUserPassword(id, body.password);
|
||||
return '密码修改成功';
|
||||
}
|
||||
|
||||
getIp() {
|
||||
let ipAddress: string;
|
||||
const interfaces = os.networkInterfaces();
|
||||
Object.keys(interfaces).forEach(interfaceName => {
|
||||
const interfaceInfo = interfaces[interfaceName];
|
||||
for (let i = 0; i < interfaceInfo.length; i++) {
|
||||
const alias = interfaceInfo[i];
|
||||
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
|
||||
ipAddress = alias.address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
this.ipAddress = ipAddress;
|
||||
}
|
||||
|
||||
/* 发送验证证码 */
|
||||
async sendCode(body: any) {
|
||||
const { contact, isLogin } = body;
|
||||
|
||||
let email = '',
|
||||
phone = '';
|
||||
const code = createRandomCode();
|
||||
|
||||
// 判断 contact 是邮箱还是手机号
|
||||
const isEmail = /\S+@\S+\.\S+/.test(contact);
|
||||
const isPhone = /^\d{10,}$/.test(contact); // 根据实际需求调整正则表达式
|
||||
Logger.debug(`发送验证码 | 联系方式: ${contact}`);
|
||||
|
||||
if (!isEmail && !isPhone) {
|
||||
throw new HttpException('请提供有效的邮箱地址或手机号码。', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// 注册时才检查用户是否已存在
|
||||
if (!isLogin) {
|
||||
if (isEmail) {
|
||||
email = contact;
|
||||
} else if (isPhone) {
|
||||
phone = contact;
|
||||
}
|
||||
}
|
||||
|
||||
const nameSpace = await this.globalConfigService.getNamespace();
|
||||
const key = `${nameSpace}:CODE:${contact}`;
|
||||
|
||||
// 检查Redis中是否已经有验证码且未过期
|
||||
const ttl = await this.redisCacheService.ttl(key);
|
||||
if (ttl && ttl > 0 && isPhone) {
|
||||
throw new HttpException(`${ttl}秒内不得重复发送验证码!`, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (isEmail) {
|
||||
// 检查Redis中是否已经有验证码
|
||||
const existingCode = await this.redisCacheService.get({ key });
|
||||
if (existingCode) {
|
||||
// 如果存在有效的验证码,则直接使用这个验证码,而不生成新的
|
||||
await this.mailerService.sendMail({
|
||||
to: email,
|
||||
context: {
|
||||
// 这里传入模板中使用的变量和数据
|
||||
code: existingCode,
|
||||
},
|
||||
});
|
||||
Logger.log(`重发验证码 | 邮箱: ${email}`, 'authService');
|
||||
return `验证码发送成功、请填写验证码完成${isLogin ? '登录' : '注册'}!`;
|
||||
} else {
|
||||
// 如果没有现有验证码或验证码已过期,则生成新的验证码
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: email,
|
||||
context: {
|
||||
// 这里传入模板中使用的变量和数据
|
||||
code: code,
|
||||
},
|
||||
});
|
||||
Logger.log(`发送新验证码 | 邮箱: ${email}`, 'authService');
|
||||
} catch (error) {
|
||||
Logger.error(`邮件发送失败: ${error.message}`, 'authService');
|
||||
throw new HttpException('验证码发送失败,请稍后重试', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
await this.redisCacheService.set({ key, val: code }, 10 * 60); // 设置验证码600秒过期
|
||||
return `验证码发送成功、请填写验证码完成${isLogin ? '登录' : '注册'}!`;
|
||||
}
|
||||
} else if (isPhone) {
|
||||
const messageInfo = { phone, code };
|
||||
await this.verificationService.sendPhoneCode(messageInfo);
|
||||
await this.redisCacheService.set({ key, val: code }, 10 * 60);
|
||||
Logger.log(`发送验证码 | 手机号: ${phone}`, 'authService');
|
||||
return `验证码发送成功、请填写验证码完成${isLogin ? '登录' : '注册'}!`;
|
||||
}
|
||||
}
|
||||
|
||||
/* 发送验证证码 */
|
||||
async sendPhoneCode(body: any) {
|
||||
const { phone, isLogin } = body;
|
||||
// const { id } = req.user;
|
||||
const code = createRandomCode();
|
||||
// 判断 contact 是邮箱还是手机号
|
||||
const isPhone = /^\d{10,}$/.test(phone); // 根据实际需求调整正则表达式
|
||||
Logger.debug(`发送手机验证码 | 手机号: ${phone}`);
|
||||
|
||||
if (!isPhone) {
|
||||
throw new HttpException('请提供有效的手机号码。', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// 仅在注册流程且指定登录标记时校验已存在用户
|
||||
if (isLogin === false) {
|
||||
const isAvailable = await this.userService.verifyUserRegister({
|
||||
phone,
|
||||
});
|
||||
if (!isAvailable) {
|
||||
throw new HttpException('当前手机号已注册,请勿重复注册!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
const nameSpace = await this.globalConfigService.getNamespace();
|
||||
const key = `${nameSpace}:CODE:${phone}`;
|
||||
|
||||
// 检查Redis中是否已经有验证码且未过期
|
||||
const ttl = await this.redisCacheService.ttl(key);
|
||||
if (ttl && ttl > 0 && isPhone) {
|
||||
throw new HttpException(`${ttl}秒内不得重复发送验证码!`, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const messageInfo = { phone, code };
|
||||
await this.redisCacheService.set({ key, val: code }, 10 * 60);
|
||||
await this.verificationService.sendPhoneCode(messageInfo);
|
||||
Logger.log(`发送验证码 | 手机号: ${phone}`, 'authService');
|
||||
|
||||
return `验证码发送成功、请填写验证码完成${isLogin === false ? '注册' : '验证/登录'}!`;
|
||||
}
|
||||
|
||||
/* create token */
|
||||
createTokenFromFingerprint(fingerprint) {
|
||||
const token = this.jwtService.sign({
|
||||
username: `游客${fingerprint}`,
|
||||
id: fingerprint,
|
||||
email: `${fingerprint}@visitor.com`,
|
||||
role: 'visitor',
|
||||
openId: null,
|
||||
client: null,
|
||||
});
|
||||
return token;
|
||||
}
|
||||
|
||||
async verifyIdentity(req: Request, body) {
|
||||
Logger.debug('开始实名认证流程');
|
||||
const { name, idCard } = body;
|
||||
|
||||
const { id } = req.user;
|
||||
|
||||
try {
|
||||
// 调用验证服务进行身份验证
|
||||
const result = await this.verificationService.verifyIdentity(body);
|
||||
|
||||
// 输出验证结果到日志
|
||||
Logger.debug(`实名认证结果: ${result}`);
|
||||
|
||||
// 检查验证结果
|
||||
if (!result) {
|
||||
throw new HttpException('身份验证错误,请检查实名信息', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
// 保存用户的实名信息
|
||||
await this.userService.saveRealNameInfo(id, name, idCard);
|
||||
return '认证成功';
|
||||
} catch (error) {
|
||||
// 处理可能的错误并记录错误信息
|
||||
Logger.error('验证过程出现错误', error);
|
||||
throw new HttpException('认证失败,请检查相关信息', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
async verifyPhoneIdentity(req: Request, body) {
|
||||
Logger.debug('开始手机号认证流程');
|
||||
const { phone, username, password, code } = body;
|
||||
const { id } = req.user;
|
||||
|
||||
// 校验验证码是否过期或错误
|
||||
const nameSpace = this.globalConfigService.getNamespace();
|
||||
const key = `${nameSpace}:CODE:${phone}`;
|
||||
const redisCode = await this.redisCacheService.get({ key });
|
||||
Logger.debug(`Retrieved redisCode for ${phone}: ${redisCode}`);
|
||||
if (code === '') {
|
||||
throw new HttpException('请输入验证码', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
if (!redisCode) {
|
||||
Logger.log(`验证码过期: ${phone}`, 'authService');
|
||||
throw new HttpException('验证码已过期,请重新发送!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (code !== redisCode) {
|
||||
Logger.log(
|
||||
`验证码错误: ${phone} 输入的验证码: ${code}, 期望的验证码: ${redisCode}`,
|
||||
'authService',
|
||||
);
|
||||
throw new HttpException('验证码填写错误,请重新输入!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// 验证用户名是否已存在
|
||||
if (username) {
|
||||
const usernameTaken = await this.userService.isUsernameTaken(body.username, id);
|
||||
if (usernameTaken) {
|
||||
throw new HttpException('用户名已存在!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 保存用户的实名信息
|
||||
await this.userService.updateUserPhone(id, phone, username, password);
|
||||
return '认证成功';
|
||||
} catch (error) {
|
||||
// 处理可能的错误并记录错误信息
|
||||
Logger.error('验证过程出现错误', error);
|
||||
throw new HttpException('身份验证错误,请检查相关信息', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
service/src/modules/auth/dto/adminLogin.dto.ts
Normal file
17
service/src/modules/auth/dto/adminLogin.dto.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { IsNotEmpty, MinLength, MaxLength, IsOptional } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class AdminLoginDto {
|
||||
@ApiProperty({ example: 'super', description: '邮箱' })
|
||||
@IsNotEmpty({ message: '用户名不能为空!' })
|
||||
@MinLength(2, { message: '用户名最短是两位数!' })
|
||||
@MaxLength(30, { message: '用户名最长不得超过30位!' })
|
||||
@IsOptional()
|
||||
username?: string;
|
||||
|
||||
@ApiProperty({ example: '999999', description: '密码' })
|
||||
@IsNotEmpty({ message: '用户密码不能为空!' })
|
||||
@MinLength(6, { message: '用户密码最低需要大于6位数!' })
|
||||
@MaxLength(30, { message: '用户密码最长不能超过30位数!' })
|
||||
password: string;
|
||||
}
|
||||
25
service/src/modules/auth/dto/authLogin.dto.ts
Normal file
25
service/src/modules/auth/dto/authLogin.dto.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsOptional, MaxLength, MinLength } from 'class-validator';
|
||||
|
||||
export class UserLoginDto {
|
||||
@ApiProperty({ example: 'super', description: '邮箱' })
|
||||
@IsNotEmpty({ message: '用户名不能为空!' })
|
||||
@MinLength(2, { message: '用户名最短是两位数!' })
|
||||
@MaxLength(30, { message: '用户名最长不得超过30位!' })
|
||||
@IsOptional()
|
||||
username?: string;
|
||||
|
||||
@ApiProperty({ example: 1, description: '用户ID' })
|
||||
@IsOptional()
|
||||
uid?: number;
|
||||
|
||||
@ApiProperty({ example: '999999', description: '密码' })
|
||||
@IsOptional()
|
||||
@MinLength(6, { message: '用户密码最低需要大于6位数!' })
|
||||
@MaxLength(30, { message: '用户密码最长不能超过30位数!' })
|
||||
password?: string;
|
||||
|
||||
@ApiProperty({ example: 'abc123', description: '图形验证码ID' })
|
||||
@IsOptional()
|
||||
captchaId?: string;
|
||||
}
|
||||
37
service/src/modules/auth/dto/authRegister.dto.ts
Normal file
37
service/src/modules/auth/dto/authRegister.dto.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsOptional, MaxLength, MinLength } from 'class-validator';
|
||||
|
||||
export class UserRegisterDto {
|
||||
@ApiProperty({ example: 'cooper', description: '用户名称' })
|
||||
// @IsNotEmpty({ message: '用户名不能为空!' })
|
||||
// @MinLength(2, { message: '用户名最低需要大于2位数!' })
|
||||
// @MaxLength(12, { message: '用户名不得超过12位!' })
|
||||
username?: string;
|
||||
|
||||
@ApiProperty({ example: '123456', description: '用户密码' })
|
||||
@IsNotEmpty({ message: '用户密码不能为空' })
|
||||
@MinLength(6, { message: '用户密码最低需要大于6位数!' })
|
||||
@MaxLength(30, { message: '用户密码最长不能超过30位数!' })
|
||||
password: string;
|
||||
|
||||
@ApiProperty({ example: 'ai@aiweb.com', description: '用户邮箱' })
|
||||
// @IsEmail({}, { message: '请填写正确格式的邮箱!' })
|
||||
// @IsNotEmpty({ message: '邮箱不能为空!' })
|
||||
email: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '',
|
||||
description: '用户头像',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
avatar: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'default',
|
||||
description: '用户注册来源',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
client: string;
|
||||
}
|
||||
15
service/src/modules/auth/dto/loginByPhone.dt.ts
Normal file
15
service/src/modules/auth/dto/loginByPhone.dt.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { IsNotEmpty, MinLength, MaxLength, IsPhoneNumber } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class LoginByPhoneDto {
|
||||
@ApiProperty({ example: '19999999', description: '手机号' })
|
||||
@IsNotEmpty({ message: '手机号不能为空!' })
|
||||
@IsPhoneNumber('CN', { message: '手机号格式不正确!' })
|
||||
phone?: string;
|
||||
|
||||
@ApiProperty({ example: '999999', description: '密码' })
|
||||
@IsNotEmpty({ message: '用户密码不能为空!' })
|
||||
@MinLength(6, { message: '用户密码最低需要大于6位数!' })
|
||||
@MaxLength(30, { message: '用户密码最长不能超过30位数!' })
|
||||
password: string;
|
||||
}
|
||||
18
service/src/modules/auth/dto/sendPhoneCode.dto.ts
Normal file
18
service/src/modules/auth/dto/sendPhoneCode.dto.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { IsNotEmpty, MinLength, MaxLength } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class SendPhoneCodeDto {
|
||||
@ApiProperty({ example: '199999999', description: '手机号' })
|
||||
@IsNotEmpty({ message: '手机号不能为空' })
|
||||
@MinLength(11, { message: '手机号长度为11位' })
|
||||
@MaxLength(11, { message: '手机号长度为11位!' })
|
||||
phone?: string;
|
||||
|
||||
@ApiProperty({ example: '2b4i1b4', description: '图形验证码KEY' })
|
||||
@IsNotEmpty({ message: '验证码KEY不能为空' })
|
||||
captchaId?: string;
|
||||
|
||||
@ApiProperty({ example: '1g4d', description: '图形验证码' })
|
||||
@IsNotEmpty({ message: '验证码不能为空' })
|
||||
captchaCode?: string;
|
||||
}
|
||||
10
service/src/modules/auth/dto/updatePassByOther.dto.ts
Normal file
10
service/src/modules/auth/dto/updatePassByOther.dto.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { IsNotEmpty, MinLength, MaxLength } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class UpdatePassByOtherDto {
|
||||
@ApiProperty({ example: '666666', description: '三方用户更新新密码' })
|
||||
@IsNotEmpty({ message: '用户密码不能为空!' })
|
||||
@MinLength(6, { message: '用户密码最低需要大于6位数!' })
|
||||
@MaxLength(30, { message: '用户密码最长不能超过30位数!' })
|
||||
password: string;
|
||||
}
|
||||
10
service/src/modules/auth/dto/updatePassword.dto.ts
Normal file
10
service/src/modules/auth/dto/updatePassword.dto.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, MaxLength, MinLength } from 'class-validator';
|
||||
|
||||
export class UpdatePasswordDto {
|
||||
@ApiProperty({ example: '666666', description: '用户更新新密码' })
|
||||
@IsNotEmpty({ message: '用户密码不能为空!' })
|
||||
@MinLength(6, { message: '用户密码最低需要大于6位数!' })
|
||||
@MaxLength(30, { message: '用户密码最长不能超过30位数!' })
|
||||
password: string;
|
||||
}
|
||||
25
service/src/modules/auth/dto/userRegisterByPhone.dto.ts
Normal file
25
service/src/modules/auth/dto/userRegisterByPhone.dto.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsPhoneNumber, MaxLength, MinLength } from 'class-validator';
|
||||
|
||||
export class UserRegisterByPhoneDto {
|
||||
@ApiProperty({ example: 'cooper', description: '用户名称' })
|
||||
@IsNotEmpty({ message: '用户名不能为空!' })
|
||||
@MinLength(2, { message: '用户名最低需要大于2位数!' })
|
||||
@MaxLength(12, { message: '用户名不得超过12位!' })
|
||||
username?: string;
|
||||
|
||||
@ApiProperty({ example: '123456', description: '用户密码' })
|
||||
@IsNotEmpty({ message: '用户密码不能为空' })
|
||||
@MinLength(6, { message: '用户密码最低需要大于6位数!' })
|
||||
@MaxLength(30, { message: '用户密码最长不能超过30位数!' })
|
||||
password: string;
|
||||
|
||||
@ApiProperty({ example: '19999999999', description: '用户手机号码' })
|
||||
@IsPhoneNumber('CN', { message: '手机号码格式不正确!' })
|
||||
@IsNotEmpty({ message: '手机号码不能为空!' })
|
||||
phone: string;
|
||||
|
||||
@ApiProperty({ example: '152546', description: '手机验证码' })
|
||||
@IsNotEmpty({ message: '手机验证码不能为空!' })
|
||||
phoneCode: string;
|
||||
}
|
||||
47
service/src/modules/autoReply/autoReply.controller.ts
Normal file
47
service/src/modules/autoReply/autoReply.controller.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { AdminAuthGuard } from '@/common/auth/adminAuth.guard';
|
||||
import { SuperAuthGuard } from '@/common/auth/superAuth.guard';
|
||||
import { Body, Controller, Get, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { AutoReplyService } from './autoReply.service';
|
||||
import { AddAutoReplyDto } from './dto/addAutoReply.dto';
|
||||
import { DelAutoReplyDto } from './dto/delBadWords.dto';
|
||||
import { QueryAutoReplyDto } from './dto/queryAutoReply.dto';
|
||||
import { UpdateAutoReplyDto } from './dto/updateAutoReply.dto';
|
||||
|
||||
@ApiTags('autoReply')
|
||||
@Controller('autoReply')
|
||||
export class AutoReplyController {
|
||||
constructor(private readonly autoReplyService: AutoReplyService) {}
|
||||
|
||||
@Get('query')
|
||||
@ApiOperation({ summary: '查询自动回复' })
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
queryAutoReply(@Query() query: QueryAutoReplyDto) {
|
||||
return this.autoReplyService.queryAutoReply(query);
|
||||
}
|
||||
|
||||
@Post('add')
|
||||
@ApiOperation({ summary: '添加自动回复' })
|
||||
@UseGuards(SuperAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
addAutoReply(@Body() body: AddAutoReplyDto) {
|
||||
return this.autoReplyService.addAutoReply(body);
|
||||
}
|
||||
|
||||
@Post('update')
|
||||
@ApiOperation({ summary: '修改自动回复' })
|
||||
@UseGuards(SuperAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
updateAutoReply(@Body() body: UpdateAutoReplyDto) {
|
||||
return this.autoReplyService.updateAutoReply(body);
|
||||
}
|
||||
|
||||
@Post('del')
|
||||
@ApiOperation({ summary: '删除自动回复' })
|
||||
@UseGuards(SuperAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
delAutoReply(@Body() body: DelAutoReplyDto) {
|
||||
return this.autoReplyService.delAutoReply(body);
|
||||
}
|
||||
}
|
||||
17
service/src/modules/autoReply/autoReply.entity.ts
Normal file
17
service/src/modules/autoReply/autoReply.entity.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { BaseEntity } from 'src/common/entity/baseEntity';
|
||||
import { Column, Entity } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'auto_reply' })
|
||||
export class AutoReplyEntity extends BaseEntity {
|
||||
@Column({ comment: '提问的问题', type: 'text' })
|
||||
prompt: string;
|
||||
|
||||
@Column({ comment: '定义的答案', type: 'text' })
|
||||
answer: string;
|
||||
|
||||
@Column({ default: 1, comment: '是否开启AI回复,0:关闭 1:启用' })
|
||||
isAIReplyEnabled: number;
|
||||
|
||||
@Column({ default: 1, comment: '启用当前自动回复状态, 0:关闭 1:启用' })
|
||||
status: number;
|
||||
}
|
||||
14
service/src/modules/autoReply/autoReply.module.ts
Normal file
14
service/src/modules/autoReply/autoReply.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AutoReplyController } from './autoReply.controller';
|
||||
import { AutoReplyEntity } from './autoReply.entity';
|
||||
import { AutoReplyService } from './autoReply.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([AutoReplyEntity])],
|
||||
controllers: [AutoReplyController],
|
||||
providers: [AutoReplyService],
|
||||
exports: [AutoReplyService],
|
||||
})
|
||||
export class AutoReplyModule {}
|
||||
128
service/src/modules/autoReply/autoReply.service.ts
Normal file
128
service/src/modules/autoReply/autoReply.service.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { HttpException, HttpStatus, Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Like, Repository } from 'typeorm';
|
||||
import { AutoReplyEntity } from './autoReply.entity';
|
||||
import { AddAutoReplyDto } from './dto/addAutoReply.dto';
|
||||
import { DelAutoReplyDto } from './dto/delBadWords.dto';
|
||||
import { QueryAutoReplyDto } from './dto/queryAutoReply.dto';
|
||||
import { UpdateAutoReplyDto } from './dto/updateAutoReply.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AutoReplyService implements OnModuleInit {
|
||||
private autoReplyKes: { prompt: string; keywords: string[] }[] = [];
|
||||
private autoReplyMap = {};
|
||||
private autoReplyFuzzyMatch = true;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(AutoReplyEntity)
|
||||
private readonly autoReplyEntity: Repository<AutoReplyEntity>,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.loadAutoReplyList();
|
||||
}
|
||||
|
||||
async loadAutoReplyList() {
|
||||
const res = await this.autoReplyEntity.find({
|
||||
where: { status: 1 },
|
||||
select: ['prompt', 'answer', 'isAIReplyEnabled'],
|
||||
});
|
||||
this.autoReplyMap = {};
|
||||
this.autoReplyKes = [];
|
||||
|
||||
res.forEach(t => {
|
||||
this.autoReplyMap[t.prompt] = {
|
||||
answer: t.answer,
|
||||
isAIReplyEnabled: t.isAIReplyEnabled,
|
||||
};
|
||||
const keywords = t.prompt.split(' ').map(k => k.trim()); // 关键词以空格分词
|
||||
this.autoReplyKes.push({ prompt: t.prompt, keywords });
|
||||
});
|
||||
}
|
||||
|
||||
async checkAutoReply(prompt: string) {
|
||||
const answers = [];
|
||||
let isAIReplyEnabled = 0;
|
||||
const seenGroups = new Set<string>();
|
||||
|
||||
// Logger.debug('checkAutoReply', prompt);
|
||||
// Logger.debug('checkAutoReply', this.autoReplyKes);
|
||||
// Logger.debug('autoReplyMap', this.autoReplyMap);
|
||||
|
||||
if (this.autoReplyFuzzyMatch) {
|
||||
for (const item of this.autoReplyKes) {
|
||||
if (item.keywords.some(keyword => prompt.includes(keyword))) {
|
||||
if (!seenGroups.has(item.prompt)) {
|
||||
answers.push(this.autoReplyMap[item.prompt].answer);
|
||||
seenGroups.add(item.prompt);
|
||||
if (this.autoReplyMap[item.prompt].isAIReplyEnabled === 1) {
|
||||
isAIReplyEnabled = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const matches = this.autoReplyKes.filter(item => item.prompt === prompt);
|
||||
for (const match of matches) {
|
||||
if (!seenGroups.has(match.prompt)) {
|
||||
answers.push(this.autoReplyMap[match.prompt].answer);
|
||||
seenGroups.add(match.prompt);
|
||||
if (this.autoReplyMap[match.prompt].isAIReplyEnabled === 1) {
|
||||
isAIReplyEnabled = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
answer: answers.join('\n'), // 拼接所有匹配到的答案
|
||||
isAIReplyEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
async queryAutoReply(query: QueryAutoReplyDto) {
|
||||
const { page = 1, size = 10, prompt, status } = query;
|
||||
const where: any = {};
|
||||
[0, 1, '0', '1'].includes(status) && (where.status = status);
|
||||
prompt && (where.prompt = Like(`%${prompt}%`));
|
||||
const [rows, count] = await this.autoReplyEntity.findAndCount({
|
||||
where,
|
||||
skip: (page - 1) * size,
|
||||
take: size,
|
||||
order: { id: 'DESC' },
|
||||
});
|
||||
return { rows, count };
|
||||
}
|
||||
|
||||
async addAutoReply(body: AddAutoReplyDto) {
|
||||
// 直接保存新的自动回复
|
||||
await this.autoReplyEntity.save(body);
|
||||
// 重新加载自动回复列表
|
||||
await this.loadAutoReplyList();
|
||||
return '添加问题成功!';
|
||||
}
|
||||
|
||||
async updateAutoReply(body: UpdateAutoReplyDto) {
|
||||
const { id } = body;
|
||||
const res = await this.autoReplyEntity.update({ id }, body);
|
||||
if (res.affected > 0) {
|
||||
await this.loadAutoReplyList();
|
||||
return '更新问题成功';
|
||||
}
|
||||
throw new HttpException('更新失败', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
async delAutoReply(body: DelAutoReplyDto) {
|
||||
const { id } = body;
|
||||
const z = await this.autoReplyEntity.findOne({ where: { id } });
|
||||
if (!z) {
|
||||
throw new HttpException('该问题不存在,请检查您的提交信息', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
const res = await this.autoReplyEntity.delete({ id });
|
||||
if (res.affected > 0) {
|
||||
await this.loadAutoReplyList();
|
||||
return '删除问题成功';
|
||||
}
|
||||
throw new HttpException('删除失败', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
13
service/src/modules/autoReply/dto/addAutoReply.dto.ts
Normal file
13
service/src/modules/autoReply/dto/addAutoReply.dto.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class AddAutoReplyDto {
|
||||
@ApiProperty({ example: '你是谁', description: '提问的问题', required: true })
|
||||
prompt: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '我是AIWeb提供的Ai服务机器人',
|
||||
description: '回答的答案',
|
||||
required: true,
|
||||
})
|
||||
answer: string;
|
||||
}
|
||||
6
service/src/modules/autoReply/dto/delBadWords.dto.ts
Normal file
6
service/src/modules/autoReply/dto/delBadWords.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class DelAutoReplyDto {
|
||||
@ApiProperty({ example: 1, description: '自动回复id', required: true })
|
||||
id: number;
|
||||
}
|
||||
20
service/src/modules/autoReply/dto/queryAutoReply.dto.ts
Normal file
20
service/src/modules/autoReply/dto/queryAutoReply.dto.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { IsOptional } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class QueryAutoReplyDto {
|
||||
@ApiProperty({ example: 1, description: '查询页数', required: false })
|
||||
@IsOptional()
|
||||
page: number;
|
||||
|
||||
@ApiProperty({ example: 10, description: '每页数量', required: false })
|
||||
@IsOptional()
|
||||
size: number;
|
||||
|
||||
@ApiProperty({ example: '你是谁', description: '提问问题', required: false })
|
||||
@IsOptional()
|
||||
prompt: string;
|
||||
|
||||
@ApiProperty({ example: 1, description: '问题状态', required: false })
|
||||
@IsOptional()
|
||||
status: number;
|
||||
}
|
||||
24
service/src/modules/autoReply/dto/updateAutoReply.dto.ts
Normal file
24
service/src/modules/autoReply/dto/updateAutoReply.dto.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsOptional } from 'class-validator';
|
||||
|
||||
export class UpdateAutoReplyDto {
|
||||
@ApiProperty({ example: 1, description: '自动回复id', required: true })
|
||||
@IsOptional()
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ example: '你可以干嘛', description: '问题', required: false })
|
||||
@IsOptional()
|
||||
prompt: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '我可以干很多事情.......',
|
||||
description: '答案',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
answer: string;
|
||||
|
||||
@ApiProperty({ example: 0, description: '状态', required: false })
|
||||
@IsOptional()
|
||||
status: number;
|
||||
}
|
||||
55
service/src/modules/badWords/badWords.controller.ts
Normal file
55
service/src/modules/badWords/badWords.controller.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { AdminAuthGuard } from '@/common/auth/adminAuth.guard';
|
||||
import { SuperAuthGuard } from '@/common/auth/superAuth.guard';
|
||||
import { Body, Controller, Get, Post, Query, Req, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { Request } from 'express';
|
||||
import { BadWordsService } from './badWords.service';
|
||||
import { AddBadWordDto } from './dto/addBadWords.dto';
|
||||
import { DelBadWordsDto } from './dto/delBadWords.dto';
|
||||
import { QueryBadWordsDto } from './dto/queryBadWords.dto';
|
||||
import { QueryViolationDto } from './dto/queryViolation.dto';
|
||||
import { UpdateBadWordsDto } from './dto/updateBadWords.dto';
|
||||
|
||||
@ApiTags('badWords')
|
||||
@Controller('badWords')
|
||||
export class BadWordsController {
|
||||
constructor(private readonly badWordsService: BadWordsService) {}
|
||||
|
||||
@Get('query')
|
||||
@ApiOperation({ summary: '查询所有敏感词' })
|
||||
queryBadWords(@Query() query: QueryBadWordsDto) {
|
||||
return this.badWordsService.queryBadWords(query);
|
||||
}
|
||||
|
||||
@Post('del')
|
||||
@ApiOperation({ summary: '删除敏感词' })
|
||||
@UseGuards(SuperAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
delBadWords(@Body() body: DelBadWordsDto) {
|
||||
return this.badWordsService.delBadWords(body);
|
||||
}
|
||||
|
||||
@Post('update')
|
||||
@ApiOperation({ summary: '更新敏感词' })
|
||||
@UseGuards(SuperAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
updateBadWords(@Body() body: UpdateBadWordsDto) {
|
||||
return this.badWordsService.updateBadWords(body);
|
||||
}
|
||||
|
||||
@Post('add')
|
||||
@ApiOperation({ summary: '新增敏感词' })
|
||||
@UseGuards(SuperAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
addBadWord(@Body() body: AddBadWordDto) {
|
||||
return this.badWordsService.addBadWord(body);
|
||||
}
|
||||
|
||||
@Get('violation')
|
||||
@ApiOperation({ summary: '查询违规记录' })
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
violation(@Req() req: Request, @Query() query: QueryViolationDto) {
|
||||
return this.badWordsService.violation(req, query);
|
||||
}
|
||||
}
|
||||
14
service/src/modules/badWords/badWords.entity.ts
Normal file
14
service/src/modules/badWords/badWords.entity.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { BaseEntity } from 'src/common/entity/baseEntity';
|
||||
import { Column, Entity } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'bad_words' })
|
||||
export class BadWordsEntity extends BaseEntity {
|
||||
@Column({ length: 20, comment: '敏感词' })
|
||||
word: string;
|
||||
|
||||
@Column({ default: 1, comment: '敏感词开启状态' })
|
||||
status: number;
|
||||
|
||||
@Column({ default: 0, comment: '敏感词触发次数' })
|
||||
count: number;
|
||||
}
|
||||
16
service/src/modules/badWords/badWords.module.ts
Normal file
16
service/src/modules/badWords/badWords.module.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UserEntity } from '../user/user.entity';
|
||||
import { BadWordsController } from './badWords.controller';
|
||||
import { BadWordsEntity } from './badWords.entity';
|
||||
import { BadWordsService } from './badWords.service';
|
||||
import { ViolationLogEntity } from './violationLog.entity';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([BadWordsEntity, ViolationLogEntity, UserEntity])],
|
||||
providers: [BadWordsService],
|
||||
controllers: [BadWordsController],
|
||||
exports: [BadWordsService],
|
||||
})
|
||||
export class BadWordsModule {}
|
||||
244
service/src/modules/badWords/badWords.service.ts
Normal file
244
service/src/modules/badWords/badWords.service.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import { hideString } from '@/common/utils';
|
||||
import { HttpException, HttpStatus, Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import axios from 'axios';
|
||||
import { In, Like, Repository } from 'typeorm';
|
||||
import { GlobalConfigService } from '../globalConfig/globalConfig.service';
|
||||
import { UserEntity } from '../user/user.entity';
|
||||
import { BadWordsEntity } from './badWords.entity';
|
||||
import { AddBadWordDto } from './dto/addBadWords.dto';
|
||||
import { DelBadWordsDto } from './dto/delBadWords.dto';
|
||||
import { QueryBadWordsDto } from './dto/queryBadWords.dto';
|
||||
import { UpdateBadWordsDto } from './dto/updateBadWords.dto';
|
||||
import { ViolationLogEntity } from './violationLog.entity';
|
||||
|
||||
@Injectable()
|
||||
export class BadWordsService implements OnModuleInit {
|
||||
private badWords: string[];
|
||||
constructor(
|
||||
@InjectRepository(BadWordsEntity)
|
||||
private readonly badWordsEntity: Repository<BadWordsEntity>,
|
||||
@InjectRepository(ViolationLogEntity)
|
||||
private readonly violationLogEntity: Repository<ViolationLogEntity>,
|
||||
@InjectRepository(UserEntity)
|
||||
private readonly userEntity: Repository<UserEntity>,
|
||||
private readonly globalConfigService: GlobalConfigService,
|
||||
) {
|
||||
this.badWords = [];
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
this.loadBadWords();
|
||||
}
|
||||
|
||||
/* 敏感词匹配 */
|
||||
async customSensitiveWords(content, userId) {
|
||||
const triggeredWords = [];
|
||||
|
||||
// 遍历敏感词列表,查找内容中是否包含敏感词
|
||||
for (let i = 0; i < this.badWords.length; i++) {
|
||||
const word = this.badWords[i];
|
||||
if (content.includes(word)) {
|
||||
triggeredWords.push(word); // 如果包含敏感词,将其加入到数组中
|
||||
}
|
||||
}
|
||||
|
||||
if (triggeredWords.length) {
|
||||
// 如果找到敏感词,记录用户提交的违规内容
|
||||
await this.recordUserBadWords(userId, content, triggeredWords, ['自定义'], '自定义检测');
|
||||
}
|
||||
|
||||
// 返回检测到的敏感词列表(如果没有敏感词,返回空数组)
|
||||
return triggeredWords;
|
||||
}
|
||||
|
||||
/* 敏感词检测 先检测百度敏感词 后检测自定义的 */
|
||||
async checkBadWords(content: string, userId: number) {
|
||||
const config = await this.globalConfigService.getSensitiveConfig();
|
||||
/* 如果有则启动配置检测 没有则跳过 */
|
||||
if (config) {
|
||||
await this.checkBadWordsByConfig(content, config, userId);
|
||||
}
|
||||
/* 自定义敏感词检测 */
|
||||
return await this.customSensitiveWords(content, userId);
|
||||
}
|
||||
|
||||
/* 通过配置信息去检测敏感词 */
|
||||
async checkBadWordsByConfig(content: string, config: any, userId) {
|
||||
const { useType } = config;
|
||||
useType === 'baidu' &&
|
||||
(await this.baiduCheckBadWords(content, config.baiduTextAccessToken, userId));
|
||||
}
|
||||
|
||||
/* 提取百度云敏感词违规类型 */
|
||||
extractContent(str) {
|
||||
const pattern = /存在(.*?)不合规/;
|
||||
const match = str.match(pattern);
|
||||
return match ? match[1] : '';
|
||||
}
|
||||
|
||||
/* 通过百度云敏感词检测 */
|
||||
async baiduCheckBadWords(content: string, accessToken: string, userId: number) {
|
||||
if (!accessToken) return;
|
||||
if (!content || content.trim() === '') {
|
||||
Logger.debug('提交的内容为空,跳过百度敏感词检测', 'BadWordsService');
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `https://aip.baidubce.com/rest/2.0/solution/v1/text_censor/v2/user_defined?access_token=${accessToken}}`;
|
||||
const headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: 'application/json',
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.post(url, { text: content }, { headers });
|
||||
const { conclusion, error_code, error_msg, conclusionType, data } = response.data;
|
||||
|
||||
// 如果API返回错误,记录错误并直接返回
|
||||
if (error_code) {
|
||||
Logger.warn(`百度文本检测出现错误、请查看配置信息: ${error_msg}`, 'BadWordsService');
|
||||
return;
|
||||
}
|
||||
|
||||
// conclusion 审核结果,可取值:合规、不合规、疑似、审核失败
|
||||
// conclusionType 1.合规,2.不合规,3.疑似,4.审核失败
|
||||
if (conclusionType !== 1 && data && Array.isArray(data)) {
|
||||
const types = [...new Set(data.map(item => this.extractContent(item.msg)))];
|
||||
await this.recordUserBadWords(userId, content, ['***'], types, '百度云检测');
|
||||
const tips = `您提交的信息中包含${types.join(
|
||||
',',
|
||||
)}的内容、我们已对您的账户进行标记、请合规使用!`;
|
||||
throw new HttpException(tips, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`百度敏感词检测服务异常: ${error.message}`, error.stack, 'BadWordsService');
|
||||
// 出错时不阻止后续流程,继续使用自定义词库检测
|
||||
}
|
||||
}
|
||||
|
||||
/* formarTips */
|
||||
formarTips(wordList) {
|
||||
const categorys = wordList.map(t => t.category);
|
||||
const unSet = [...new Set(categorys)];
|
||||
return `您提交的内容中包含${unSet.join(',')}的信息、我们已对您账号进行标记、请合规使用!`;
|
||||
}
|
||||
|
||||
/* 加载自定义的敏感词 */
|
||||
async loadBadWords() {
|
||||
const data = await this.badWordsEntity.find({
|
||||
where: { status: 1 },
|
||||
select: ['word'],
|
||||
});
|
||||
this.badWords = data.map(t => t.word);
|
||||
}
|
||||
|
||||
/* 查询自定义的敏感词 */
|
||||
async queryBadWords(query: QueryBadWordsDto) {
|
||||
const { page = 1, size = 500, word, status } = query;
|
||||
const where: any = {};
|
||||
[0, 1, '0', '1'].includes(status) && (where.status = status);
|
||||
word && (where.word = Like(`%${word}%`));
|
||||
const [rows, count] = await this.badWordsEntity.findAndCount({
|
||||
where,
|
||||
skip: (page - 1) * size,
|
||||
take: size,
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
return { rows, count };
|
||||
}
|
||||
|
||||
/* 删除自定义敏感词 */
|
||||
async delBadWords(body: DelBadWordsDto) {
|
||||
const b = await this.badWordsEntity.findOne({ where: { id: body.id } });
|
||||
if (!b) {
|
||||
throw new HttpException('敏感词不存在,请检查您的提交信息', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
const res = await this.badWordsEntity.delete({ id: body.id });
|
||||
if (res.affected > 0) {
|
||||
await this.loadBadWords();
|
||||
return '删除敏感词成功';
|
||||
} else {
|
||||
throw new HttpException('删除敏感词失败', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
/* 修改自定义敏感词 */
|
||||
async updateBadWords(body: UpdateBadWordsDto) {
|
||||
const { id, word, status } = body;
|
||||
const b = await this.badWordsEntity.findOne({ where: { word } });
|
||||
if (b) {
|
||||
throw new HttpException('敏感词已经存在了、请勿重复添加', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
const res = await this.badWordsEntity.update({ id }, { word, status });
|
||||
if (res.affected > 0) {
|
||||
await this.loadBadWords();
|
||||
return '更新敏感词成功';
|
||||
} else {
|
||||
throw new HttpException('更新敏感词失败', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
async addBadWord(body: AddBadWordDto) {
|
||||
const { word } = body;
|
||||
const b = await this.badWordsEntity.findOne({ where: { word } });
|
||||
if (b) {
|
||||
throw new HttpException('敏感词已存在,请检查您的提交信息', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
await this.badWordsEntity.save({ word });
|
||||
await this.loadBadWords();
|
||||
return '添加敏感词成功';
|
||||
}
|
||||
|
||||
/* 记录用户违规次数内容 */
|
||||
async recordUserBadWords(userId, content, words, typeCn, typeOriginCn) {
|
||||
const data = {
|
||||
userId,
|
||||
content,
|
||||
words: JSON.stringify(words),
|
||||
typeCn: JSON.stringify(typeCn),
|
||||
typeOriginCn,
|
||||
};
|
||||
try {
|
||||
await this.userEntity
|
||||
.createQueryBuilder()
|
||||
.update(UserEntity)
|
||||
.set({ violationCount: () => 'violationCount + 1' })
|
||||
.where('id = :userId', { userId })
|
||||
.execute();
|
||||
await this.violationLogEntity.save(data);
|
||||
} catch (error) {
|
||||
console.log('error: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
/* 违规记录 */
|
||||
async violation(req, query) {
|
||||
const { role } = req.user;
|
||||
const { page = 1, size = 10, userId, typeOriginCn } = query;
|
||||
const where = {};
|
||||
userId && (where['userId'] = userId);
|
||||
typeOriginCn && (where['typeOriginCn'] = typeOriginCn);
|
||||
const [rows, count] = await this.violationLogEntity.findAndCount({
|
||||
where,
|
||||
skip: (page - 1) * size,
|
||||
take: size,
|
||||
order: { id: 'DESC' },
|
||||
});
|
||||
const userIds = [...new Set(rows.map(t => t.userId))];
|
||||
const usersInfo = await this.userEntity.find({
|
||||
where: { id: In(userIds) },
|
||||
select: ['id', 'avatar', 'username', 'email', 'violationCount', 'status'],
|
||||
});
|
||||
rows.forEach((t: any) => {
|
||||
// 查找用户信息,如果没有找到,返回空对象
|
||||
const user: any = usersInfo.find(u => u.id === t.userId) || {};
|
||||
|
||||
// const user: any = usersInfo.find((u) => u.id === t.userId);
|
||||
role !== 'super' && (user.email = hideString(user.email));
|
||||
t.userInfo = user;
|
||||
});
|
||||
|
||||
return { rows, count };
|
||||
}
|
||||
}
|
||||
6
service/src/modules/badWords/dto/addBadWords.dto.ts
Normal file
6
service/src/modules/badWords/dto/addBadWords.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class AddBadWordDto {
|
||||
@ApiProperty({ example: 'test', description: '敏感词', required: true })
|
||||
word: string;
|
||||
}
|
||||
6
service/src/modules/badWords/dto/delBadWords.dto.ts
Normal file
6
service/src/modules/badWords/dto/delBadWords.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class DelBadWordsDto {
|
||||
@ApiProperty({ example: 1, description: '敏感词id', required: true })
|
||||
id: number;
|
||||
}
|
||||
20
service/src/modules/badWords/dto/queryBadWords.dto.ts
Normal file
20
service/src/modules/badWords/dto/queryBadWords.dto.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { IsOptional } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class QueryBadWordsDto {
|
||||
@ApiProperty({ example: 1, description: '查询页数', required: false })
|
||||
@IsOptional()
|
||||
page: number;
|
||||
|
||||
@ApiProperty({ example: 10, description: '每页数量', required: false })
|
||||
@IsOptional()
|
||||
size: number;
|
||||
|
||||
@ApiProperty({ example: 'test', description: '敏感词内容', required: false })
|
||||
@IsOptional()
|
||||
word: string;
|
||||
|
||||
@ApiProperty({ example: 1, description: '关键词状态', required: false })
|
||||
@IsOptional()
|
||||
status: number;
|
||||
}
|
||||
24
service/src/modules/badWords/dto/queryViolation.dto.ts
Normal file
24
service/src/modules/badWords/dto/queryViolation.dto.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { IsOptional } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class QueryViolationDto {
|
||||
@ApiProperty({ example: 1, description: '查询页数', required: false })
|
||||
@IsOptional()
|
||||
page: number;
|
||||
|
||||
@ApiProperty({ example: 10, description: '每页数量', required: false })
|
||||
@IsOptional()
|
||||
size: number;
|
||||
|
||||
@ApiProperty({ example: 1, description: '用户ID', required: false })
|
||||
@IsOptional()
|
||||
userId: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: '百度云检测',
|
||||
description: '检测平台来源',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
typeOriginCn: string;
|
||||
}
|
||||
16
service/src/modules/badWords/dto/updateBadWords.dto.ts
Normal file
16
service/src/modules/badWords/dto/updateBadWords.dto.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { IsOptional } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class UpdateBadWordsDto {
|
||||
@ApiProperty({ example: 1, description: '敏感词id', required: true })
|
||||
@IsOptional()
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ example: 'test', description: '敏感词内容', required: false })
|
||||
@IsOptional()
|
||||
word: string;
|
||||
|
||||
@ApiProperty({ example: 1, description: '关键词状态', required: false })
|
||||
@IsOptional()
|
||||
status: number;
|
||||
}
|
||||
20
service/src/modules/badWords/violationLog.entity.ts
Normal file
20
service/src/modules/badWords/violationLog.entity.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Column, Entity } from 'typeorm';
|
||||
import { BaseEntity } from 'src/common/entity/baseEntity';
|
||||
|
||||
@Entity({ name: 'violation_log' })
|
||||
export class ViolationLogEntity extends BaseEntity {
|
||||
@Column({ comment: '用户id' })
|
||||
userId: number;
|
||||
|
||||
@Column({ comment: '违规内容', type: 'text' })
|
||||
content: string;
|
||||
|
||||
@Column({ comment: '敏感词', type: 'text' })
|
||||
words: string;
|
||||
|
||||
@Column({ comment: '违规类型' })
|
||||
typeCn: string;
|
||||
|
||||
@Column({ comment: '违规检测失败于哪个平台' })
|
||||
typeOriginCn: string;
|
||||
}
|
||||
29
service/src/modules/chat/chat.controller.ts
Normal file
29
service/src/modules/chat/chat.controller.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../../common/auth/jwtAuth.guard';
|
||||
import { ChatService } from './chat.service';
|
||||
|
||||
import { Body, Controller, Post, Req, Res, UseGuards } from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
import { ChatProcessDto } from './dto/chatProcess.dto';
|
||||
|
||||
@ApiTags('chatgpt')
|
||||
@Controller('chatgpt')
|
||||
export class ChatController {
|
||||
constructor(private readonly chatService: ChatService) {}
|
||||
|
||||
@Post('chat-process')
|
||||
@ApiOperation({ summary: 'gpt聊天对话' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
chatProcess(@Body() body: ChatProcessDto, @Req() req: Request, @Res() res: Response) {
|
||||
return this.chatService.chatProcess(body, req, res);
|
||||
}
|
||||
|
||||
@Post('tts-process')
|
||||
@ApiOperation({ summary: 'tts语音播报' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
ttsProcess(@Body() body: any, @Req() req: Request, @Res() res: Response) {
|
||||
return this.chatService.ttsProcess(body, req, res);
|
||||
}
|
||||
}
|
||||
85
service/src/modules/chat/chat.module.ts
Normal file
85
service/src/modules/chat/chat.module.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { OpenAIChatService } from '../aiTool/chat/chat.service';
|
||||
import { NetSearchService } from '../aiTool/search/netSearch.service';
|
||||
import { AppEntity } from '../app/app.entity';
|
||||
import { AppService } from '../app/app.service';
|
||||
import { AppCatsEntity } from '../app/appCats.entity';
|
||||
import { UserAppsEntity } from '../app/userApps.entity';
|
||||
import { AutoReplyEntity } from '../autoReply/autoReply.entity';
|
||||
import { AutoReplyService } from '../autoReply/autoReply.service';
|
||||
import { BadWordsEntity } from '../badWords/badWords.entity';
|
||||
import { BadWordsService } from '../badWords/badWords.service';
|
||||
import { ViolationLogEntity } from '../badWords/violationLog.entity';
|
||||
import { ChatGroupEntity } from '../chatGroup/chatGroup.entity';
|
||||
import { ChatGroupService } from '../chatGroup/chatGroup.service';
|
||||
import { ChatLogEntity } from '../chatLog/chatLog.entity';
|
||||
import { ChatLogService } from '../chatLog/chatLog.service';
|
||||
import { CramiPackageEntity } from '../crami/cramiPackage.entity';
|
||||
import { ConfigEntity } from '../globalConfig/config.entity';
|
||||
import { GlobalConfigService } from '../globalConfig/globalConfig.service';
|
||||
import { MailerService } from '../mailer/mailer.service';
|
||||
import { ModelsEntity } from '../models/models.entity';
|
||||
import { ModelsService } from '../models/models.service';
|
||||
import { PluginEntity } from '../plugin/plugin.entity';
|
||||
import { RedisCacheService } from '../redisCache/redisCache.service';
|
||||
import { UploadService } from '../upload/upload.service';
|
||||
import { UserEntity } from '../user/user.entity';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { AccountLogEntity } from '../userBalance/accountLog.entity';
|
||||
import { BalanceEntity } from '../userBalance/balance.entity';
|
||||
import { FingerprintLogEntity } from '../userBalance/fingerprint.entity';
|
||||
import { UserBalanceEntity } from '../userBalance/userBalance.entity';
|
||||
import { UserBalanceService } from '../userBalance/userBalance.service';
|
||||
import { VerificationEntity } from '../verification/verification.entity';
|
||||
import { VerificationService } from '../verification/verification.service';
|
||||
import { ChatController } from './chat.controller';
|
||||
import { ChatService } from './chat.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
BalanceEntity,
|
||||
UserEntity,
|
||||
PluginEntity,
|
||||
VerificationEntity,
|
||||
ChatLogEntity,
|
||||
AccountLogEntity,
|
||||
ConfigEntity,
|
||||
UserEntity,
|
||||
CramiPackageEntity,
|
||||
ChatGroupEntity,
|
||||
AppEntity,
|
||||
UserBalanceEntity,
|
||||
FingerprintLogEntity,
|
||||
AppCatsEntity,
|
||||
UserAppsEntity,
|
||||
AutoReplyEntity,
|
||||
BadWordsEntity,
|
||||
ViolationLogEntity,
|
||||
ModelsEntity,
|
||||
]),
|
||||
],
|
||||
controllers: [ChatController],
|
||||
providers: [
|
||||
ChatService,
|
||||
UserBalanceService,
|
||||
UserService,
|
||||
VerificationService,
|
||||
ChatLogService,
|
||||
RedisCacheService,
|
||||
MailerService,
|
||||
GlobalConfigService,
|
||||
UploadService,
|
||||
AutoReplyService,
|
||||
BadWordsService,
|
||||
ChatGroupService,
|
||||
ModelsService,
|
||||
OpenAIChatService,
|
||||
NetSearchService,
|
||||
AppService,
|
||||
],
|
||||
exports: [ChatService],
|
||||
})
|
||||
export class ChatModule {}
|
||||
1039
service/src/modules/chat/chat.service.ts
Normal file
1039
service/src/modules/chat/chat.service.ts
Normal file
File diff suppressed because it is too large
Load Diff
71
service/src/modules/chat/dto/chatDraw.dto.ts
Normal file
71
service/src/modules/chat/dto/chatDraw.dto.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { IsOptional } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class ChatDrawDto {
|
||||
@ApiProperty({
|
||||
example: 'Draw a cute little dog',
|
||||
description: '绘画描述信息',
|
||||
})
|
||||
prompt: string;
|
||||
|
||||
@ApiProperty({ example: 1, description: '绘画张数', required: true })
|
||||
n: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: '1024x1024',
|
||||
description: '图片尺寸',
|
||||
required: true,
|
||||
})
|
||||
size: string;
|
||||
|
||||
@ApiProperty({ example: 'standard', description: '图片质量', required: true })
|
||||
quality: string;
|
||||
|
||||
@ApiProperty({
|
||||
example:
|
||||
'close-up polaroid photo, of a little joyful cute panda, in the forest, sun rays coming, photographic, sharp focus, depth of field, soft lighting, heigh quality, 24mm, Nikon Z FX',
|
||||
description: '绘画提示词!',
|
||||
required: true,
|
||||
})
|
||||
@ApiProperty({
|
||||
example: '--ar 16:9 --c 0',
|
||||
description: '除了prompt的额外参数',
|
||||
})
|
||||
@IsOptional()
|
||||
extraParam?: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'https://xsdasdasd.com',
|
||||
description: '垫图图片地址',
|
||||
})
|
||||
@IsOptional()
|
||||
imgUrl?: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'IMAGINE',
|
||||
description:
|
||||
'任务类型,可用值:IMAGINE,UPSCALE,VARIATION,ZOOM,PAN,DESCRIBE,BLEND,SHORTEN,SWAP_FACE',
|
||||
})
|
||||
@IsOptional()
|
||||
action: string;
|
||||
|
||||
@ApiProperty({ example: 1, description: '变体或者放大的序号' })
|
||||
@IsOptional()
|
||||
orderId: number;
|
||||
|
||||
@ApiProperty({ example: 1, description: '绘画的DBID' })
|
||||
@IsOptional()
|
||||
drawId: number;
|
||||
|
||||
@ApiProperty({ example: 1, description: 'customId' })
|
||||
@IsOptional()
|
||||
customId: string;
|
||||
|
||||
@ApiProperty({ example: 1, description: 'base64' })
|
||||
@IsOptional()
|
||||
base64: string;
|
||||
|
||||
@ApiProperty({ example: 1, description: '任务ID' })
|
||||
@IsOptional()
|
||||
taskId: number;
|
||||
}
|
||||
53
service/src/modules/chat/dto/chatProcess.dto.ts
Normal file
53
service/src/modules/chat/dto/chatProcess.dto.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { IsNotEmpty, IsString, IsOptional } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class Options {
|
||||
@IsString()
|
||||
parentMessageId: string;
|
||||
model?: string;
|
||||
temperature?: number;
|
||||
top_p?: number;
|
||||
groupId?: number;
|
||||
}
|
||||
|
||||
export class ChatProcessDto {
|
||||
@ApiProperty({ example: 'hello, Who are you', description: '对话信息' })
|
||||
@IsNotEmpty({ message: '提问信息不能为空!' })
|
||||
prompt: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'https://aiweb.com',
|
||||
description: '对话附带的链接',
|
||||
required: false,
|
||||
})
|
||||
url: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '{ parentMessageId: 0 }',
|
||||
description: '上次对话信息',
|
||||
required: false,
|
||||
})
|
||||
@Type(() => Options)
|
||||
options: Options;
|
||||
|
||||
@ApiProperty({
|
||||
example:
|
||||
"You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown.",
|
||||
description: '系统预设信息',
|
||||
})
|
||||
@IsOptional()
|
||||
systemMessage?: string;
|
||||
|
||||
@ApiProperty({ example: 1, description: '应用id', required: false })
|
||||
@IsOptional()
|
||||
appId: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'gpt-3.5-turbo',
|
||||
description: '使用模型',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
model: string;
|
||||
}
|
||||
69
service/src/modules/chat/helper.ts
Normal file
69
service/src/modules/chat/helper.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @desc 处理不同模型返回的最后一次汇总内容 输出为相同格式 方便后面使用
|
||||
* @param keyType 模型key类型
|
||||
* @param response 模型返回的整体内容
|
||||
*/
|
||||
export function unifiedFormattingResponse(keyType, response, others) {
|
||||
let formatRes = {
|
||||
keyType, // 模型类型
|
||||
parentMessageId: '', // 父级对话id
|
||||
text: '', //本次回复内容
|
||||
usage: {
|
||||
prompt_tokens: 0, //提问token
|
||||
completion_tokens: 0, // 回答token
|
||||
total_tokens: 0, // 总消耗token
|
||||
},
|
||||
};
|
||||
/* openai */
|
||||
// if([1].includes(Number(keyType))){
|
||||
const { parentMessageId } = response?.detail;
|
||||
let { usage } = response?.detail;
|
||||
if (!usage) {
|
||||
usage = {
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
};
|
||||
}
|
||||
const { prompt_tokens, completion_tokens, total_tokens } = usage;
|
||||
formatRes = {
|
||||
keyType,
|
||||
parentMessageId,
|
||||
text: response.text,
|
||||
usage: {
|
||||
prompt_tokens,
|
||||
completion_tokens,
|
||||
total_tokens,
|
||||
},
|
||||
};
|
||||
// }
|
||||
|
||||
/* 百度 */
|
||||
// if([2, 3].includes(Number(keyType))) {
|
||||
// const { usage, text } = response
|
||||
// const { prompt_tokens, completion_tokens, total_tokens } = usage
|
||||
// const { model, parentMessageId } = others
|
||||
// formatRes = {
|
||||
// keyType,
|
||||
// model,
|
||||
// parentMessageId,
|
||||
// text,
|
||||
// usage: {
|
||||
// prompt_tokens,
|
||||
// completion_tokens,
|
||||
// total_tokens
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
return formatRes;
|
||||
}
|
||||
|
||||
/*百度的模型不允许传入偶数的message数组 让round为奇数的时候 加一 */
|
||||
export function addOneIfOdd(num) {
|
||||
if (num % 2 !== 0) {
|
||||
return num + 1;
|
||||
} else {
|
||||
return num;
|
||||
}
|
||||
}
|
||||
54
service/src/modules/chatGroup/chatGroup.controller.ts
Normal file
54
service/src/modules/chatGroup/chatGroup.controller.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { ChatGroupService } from './chatGroup.service';
|
||||
import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common';
|
||||
import { CreateGroupDto } from './dto/createGroup.dto';
|
||||
import { Request } from 'express';
|
||||
import { JwtAuthGuard } from '@/common/auth/jwtAuth.guard';
|
||||
import { DelGroupDto } from './dto/delGroup.dto';
|
||||
import { UpdateGroupDto } from './dto/updateGroup.dto';
|
||||
|
||||
@ApiTags('group')
|
||||
@Controller('group')
|
||||
export class ChatGroupController {
|
||||
constructor(private readonly chatGroupService: ChatGroupService) {}
|
||||
|
||||
@Post('create')
|
||||
@ApiOperation({ summary: '创建对话组' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
create(@Body() body: CreateGroupDto, @Req() req: Request) {
|
||||
return this.chatGroupService.create(body, req);
|
||||
}
|
||||
|
||||
@Get('query')
|
||||
@ApiOperation({ summary: '查询对话组' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
query(@Req() req: Request) {
|
||||
return this.chatGroupService.query(req);
|
||||
}
|
||||
|
||||
@Post('update')
|
||||
@ApiOperation({ summary: '更新对话组' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
update(@Body() body: UpdateGroupDto, @Req() req: Request) {
|
||||
return this.chatGroupService.update(body, req);
|
||||
}
|
||||
|
||||
@Post('del')
|
||||
@ApiOperation({ summary: '删除对话组' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
del(@Body() body: DelGroupDto, @Req() req: Request) {
|
||||
return this.chatGroupService.del(body, req);
|
||||
}
|
||||
|
||||
@Post('delAll')
|
||||
@ApiOperation({ summary: '删除对话组' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
delAll(@Req() req: Request) {
|
||||
return this.chatGroupService.delAll(req);
|
||||
}
|
||||
}
|
||||
50
service/src/modules/chatGroup/chatGroup.entity.ts
Normal file
50
service/src/modules/chatGroup/chatGroup.entity.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { BaseEntity } from 'src/common/entity/baseEntity';
|
||||
import { Column, Entity } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'chat_group' })
|
||||
export class ChatGroupEntity extends BaseEntity {
|
||||
@Column({ comment: '用户ID' })
|
||||
userId: number;
|
||||
|
||||
@Column({ comment: '是否置顶聊天', type: 'boolean', default: false })
|
||||
isSticky: boolean;
|
||||
|
||||
@Column({ comment: '分组名称', nullable: true })
|
||||
title: string;
|
||||
|
||||
@Column({ comment: '应用ID', nullable: true })
|
||||
appId: number;
|
||||
|
||||
// @Column({ comment: '模型', nullable: true })
|
||||
// model: string;
|
||||
|
||||
// @Column({ comment: '模型名称', nullable: true })
|
||||
// modelName: string;
|
||||
|
||||
// @Column({ comment: '扣费类型', nullable: true })
|
||||
// mdeductType: string;
|
||||
|
||||
// @Column({ comment: '是否支持文件上传', nullable: true })
|
||||
// isFileUpload: boolean;
|
||||
|
||||
// @Column({ comment: '是否固定模型', default: 0 })
|
||||
// isFixedModel: boolean;
|
||||
|
||||
// @Column({ comment: '模型类型', nullable: true })
|
||||
// keyType: number;
|
||||
|
||||
@Column({ comment: '是否删除了', default: false })
|
||||
isDelete: boolean;
|
||||
|
||||
@Column({ comment: '配置', nullable: true, default: null, type: 'text' })
|
||||
config: string;
|
||||
|
||||
@Column({ comment: '附加参数', nullable: true, type: 'text' })
|
||||
params: string;
|
||||
|
||||
@Column({ comment: '文件链接', nullable: true, default: null, type: 'text' })
|
||||
fileUrl: string;
|
||||
|
||||
@Column({ comment: 'PDF中的文字内容', nullable: true, type: 'mediumtext' })
|
||||
pdfTextContent: string;
|
||||
}
|
||||
15
service/src/modules/chatGroup/chatGroup.module.ts
Normal file
15
service/src/modules/chatGroup/chatGroup.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { ChatGroupController } from './chatGroup.controller';
|
||||
import { ChatGroupService } from './chatGroup.service';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ChatGroupEntity } from './chatGroup.entity';
|
||||
import { AppEntity } from '../app/app.entity';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ChatGroupEntity, AppEntity])],
|
||||
controllers: [ChatGroupController],
|
||||
providers: [ChatGroupService],
|
||||
exports: [ChatGroupService],
|
||||
})
|
||||
export class ChatGroupModule {}
|
||||
252
service/src/modules/chatGroup/chatGroup.service.ts
Normal file
252
service/src/modules/chatGroup/chatGroup.service.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import axios from 'axios';
|
||||
import { Request } from 'express';
|
||||
import * as pdf from 'pdf-parse';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { AppEntity } from '../app/app.entity';
|
||||
import { ModelsService } from '../models/models.service';
|
||||
import { ChatGroupEntity } from './chatGroup.entity';
|
||||
import { CreateGroupDto } from './dto/createGroup.dto';
|
||||
import { DelGroupDto } from './dto/delGroup.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ChatGroupService {
|
||||
constructor(
|
||||
@InjectRepository(ChatGroupEntity)
|
||||
private readonly chatGroupEntity: Repository<ChatGroupEntity>,
|
||||
@InjectRepository(AppEntity)
|
||||
private readonly appEntity: Repository<AppEntity>,
|
||||
private readonly modelsService: ModelsService,
|
||||
) {}
|
||||
|
||||
async create(body: CreateGroupDto, req: Request) {
|
||||
const { id } = req.user; // 从请求中获取用户ID
|
||||
const { appId, modelConfig: bodyModelConfig, params } = body; // 从请求体中提取appId和modelConfig
|
||||
|
||||
// 尝试使用从请求体中提供的 modelConfig,否则获取默认配置
|
||||
let modelConfig = bodyModelConfig || (await this.modelsService.getBaseConfig());
|
||||
const modelDetail = await this.modelsService.getModelDetailByName(modelConfig.modelInfo.model);
|
||||
if (modelDetail) {
|
||||
modelConfig.modelInfo.modelName = modelDetail.modelName;
|
||||
modelConfig.modelInfo.deductType = modelDetail.deductType;
|
||||
modelConfig.modelInfo.deduct = modelDetail.deduct;
|
||||
modelConfig.modelInfo.isFileUpload = modelDetail.isFileUpload;
|
||||
modelConfig.modelInfo.isImageUpload = modelDetail.isImageUpload;
|
||||
modelConfig.modelInfo.isNetworkSearch = modelDetail.isNetworkSearch;
|
||||
modelConfig.modelInfo.deepThinkingType = modelDetail.deepThinkingType;
|
||||
modelConfig.modelInfo.isMcpTool = modelDetail.isMcpTool;
|
||||
}
|
||||
|
||||
if (!modelConfig) {
|
||||
throw new HttpException(
|
||||
'管理员未配置任何AI模型、请先联系管理员开通聊天模型配置!',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
// 使用 JSON.parse(JSON.stringify(object)) 进行深拷贝以避免直接修改原对象
|
||||
modelConfig = JSON.parse(JSON.stringify(modelConfig));
|
||||
|
||||
// 初始化创建对话组的参数
|
||||
const groupParams = { title: '新对话', userId: id, appId, params };
|
||||
// const params = { title: 'New chat', userId: id };
|
||||
|
||||
// 如果指定了appId,查找并验证应用信息
|
||||
if (appId) {
|
||||
const appInfo = await this.appEntity.findOne({ where: { id: appId } });
|
||||
if (!appInfo) {
|
||||
throw new HttpException('非法操作、您在使用一个不存在的应用!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// 应用存在,提取并验证应用信息
|
||||
const { status, name, isFixedModel, isGPTs, coverImg, appModel, isFlowith } = appInfo;
|
||||
|
||||
if (isFixedModel && appModel) {
|
||||
const modelDetail = await this.modelsService.getModelDetailByName(appModel);
|
||||
Logger.debug(`modelDetail: ${modelDetail}`);
|
||||
if (modelDetail) {
|
||||
modelConfig.modelInfo.modelName = modelDetail.modelName;
|
||||
modelConfig.modelInfo.deductType = modelDetail.deductType;
|
||||
modelConfig.modelInfo.deduct = modelDetail.deduct;
|
||||
modelConfig.modelInfo.isFileUpload = modelDetail.isFileUpload;
|
||||
modelConfig.modelInfo.isImageUpload = modelDetail.isImageUpload;
|
||||
modelConfig.modelInfo.isNetworkSearch = modelDetail.isNetworkSearch;
|
||||
modelConfig.modelInfo.deepThinkingType = modelDetail.deepThinkingType;
|
||||
modelConfig.modelInfo.isMcpTool = modelDetail.isMcpTool;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 modelConfig 以反映应用的特定配置
|
||||
Object.assign(modelConfig.modelInfo, {
|
||||
isGPTs,
|
||||
isFixedModel,
|
||||
isFlowith,
|
||||
modelAvatar: coverImg,
|
||||
modelName: name,
|
||||
});
|
||||
|
||||
// 如果是固定模型或GPTs模型,获取并设置额外的模型信息
|
||||
if (isGPTs === 1 || isFixedModel === 1 || isFlowith === 1) {
|
||||
const appModelKey = await this.modelsService.getCurrentModelKeyInfo(
|
||||
isFixedModel === 1 ? appModel : isFlowith === 1 ? 'flowith' : isGPTs === 1 ? 'gpts' : '',
|
||||
);
|
||||
Object.assign(modelConfig.modelInfo, {
|
||||
deductType: appModelKey.deductType,
|
||||
deduct: appModelKey.deduct,
|
||||
model: appModel,
|
||||
isFileUpload: appModelKey.isFileUpload,
|
||||
isImageUpload: appModelKey.isImageUpload,
|
||||
});
|
||||
}
|
||||
|
||||
// 检查应用状态是否允许创建对话组
|
||||
if (![1, 3, 4, 5].includes(status)) {
|
||||
throw new HttpException('非法操作、您在使用一个未启用的应用!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// 如果应用有名称,则使用它作为对话组标题
|
||||
if (name) {
|
||||
groupParams.title = name;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新的聊天组并保存
|
||||
const newGroup = await this.chatGroupEntity.save({
|
||||
...groupParams,
|
||||
config: JSON.stringify(modelConfig), // 将 modelConfig 对象转换为 JSON 字符串进行保存
|
||||
});
|
||||
|
||||
return newGroup; // 返回新创建的聊天组
|
||||
}
|
||||
|
||||
async query(req: Request) {
|
||||
try {
|
||||
const { id } = req.user;
|
||||
const params = { userId: id, isDelete: false };
|
||||
const res = await this.chatGroupEntity.find({
|
||||
where: params,
|
||||
order: { isSticky: 'DESC', updatedAt: 'DESC' },
|
||||
});
|
||||
return res;
|
||||
// const res = await this.chatGroupEntity.find({ where: params, order: { isSticky: 'DESC', id: 'DESC' } });
|
||||
const appIds = res.filter(t => t.appId).map(t => t.appId);
|
||||
const appInfos = await this.appEntity.find({ where: { id: In(appIds) } });
|
||||
return res.map((item: any) => {
|
||||
item.appLogo = appInfos.find(t => t.id === item.appId)?.coverImg;
|
||||
return item;
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('error: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
async update(body: any, req: Request) {
|
||||
// Logger.debug(`body: ${JSON.stringify(body)}`);
|
||||
const { title, isSticky, groupId, config, fileUrl } = body;
|
||||
const { id } = req.user;
|
||||
const g = await this.chatGroupEntity.findOne({
|
||||
where: { id: groupId, userId: id },
|
||||
});
|
||||
if (!g) {
|
||||
throw new HttpException('请先选择一个对话或者新加一个对话再操作!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
const { appId } = g;
|
||||
if (appId && !title) {
|
||||
try {
|
||||
const parseData = JSON.parse(config);
|
||||
if (Number(parseData.keyType) !== 1) {
|
||||
throw new HttpException('应用对话名称不能修改哟!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
const data = {};
|
||||
title && (data['title'] = title);
|
||||
typeof isSticky !== 'undefined' && (data['isSticky'] = isSticky);
|
||||
config && (data['config'] = config);
|
||||
typeof fileUrl !== 'undefined' && (data['fileUrl'] = fileUrl);
|
||||
const u = await this.chatGroupEntity.update({ id: groupId }, data);
|
||||
if (u.affected) {
|
||||
// // 如果 fileUrl 不为空,异步处理 PDF 内容读取
|
||||
// if (fileUrl) {
|
||||
// this.handlePdfExtraction(fileUrl, groupId);
|
||||
// }
|
||||
return true;
|
||||
} else {
|
||||
throw new HttpException('更新对话失败!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
// 从 PDF 文件 URL 中提取文本内容
|
||||
private async extractPdfText(fileUrl: string): Promise<string> {
|
||||
try {
|
||||
const response = await axios.get(fileUrl, {
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
const dataBuffer = Buffer.from(response.data);
|
||||
const pdfData = await pdf(dataBuffer);
|
||||
return pdfData.text;
|
||||
} catch (error) {
|
||||
console.error('PDF 解析失败:', error);
|
||||
throw new Error('PDF 解析失败');
|
||||
}
|
||||
}
|
||||
|
||||
async updateTime(groupId: number) {
|
||||
await this.chatGroupEntity.update(groupId, {
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
async del(body: DelGroupDto, req: Request) {
|
||||
const { groupId } = body;
|
||||
const { id } = req.user;
|
||||
const g = await this.chatGroupEntity.findOne({
|
||||
where: { id: groupId, userId: id },
|
||||
});
|
||||
if (!g) {
|
||||
throw new HttpException('非法操作、您在删除一个非法资源!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
const r = await this.chatGroupEntity.update({ id: groupId }, { isDelete: true });
|
||||
if (r.affected) {
|
||||
return '删除成功';
|
||||
} else {
|
||||
throw new HttpException('删除失败!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
/* 删除非置顶开启的所有对话记录 */
|
||||
async delAll(req: Request) {
|
||||
const { id } = req.user;
|
||||
const r = await this.chatGroupEntity.update(
|
||||
{ userId: id, isSticky: false, isDelete: false },
|
||||
{ isDelete: true },
|
||||
);
|
||||
if (r.affected) {
|
||||
return '删除成功';
|
||||
} else {
|
||||
throw new HttpException('删除失败!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
/* 通过groupId查询当前对话组的详细信息 */
|
||||
async getGroupInfoFromId(id) {
|
||||
if (!id) return;
|
||||
const groupInfo = await this.chatGroupEntity.findOne({ where: { id } });
|
||||
if (groupInfo) {
|
||||
const { pdfTextContent, ...rest } = groupInfo;
|
||||
return rest;
|
||||
}
|
||||
}
|
||||
|
||||
async getGroupPdfText(groupId: number) {
|
||||
const groupInfo = await this.chatGroupEntity.findOne({
|
||||
where: { id: groupId },
|
||||
});
|
||||
if (groupInfo) {
|
||||
return groupInfo.pdfTextContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
service/src/modules/chatGroup/dto/createGroup.dto.ts
Normal file
22
service/src/modules/chatGroup/dto/createGroup.dto.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsOptional } from 'class-validator';
|
||||
|
||||
export class CreateGroupDto {
|
||||
@ApiProperty({ example: 10, description: '应用ID', required: false })
|
||||
@IsOptional()
|
||||
appId: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: '',
|
||||
description: '对话模型配置项序列化的字符串',
|
||||
required: false,
|
||||
})
|
||||
modelConfig?: any;
|
||||
|
||||
@ApiProperty({
|
||||
example: '',
|
||||
description: '对话组参数序列化的字符串',
|
||||
required: false,
|
||||
})
|
||||
params?: string;
|
||||
}
|
||||
6
service/src/modules/chatGroup/dto/delGroup.dto.ts
Normal file
6
service/src/modules/chatGroup/dto/delGroup.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class DelGroupDto {
|
||||
@ApiProperty({ example: 1, description: '对话分组ID', required: true })
|
||||
groupId: number;
|
||||
}
|
||||
23
service/src/modules/chatGroup/dto/updateGroup.dto.ts
Normal file
23
service/src/modules/chatGroup/dto/updateGroup.dto.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsOptional } from 'class-validator';
|
||||
|
||||
export class UpdateGroupDto {
|
||||
@ApiProperty({ example: 1, description: '修改的对话ID', required: false })
|
||||
@IsOptional()
|
||||
groupId: number;
|
||||
|
||||
@ApiProperty({ example: 10, description: '对话组title', required: false })
|
||||
@IsOptional()
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ example: 10, description: '对话组是否置顶', required: false })
|
||||
@IsOptional()
|
||||
isSticky: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
example: '',
|
||||
description: '对话模型配置项序列化的字符串',
|
||||
required: false,
|
||||
})
|
||||
config: string;
|
||||
}
|
||||
101
service/src/modules/chatLog/chatLog.controller.ts
Normal file
101
service/src/modules/chatLog/chatLog.controller.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { AdminAuthGuard } from '@/common/auth/adminAuth.guard';
|
||||
import { JwtAuthGuard } from '@/common/auth/jwtAuth.guard';
|
||||
import { SuperAuthGuard } from '@/common/auth/superAuth.guard';
|
||||
import { Body, Controller, Get, Post, Query, Req, Res, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { Request, Response } from 'express';
|
||||
import { ChatLogService } from './chatLog.service';
|
||||
import { ChatListDto } from './dto/chatList.dto';
|
||||
import { DelDto } from './dto/del.dto';
|
||||
import { DelByGroupDto } from './dto/delByGroup.dto';
|
||||
import { ExportExcelChatlogDto } from './dto/exportExcelChatlog.dto';
|
||||
import { QuerAllChatLogDto } from './dto/queryAllChatLog.dto';
|
||||
import { QueryByAppIdDto } from './dto/queryByAppId.dto';
|
||||
import { QuerMyChatLogDto } from './dto/queryMyChatLog.dto';
|
||||
import { QuerySingleChatDto } from './dto/querySingleChat.dto';
|
||||
import { recDrawImgDto } from './dto/recDrawImg.dto';
|
||||
|
||||
@Controller('chatLog')
|
||||
@ApiTags('chatLog')
|
||||
export class ChatLogController {
|
||||
constructor(private readonly chatLogService: ChatLogService) {}
|
||||
|
||||
@Get('draw')
|
||||
@ApiOperation({ summary: '查询我的绘制记录' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
querDrawLog(@Query() query: QuerMyChatLogDto, @Req() req: Request) {
|
||||
return this.chatLogService.querDrawLog(req, query);
|
||||
}
|
||||
|
||||
@Post('recDrawImg')
|
||||
@ApiOperation({ summary: '推荐此图片对外展示' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(SuperAuthGuard)
|
||||
recDrawImg(@Body() body: recDrawImgDto) {
|
||||
return this.chatLogService.recDrawImg(body);
|
||||
}
|
||||
|
||||
@Get('chatAll')
|
||||
@ApiOperation({ summary: '查询所有的问答记录' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AdminAuthGuard)
|
||||
queryAllChatLog(@Query() params: QuerAllChatLogDto, @Req() req: Request) {
|
||||
return this.chatLogService.querAllChatLog(params, req);
|
||||
}
|
||||
|
||||
@Post('exportExcel')
|
||||
@ApiOperation({ summary: '导出问答记录' })
|
||||
@ApiBearerAuth()
|
||||
exportExcel(@Body() body: ExportExcelChatlogDto, @Res() res: Response) {
|
||||
return this.chatLogService.exportExcel(body, res);
|
||||
}
|
||||
|
||||
@Get('chatList')
|
||||
@ApiOperation({ summary: '查询我的问答记录' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
chatList(@Req() req: Request, @Query() params: ChatListDto) {
|
||||
return this.chatLogService.chatList(req, params);
|
||||
}
|
||||
|
||||
@Post('del')
|
||||
@ApiOperation({ summary: '删除我的问答记录' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
del(@Req() req: Request, @Body() body: DelDto) {
|
||||
return this.chatLogService.deleteChatLog(req, body);
|
||||
}
|
||||
|
||||
@Post('delByGroupId')
|
||||
@ApiOperation({ summary: '清空一组对话' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
delByGroupId(@Req() req: Request, @Body() body: DelByGroupDto) {
|
||||
return this.chatLogService.delByGroupId(req, body);
|
||||
}
|
||||
|
||||
@Post('deleteChatsAfterId')
|
||||
@ApiOperation({ summary: '删除对话组中某条对话及其后的所有对话' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
deleteChatsAfterId(@Req() req: Request, @Body() body: any) {
|
||||
return this.chatLogService.deleteChatsAfterId(req, body);
|
||||
}
|
||||
|
||||
@Get('byAppId')
|
||||
@ApiOperation({ summary: '查询某个应用的问答记录' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
byAppId(@Req() req: Request, @Query() params: QueryByAppIdDto) {
|
||||
return this.chatLogService.byAppId(req, params);
|
||||
}
|
||||
|
||||
@Get('querySingleChat')
|
||||
@ApiOperation({ summary: '查询单条消息的状态和内容' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
querySingleChat(@Req() req: Request, @Query() params: QuerySingleChatDto) {
|
||||
return this.chatLogService.querySingleChat(req, params);
|
||||
}
|
||||
}
|
||||
122
service/src/modules/chatLog/chatLog.entity.ts
Normal file
122
service/src/modules/chatLog/chatLog.entity.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { BaseEntity } from 'src/common/entity/baseEntity';
|
||||
import { Column, Entity } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'chatlog' })
|
||||
export class ChatLogEntity extends BaseEntity {
|
||||
@Column({ comment: '用户ID' })
|
||||
userId: number;
|
||||
|
||||
@Column({ comment: '使用的模型', nullable: true })
|
||||
model: string;
|
||||
|
||||
@Column({ comment: 'role system user assistant', nullable: true })
|
||||
role: string;
|
||||
|
||||
@Column({ comment: '模型内容', nullable: true, type: 'mediumtext' })
|
||||
content: string;
|
||||
|
||||
@Column({ comment: '模型推理内容', nullable: true, type: 'text' })
|
||||
reasoning_content: string;
|
||||
|
||||
@Column({ comment: '模型工具调用', nullable: true, type: 'text' })
|
||||
tool_calls: string;
|
||||
|
||||
@Column({ comment: '图片Url', nullable: true, type: 'text' })
|
||||
imageUrl: string;
|
||||
|
||||
@Column({ comment: '视频Url', nullable: true, type: 'text' })
|
||||
videoUrl: string;
|
||||
|
||||
@Column({ comment: '音频Url', nullable: true, type: 'text' })
|
||||
audioUrl: string;
|
||||
|
||||
@Column({ comment: '文件Url', nullable: true, type: 'text' })
|
||||
fileUrl: string;
|
||||
|
||||
@Column({
|
||||
comment: '使用类型1: 普通对话 2: 绘图 3: 拓展性对话',
|
||||
nullable: true,
|
||||
default: 1,
|
||||
})
|
||||
type: number;
|
||||
|
||||
@Column({ comment: '自定义的模型名称', nullable: true, default: 'AI' })
|
||||
modelName: string;
|
||||
|
||||
@Column({ comment: '自定义的模型头像', nullable: false, default: '' })
|
||||
modelAvatar: string;
|
||||
|
||||
@Column({ comment: 'Ip地址', nullable: true })
|
||||
curIp: string;
|
||||
|
||||
//废弃字段
|
||||
|
||||
@Column({ comment: '询问的问题', type: 'text', nullable: true })
|
||||
prompt: string;
|
||||
|
||||
@Column({ comment: '附加参数', nullable: true })
|
||||
extraParam: string;
|
||||
|
||||
@Column({ comment: '插件参数', nullable: true })
|
||||
pluginParam: string;
|
||||
|
||||
@Column({ comment: '回答的答案', type: 'text', nullable: true })
|
||||
answer: string;
|
||||
|
||||
@Column({ comment: '提问的token', nullable: true })
|
||||
promptTokens: number;
|
||||
|
||||
@Column({ comment: '回答的token', nullable: true })
|
||||
completionTokens: number;
|
||||
|
||||
@Column({ comment: '总花费的token', nullable: true })
|
||||
totalTokens: number;
|
||||
|
||||
@Column({ comment: '任务进度', nullable: true })
|
||||
progress: string;
|
||||
|
||||
@Column({ comment: '任务状态', nullable: true, default: 3 })
|
||||
status: number;
|
||||
|
||||
@Column({ comment: '任务类型', nullable: true })
|
||||
action: string;
|
||||
|
||||
@Column({ comment: '对图片操作的按钮ID', type: 'text', nullable: true })
|
||||
customId: string;
|
||||
|
||||
@Column({ comment: '绘画的ID每条不一样', nullable: true })
|
||||
drawId: string;
|
||||
|
||||
@Column({ comment: '对话转语音的链接', nullable: true, type: 'text' })
|
||||
ttsUrl: string;
|
||||
|
||||
@Column({ comment: '是否推荐0: 默认 1: 推荐', nullable: true, default: 0 })
|
||||
rec: number;
|
||||
|
||||
@Column({ comment: '分组ID', nullable: true })
|
||||
groupId: number;
|
||||
|
||||
@Column({ comment: '使用的应用id', nullable: true })
|
||||
appId: number;
|
||||
|
||||
@Column({ comment: '是否删除', default: false })
|
||||
isDelete: boolean;
|
||||
|
||||
@Column({ comment: '任务ID', nullable: true })
|
||||
taskId: string;
|
||||
|
||||
@Column({ comment: '任务数据', nullable: true, type: 'text' })
|
||||
taskData: string;
|
||||
|
||||
@Column({ comment: '文件信息', nullable: true, type: 'text' })
|
||||
fileInfo: string;
|
||||
|
||||
@Column({ comment: '提问参考', nullable: true })
|
||||
promptReference: string;
|
||||
|
||||
@Column({ comment: '联网搜索结果', nullable: true, type: 'text' })
|
||||
networkSearchResult: string;
|
||||
|
||||
@Column({ comment: '文件向量搜索结果', nullable: true, type: 'mediumtext' })
|
||||
fileVectorResult: string;
|
||||
}
|
||||
16
service/src/modules/chatLog/chatLog.module.ts
Normal file
16
service/src/modules/chatLog/chatLog.module.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ChatGroupEntity } from '../chatGroup/chatGroup.entity';
|
||||
import { UserEntity } from '../user/user.entity';
|
||||
import { ChatLogController } from './chatLog.controller';
|
||||
import { ChatLogEntity } from './chatLog.entity';
|
||||
import { ChatLogService } from './chatLog.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ChatLogEntity, UserEntity, ChatGroupEntity])],
|
||||
controllers: [ChatLogController],
|
||||
providers: [ChatLogService],
|
||||
exports: [ChatLogService],
|
||||
})
|
||||
export class ChatLogModule {}
|
||||
527
service/src/modules/chatLog/chatLog.service.ts
Normal file
527
service/src/modules/chatLog/chatLog.service.ts
Normal file
@@ -0,0 +1,527 @@
|
||||
import { ChatType } from '@/common/constants/balance.constant';
|
||||
import { formatDate, maskEmail } from '@/common/utils';
|
||||
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import excel from 'exceljs';
|
||||
import { Request, Response } from 'express';
|
||||
import { In, Like, MoreThan, MoreThanOrEqual, Repository } from 'typeorm';
|
||||
import { ChatGroupEntity } from '../chatGroup/chatGroup.entity';
|
||||
import { UserEntity } from '../user/user.entity';
|
||||
import { ChatLogEntity } from './chatLog.entity';
|
||||
import { ChatListDto } from './dto/chatList.dto';
|
||||
import { DelDto } from './dto/del.dto';
|
||||
import { DelByGroupDto } from './dto/delByGroup.dto';
|
||||
import { ExportExcelChatlogDto } from './dto/exportExcelChatlog.dto';
|
||||
import { QuerAllChatLogDto } from './dto/queryAllChatLog.dto';
|
||||
import { QueryByAppIdDto } from './dto/queryByAppId.dto';
|
||||
import { QuerMyChatLogDto } from './dto/queryMyChatLog.dto';
|
||||
import { recDrawImgDto } from './dto/recDrawImg.dto';
|
||||
// import { ModelsTypeEntity } from '../models/modelType.entity';
|
||||
import { JwtPayload } from 'src/types/express';
|
||||
import { ModelsService } from '../models/models.service';
|
||||
import { QuerySingleChatDto } from './dto/querySingleChat.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ChatLogService {
|
||||
constructor(
|
||||
@InjectRepository(ChatLogEntity)
|
||||
private readonly chatLogEntity: Repository<ChatLogEntity>,
|
||||
@InjectRepository(UserEntity)
|
||||
private readonly userEntity: Repository<UserEntity>,
|
||||
@InjectRepository(ChatGroupEntity)
|
||||
private readonly chatGroupEntity: Repository<ChatGroupEntity>,
|
||||
private readonly modelsService: ModelsService,
|
||||
) {}
|
||||
|
||||
/* 记录问答日志 */
|
||||
async saveChatLog(logInfo): Promise<any> {
|
||||
const savedLog = await this.chatLogEntity.save(logInfo);
|
||||
return savedLog; // 这里返回保存后的实体,包括其 ID
|
||||
}
|
||||
|
||||
/* 更新问答日志 */
|
||||
async updateChatLog(id, logInfo) {
|
||||
return await this.chatLogEntity.update({ id }, logInfo);
|
||||
}
|
||||
|
||||
async findOneChatLog(id) {
|
||||
return await this.chatLogEntity.findOne({ where: { id } });
|
||||
}
|
||||
|
||||
/* 查询我的绘制记录 */
|
||||
async querDrawLog(req: Request, query: QuerMyChatLogDto) {
|
||||
const { id } = req.user;
|
||||
const { model } = query;
|
||||
const where: any = { userId: id, type: ChatType.PAINT };
|
||||
if (model) {
|
||||
where.model = model;
|
||||
if (model === 'DALL-E2') {
|
||||
where.model = In(['DALL-E2', 'dall-e-3']);
|
||||
}
|
||||
}
|
||||
const data = await this.chatLogEntity.find({
|
||||
where,
|
||||
order: { id: 'DESC' },
|
||||
select: ['id', 'answer', 'prompt', 'model', 'type', 'fileInfo'],
|
||||
});
|
||||
data.forEach((r: any) => {
|
||||
if (r.type === 'paintCount') {
|
||||
const w = r.model === 'mj' ? 310 : 160;
|
||||
const imgType = r.answer.includes('cos') ? 'tencent' : 'ali';
|
||||
const compress =
|
||||
imgType === 'tencent'
|
||||
? `?imageView2/1/w/${w}/q/55`
|
||||
: `?x-oss-process=image/resize,w_${w}`;
|
||||
r.thumbImg = r.answer + compress;
|
||||
try {
|
||||
r.fileInfo = r.fileInfo ? JSON.parse(r.fileInfo) : null;
|
||||
} catch (error) {
|
||||
r.fileInfo = {};
|
||||
}
|
||||
}
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
/* 推荐图片到对外展示 */
|
||||
async recDrawImg(body: recDrawImgDto) {
|
||||
const { id } = body;
|
||||
const l = await this.chatLogEntity.findOne({
|
||||
where: { id, type: ChatType.PAINT },
|
||||
});
|
||||
if (!l) {
|
||||
throw new HttpException('你推荐的图片不存在、请检查!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
const rec = l.rec === 1 ? 0 : 1;
|
||||
const res = await this.chatLogEntity.update({ id }, { rec });
|
||||
if (res.affected > 0) {
|
||||
return `${rec ? '推荐' : '取消推荐'}图片成功!`;
|
||||
}
|
||||
throw new HttpException('你操作的图片不存在、请检查!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
/* 导出为excel对话记录 */
|
||||
async exportExcel(body: ExportExcelChatlogDto, res: Response) {
|
||||
const where = { type: ChatType.NORMAL_CHAT };
|
||||
const { page = 1, size = 30, prompt, email } = body;
|
||||
prompt && Object.assign(where, { prompt: Like(`%${prompt}%`) });
|
||||
if (email) {
|
||||
const user = await this.userEntity.findOne({ where: { email } });
|
||||
user?.id && Object.assign(where, { userId: user.id });
|
||||
}
|
||||
const [rows, count] = await this.chatLogEntity.findAndCount({
|
||||
order: { id: 'DESC' },
|
||||
skip: (page - 1) * size,
|
||||
take: size,
|
||||
where,
|
||||
});
|
||||
|
||||
const userIds = rows.map(r => r.userId);
|
||||
const userInfos = await this.userEntity.find({
|
||||
where: { id: In(userIds) },
|
||||
});
|
||||
const data = rows.map(r => {
|
||||
const userInfo = userInfos.find(u => u.id === r.userId);
|
||||
return {
|
||||
username: userInfo ? userInfo.username : '',
|
||||
email: userInfo ? userInfo.email : '',
|
||||
prompt: r.prompt,
|
||||
answer: r.answer,
|
||||
createdAt: formatDate(r.createdAt),
|
||||
};
|
||||
});
|
||||
|
||||
const workbook = new excel.Workbook();
|
||||
|
||||
const worksheet = workbook.addWorksheet('chatlog');
|
||||
|
||||
worksheet.columns = [
|
||||
{ header: '用户名', key: 'username', width: 20 },
|
||||
{ header: '用户邮箱', key: 'email', width: 20 },
|
||||
{ header: '提问时间', key: 'createdAt', width: 20 },
|
||||
{ header: '提问问题', key: 'prompt', width: 80 },
|
||||
{ header: '回答答案', key: 'answer', width: 150 },
|
||||
];
|
||||
|
||||
data.forEach(row => worksheet.addRow(row));
|
||||
|
||||
res.setHeader(
|
||||
'Content-Type',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
);
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=' + 'chat.xlsx');
|
||||
await workbook.xlsx.write(res);
|
||||
res.end();
|
||||
}
|
||||
|
||||
/* 查询所有对话记录 */
|
||||
async querAllChatLog(params: QuerAllChatLogDto, req: Request) {
|
||||
const { page = 1, size = 20, userId, prompt, type, model } = params;
|
||||
// const where = { type: ChatType.NORMAL_CHAT, content: Not('') };
|
||||
const where: any = {};
|
||||
userId && Object.assign(where, { userId });
|
||||
prompt && Object.assign(where, { prompt: Like(`%${prompt}%`) });
|
||||
type && Object.assign(where, { type });
|
||||
model && Object.assign(where, { model });
|
||||
const [rows, count] = await this.chatLogEntity.findAndCount({
|
||||
order: { id: 'DESC' },
|
||||
skip: (page - 1) * size,
|
||||
take: size,
|
||||
where,
|
||||
});
|
||||
const userIds = rows.map(item => item.userId);
|
||||
const userInfo = await this.userEntity.find({
|
||||
where: { id: In(userIds) },
|
||||
select: ['id', 'username', 'email', 'nickname'],
|
||||
});
|
||||
rows.forEach((item: any) => {
|
||||
const { username, email, nickname } = userInfo.find(u => u.id === item.userId) || {};
|
||||
item.username = username;
|
||||
item.email = email;
|
||||
item.nickname = nickname;
|
||||
});
|
||||
req.user.role !== 'super' && rows.forEach((t: any) => (t.email = maskEmail(t.email)));
|
||||
rows.forEach((item: any) => {
|
||||
!item.email && (item.email = `${item?.userId}@aiweb.com`);
|
||||
!item.username && (item.username = `游客${item?.userId}`);
|
||||
});
|
||||
return { rows, count };
|
||||
}
|
||||
|
||||
/* 查询当前对话的列表 */
|
||||
async chatList(req: Request, params: ChatListDto) {
|
||||
const { id } = req.user;
|
||||
const { groupId } = params;
|
||||
const where = { userId: id, isDelete: false };
|
||||
groupId && Object.assign(where, { groupId });
|
||||
if (groupId) {
|
||||
const count = await this.chatGroupEntity.count({
|
||||
where: { isDelete: false },
|
||||
});
|
||||
if (count === 0) return [];
|
||||
}
|
||||
const list = await this.chatLogEntity.find({ where });
|
||||
return list.map(item => {
|
||||
const {
|
||||
prompt,
|
||||
role,
|
||||
answer,
|
||||
createdAt,
|
||||
model,
|
||||
modelName,
|
||||
type,
|
||||
status,
|
||||
action,
|
||||
drawId,
|
||||
id,
|
||||
imageUrl,
|
||||
fileInfo,
|
||||
fileUrl,
|
||||
ttsUrl,
|
||||
videoUrl,
|
||||
audioUrl,
|
||||
customId,
|
||||
pluginParam,
|
||||
progress,
|
||||
modelAvatar,
|
||||
taskData,
|
||||
promptReference,
|
||||
networkSearchResult,
|
||||
fileVectorResult,
|
||||
taskId,
|
||||
reasoning_content,
|
||||
tool_calls,
|
||||
content,
|
||||
} = item;
|
||||
return {
|
||||
chatId: id,
|
||||
dateTime: formatDate(createdAt),
|
||||
content: content || (role === 'assistant' ? answer : prompt),
|
||||
reasoningText: reasoning_content,
|
||||
tool_calls: tool_calls,
|
||||
modelType: type,
|
||||
status: status,
|
||||
action: action,
|
||||
drawId: drawId,
|
||||
customId: customId,
|
||||
role: role,
|
||||
error: false,
|
||||
imageUrl: imageUrl || fileInfo || '',
|
||||
fileUrl: fileUrl,
|
||||
ttsUrl: ttsUrl,
|
||||
videoUrl: videoUrl,
|
||||
audioUrl: audioUrl,
|
||||
progress,
|
||||
model: model,
|
||||
modelName: modelName,
|
||||
pluginParam: pluginParam,
|
||||
modelAvatar: modelAvatar,
|
||||
taskData: taskData,
|
||||
promptReference: promptReference,
|
||||
networkSearchResult: networkSearchResult,
|
||||
fileVectorResult: fileVectorResult,
|
||||
taskId: taskId,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/* 查询历史对话的列表 */
|
||||
async chatHistory(groupId: number, rounds: number) {
|
||||
// Logger.debug(`查询历史对话的列表, groupId: ${groupId}, rounds: ${rounds}`);
|
||||
|
||||
if (rounds === 0) {
|
||||
// Logger.debug('轮次为0,返回空数组');
|
||||
return [];
|
||||
}
|
||||
|
||||
const where = { isDelete: false, groupId: groupId };
|
||||
// Logger.debug('查询条件:', JSON.stringify(where, null, 2));
|
||||
|
||||
const list = await this.chatLogEntity.find({
|
||||
where,
|
||||
order: {
|
||||
createdAt: 'DESC',
|
||||
},
|
||||
take: rounds * 2, // 只取最新的rounds条记录
|
||||
});
|
||||
|
||||
// Logger.debug('查询结果:', JSON.stringify(list, null, 2));
|
||||
|
||||
const result = list
|
||||
.map(item => {
|
||||
const {
|
||||
role,
|
||||
content,
|
||||
answer,
|
||||
prompt,
|
||||
imageUrl,
|
||||
fileInfo,
|
||||
fileUrl,
|
||||
ttsUrl,
|
||||
videoUrl,
|
||||
audioUrl,
|
||||
reasoning_content,
|
||||
tool_calls,
|
||||
progress,
|
||||
} = item;
|
||||
const record = {
|
||||
role: role,
|
||||
content: content || (role === 'assistant' ? answer : prompt),
|
||||
imageUrl: imageUrl || fileInfo || '',
|
||||
fileUrl: fileUrl,
|
||||
ttsUrl: ttsUrl,
|
||||
videoUrl: videoUrl,
|
||||
audioUrl: audioUrl,
|
||||
reasoningText: reasoning_content,
|
||||
tool_calls: tool_calls,
|
||||
progress,
|
||||
};
|
||||
// Logger.debug('处理记录:', JSON.stringify(record, null, 2));
|
||||
return record;
|
||||
})
|
||||
.reverse(); // 添加.reverse()来反转数组,使结果按时间从旧到新排列
|
||||
|
||||
// Logger.debug('处理后的结果:', JSON.stringify(result, null, 2));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/* 删除单条对话记录 */
|
||||
async deleteChatLog(req: Request, body: DelDto) {
|
||||
const { id: userId } = req.user;
|
||||
const { id } = body;
|
||||
const c = await this.chatLogEntity.findOne({ where: { id, userId } });
|
||||
if (!c) {
|
||||
throw new HttpException('你删除的对话记录不存在、请检查!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
const r = await this.chatLogEntity.update({ id }, { isDelete: true });
|
||||
if (r.affected > 0) {
|
||||
return '删除对话记录成功!';
|
||||
} else {
|
||||
throw new HttpException('你删除的对话记录不存在、请检查!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
/* 清空一组对话记录 */
|
||||
async delByGroupId(req: Request, body: DelByGroupDto) {
|
||||
const { groupId } = body;
|
||||
const { id } = req.user;
|
||||
const g = await this.chatGroupEntity.findOne({
|
||||
where: { id: groupId, userId: id },
|
||||
});
|
||||
|
||||
if (!g) {
|
||||
throw new HttpException('你删除的对话记录不存在、请检查!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const r = await this.chatLogEntity.update({ groupId }, { isDelete: true });
|
||||
|
||||
if (r.affected > 0) {
|
||||
return '删除对话记录成功!';
|
||||
}
|
||||
|
||||
if (r.affected === 0) {
|
||||
throw new HttpException('当前页面已经没有东西可以删除了!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
/* 删除对话组中某条对话及其后的所有对话 */
|
||||
async deleteChatsAfterId(req: Request, body: any) {
|
||||
const { id } = body; // 从请求体中获取对话记录 id
|
||||
const { id: userId } = req.user; // 从请求中获取用户ID
|
||||
|
||||
// 查找该对话记录,确保其存在且属于当前用户
|
||||
const chatLog = await this.chatLogEntity.findOne({ where: { id, userId } });
|
||||
if (!chatLog) {
|
||||
// 如果对话记录不存在,抛出异常
|
||||
throw new HttpException('你删除的对话记录不存在、请检查!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const { groupId } = chatLog; // 获取该对话记录所在的对话组ID
|
||||
|
||||
// 删除该对话组中所有 ID 大于等于当前 id 的对话记录
|
||||
const result = await this.chatLogEntity.update(
|
||||
{ groupId, id: MoreThanOrEqual(id) },
|
||||
{ isDelete: true },
|
||||
);
|
||||
|
||||
if (result.affected > 0) {
|
||||
// 如果更新成功,返回成功消息
|
||||
return '删除对话记录成功!';
|
||||
} else {
|
||||
// 如果没有任何记录被更新,抛出异常
|
||||
throw new HttpException('当前页面已经没有东西可以删除了!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
/* 查询单个应用的使用记录 */
|
||||
async byAppId(req: Request, body: QueryByAppIdDto) {
|
||||
const { id } = req.user;
|
||||
const { appId, page = 1, size = 10 } = body;
|
||||
const [rows, count] = await this.chatLogEntity.findAndCount({
|
||||
where: { userId: id, appId, role: 'assistant' },
|
||||
order: { id: 'DESC' },
|
||||
take: size,
|
||||
skip: (page - 1) * size,
|
||||
});
|
||||
return { rows, count };
|
||||
}
|
||||
|
||||
async checkModelLimits(userId: JwtPayload, model: string) {
|
||||
const ONE_HOUR_IN_MS = 3600 * 1000;
|
||||
const oneHourAgo = new Date(Date.now() - ONE_HOUR_IN_MS);
|
||||
|
||||
try {
|
||||
// 计算一小时内模型的使用次数
|
||||
const usageCount = await this.chatLogEntity.count({
|
||||
where: {
|
||||
userId: userId.id,
|
||||
model,
|
||||
createdAt: MoreThan(oneHourAgo),
|
||||
},
|
||||
});
|
||||
|
||||
const adjustedUsageCount = Math.ceil(usageCount / 2);
|
||||
|
||||
Logger.log(
|
||||
`用户ID: ${userId.id} 一小时内调用 ${model} 模型 ${adjustedUsageCount + 1} 次`,
|
||||
'ChatLogService',
|
||||
);
|
||||
|
||||
// 获取模型的使用限制
|
||||
|
||||
let modelInfo;
|
||||
if (model.startsWith('gpt-4-gizmo')) {
|
||||
modelInfo = await this.modelsService.getCurrentModelKeyInfo('gpts');
|
||||
} else {
|
||||
modelInfo = await this.modelsService.getCurrentModelKeyInfo(model);
|
||||
}
|
||||
const modelLimits = Number(modelInfo.modelLimits);
|
||||
|
||||
Logger.log(`模型 ${model} 的使用次数限制为 ${modelLimits}`, 'ChatLogService');
|
||||
|
||||
// 检查是否超过使用限制
|
||||
if (adjustedUsageCount > modelLimits) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
Logger.error(
|
||||
`查询数据库出错 - 用户ID: ${userId.id}, 模型: ${model}, 错误信息: ${error.message}`,
|
||||
error.stack,
|
||||
'ChatLogService',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询单条聊天记录
|
||||
* @param req 请求对象
|
||||
* @param params 查询参数,包含 chatId
|
||||
* @returns 返回格式化后的查询结果
|
||||
*/
|
||||
async querySingleChat(req: Request, params: QuerySingleChatDto) {
|
||||
try {
|
||||
const { chatId } = params;
|
||||
|
||||
// 参数验证
|
||||
if (!chatId) {
|
||||
return '请输入正确的聊天ID';
|
||||
}
|
||||
|
||||
// 查询单条消息,将chatId转换为数字类型
|
||||
const chatLog = await this.chatLogEntity.findOne({
|
||||
where: { id: Number(chatId) },
|
||||
});
|
||||
|
||||
// 消息不存在处理
|
||||
if (!chatLog) {
|
||||
Logger.warn(`未找到ID为 ${chatId} 的消息记录`, 'ChatLogService');
|
||||
return '未找到该消息';
|
||||
}
|
||||
|
||||
// 格式化查询结果
|
||||
const formattedResult = {
|
||||
id: chatLog.id,
|
||||
action: chatLog.action || '',
|
||||
taskData: chatLog.taskData || '',
|
||||
chatId: chatLog.id, // 保持兼容性
|
||||
content:
|
||||
chatLog.content || (chatLog.role === 'assistant' ? chatLog.answer : chatLog.prompt) || '',
|
||||
reasoningText: chatLog.reasoning_content || '',
|
||||
tool_calls: chatLog.tool_calls || '',
|
||||
role: chatLog.role || 'assistant',
|
||||
status: chatLog.status || 0,
|
||||
model: chatLog.model || '',
|
||||
modelName: chatLog.modelName || '',
|
||||
modelType: chatLog.type || 1,
|
||||
imageUrl: chatLog.imageUrl || chatLog.fileInfo || '',
|
||||
fileUrl: chatLog.fileUrl || '',
|
||||
drawId: chatLog.drawId || '',
|
||||
customId: chatLog.customId || '',
|
||||
inversion: chatLog.role === 'user',
|
||||
createdAt: chatLog.createdAt,
|
||||
progress: chatLog.progress || 0,
|
||||
updatedAt: chatLog.updatedAt,
|
||||
ttsUrl: chatLog.ttsUrl || '',
|
||||
videoUrl: chatLog.videoUrl || '',
|
||||
audioUrl: chatLog.audioUrl || '',
|
||||
taskId: chatLog.taskId || '',
|
||||
promptReference: chatLog.promptReference || '',
|
||||
networkSearchResult: chatLog.networkSearchResult || '',
|
||||
fileVectorResult: chatLog.fileVectorResult || '',
|
||||
pluginParam: chatLog.pluginParam || '',
|
||||
modelAvatar: chatLog.modelAvatar || '',
|
||||
};
|
||||
|
||||
// 返回成功结果
|
||||
return formattedResult;
|
||||
} catch (error) {
|
||||
// 详细记录错误信息
|
||||
Logger.error(`查询单条消息失败: ${error.message}`, error.stack, 'ChatLogService');
|
||||
|
||||
// 返回错误响应
|
||||
return error.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
8
service/src/modules/chatLog/dto/chatList.dto.ts
Normal file
8
service/src/modules/chatLog/dto/chatList.dto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { IsOptional } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class ChatListDto {
|
||||
@ApiProperty({ example: 1, description: '对话分组ID', required: false })
|
||||
@IsOptional()
|
||||
groupId: number;
|
||||
}
|
||||
6
service/src/modules/chatLog/dto/del.dto.ts
Normal file
6
service/src/modules/chatLog/dto/del.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class DelDto {
|
||||
@ApiProperty({ example: 1, description: '对话Id', required: true })
|
||||
id: number;
|
||||
}
|
||||
6
service/src/modules/chatLog/dto/delByGroup.dto.ts
Normal file
6
service/src/modules/chatLog/dto/delByGroup.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class DelByGroupDto {
|
||||
@ApiProperty({ example: 1, description: '对话组Id', required: true })
|
||||
groupId: number;
|
||||
}
|
||||
28
service/src/modules/chatLog/dto/exportExcelChatlog.dto.ts
Normal file
28
service/src/modules/chatLog/dto/exportExcelChatlog.dto.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsOptional } from 'class-validator';
|
||||
|
||||
export class ExportExcelChatlogDto {
|
||||
@ApiProperty({ example: 1, description: '查询页数', required: false })
|
||||
@IsOptional()
|
||||
page: number;
|
||||
|
||||
@ApiProperty({ example: 10, description: '每页数量', required: false })
|
||||
@IsOptional()
|
||||
size: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: '您好',
|
||||
description: '用户询问的问题',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
prompt: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'aiweb@aiweb.com',
|
||||
description: '用户邮箱',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
email: string;
|
||||
}
|
||||
35
service/src/modules/chatLog/dto/queryAllChatLog.dto.ts
Normal file
35
service/src/modules/chatLog/dto/queryAllChatLog.dto.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsOptional } from 'class-validator';
|
||||
|
||||
export class QuerAllChatLogDto {
|
||||
@ApiProperty({ example: 1, description: '查询页数', required: false })
|
||||
@IsOptional()
|
||||
page: number;
|
||||
|
||||
@ApiProperty({ example: 10, description: '每页数量', required: false })
|
||||
@IsOptional()
|
||||
size: number;
|
||||
|
||||
@ApiProperty({ example: 99, description: '对话的用户id', required: false })
|
||||
@IsOptional()
|
||||
userId: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: '您好',
|
||||
description: '用户询问的问题',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
prompt: string;
|
||||
|
||||
@ApiProperty({ example: 'user', description: '身份', required: false })
|
||||
role: string;
|
||||
|
||||
@ApiProperty({ example: '1', description: '类型', required: false })
|
||||
@IsOptional()
|
||||
type: string;
|
||||
|
||||
@ApiProperty({ example: 'gpt-4o-mini', description: '模型', required: false })
|
||||
@IsOptional()
|
||||
model: string;
|
||||
}
|
||||
36
service/src/modules/chatLog/dto/queryAllDrawLog.dto.ts
Normal file
36
service/src/modules/chatLog/dto/queryAllDrawLog.dto.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { IsOptional } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class QuerAllDrawLogDto {
|
||||
@ApiProperty({ example: 1, description: '查询页数', required: false })
|
||||
@IsOptional()
|
||||
page: number;
|
||||
|
||||
@ApiProperty({ example: 10, description: '每页数量', required: false })
|
||||
@IsOptional()
|
||||
size: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 1,
|
||||
description: '是否推荐0: 默认 1: 推荐',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
rec: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 99,
|
||||
description: '生成图片的用户id',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
userId: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'DALL-E2',
|
||||
description: '生成图片使用的模型',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
model: string;
|
||||
}
|
||||
16
service/src/modules/chatLog/dto/queryByAppId.dto.ts
Normal file
16
service/src/modules/chatLog/dto/queryByAppId.dto.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { IsOptional } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class QueryByAppIdDto {
|
||||
@ApiProperty({ example: 1, description: '应用Id', required: true })
|
||||
@IsOptional()
|
||||
appId: number;
|
||||
|
||||
@ApiProperty({ example: 1, description: '查询页数', required: false })
|
||||
@IsOptional()
|
||||
page: number;
|
||||
|
||||
@ApiProperty({ example: 10, description: '每页数量', required: false })
|
||||
@IsOptional()
|
||||
size: number;
|
||||
}
|
||||
8
service/src/modules/chatLog/dto/queryMyChatLog.dto.ts
Normal file
8
service/src/modules/chatLog/dto/queryMyChatLog.dto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { IsOptional } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class QuerMyChatLogDto {
|
||||
@ApiProperty({ example: 'mj', description: '使用的模型', required: false })
|
||||
@IsOptional()
|
||||
model: string;
|
||||
}
|
||||
8
service/src/modules/chatLog/dto/querySingleChat.dto.ts
Normal file
8
service/src/modules/chatLog/dto/querySingleChat.dto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class QuerySingleChatDto {
|
||||
@ApiProperty({ example: '123', description: '聊天记录ID' })
|
||||
@IsNotEmpty({ message: '聊天记录ID不能为空' })
|
||||
chatId: number;
|
||||
}
|
||||
6
service/src/modules/chatLog/dto/recDrawImg.dto.ts
Normal file
6
service/src/modules/chatLog/dto/recDrawImg.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class recDrawImgDto {
|
||||
@ApiProperty({ example: 1, description: '推荐图片的id' })
|
||||
id: number;
|
||||
}
|
||||
97
service/src/modules/crami/crami.controller.ts
Normal file
97
service/src/modules/crami/crami.controller.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { AdminAuthGuard } from '@/common/auth/adminAuth.guard';
|
||||
import { JwtAuthGuard } from '@/common/auth/jwtAuth.guard';
|
||||
import { SuperAuthGuard } from '@/common/auth/superAuth.guard';
|
||||
import { Body, Controller, Get, Post, Query, Req, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { Request } from 'express';
|
||||
import { CramiService } from './crami.service';
|
||||
import { BatchDelCramiDto } from './dto/batchDelCrami.dto';
|
||||
import { CreatCramiDto } from './dto/createCrami.dto';
|
||||
import { CreatePackageDto } from './dto/createPackage.dto';
|
||||
import { DeletePackageDto } from './dto/deletePackage.dto';
|
||||
import { QuerAllCramiDto } from './dto/queryAllCrami.dto';
|
||||
import { QuerAllPackageDto } from './dto/queryAllPackage.dto';
|
||||
import { UpdatePackageDto } from './dto/updatePackage.dto';
|
||||
import { UseCramiDto } from './dto/useCrami.dto';
|
||||
|
||||
@ApiTags('crami')
|
||||
@Controller('crami')
|
||||
export class CramiController {
|
||||
constructor(private readonly cramiService: CramiService) {}
|
||||
|
||||
@Get('queryOnePackage')
|
||||
@ApiOperation({ summary: '查询单个套餐' })
|
||||
async queryOnePackage(@Query('id') id: number) {
|
||||
return this.cramiService.queryOnePackage(id);
|
||||
}
|
||||
|
||||
@Get('queryAllPackage')
|
||||
@ApiOperation({ summary: '查询所有套餐' })
|
||||
async queryAllPackage(@Query() query: QuerAllPackageDto) {
|
||||
return this.cramiService.queryAllPackage(query);
|
||||
}
|
||||
|
||||
@Post('createPackage')
|
||||
@ApiOperation({ summary: '创建套餐' })
|
||||
@UseGuards(SuperAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
async createPackage(@Body() body: CreatePackageDto) {
|
||||
return this.cramiService.createPackage(body);
|
||||
}
|
||||
|
||||
@Post('updatePackage')
|
||||
@ApiOperation({ summary: '更新套餐' })
|
||||
@UseGuards(SuperAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
async updatePackage(@Body() body: UpdatePackageDto) {
|
||||
return this.cramiService.updatePackage(body);
|
||||
}
|
||||
|
||||
@Post('delPackage')
|
||||
@ApiOperation({ summary: '删除套餐' })
|
||||
@UseGuards(SuperAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
async delPackage(@Body() body: DeletePackageDto) {
|
||||
return this.cramiService.delPackage(body);
|
||||
}
|
||||
|
||||
@Post('createCrami')
|
||||
@ApiOperation({ summary: '生成卡密' })
|
||||
@UseGuards(SuperAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
async createCrami(@Body() body: CreatCramiDto) {
|
||||
return this.cramiService.createCrami(body);
|
||||
}
|
||||
|
||||
@Post('useCrami')
|
||||
@ApiOperation({ summary: '使用卡密' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
async useCrami(@Req() req: Request, @Body() body: UseCramiDto) {
|
||||
return this.cramiService.useCrami(req, body);
|
||||
}
|
||||
|
||||
@Get('queryAllCrami')
|
||||
@ApiOperation({ summary: '查询所有卡密' })
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
async queryAllCrami(@Query() params: QuerAllCramiDto, @Req() req: Request) {
|
||||
return this.cramiService.queryAllCrami(params, req);
|
||||
}
|
||||
|
||||
@Post('delCrami')
|
||||
@ApiOperation({ summary: '删除卡密' })
|
||||
@UseGuards(SuperAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
async delCrami(@Body('id') id: number) {
|
||||
return this.cramiService.delCrami(id);
|
||||
}
|
||||
|
||||
@Post('batchDelCrami')
|
||||
@ApiOperation({ summary: '批量删除卡密' })
|
||||
@UseGuards(SuperAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
async batchDelCrami(@Body() body: BatchDelCramiDto) {
|
||||
return this.cramiService.batchDelCrami(body);
|
||||
}
|
||||
}
|
||||
44
service/src/modules/crami/crami.entity.ts
Normal file
44
service/src/modules/crami/crami.entity.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { BaseEntity } from 'src/common/entity/baseEntity';
|
||||
import { Column, Entity } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'crami' })
|
||||
export class CramiEntity extends BaseEntity {
|
||||
@Column({ unique: true, comment: '存储卡密CDK编码', length: 50 })
|
||||
code: string;
|
||||
|
||||
@Column({
|
||||
comment: '卡密CDK类型:1: 普通 | 2: 单人可使用一次 ',
|
||||
default: 1,
|
||||
})
|
||||
cramiType: number;
|
||||
|
||||
@Column({
|
||||
comment: '卡密CDK类型: 默认套餐类型 | 不填就是自定义类型',
|
||||
nullable: true,
|
||||
})
|
||||
packageId: number;
|
||||
|
||||
@Column({ comment: '卡密CDK状态,如已使用、未使用等', default: 0 })
|
||||
status: number;
|
||||
|
||||
@Column({ comment: '卡密使用账户用户ID信息', nullable: true })
|
||||
useId: number;
|
||||
|
||||
@Column({
|
||||
comment: '卡密有效期天数、从生成创建的时候开始计算,设为0则不限时间',
|
||||
default: 0,
|
||||
})
|
||||
days: number;
|
||||
|
||||
@Column({ comment: '卡密模型3额度', nullable: true })
|
||||
model3Count: number;
|
||||
|
||||
@Column({ comment: '卡密模型4额度', nullable: true })
|
||||
model4Count: number;
|
||||
|
||||
@Column({ comment: '卡密MJ绘画额度', nullable: true })
|
||||
drawMjCount: number;
|
||||
|
||||
@Column({ comment: '卡密应用分类列表', nullable: true, default: '' })
|
||||
appCats: string;
|
||||
}
|
||||
37
service/src/modules/crami/crami.module.ts
Normal file
37
service/src/modules/crami/crami.module.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ChatGroupEntity } from '../chatGroup/chatGroup.entity';
|
||||
import { ChatLogEntity } from '../chatLog/chatLog.entity';
|
||||
import { ConfigEntity } from '../globalConfig/config.entity';
|
||||
import { UserEntity } from '../user/user.entity';
|
||||
import { AccountLogEntity } from '../userBalance/accountLog.entity';
|
||||
import { BalanceEntity } from '../userBalance/balance.entity';
|
||||
import { FingerprintLogEntity } from '../userBalance/fingerprint.entity';
|
||||
import { UserBalanceEntity } from '../userBalance/userBalance.entity';
|
||||
import { UserBalanceService } from '../userBalance/userBalance.service';
|
||||
import { CramiController } from './crami.controller';
|
||||
import { CramiEntity } from './crami.entity';
|
||||
import { CramiService } from './crami.service';
|
||||
import { CramiPackageEntity } from './cramiPackage.entity';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
CramiEntity,
|
||||
CramiPackageEntity,
|
||||
UserEntity,
|
||||
BalanceEntity,
|
||||
AccountLogEntity,
|
||||
ConfigEntity,
|
||||
UserBalanceEntity,
|
||||
FingerprintLogEntity,
|
||||
ChatLogEntity,
|
||||
ChatGroupEntity,
|
||||
]),
|
||||
],
|
||||
providers: [CramiService, UserBalanceService],
|
||||
controllers: [CramiController],
|
||||
exports: [CramiService],
|
||||
})
|
||||
export class CramiModule {}
|
||||
264
service/src/modules/crami/crami.service.ts
Normal file
264
service/src/modules/crami/crami.service.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
import { RechargeType } from '@/common/constants/balance.constant';
|
||||
import { generateCramiCode, maskCrami, maskEmail } from '@/common/utils';
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Request } from 'express';
|
||||
import { In, LessThanOrEqual, Like, MoreThan, Not, Repository } from 'typeorm';
|
||||
import { UserEntity } from '../user/user.entity';
|
||||
import { UserBalanceService } from '../userBalance/userBalance.service';
|
||||
import { CramiEntity } from './crami.entity';
|
||||
import { CramiPackageEntity } from './cramiPackage.entity';
|
||||
import { BatchDelCramiDto } from './dto/batchDelCrami.dto';
|
||||
import { CreatCramiDto } from './dto/createCrami.dto';
|
||||
import { CreatePackageDto } from './dto/createPackage.dto';
|
||||
import { DeletePackageDto } from './dto/deletePackage.dto';
|
||||
import { QuerAllCramiDto } from './dto/queryAllCrami.dto';
|
||||
import { QuerAllPackageDto } from './dto/queryAllPackage.dto';
|
||||
import { UseCramiDto } from './dto/useCrami.dto';
|
||||
|
||||
@Injectable()
|
||||
export class CramiService {
|
||||
constructor(
|
||||
@InjectRepository(CramiEntity)
|
||||
private readonly cramiEntity: Repository<CramiEntity>,
|
||||
@InjectRepository(CramiPackageEntity)
|
||||
private readonly cramiPackageEntity: Repository<CramiPackageEntity>,
|
||||
@InjectRepository(UserEntity)
|
||||
private readonly userEntity: Repository<UserEntity>,
|
||||
private readonly userBalanceService: UserBalanceService,
|
||||
) {}
|
||||
|
||||
/* 查询单个套餐 */
|
||||
async queryOnePackage(id) {
|
||||
return await this.cramiPackageEntity.findOne({ where: { id } });
|
||||
}
|
||||
|
||||
/* 查询所有套餐 */
|
||||
async queryAllPackage(query: QuerAllPackageDto) {
|
||||
try {
|
||||
const { page = 1, size = 10, name, status, type } = query;
|
||||
const where = {};
|
||||
name && Object.assign(where, { name: Like(`%${name}%`) });
|
||||
status && Object.assign(where, { status });
|
||||
if (type) {
|
||||
if (type > 0) {
|
||||
Object.assign(where, { days: MoreThan(0) });
|
||||
} else {
|
||||
Object.assign(where, { days: LessThanOrEqual(0) });
|
||||
}
|
||||
}
|
||||
const [rows, count] = await this.cramiPackageEntity.findAndCount({
|
||||
skip: (page - 1) * size,
|
||||
take: size,
|
||||
where,
|
||||
order: { order: 'DESC' },
|
||||
});
|
||||
return { rows, count };
|
||||
} catch (error) {
|
||||
console.log('error: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
/* 创建套餐 */
|
||||
async createPackage(body: CreatePackageDto) {
|
||||
const { name, weight } = body;
|
||||
const p = await this.cramiPackageEntity.findOne({
|
||||
where: [{ name }, { weight }],
|
||||
});
|
||||
if (p) {
|
||||
throw new HttpException('套餐名称或套餐等级重复、请检查!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
try {
|
||||
return await this.cramiPackageEntity.save(body);
|
||||
} catch (error) {
|
||||
console.log('error: ', error);
|
||||
throw new HttpException(error, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
/* 更新套餐 E */
|
||||
async updatePackage(body) {
|
||||
const { id, name, weight } = body;
|
||||
const op = await this.cramiPackageEntity.findOne({ where: { id } });
|
||||
if (!op) {
|
||||
throw new HttpException('当前套餐不存在、请检查你的输入参数!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
const count = await this.cramiPackageEntity.count({
|
||||
where: [
|
||||
{ name, id: Not(id) },
|
||||
{ weight, id: Not(id) },
|
||||
],
|
||||
});
|
||||
if (count) {
|
||||
throw new HttpException('套餐名称或套餐等级重复、请检查!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
const res = await this.cramiPackageEntity.update({ id }, body);
|
||||
if (res.affected > 0) {
|
||||
return '更新套餐成功!';
|
||||
} else {
|
||||
throw new HttpException('更新套餐失败、请重试!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
/* 删除套餐 */
|
||||
async delPackage(body: DeletePackageDto) {
|
||||
const { id } = body;
|
||||
const count = await this.cramiEntity.count({ where: { packageId: id } });
|
||||
if (count) {
|
||||
throw new HttpException(
|
||||
'当前套餐下存在卡密、请先删除卡密后才可删除套餐!',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
return await this.cramiPackageEntity.delete({ id });
|
||||
}
|
||||
|
||||
/* 生成卡密 */
|
||||
async createCrami(body: CreatCramiDto) {
|
||||
const { packageId, count = 1 } = body;
|
||||
/* 创建有套餐的卡密 */
|
||||
if (packageId) {
|
||||
const pkg = await this.cramiPackageEntity.findOne({
|
||||
where: { id: packageId },
|
||||
});
|
||||
if (!pkg) {
|
||||
throw new HttpException(
|
||||
'当前套餐不存在、请确认您选择的套餐是否存在!',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
const { days = -1, model3Count = 0, model4Count = 0, drawMjCount = 0, appCats = '' } = pkg;
|
||||
const baseCrami = {
|
||||
packageId,
|
||||
days,
|
||||
model3Count,
|
||||
model4Count,
|
||||
drawMjCount,
|
||||
appCats,
|
||||
};
|
||||
return await this.generateCrami(baseCrami, count);
|
||||
}
|
||||
/* 创建自定义的卡密 */
|
||||
if (!packageId) {
|
||||
const { model3Count = 0, model4Count = 0, drawMjCount = 0 } = body;
|
||||
if ([model3Count, model4Count, drawMjCount].every(v => !v)) {
|
||||
throw new HttpException('自定义卡密必须至少一项余额不为0️零!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
const baseCrami = { days: -1, model3Count, model4Count, drawMjCount };
|
||||
return await this.generateCrami(baseCrami, count);
|
||||
}
|
||||
}
|
||||
|
||||
/* 创建卡密 */
|
||||
async generateCrami(cramiInfo, count: number) {
|
||||
const cramiList = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const code = generateCramiCode();
|
||||
const crami = this.cramiEntity.create({ ...cramiInfo, code });
|
||||
cramiList.push(crami);
|
||||
}
|
||||
return await this.cramiEntity.save(cramiList);
|
||||
}
|
||||
|
||||
/* 使用卡密 */
|
||||
async useCrami(req: Request, body: UseCramiDto) {
|
||||
const { id } = req.user;
|
||||
const crami = await this.cramiEntity.findOne({
|
||||
where: { code: body.code },
|
||||
});
|
||||
if (!crami) {
|
||||
throw new HttpException(
|
||||
'当前卡密不存在、请确认您输入的卡密是否正确!',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
const {
|
||||
status,
|
||||
days = -1,
|
||||
model3Count = 0,
|
||||
model4Count = 0,
|
||||
drawMjCount = 0,
|
||||
packageId,
|
||||
appCats,
|
||||
} = crami;
|
||||
if (status === 1) {
|
||||
throw new HttpException(
|
||||
'当前卡密已被使用、请确认您输入的卡密是否正确!',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
const balanceInfo = {
|
||||
model3Count,
|
||||
model4Count,
|
||||
drawMjCount,
|
||||
packageId,
|
||||
appCats,
|
||||
};
|
||||
await this.userBalanceService.addBalanceToUser(id, { ...balanceInfo }, days);
|
||||
await this.userBalanceService.saveRecordRechargeLog({
|
||||
userId: id,
|
||||
rechargeType: RechargeType.PACKAGE_GIFT,
|
||||
model3Count,
|
||||
model4Count,
|
||||
drawMjCount,
|
||||
days,
|
||||
});
|
||||
await this.cramiEntity.update({ code: body.code }, { useId: id, status: 1 });
|
||||
return '使用卡密成功';
|
||||
}
|
||||
|
||||
/* 查询所有卡密 */
|
||||
async queryAllCrami(params: QuerAllCramiDto, req: Request) {
|
||||
const { page = 1, size = 10, status, useId } = params;
|
||||
const where = {};
|
||||
status && Object.assign(where, { status });
|
||||
useId && Object.assign(where, { useId });
|
||||
const [rows, count] = await this.cramiEntity.findAndCount({
|
||||
skip: (page - 1) * size,
|
||||
take: size,
|
||||
order: { createdAt: 'DESC' },
|
||||
where,
|
||||
});
|
||||
const userIds = rows.map(t => t.useId);
|
||||
const packageIds = rows.map(t => t.packageId);
|
||||
const userInfos = await this.userEntity.find({
|
||||
where: { id: In(userIds) },
|
||||
});
|
||||
const packageInfos = await this.cramiPackageEntity.find({
|
||||
where: { id: In(packageIds) },
|
||||
});
|
||||
rows.forEach((t: any) => {
|
||||
t.username = userInfos.find(u => u.id === t.useId)?.username;
|
||||
t.email = userInfos.find(u => u.id === t.useId)?.email;
|
||||
t.packageName = packageInfos.find(p => p.id === t.packageId)?.name;
|
||||
});
|
||||
req.user.role !== 'super' && rows.forEach((t: any) => (t.email = maskEmail(t.email)));
|
||||
req.user.role !== 'super' && rows.forEach((t: any) => (t.code = maskCrami(t.code)));
|
||||
return { rows, count };
|
||||
}
|
||||
|
||||
/* 删除卡密 */
|
||||
async delCrami(id) {
|
||||
const c = await this.cramiEntity.findOne({ where: { id } });
|
||||
if (!c) {
|
||||
throw new HttpException(
|
||||
'当前卡密不存在、请确认您要删除的卡密是否存在!',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
if (c.status === 1) {
|
||||
throw new HttpException('当前卡密已被使用、已使用的卡密禁止删除!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
return await this.cramiEntity.delete({ id });
|
||||
}
|
||||
|
||||
async batchDelCrami(body: BatchDelCramiDto) {
|
||||
const { ids } = body;
|
||||
const res = await this.cramiEntity.delete(ids);
|
||||
if (res.affected > 0) {
|
||||
return '删除卡密成功!';
|
||||
} else {
|
||||
throw new HttpException('删除卡密失败、请重试!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
44
service/src/modules/crami/cramiPackage.entity.ts
Normal file
44
service/src/modules/crami/cramiPackage.entity.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { BaseEntity } from 'src/common/entity/baseEntity';
|
||||
import { Column, Entity } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'crami_package' })
|
||||
export class CramiPackageEntity extends BaseEntity {
|
||||
@Column({ unique: true, comment: '套餐名称' })
|
||||
name: string;
|
||||
|
||||
@Column({ comment: '套餐介绍详细信息' })
|
||||
des: string;
|
||||
|
||||
@Column({ comment: '套餐封面图片', nullable: true })
|
||||
coverImg: string;
|
||||
|
||||
@Column({ comment: '套餐价格¥', type: 'decimal', scale: 2, precision: 10 })
|
||||
price: number;
|
||||
|
||||
@Column({ comment: '套餐排序、数字越大越靠前', default: 100 })
|
||||
order: number;
|
||||
|
||||
@Column({ comment: '套餐是否启用中 0:禁用 1:启用', default: 1 })
|
||||
status: number;
|
||||
|
||||
@Column({ comment: '套餐权重、数字越大表示套餐等级越高越贵', unique: true })
|
||||
weight: number;
|
||||
|
||||
@Column({
|
||||
comment: '卡密有效期天数、从使用的时候开始计算,设为-1则不限时间',
|
||||
default: 0,
|
||||
})
|
||||
days: number;
|
||||
|
||||
@Column({ comment: '套餐包含的模型3数量', default: 0, nullable: true })
|
||||
model3Count: number;
|
||||
|
||||
@Column({ comment: '套餐包含的模型4数量', default: 0, nullable: true })
|
||||
model4Count: number;
|
||||
|
||||
@Column({ comment: '套餐包含的MJ绘画数量', default: 0, nullable: true })
|
||||
drawMjCount: number;
|
||||
|
||||
@Column({ comment: '套餐包含的应用分类列表', default: '', nullable: true })
|
||||
appCats: string;
|
||||
}
|
||||
9
service/src/modules/crami/dto/batchDelCrami.dto.ts
Normal file
9
service/src/modules/crami/dto/batchDelCrami.dto.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ArrayMinSize, IsArray } from 'class-validator';
|
||||
|
||||
export class BatchDelCramiDto {
|
||||
@ApiProperty({ example: 1, description: '要删除的套餐Ids', required: true })
|
||||
@IsArray({ message: '参数类型为数组' })
|
||||
@ArrayMinSize(1, { message: '最短长度为1' })
|
||||
ids: number[];
|
||||
}
|
||||
31
service/src/modules/crami/dto/createCrami.dto.ts
Normal file
31
service/src/modules/crami/dto/createCrami.dto.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { IsOptional, IsNumber, Max, Min } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreatCramiDto {
|
||||
@ApiProperty({ example: 1, description: '套餐类型', required: true })
|
||||
@IsNumber({}, { message: '套餐类型必须是number' })
|
||||
@IsOptional()
|
||||
packageId: number;
|
||||
|
||||
@ApiProperty({ example: 1, description: '单次生成卡密数量' })
|
||||
@IsNumber({}, { message: '创建卡密的张数数量' })
|
||||
@Max(50, { message: '单次创建卡密的张数数量不能超过50张' })
|
||||
@Min(1, { message: '单次创建卡密的张数数量不能少于1张' })
|
||||
@IsOptional()
|
||||
count: number;
|
||||
|
||||
@ApiProperty({ example: 0, description: '卡密携带模型3额度' })
|
||||
@IsNumber({}, { message: '卡密携带的余额必须是number' })
|
||||
@IsOptional()
|
||||
model3Count: number;
|
||||
|
||||
@ApiProperty({ example: 100, description: '卡密携带模型4额度' })
|
||||
@IsNumber({}, { message: '卡密携带额度类型必须是number' })
|
||||
@IsOptional()
|
||||
model4Count: number;
|
||||
|
||||
@ApiProperty({ example: 3, description: '卡密携带MJ绘画额度' })
|
||||
@IsNumber({}, { message: '卡密携带额度类型必须是number' })
|
||||
@IsOptional()
|
||||
drawMjCount: number;
|
||||
}
|
||||
77
service/src/modules/crami/dto/createPackage.dto.ts
Normal file
77
service/src/modules/crami/dto/createPackage.dto.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsDefined, IsIn, IsNumber, IsOptional } from 'class-validator';
|
||||
|
||||
export class CreatePackageDto {
|
||||
@ApiProperty({
|
||||
example: '基础套餐100次卡',
|
||||
description: '套餐名称',
|
||||
required: true,
|
||||
})
|
||||
@IsDefined({ message: '套餐名称是必传参数' })
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
example:
|
||||
'这是一个100次对话余额的套餐,我们将为您额外赠送3次绘画余额,活动期间,我们将在套餐基础上额外赠送您十次对话余额和1次绘画余额',
|
||||
description: '套餐详情描述',
|
||||
required: true,
|
||||
})
|
||||
@IsDefined({ message: '套餐描述是必传参数' })
|
||||
des: string;
|
||||
|
||||
@ApiProperty({ example: 7, default: 0, description: '套餐等级设置' })
|
||||
@IsNumber({}, { message: '套餐等级权重必须为数字' })
|
||||
weight: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 1,
|
||||
description: '套餐扣费类型 1:按次数 2:按Token',
|
||||
required: true,
|
||||
})
|
||||
deductionType: number;
|
||||
|
||||
@ApiProperty({ example: 'https://xxxx.png', description: '套餐封面图片' })
|
||||
@IsOptional()
|
||||
coverImg: string;
|
||||
|
||||
@Transform(({ value }) => parseFloat(value))
|
||||
price: number;
|
||||
|
||||
@ApiProperty({ example: 100, description: '套餐排序、数字越大越靠前' })
|
||||
@IsOptional()
|
||||
order?: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 1,
|
||||
description: '套餐状态 0:禁用 1:启用',
|
||||
required: true,
|
||||
})
|
||||
@IsNumber({}, { message: '套餐状态必须是Number' })
|
||||
@IsIn([0, 1], { message: '套餐状态错误' })
|
||||
status: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 7,
|
||||
default: 0,
|
||||
description: '套餐有效期 -1为永久不过期',
|
||||
})
|
||||
@IsNumber({}, { message: '套餐有效期天数类型必须是number' })
|
||||
days: number;
|
||||
|
||||
@ApiProperty({ example: 1000, default: 0, description: '模型3对话次数' })
|
||||
@IsNumber({}, { message: '模型3对话次数必须是number类型' })
|
||||
model3Count: number;
|
||||
|
||||
@ApiProperty({ example: 10, default: 0, description: '模型4对话次数' })
|
||||
@IsNumber({}, { message: '模型4对话次数必须是number类型' })
|
||||
model4Count: number;
|
||||
|
||||
@ApiProperty({ example: 10, default: 0, description: 'MJ绘画次数' })
|
||||
@IsNumber({}, { message: 'MJ绘画次数必须是number类型' })
|
||||
drawMjCount: number;
|
||||
|
||||
@ApiProperty({ example: '1,2,3', description: '套餐包含的应用分类' })
|
||||
@IsOptional()
|
||||
appCats?: string;
|
||||
}
|
||||
8
service/src/modules/crami/dto/deletePackage.dto.ts
Normal file
8
service/src/modules/crami/dto/deletePackage.dto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumber } from 'class-validator';
|
||||
|
||||
export class DeletePackageDto {
|
||||
@ApiProperty({ example: 1, description: '要修改的套餐Id', required: true })
|
||||
@IsNumber({}, { message: '套餐ID必须是Number' })
|
||||
id: number;
|
||||
}
|
||||
24
service/src/modules/crami/dto/queryAllCrami.dto.ts
Normal file
24
service/src/modules/crami/dto/queryAllCrami.dto.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { IsOptional } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class QuerAllCramiDto {
|
||||
@ApiProperty({ example: 1, description: '查询页数', required: false })
|
||||
@IsOptional()
|
||||
page: number;
|
||||
|
||||
@ApiProperty({ example: 10, description: '每页数量', required: false })
|
||||
@IsOptional()
|
||||
size: number;
|
||||
|
||||
@ApiProperty({ example: 1, description: '使用人Id', required: false })
|
||||
@IsOptional()
|
||||
useId: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 1,
|
||||
description: '卡密状态 0:未使用 1:已消费',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
status: number;
|
||||
}
|
||||
32
service/src/modules/crami/dto/queryAllPackage.dto.ts
Normal file
32
service/src/modules/crami/dto/queryAllPackage.dto.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { IsOptional } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class QuerAllPackageDto {
|
||||
@ApiProperty({ example: 1, description: '查询页数', required: false })
|
||||
@IsOptional()
|
||||
page: number;
|
||||
|
||||
@ApiProperty({ example: 10, description: '每页数量', required: false })
|
||||
@IsOptional()
|
||||
size: number;
|
||||
|
||||
@ApiProperty({ example: 'name', description: '套餐名称', required: false })
|
||||
@IsOptional()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 1,
|
||||
description: '套餐状态 0:禁用 1:启用',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
status: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 1,
|
||||
description: '套餐类型 -1:永久套餐 1:限时套餐',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
type: number;
|
||||
}
|
||||
9
service/src/modules/crami/dto/updatePackage.dto.ts
Normal file
9
service/src/modules/crami/dto/updatePackage.dto.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { IsNumber } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { CreatePackageDto } from './createPackage.dto';
|
||||
|
||||
export class UpdatePackageDto extends CreatePackageDto {
|
||||
@ApiProperty({ example: 1, description: '要修改的套餐Id', required: true })
|
||||
@IsNumber({}, { message: '套餐ID必须是Number' })
|
||||
id: number;
|
||||
}
|
||||
12
service/src/modules/crami/dto/useCrami.dto.ts
Normal file
12
service/src/modules/crami/dto/useCrami.dto.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { IsDefined } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class UseCramiDto {
|
||||
@ApiProperty({
|
||||
example: 'ffar684rv254fs4f',
|
||||
description: '卡密信息',
|
||||
required: true,
|
||||
})
|
||||
@IsDefined({ message: '套餐名称是必传参数' })
|
||||
code: string;
|
||||
}
|
||||
84
service/src/modules/database/database.module.ts
Normal file
84
service/src/modules/database/database.module.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { DataSource, DataSourceOptions } from 'typeorm';
|
||||
import { DatabaseService } from './database.service';
|
||||
|
||||
// Import all entities explicitly
|
||||
import { AppEntity } from '../app/app.entity';
|
||||
import { AppCatsEntity } from '../app/appCats.entity';
|
||||
import { UserAppsEntity } from '../app/userApps.entity';
|
||||
import { AutoReplyEntity } from '../autoReply/autoReply.entity';
|
||||
import { BadWordsEntity } from '../badWords/badWords.entity';
|
||||
import { ViolationLogEntity } from '../badWords/violationLog.entity';
|
||||
import { ChatGroupEntity } from '../chatGroup/chatGroup.entity';
|
||||
import { ChatLogEntity } from '../chatLog/chatLog.entity';
|
||||
import { CramiEntity } from '../crami/crami.entity';
|
||||
import { CramiPackageEntity } from '../crami/cramiPackage.entity';
|
||||
import { ConfigEntity } from '../globalConfig/config.entity';
|
||||
import { ModelsEntity } from '../models/models.entity';
|
||||
import { OrderEntity } from '../order/order.entity';
|
||||
import { PluginEntity } from '../plugin/plugin.entity';
|
||||
import { Share } from '../share/share.entity';
|
||||
import { SigninEntity } from '../signin/signIn.entity';
|
||||
import { UserEntity } from '../user/user.entity';
|
||||
import { AccountLogEntity } from '../userBalance/accountLog.entity';
|
||||
import { BalanceEntity } from '../userBalance/balance.entity';
|
||||
import { FingerprintLogEntity } from '../userBalance/fingerprint.entity';
|
||||
import { UserBalanceEntity } from '../userBalance/userBalance.entity';
|
||||
import { VerificationEntity } from '../verification/verification.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forRootAsync({
|
||||
useFactory: () =>
|
||||
({
|
||||
type: 'mysql',
|
||||
host: process.env.DB_HOST,
|
||||
port: parseInt(process.env.DB_PORT, 10),
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
database: process.env.DB_DATABASE,
|
||||
// entities: [__dirname + '/../**/*.entity{.ts,.js}'], // <-- Remove glob pattern
|
||||
entities: [
|
||||
// <-- Use explicit array of imported classes
|
||||
Share,
|
||||
AutoReplyEntity,
|
||||
CramiEntity,
|
||||
CramiPackageEntity,
|
||||
BadWordsEntity,
|
||||
ChatGroupEntity,
|
||||
VerificationEntity,
|
||||
SigninEntity,
|
||||
ViolationLogEntity,
|
||||
ModelsEntity,
|
||||
UserEntity,
|
||||
AccountLogEntity,
|
||||
FingerprintLogEntity,
|
||||
BalanceEntity,
|
||||
UserBalanceEntity,
|
||||
PluginEntity,
|
||||
ConfigEntity,
|
||||
ChatLogEntity,
|
||||
UserAppsEntity,
|
||||
AppCatsEntity,
|
||||
AppEntity,
|
||||
OrderEntity,
|
||||
],
|
||||
synchronize: false,
|
||||
logging: false,
|
||||
charset: 'utf8mb4',
|
||||
timezone: '+08:00',
|
||||
} as DataSourceOptions),
|
||||
}),
|
||||
],
|
||||
providers: [DatabaseService],
|
||||
})
|
||||
export class DatabaseModule implements OnModuleInit {
|
||||
constructor(private readonly connection: DataSource) {}
|
||||
private readonly logger = new Logger(DatabaseModule.name);
|
||||
|
||||
async onModuleInit(): Promise<void> {
|
||||
const { database } = this.connection.options;
|
||||
this.logger.log(`Your MySQL database named ${database} has been connected`);
|
||||
}
|
||||
}
|
||||
209
service/src/modules/database/database.service.ts
Normal file
209
service/src/modules/database/database.service.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { HttpException, HttpStatus, Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { Connection } from 'typeorm';
|
||||
|
||||
interface UserInfo {
|
||||
username: string;
|
||||
password: string;
|
||||
status: number;
|
||||
email: string;
|
||||
sex: number;
|
||||
role: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DatabaseService implements OnModuleInit {
|
||||
constructor(private connection: Connection) {}
|
||||
async onModuleInit() {
|
||||
await this.checkSuperAdmin();
|
||||
await this.checkSiteBaseConfig();
|
||||
}
|
||||
|
||||
/* 默认创建一个超级管理员账户 */
|
||||
async checkSuperAdmin() {
|
||||
const user = await this.connection.query(`SELECT * FROM users WHERE role = 'super'`);
|
||||
if (!user || user.length === 0) {
|
||||
const superPassword = bcrypt.hashSync('123456', 10);
|
||||
// const adminPassword = bcrypt.hashSync('123456', 10);
|
||||
const superEmail = 'super';
|
||||
// const adminEmail = 'admin';
|
||||
const superUserinfo = {
|
||||
username: 'super',
|
||||
password: superPassword,
|
||||
status: 1,
|
||||
email: superEmail,
|
||||
sex: 1,
|
||||
role: 'super',
|
||||
};
|
||||
// const adminUserinfo = {
|
||||
// username: 'admin',
|
||||
// password: adminPassword,
|
||||
// status: 0,
|
||||
// email: adminEmail,
|
||||
// sex: 1,
|
||||
// role: 'admin',
|
||||
// };
|
||||
await this.createDefaultUser(superUserinfo);
|
||||
// await this.createDefaultUser(adminUserinfo);
|
||||
}
|
||||
}
|
||||
|
||||
/* 初始化创建 超级管理员和管理员 */
|
||||
async createDefaultUser(userInfo: UserInfo) {
|
||||
try {
|
||||
const { username, password, status, email, role } = userInfo;
|
||||
const user = await this.connection.query(
|
||||
`INSERT INTO users (username, password, status, email, role) VALUES ('${username}', '${password}', '${status}', '${email}', '${role}')`,
|
||||
);
|
||||
const userId = user.insertId;
|
||||
await this.connection.query(
|
||||
`INSERT INTO balance (userId, balance, usesLeft, paintCount) VALUES ('${userId}', 0, 1000, 100)`,
|
||||
);
|
||||
Logger.log(
|
||||
`初始化创建${role}用户成功、用户名为[${username}]、初始密码为[${
|
||||
username === 'super' ? 'super' : '123456'
|
||||
}] ==============> 请注意查阅`,
|
||||
'DatabaseService',
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('error: ', error);
|
||||
throw new HttpException('创建默认超级管理员失败!', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/* 检测有没有网站基础配置 */
|
||||
async checkSiteBaseConfig() {
|
||||
const keys = ['siteName', 'robotAvatar'];
|
||||
const result = await this.connection.query(`
|
||||
SELECT COUNT(*) AS count FROM config WHERE \`configKey\` IN (${keys.map(k => `'${k}'`).join(',')})
|
||||
`);
|
||||
const count = parseInt(result[0].count);
|
||||
if (count === 0) {
|
||||
await this.createBaseSiteConfig();
|
||||
}
|
||||
}
|
||||
|
||||
/* 创建基础的网站数据 */
|
||||
async createBaseSiteConfig() {
|
||||
try {
|
||||
const code = ``;
|
||||
|
||||
const noticeInfo = `
|
||||
#### AIWeb 欢迎您
|
||||
- 欢迎使用 AIWeb
|
||||
- 初始管理员账号密码 super 123456 【前台后台登录都可以修改】
|
||||
- 初始预览账号密码 admin 123456 【为后台查看账号 仅可查看部分非敏感数据】
|
||||
`;
|
||||
|
||||
const defaultConfig = [
|
||||
{ configKey: 'siteName', configVal: '', public: 1, encrypt: 0 },
|
||||
{ configKey: 'robotAvatar', configVal: '', public: 1, encrypt: 0 },
|
||||
{
|
||||
configKey: 'userDefaultAvatar',
|
||||
configVal: '',
|
||||
public: 0,
|
||||
encrypt: 0,
|
||||
},
|
||||
{ configKey: 'baiduCode', configVal: code, public: 1, encrypt: 0 },
|
||||
{ configKey: 'baiduSiteId', configVal: '', public: 0, encrypt: 0 },
|
||||
{
|
||||
configKey: 'baiduToken',
|
||||
configVal: '',
|
||||
public: 0,
|
||||
encrypt: 0,
|
||||
},
|
||||
{
|
||||
configKey: 'openaiBaseUrl',
|
||||
configVal: 'https://api.lightai.io',
|
||||
public: 0,
|
||||
encrypt: 0,
|
||||
},
|
||||
{ configKey: 'openaiBaseKey', configVal: 'sk-', public: 0, encrypt: 0 },
|
||||
{ configKey: 'noticeInfo', configVal: noticeInfo, public: 1, encrypt: 0 },
|
||||
{
|
||||
configKey: 'registerSendStatus',
|
||||
configVal: '1',
|
||||
public: 1,
|
||||
encrypt: 0,
|
||||
},
|
||||
{
|
||||
configKey: 'registerSendModel3Count',
|
||||
configVal: '30',
|
||||
public: 1,
|
||||
encrypt: 0,
|
||||
},
|
||||
{
|
||||
configKey: 'registerSendModel4Count',
|
||||
configVal: '3',
|
||||
public: 1,
|
||||
encrypt: 0,
|
||||
},
|
||||
{
|
||||
configKey: 'registerSendDrawMjCount',
|
||||
configVal: '3',
|
||||
public: 1,
|
||||
encrypt: 0,
|
||||
},
|
||||
{
|
||||
configKey: 'firstRegisterSendStatus',
|
||||
configVal: '1',
|
||||
public: 1,
|
||||
encrypt: 0,
|
||||
},
|
||||
{
|
||||
configKey: 'firstRegisterSendRank',
|
||||
configVal: '500',
|
||||
public: 1,
|
||||
encrypt: 0,
|
||||
},
|
||||
{
|
||||
configKey: 'firstRegisterSendModel3Count',
|
||||
configVal: '10',
|
||||
public: 1,
|
||||
encrypt: 0,
|
||||
},
|
||||
{
|
||||
configKey: 'firstRegisterSendModel4Count',
|
||||
configVal: '10',
|
||||
public: 1,
|
||||
encrypt: 0,
|
||||
},
|
||||
{
|
||||
configKey: 'firstRegisterSendDrawMjCount',
|
||||
configVal: '10',
|
||||
public: 1,
|
||||
encrypt: 0,
|
||||
},
|
||||
{ configKey: 'isVerifyEmail', configVal: '1', public: 1, encrypt: 0 },
|
||||
{ configKey: 'model3Name', configVal: '普通积分', public: 1, encrypt: 0 },
|
||||
{ configKey: 'model4Name', configVal: '高级积分', public: 1, encrypt: 0 },
|
||||
{ configKey: 'drawMjName', configVal: '绘画积分', public: 1, encrypt: 0 },
|
||||
{
|
||||
configKey: 'drawingStyles',
|
||||
configVal:
|
||||
'油画风格,像素风格,赛博朋克,动漫,日系,超现实主义,油画,卡通,插画,海报,写实,扁平,中国风,水墨画,唯美二次元,印象派,炫彩插画,像素艺术,艺术创想,色彩主义,数字艺术',
|
||||
public: 1,
|
||||
encrypt: 0,
|
||||
},
|
||||
];
|
||||
|
||||
const res = await this.connection.query(
|
||||
`INSERT INTO config (configKey, configVal, public, encrypt) VALUES ${defaultConfig
|
||||
.map(
|
||||
d =>
|
||||
`('${d.configKey}', '${d.configVal.replace(/'/g, "\\'")}', '${d.public}', '${
|
||||
d.encrypt
|
||||
}')`,
|
||||
)
|
||||
.join(', ')}`,
|
||||
);
|
||||
Logger.log(
|
||||
`初始化网站配置信息成功、如您需要修改网站配置信息,请前往管理系统系统配置设置 ==============> 请注意查阅`,
|
||||
'DatabaseService',
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('error: ', error);
|
||||
throw new HttpException('创建默认网站配置失败!', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
280
service/src/modules/database/initDatabase.ts
Normal file
280
service/src/modules/database/initDatabase.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { config as loadEnv } from 'dotenv';
|
||||
import * as mysql from 'mysql2/promise';
|
||||
import { DataSource, DataSourceOptions } from 'typeorm';
|
||||
import { AppEntity } from '../app/app.entity';
|
||||
import { AppCatsEntity } from '../app/appCats.entity';
|
||||
import { UserAppsEntity } from '../app/userApps.entity';
|
||||
import { AutoReplyEntity } from '../autoReply/autoReply.entity';
|
||||
import { BadWordsEntity } from '../badWords/badWords.entity';
|
||||
import { ViolationLogEntity } from '../badWords/violationLog.entity';
|
||||
import { ChatGroupEntity } from '../chatGroup/chatGroup.entity';
|
||||
import { ChatLogEntity } from '../chatLog/chatLog.entity';
|
||||
import { CramiEntity } from '../crami/crami.entity';
|
||||
import { CramiPackageEntity } from '../crami/cramiPackage.entity';
|
||||
import { ConfigEntity } from '../globalConfig/config.entity';
|
||||
import { ModelsEntity } from '../models/models.entity';
|
||||
import { OrderEntity } from '../order/order.entity';
|
||||
import { PluginEntity } from '../plugin/plugin.entity';
|
||||
import { Share } from '../share/share.entity';
|
||||
import { SigninEntity } from '../signin/signIn.entity';
|
||||
import { UserEntity } from '../user/user.entity';
|
||||
import { AccountLogEntity } from '../userBalance/accountLog.entity';
|
||||
import { BalanceEntity } from '../userBalance/balance.entity';
|
||||
import { FingerprintLogEntity } from '../userBalance/fingerprint.entity';
|
||||
import { UserBalanceEntity } from '../userBalance/userBalance.entity';
|
||||
import { VerificationEntity } from '../verification/verification.entity';
|
||||
|
||||
loadEnv();
|
||||
|
||||
const dataSourceOptions: DataSourceOptions = {
|
||||
type: 'mysql',
|
||||
port: parseInt(process.env.DB_PORT, 10),
|
||||
host: process.env.DB_HOST,
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
database: process.env.DB_DATABASE,
|
||||
entities: [
|
||||
Share,
|
||||
AutoReplyEntity,
|
||||
CramiEntity,
|
||||
CramiPackageEntity,
|
||||
BadWordsEntity,
|
||||
ChatGroupEntity,
|
||||
VerificationEntity,
|
||||
SigninEntity,
|
||||
ViolationLogEntity,
|
||||
ModelsEntity,
|
||||
UserEntity,
|
||||
AccountLogEntity,
|
||||
FingerprintLogEntity,
|
||||
BalanceEntity,
|
||||
UserBalanceEntity,
|
||||
PluginEntity,
|
||||
ConfigEntity,
|
||||
ChatLogEntity,
|
||||
UserAppsEntity,
|
||||
AppCatsEntity,
|
||||
AppEntity,
|
||||
OrderEntity,
|
||||
],
|
||||
synchronize: false, // 禁用自动同步,改为根据情况动态开启
|
||||
charset: 'utf8mb4',
|
||||
timezone: '+08:00',
|
||||
};
|
||||
|
||||
/**
|
||||
* 通用的列类型迁移函数
|
||||
* @param tableName 表名
|
||||
* @param columnName 列名
|
||||
* @param targetType 目标数据类型
|
||||
* @param conn 数据库连接
|
||||
* @returns 是否进行了迁移操作
|
||||
*/
|
||||
async function migrateColumnType(
|
||||
tableName: string,
|
||||
columnName: string,
|
||||
targetType: string,
|
||||
conn: mysql.Connection,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// 1. 检查表是否存在
|
||||
const [tables] = (await conn.execute(
|
||||
`SHOW TABLES LIKE '${tableName}'`,
|
||||
)) as mysql.RowDataPacket[][];
|
||||
|
||||
if (tables.length === 0) {
|
||||
Logger.log(`表 ${tableName} 不存在,跳过 ${columnName} 列的迁移`, 'Database');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 检查列是否存在和当前数据类型
|
||||
const [columns] = (await conn.execute(
|
||||
`SELECT COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?`,
|
||||
[process.env.DB_DATABASE, tableName, columnName],
|
||||
)) as mysql.RowDataPacket[][];
|
||||
|
||||
if (columns.length === 0) {
|
||||
Logger.log(`表 ${tableName} 中不存在 ${columnName} 列,跳过迁移`, 'Database');
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentType = columns[0].DATA_TYPE.toLowerCase();
|
||||
if (currentType === targetType.toLowerCase()) {
|
||||
Logger.log(`表 ${tableName} 中的 ${columnName} 列已经是 ${targetType} 类型`, 'Database');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 处理外键约束和索引(如果有)
|
||||
const [constraints] = (await conn.execute(
|
||||
`SELECT k.CONSTRAINT_NAME
|
||||
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE k
|
||||
WHERE k.TABLE_SCHEMA = ? AND k.TABLE_NAME = ? AND k.COLUMN_NAME = ?`,
|
||||
[process.env.DB_DATABASE, tableName, columnName],
|
||||
)) as mysql.RowDataPacket[][];
|
||||
|
||||
// 处理外键约束
|
||||
for (const constraint of constraints) {
|
||||
if (constraint.CONSTRAINT_NAME) {
|
||||
Logger.log(
|
||||
`发现 ${columnName} 列有约束: ${constraint.CONSTRAINT_NAME},尝试删除`,
|
||||
'Database',
|
||||
);
|
||||
try {
|
||||
await conn.execute(
|
||||
`ALTER TABLE ${tableName} DROP FOREIGN KEY ${constraint.CONSTRAINT_NAME}`,
|
||||
);
|
||||
Logger.log(`成功删除 ${columnName} 外键约束`, 'Database');
|
||||
} catch (constraintError) {
|
||||
Logger.error(`删除外键约束失败: ${constraintError.message}`, 'Database');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查并处理索引
|
||||
const [indexes] = (await conn.execute(
|
||||
`SELECT INDEX_NAME FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?`,
|
||||
[process.env.DB_DATABASE, tableName, columnName],
|
||||
)) as mysql.RowDataPacket[][];
|
||||
|
||||
for (const idx of indexes) {
|
||||
if (idx.INDEX_NAME !== 'PRIMARY') {
|
||||
// 不处理主键
|
||||
Logger.log(
|
||||
`发现 ${columnName} 是索引 ${idx.INDEX_NAME} 的一部分,尝试删除索引`,
|
||||
'Database',
|
||||
);
|
||||
try {
|
||||
await conn.execute(`ALTER TABLE ${tableName} DROP INDEX ${idx.INDEX_NAME}`);
|
||||
Logger.log(`成功删除索引 ${idx.INDEX_NAME}`, 'Database');
|
||||
} catch (indexError) {
|
||||
Logger.error(`删除索引失败: ${indexError.message}`, 'Database');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 执行列类型迁移
|
||||
Logger.log(
|
||||
`开始将 ${tableName} 表中的 ${columnName} 列类型从 ${currentType} 升级为 ${targetType}`,
|
||||
'Database',
|
||||
);
|
||||
await conn.execute(`ALTER TABLE ${tableName} MODIFY COLUMN \`${columnName}\` ${targetType}`);
|
||||
Logger.log(`${tableName} 表中的 ${columnName} 列类型已成功升级为 ${targetType}`, 'Database');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
Logger.error(`迁移 ${tableName}.${columnName} 列类型时出错:`, error, 'Database');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行所有数据库迁移
|
||||
*/
|
||||
async function runAllMigrations() {
|
||||
const conn = await mysql.createConnection({
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
port: parseInt(process.env.DB_PORT, 10),
|
||||
database: process.env.DB_DATABASE,
|
||||
});
|
||||
|
||||
try {
|
||||
// 1. TEXT类型迁移
|
||||
try {
|
||||
await migrateColumnType('config', 'configVal', 'TEXT', conn);
|
||||
} catch (error) {
|
||||
Logger.log(`迁移config表configVal列时跳过: ${error.message}`, 'Database');
|
||||
}
|
||||
|
||||
try {
|
||||
await migrateColumnType('app', 'coverImg', 'TEXT', conn);
|
||||
} catch (error) {
|
||||
Logger.log(`迁移app表coverImg列时跳过: ${error.message}`, 'Database');
|
||||
}
|
||||
|
||||
// 2. catId迁移
|
||||
try {
|
||||
const appCatIdMigrated = await migrateColumnType('app', 'catId', 'TEXT', conn);
|
||||
if (appCatIdMigrated) {
|
||||
// 如果app表中的catId被迁移了,考虑更新所有现有数据
|
||||
const [apps] = (await conn.execute(`SELECT id, catId FROM app`)) as mysql.RowDataPacket[][];
|
||||
for (const app of apps) {
|
||||
if (app.catId !== null && app.catId !== undefined) {
|
||||
await conn.execute(`UPDATE app SET catId = ? WHERE id = ?`, [
|
||||
app.catId.toString(),
|
||||
app.id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.log(`迁移app表catId列时跳过: ${error.message}`, 'Database');
|
||||
}
|
||||
|
||||
// 3. MEDIUMTEXT类型迁移
|
||||
const chatlogColumns = ['content', 'fileVectorResult'];
|
||||
for (const column of chatlogColumns) {
|
||||
try {
|
||||
await migrateColumnType('chatlog', column, 'MEDIUMTEXT', conn);
|
||||
} catch (error) {
|
||||
Logger.log(`迁移chatlog表${column}列时跳过: ${error.message}`, 'Database');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await conn.end();
|
||||
}
|
||||
}
|
||||
|
||||
export async function initDatabase() {
|
||||
try {
|
||||
Logger.log('开始数据库初始化流程', 'Database');
|
||||
|
||||
// 执行所有迁移操作
|
||||
Logger.log('执行数据库迁移操作', 'Database');
|
||||
await runAllMigrations();
|
||||
|
||||
Logger.log('数据迁移操作完成,现在执行同步确保所有新表和字段存在', 'Database');
|
||||
|
||||
// 先使用禁用同步的连接,确保不会重置迁移后的数据
|
||||
const dataSource = new DataSource(dataSourceOptions);
|
||||
await dataSource.initialize();
|
||||
Logger.log('已连接到数据库,准备同步结构', 'Database');
|
||||
|
||||
// 关闭初始连接
|
||||
if (dataSource.isInitialized) {
|
||||
await dataSource.destroy();
|
||||
}
|
||||
|
||||
// 创建启用同步的连接,确保所有新表和字段被创建
|
||||
const syncOptions: DataSourceOptions = {
|
||||
...dataSourceOptions,
|
||||
synchronize: true,
|
||||
};
|
||||
const syncDataSource = new DataSource(syncOptions);
|
||||
await syncDataSource.initialize();
|
||||
Logger.log('数据库结构同步完成', 'Database');
|
||||
|
||||
// 关闭同步连接
|
||||
if (syncDataSource.isInitialized) {
|
||||
await syncDataSource.destroy();
|
||||
}
|
||||
|
||||
Logger.log('数据库初始化成功完成', 'Database');
|
||||
} catch (error) {
|
||||
Logger.error(`数据库初始化错误: ${error.message}`, 'Database');
|
||||
if (error instanceof SyntaxError) {
|
||||
Logger.error(
|
||||
`语法错误详情: ${JSON.stringify({
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
})}`,
|
||||
'Database',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
26
service/src/modules/globalConfig/config.entity.ts
Normal file
26
service/src/modules/globalConfig/config.entity.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { BaseEntity } from 'src/common/entity/baseEntity';
|
||||
import { Column, Entity } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'config' })
|
||||
export class ConfigEntity extends BaseEntity {
|
||||
@Column({ length: 255, comment: '配置名称', nullable: true })
|
||||
configKey: string;
|
||||
|
||||
@Column({ type: 'text', comment: '配置内容', nullable: true })
|
||||
configVal: string;
|
||||
|
||||
@Column({
|
||||
default: 0,
|
||||
comment: '配置是否公开,公开内容对前端项目展示 0:不公开 1:公开',
|
||||
})
|
||||
public: number;
|
||||
|
||||
@Column({
|
||||
default: 0,
|
||||
comment: '配置是否加密,加密内容仅仅super权限可看 0:不加 1:加',
|
||||
})
|
||||
encrypt: number;
|
||||
|
||||
@Column({ default: 1, comment: '配置状态 0:关闭 1:启用' })
|
||||
status: number;
|
||||
}
|
||||
14
service/src/modules/globalConfig/dto/queryConfig.dto.ts
Normal file
14
service/src/modules/globalConfig/dto/queryConfig.dto.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { IsArray, ArrayNotEmpty } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class QueryConfigDto {
|
||||
@ApiProperty({
|
||||
example: ['siteName', 'qqNumber'],
|
||||
description: '想要查询的配置key',
|
||||
})
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@Type(() => String)
|
||||
keys: string[];
|
||||
}
|
||||
20
service/src/modules/globalConfig/dto/setConfig.dto.ts
Normal file
20
service/src/modules/globalConfig/dto/setConfig.dto.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ArrayNotEmpty, IsArray, ValidateNested } from 'class-validator';
|
||||
|
||||
interface KeyValue {
|
||||
configKey: string;
|
||||
configVal: any;
|
||||
}
|
||||
|
||||
export class SetConfigDto {
|
||||
@ApiProperty({
|
||||
example: [{ configKey: 'siteName', configVal: 'AIWeb' }],
|
||||
description: '设置配置信息',
|
||||
})
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Object)
|
||||
settings: KeyValue[];
|
||||
}
|
||||
19
service/src/modules/globalConfig/dto/setConfigCustom.dto.ts
Normal file
19
service/src/modules/globalConfig/dto/setConfigCustom.dto.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ValidateNested } from 'class-validator';
|
||||
|
||||
interface KeyValue {
|
||||
configKey: string;
|
||||
configVal: any;
|
||||
infoKey: string;
|
||||
}
|
||||
|
||||
export class SetConfigCustomDto {
|
||||
@ApiProperty({
|
||||
example: { configKey: 'siteName', configVal: 'AIWeb', infoKey: 'AIWeb' },
|
||||
description: '设置更新配置信息',
|
||||
})
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Object)
|
||||
settings: KeyValue;
|
||||
}
|
||||
50
service/src/modules/globalConfig/globalConfig.controller.ts
Normal file
50
service/src/modules/globalConfig/globalConfig.controller.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { AdminAuthGuard } from '@/common/auth/adminAuth.guard';
|
||||
import { SuperAuthGuard } from '@/common/auth/superAuth.guard';
|
||||
import { Body, Controller, Get, Post, Query, Req, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { Request } from 'express';
|
||||
import { QueryConfigDto } from './dto/queryConfig.dto';
|
||||
import { SetConfigDto } from './dto/setConfig.dto';
|
||||
import { GlobalConfigService } from './globalConfig.service';
|
||||
|
||||
@ApiTags('config')
|
||||
@Controller('config')
|
||||
export class GlobalConfigController {
|
||||
constructor(private readonly globalConfigService: GlobalConfigService) {}
|
||||
|
||||
@ApiOperation({ summary: '查询所有配置' })
|
||||
@Get('queryAll')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
queryAllConfig(@Req() req: Request) {
|
||||
return this.globalConfigService.queryAllConfig(req);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: '查询前端网站的所有配置' })
|
||||
@Get('queryFront')
|
||||
queryFrontConfig(@Query() query: any, @Req() req: Request) {
|
||||
return this.globalConfigService.queryFrontConfig(query, req);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: '查询所有配置' })
|
||||
@Post('query')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
queryConfig(@Body() body: QueryConfigDto, @Req() req: Request) {
|
||||
return this.globalConfigService.queryConfig(body, req);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: '设置配置信息' })
|
||||
@Post('set')
|
||||
@UseGuards(SuperAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
setConfig(@Body() body: SetConfigDto) {
|
||||
return this.globalConfigService.setConfig(body);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: '用户端查询公告信息' })
|
||||
@Get('notice')
|
||||
queryNotice() {
|
||||
return this.globalConfigService.queryNotice();
|
||||
}
|
||||
}
|
||||
15
service/src/modules/globalConfig/globalConfig.module.ts
Normal file
15
service/src/modules/globalConfig/globalConfig.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ChatLogEntity } from '../chatLog/chatLog.entity';
|
||||
import { ConfigEntity } from './config.entity';
|
||||
import { GlobalConfigController } from './globalConfig.controller';
|
||||
import { GlobalConfigService } from './globalConfig.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ConfigEntity, ChatLogEntity])],
|
||||
providers: [GlobalConfigService],
|
||||
controllers: [GlobalConfigController],
|
||||
exports: [GlobalConfigService],
|
||||
})
|
||||
export class GlobalConfigModule {}
|
||||
655
service/src/modules/globalConfig/globalConfig.service.ts
Normal file
655
service/src/modules/globalConfig/globalConfig.service.ts
Normal file
@@ -0,0 +1,655 @@
|
||||
import { formatUrl, hideString } from '@/common/utils';
|
||||
import { HttpException, HttpStatus, Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import axios from 'axios';
|
||||
import { Request } from 'express';
|
||||
import * as fs from 'fs';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { ChatLogEntity } from './../chatLog/chatLog.entity';
|
||||
import { ModelsService } from './../models/models.service';
|
||||
import { ConfigEntity } from './config.entity';
|
||||
import { QueryConfigDto } from './dto/queryConfig.dto';
|
||||
import { SetConfigDto } from './dto/setConfig.dto';
|
||||
const packageJsonContent = fs.readFileSync('package.json', 'utf-8');
|
||||
const packageJson = JSON.parse(packageJsonContent);
|
||||
const version = packageJson.version;
|
||||
console.log(' current use version in ------>: ', version);
|
||||
|
||||
@Injectable()
|
||||
export class GlobalConfigService implements OnModuleInit {
|
||||
constructor(
|
||||
@InjectRepository(ConfigEntity)
|
||||
private readonly configEntity: Repository<ConfigEntity>,
|
||||
@InjectRepository(ChatLogEntity)
|
||||
private readonly chatLogEntity: Repository<ChatLogEntity>,
|
||||
private readonly modelsService: ModelsService,
|
||||
) {}
|
||||
private globalConfigs: any = {};
|
||||
private wechatAccessToken: string;
|
||||
private wechatJsapiTicket: string;
|
||||
private oldWechatAccessToken: string;
|
||||
private oldWechatJsapiTicket: string;
|
||||
|
||||
async onModuleInit() {
|
||||
await this.initGetAllConfig();
|
||||
}
|
||||
|
||||
/* 对外提供给其他service */
|
||||
async getConfigs(configKey: string[]) {
|
||||
if (configKey.length === 0) return;
|
||||
/* 微信token特殊处理 */
|
||||
if (configKey.includes('wechatAccessToken') && configKey.length === 1) {
|
||||
return this.wechatAccessToken;
|
||||
}
|
||||
if (configKey.includes('wechatJsapiTicket') && configKey.length === 1) {
|
||||
return this.wechatJsapiTicket;
|
||||
}
|
||||
/* 旧微信token特殊处理 */
|
||||
if (configKey.includes('oldWechatAccessToken') && configKey.length === 1) {
|
||||
return this.oldWechatAccessToken;
|
||||
}
|
||||
if (configKey.includes('oldWechatJsapiTicket') && configKey.length === 1) {
|
||||
return this.oldWechatJsapiTicket;
|
||||
}
|
||||
if (configKey.length === 1) {
|
||||
return this.globalConfigs[configKey[0]];
|
||||
} else {
|
||||
const result = {};
|
||||
configKey.forEach(key => (result[key] = this.globalConfigs[key]));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/* 初始化查询所有config 不对外调用 */
|
||||
async initGetAllConfig() {
|
||||
const data = await this.configEntity.find();
|
||||
this.globalConfigs = data.reduce((prev, cur) => {
|
||||
prev[cur.configKey] = cur.configVal;
|
||||
return prev;
|
||||
}, {});
|
||||
this.initBaiduSensitive();
|
||||
}
|
||||
|
||||
/* 初始化百度敏感词 拿到百度的access_token isInit: 初始化报错不检测 管理端手动修改则提示 */
|
||||
async initBaiduSensitive(isInit = true) {
|
||||
const { baiduTextApiKey, baiduTextSecretKey } = await this.getConfigs([
|
||||
'baiduTextApiKey',
|
||||
'baiduTextSecretKey',
|
||||
]);
|
||||
if (!baiduTextApiKey || !baiduTextSecretKey) {
|
||||
// Logger.error('百度敏感词初始化失败,如果需要敏感检测、请前往后台系统配置!', 'GlobalConfigService');
|
||||
return;
|
||||
}
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
};
|
||||
const url = `https://aip.baidubce.com/oauth/2.0/token?client_id=${baiduTextApiKey}&client_secret=${baiduTextSecretKey}&grant_type=client_credentials`;
|
||||
try {
|
||||
const response = await axios.post(url, { headers });
|
||||
this.globalConfigs.baiduTextAccessToken = response.data.access_token;
|
||||
} catch (error) {
|
||||
if (isInit) {
|
||||
// Logger.error('百度敏感词配置检测失败,您的参数可能配置的不正确!', 'GlobalConfigService');
|
||||
} else {
|
||||
throw new HttpException(error.response.data.error_description, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 定时刷新 access_token */
|
||||
async getWechatAccessToken(isInit = false) {
|
||||
const { wechatOfficialAppId: appId, wechatOfficialAppSecret: secret } = await this.getConfigs([
|
||||
'wechatOfficialAppId',
|
||||
'wechatOfficialAppSecret',
|
||||
]);
|
||||
if (!appId || !secret) {
|
||||
return Logger.error(
|
||||
'还未配置微信的appId和secret、配置后才可进行微信扫码登录!!!',
|
||||
'OfficialService',
|
||||
);
|
||||
}
|
||||
this.wechatAccessToken = await this.fetchBaseAccessToken(appId, secret, isInit);
|
||||
// this.wechatJsapiTicket = await this.fetchJsapiTicket(this.wechatAccessToken);
|
||||
Logger.log(`wechat refresh access_token ==> ${this.wechatAccessToken}`, 'OfficialService');
|
||||
}
|
||||
|
||||
/* 定时刷新旧账号的 access_token */
|
||||
async getOldWechatAccessToken(isInit = false) {
|
||||
try {
|
||||
const { oldWechatOfficialAppId: appId, oldWechatOfficialAppSecret: secret } =
|
||||
await this.getConfigs(['oldWechatOfficialAppId', 'oldWechatOfficialAppSecret']);
|
||||
|
||||
if (!appId || !secret) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (process.env.ISDEV === 'true') {
|
||||
Logger.log('开发环境下,返回空token', 'GlobalConfigService');
|
||||
return null;
|
||||
}
|
||||
// 获取旧公众号access_token
|
||||
this.oldWechatAccessToken = await this.fetchBaseAccessToken(appId, secret, isInit);
|
||||
// this.oldWechatJsapiTicket = await this.fetchJsapiTicket(this.oldWechatAccessToken);
|
||||
} catch (error) {
|
||||
// Logger.error(`获取旧公众号access_token异常: ${error.message}`, 'GlobalConfigService');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/* 获取微信access_token */
|
||||
async fetchBaseAccessToken(appId: string, secret: string, isInit = false) {
|
||||
Logger.log(
|
||||
`开始获取access_token, appId: ${appId.substring(0, 5)}..., isInit: ${isInit}`,
|
||||
'GlobalConfigService',
|
||||
);
|
||||
|
||||
// 检查开发模式标志,开发模式下也应该需要获取token
|
||||
if (process.env.ISDEV === 'true') {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const Url = formatUrl(
|
||||
process.env.weChatApiUrlToken || 'https://api.weixin.qq.com/cgi-bin/token',
|
||||
);
|
||||
const requestUrl = `${Url}?grant_type=client_credential&appid=${appId}&secret=${secret}`;
|
||||
Logger.log(`请求access_token URL: ${requestUrl}`, 'GlobalConfigService');
|
||||
|
||||
const response = await axios.get(requestUrl);
|
||||
Logger.log(`获取access_token响应: ${JSON.stringify(response.data)}`, 'GlobalConfigService');
|
||||
|
||||
const { errmsg, access_token, errcode } = response.data;
|
||||
|
||||
if (errmsg || errcode) {
|
||||
Logger.error(
|
||||
`获取access_token失败 - 错误码: ${errcode}, 错误信息: ${errmsg}`,
|
||||
'GlobalConfigService',
|
||||
);
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!access_token) {
|
||||
Logger.error('未获取到access_token', 'GlobalConfigService');
|
||||
return '';
|
||||
}
|
||||
|
||||
Logger.log(
|
||||
`成功获取access_token: ${access_token.substring(0, 10)}...`,
|
||||
'GlobalConfigService',
|
||||
);
|
||||
return access_token;
|
||||
} catch (error) {
|
||||
Logger.error(`获取access_token异常: ${error.message}`, 'GlobalConfigService');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/* 获取微信jsapi_ticket */
|
||||
async fetchJsapiTicket(accessToken: string) {
|
||||
if (process.env.ISDEV === 'true') {
|
||||
this.wechatJsapiTicket = '';
|
||||
return;
|
||||
}
|
||||
const Url = formatUrl(process.env.weChatApiUrl || 'https://api.weixin.qq.com');
|
||||
const res = await axios.get(
|
||||
`${Url}/cgi-bin/ticket/getticket?access_token=${accessToken}&type=jsapi`,
|
||||
);
|
||||
return res?.data?.ticket;
|
||||
}
|
||||
|
||||
/* 查询所有配置信息 */
|
||||
async queryAllConfig(req: Request) {
|
||||
const { role } = req.user;
|
||||
return this.globalConfigs;
|
||||
}
|
||||
|
||||
/* 前端网站的所有查阅权限的配置信息 */
|
||||
async queryFrontConfig(query, req) {
|
||||
/* 指定前端可以访问范围 */
|
||||
const allowKeys = [
|
||||
'registerSendStatus',
|
||||
'registerSendModel3Count',
|
||||
'registerSendModel4Count',
|
||||
'registerSendDrawMjCount',
|
||||
'firstRegisterSendStatus',
|
||||
'firstRegisterSendRank',
|
||||
'firstRegisterSendModel3Count',
|
||||
'firstRegisterSendModel4Count',
|
||||
'firstRegisterSendDrawMjCount',
|
||||
'clientHomePath',
|
||||
'clientLogoPath',
|
||||
'clientFaviconPath',
|
||||
'drawingStyles',
|
||||
'isUseWxLogin',
|
||||
'siteName',
|
||||
'siteUrl',
|
||||
'robotAvatar',
|
||||
'siteRobotName',
|
||||
'buyCramiAddress',
|
||||
'mindDefaultData',
|
||||
'baiduCode',
|
||||
'payEpayChannel',
|
||||
'payDuluPayChannel',
|
||||
'payMpayChannel',
|
||||
'payEpayApiPayUrl',
|
||||
'payEpayStatus',
|
||||
'payDuluPayStatus',
|
||||
'payHupiStatus',
|
||||
'payWechatStatus',
|
||||
'payDuluPayApiPayUrl',
|
||||
'payDuluPayRedirect',
|
||||
'payMpayStatus',
|
||||
'payLtzfStatus',
|
||||
'isAutoOpenNotice',
|
||||
'isShowAppCatIcon',
|
||||
'salesBaseRatio',
|
||||
'salesSeniorRatio',
|
||||
'salesAllowDrawMoney',
|
||||
'companyName',
|
||||
'filingNumber',
|
||||
'emailLoginStatus',
|
||||
'phoneLoginStatus',
|
||||
'openIdentity',
|
||||
'openPhoneValidation',
|
||||
'wechatRegisterStatus',
|
||||
'wechatSilentLoginStatus',
|
||||
'oldWechatMigrationStatus',
|
||||
'officialOldAccountSuccessText',
|
||||
'officialOldAccountFailText',
|
||||
'officialOldAccountNotFoundText',
|
||||
'signInStatus',
|
||||
'signInModel3Count',
|
||||
'signInModel4Count',
|
||||
'signInMjDrawToken',
|
||||
'appMenuHeaderTips',
|
||||
'pluginFirst',
|
||||
'mjUseBaiduFy',
|
||||
'mjHideNotBlock',
|
||||
'mjHideWorkIn',
|
||||
'isVerifyEmail',
|
||||
'showWatermark',
|
||||
'showCrami',
|
||||
'isHideTts',
|
||||
'isHideDefaultPreset',
|
||||
'isHideModel3Point',
|
||||
'isHideModel4Point',
|
||||
'isHideDrawMjPoint',
|
||||
'isHidePlugin',
|
||||
'model3Name',
|
||||
'model4Name',
|
||||
'drawMjName',
|
||||
'isModelInherited',
|
||||
'noVerifyRegister',
|
||||
'noticeInfo',
|
||||
'homeHtml',
|
||||
'isAutoOpenAgreement',
|
||||
'agreementInfo',
|
||||
'agreementTitle',
|
||||
'isEnableExternalLinks',
|
||||
'externalLinks',
|
||||
'clearCacheEnabled',
|
||||
'noticeTitle',
|
||||
'streamCacheEnabled',
|
||||
'homeWelcomeContent',
|
||||
'enableHtmlRender',
|
||||
'sideDrawingEditModel',
|
||||
];
|
||||
const data = await this.configEntity.find({
|
||||
where: { configKey: In(allowKeys) },
|
||||
});
|
||||
const { domain } = query;
|
||||
const domainDb = this.globalConfigs['domain'];
|
||||
if (domainDb !== domain) {
|
||||
this.createOrUpdate({
|
||||
configKey: `domain`,
|
||||
configVal: domain,
|
||||
status: 1,
|
||||
});
|
||||
await this.initGetAllConfig();
|
||||
}
|
||||
const publicConfig = data.reduce((prev, cur) => {
|
||||
prev[cur.configKey] = cur.configVal;
|
||||
return prev;
|
||||
}, {});
|
||||
/* 追加一些自定义的配置 */
|
||||
const { wechatOfficialAppId, wechatOfficialAppSecret } = await this.getConfigs([
|
||||
'wechatOfficialAppId',
|
||||
'wechatOfficialAppSecret',
|
||||
]);
|
||||
const isUseWxLogin = !!(wechatOfficialAppId && wechatOfficialAppSecret);
|
||||
|
||||
/* 查看是否有本机未同步数据 */
|
||||
return { ...publicConfig, isUseWxLogin };
|
||||
}
|
||||
|
||||
/* 查询配置 */
|
||||
async queryConfig(body: QueryConfigDto, req: Request) {
|
||||
const { role } = req.user;
|
||||
const { keys } = body;
|
||||
const data = await this.configEntity.find({
|
||||
where: { configKey: In(keys) },
|
||||
});
|
||||
/* 对演示账户的一些敏感配置修改处理 */
|
||||
if (role !== 'super') {
|
||||
// data = data.filter((t) => !t.configKey.includes('Key'));
|
||||
data.forEach(item => {
|
||||
if (
|
||||
item.configKey.includes('mj') ||
|
||||
item.configKey.includes('Key') ||
|
||||
item.configKey.includes('gpt') ||
|
||||
item.configKey.includes('cos') ||
|
||||
item.configKey.includes('baidu') ||
|
||||
item.configKey.includes('ali') ||
|
||||
item.configKey.includes('tencent') ||
|
||||
item.configKey.includes('pay') ||
|
||||
item.configKey.includes('wechat') ||
|
||||
item.configKey.includes('mjProxyImgUrl') ||
|
||||
item.configKey === 'openaiBaseUrl'
|
||||
) {
|
||||
/* 比较长的隐藏内容自定义 */
|
||||
const longKeys = ['payWeChatPublicKey', 'payWeChatPrivateKey'];
|
||||
if (longKeys.includes(item.configKey)) {
|
||||
return (item.configVal = hideString(item.configVal, '隐私内容、非超级管理员无权查看'));
|
||||
}
|
||||
const whiteListKey = ['payEpayStatus', 'payHupiStatus', 'mjProxy', 'payLtzfStatus'];
|
||||
if (!whiteListKey.includes(item.configKey) && !item.configKey.includes('Status')) {
|
||||
item.configVal = hideString(item.configVal);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return data.reduce((prev, cur) => {
|
||||
prev[cur.configKey] = cur.configVal;
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/* 设置配置信息 */
|
||||
async setConfig(body: SetConfigDto) {
|
||||
try {
|
||||
const { settings } = body;
|
||||
for (const item of settings) {
|
||||
await this.createOrUpdate(item);
|
||||
}
|
||||
await this.initGetAllConfig();
|
||||
const keys = settings.map(t => t.configKey);
|
||||
/* 如果修改的包含了百度云文本检测选择、则需要触发更新重新获取token */
|
||||
if (keys.includes('baiduTextApiKey') || keys.includes('baiduTextSecretKey')) {
|
||||
await this.initBaiduSensitive(false);
|
||||
}
|
||||
/* 如果变更微信配置 则需要手动刷新微信 access_token */
|
||||
if (keys.includes('wechatOfficialAppId') || keys.includes('wechatOfficialAppSecret')) {
|
||||
await this.getWechatAccessToken();
|
||||
}
|
||||
|
||||
return '设置完成!';
|
||||
} catch (error) {
|
||||
console.log('error: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
/* 创建或更新配置信息 */
|
||||
async createOrUpdate(setting) {
|
||||
/* 后期追加配置非自动化的需要手动追加为public让前端查找 */
|
||||
try {
|
||||
const { configKey, configVal, status = 1 } = setting;
|
||||
const c = await this.configEntity.findOne({ where: { configKey } });
|
||||
if (c) {
|
||||
const res = await this.configEntity.update({ configKey }, { configVal, status });
|
||||
} else {
|
||||
const save = await this.configEntity.save({
|
||||
configKey,
|
||||
configVal,
|
||||
status,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('error: ', error);
|
||||
throw new HttpException('设置配置信息错误!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
/* 查询公告信息 */
|
||||
async queryNotice() {
|
||||
return await this.getConfigs(['noticeInfo', 'noticeTitle']);
|
||||
}
|
||||
|
||||
/* 开启多个支付规则的时候 按顺序只使用一个 */
|
||||
async queryPayType() {
|
||||
const {
|
||||
payHupiStatus = 0,
|
||||
payEpayStatus = 0,
|
||||
payWechatStatus = 0,
|
||||
payMpayStatus = 0,
|
||||
payLtzfStatus = 0,
|
||||
payDuluPayStatus = 0,
|
||||
} = await this.getConfigs([
|
||||
'payHupiStatus',
|
||||
'payEpayStatus',
|
||||
'payMpayStatus',
|
||||
'payWechatStatus',
|
||||
'payLtzfStatus',
|
||||
'payDuluPayStatus',
|
||||
]);
|
||||
if (
|
||||
[
|
||||
payHupiStatus,
|
||||
payEpayStatus,
|
||||
payWechatStatus,
|
||||
payMpayStatus,
|
||||
payLtzfStatus,
|
||||
payDuluPayStatus,
|
||||
].every(status => status === 0)
|
||||
) {
|
||||
throw new HttpException('支付功能暂未开放!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
if (Number(payWechatStatus) === 1) {
|
||||
return 'wechat';
|
||||
}
|
||||
if (Number(payDuluPayStatus) === 1) {
|
||||
return 'dulu';
|
||||
}
|
||||
if (Number(payEpayStatus) === 1) {
|
||||
return 'epay';
|
||||
}
|
||||
if (Number(payMpayStatus) === 1) {
|
||||
return 'mpay';
|
||||
}
|
||||
if (Number(payHupiStatus) === 1) {
|
||||
return 'hupi';
|
||||
}
|
||||
if (Number(payLtzfStatus) === 1) {
|
||||
return 'ltzf';
|
||||
}
|
||||
}
|
||||
|
||||
/* get auth info */
|
||||
async getAuthInfo() {
|
||||
const { siteName, registerBaseUrl, domain } = await this.getConfigs([
|
||||
'siteName',
|
||||
'registerBaseUrl',
|
||||
'domain',
|
||||
]);
|
||||
return { siteName, registerBaseUrl, domain };
|
||||
}
|
||||
|
||||
/* get phone verify config */
|
||||
async getPhoneVerifyConfig() {
|
||||
const {
|
||||
phoneLoginStatus,
|
||||
aliPhoneAccessKeyId,
|
||||
aliPhoneAccessKeySecret,
|
||||
aliPhoneSignName,
|
||||
aliPhoneTemplateCode,
|
||||
} = await this.getConfigs([
|
||||
'phoneLoginStatus',
|
||||
'aliPhoneAccessKeyId',
|
||||
'aliPhoneAccessKeySecret',
|
||||
'aliPhoneSignName',
|
||||
'aliPhoneTemplateCode',
|
||||
]);
|
||||
if (Number(phoneLoginStatus) !== 1) {
|
||||
throw new HttpException('手机验证码功能暂未开放!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
return {
|
||||
accessKeyId: aliPhoneAccessKeyId,
|
||||
accessKeySecret: aliPhoneAccessKeySecret,
|
||||
SignName: aliPhoneSignName,
|
||||
TemplateCode: aliPhoneTemplateCode,
|
||||
};
|
||||
}
|
||||
|
||||
/* get namespace */
|
||||
getNamespace() {
|
||||
return process.env.NAMESPACE || 'AIWeb';
|
||||
}
|
||||
|
||||
/* 获取签名赠送额度 */
|
||||
async getSignatureGiftConfig() {
|
||||
const {
|
||||
signInStatus = 0,
|
||||
signInModel3Count = 0,
|
||||
signInModel4Count = 0,
|
||||
signInMjDrawToken = 0,
|
||||
} = await this.getConfigs([
|
||||
'signInStatus',
|
||||
'signInModel3Count',
|
||||
'signInModel4Count',
|
||||
'signInMjDrawToken',
|
||||
]);
|
||||
if (Number(signInStatus) !== 1) {
|
||||
throw new HttpException('签到功能暂未开放!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
return {
|
||||
model3Count: Number(signInModel3Count),
|
||||
model4Count: Number(signInModel4Count),
|
||||
drawMjCount: Number(signInMjDrawToken),
|
||||
};
|
||||
}
|
||||
|
||||
async auth() {
|
||||
const api = '';
|
||||
const response = await fetch(api, {});
|
||||
const responseData: any = await response.json();
|
||||
const { success = true, message } = responseData;
|
||||
|
||||
Logger.debug('感谢您使用AIWeb,祝您使用愉快~');
|
||||
}
|
||||
|
||||
/* 拿到敏感次配置 都开启优先使用百度云 */
|
||||
async getSensitiveConfig() {
|
||||
const { baiduTextStatus = 0, baiduTextAccessToken } = await this.getConfigs([
|
||||
'baiduTextStatus',
|
||||
'baiduTextAccessToken',
|
||||
]);
|
||||
if (Number(baiduTextStatus) === 1) {
|
||||
return {
|
||||
useType: 'baidu',
|
||||
baiduTextAccessToken,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* 保存旧账号的用户列表 */
|
||||
async saveOldWechatUserList(userList: any) {
|
||||
try {
|
||||
const oldWechatUserList = JSON.stringify(userList);
|
||||
await this.createOrUpdate({
|
||||
configKey: 'oldWechatUserList',
|
||||
configVal: oldWechatUserList,
|
||||
configDescribe: '账号迁移-旧账号的用户列表',
|
||||
});
|
||||
this.globalConfigs.oldWechatUserList = oldWechatUserList;
|
||||
return true;
|
||||
} catch (error) {
|
||||
Logger.error(`保存旧账号用户列表失败: ${error.message}`, 'GlobalConfigService');
|
||||
throw new HttpException('保存旧账号用户列表失败', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/* 保存OpenID映射关系 */
|
||||
async saveOpenidMapping(mappingList: any) {
|
||||
try {
|
||||
const openidMapping = JSON.stringify(mappingList);
|
||||
await this.createOrUpdate({
|
||||
configKey: 'openidMapping',
|
||||
configVal: openidMapping,
|
||||
configDescribe: '账号迁移-OpenID映射关系',
|
||||
});
|
||||
this.globalConfigs.openidMapping = openidMapping;
|
||||
return true;
|
||||
} catch (error) {
|
||||
Logger.error(`保存OpenID映射关系失败: ${error.message}`, 'GlobalConfigService');
|
||||
throw new HttpException('保存OpenID映射关系失败', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/* 获取旧账号的用户列表 */
|
||||
async getOldWechatUserList() {
|
||||
try {
|
||||
const oldWechatUserList = this.globalConfigs.oldWechatUserList;
|
||||
if (!oldWechatUserList) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(oldWechatUserList);
|
||||
} catch (error) {
|
||||
Logger.error(`获取旧账号用户列表失败: ${error.message}`, 'GlobalConfigService');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/* 获取OpenID映射关系 */
|
||||
async getOpenidMapping() {
|
||||
try {
|
||||
const openidMapping = this.globalConfigs.openidMapping;
|
||||
if (!openidMapping) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(openidMapping);
|
||||
} catch (error) {
|
||||
Logger.error(`获取OpenID映射关系失败: ${error.message}`, 'GlobalConfigService');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/* 保存UnionID映射关系 */
|
||||
async saveUnionidMapping(mappingData: any) {
|
||||
try {
|
||||
// 获取现有的映射数据
|
||||
let currentMapping = (await this.getOpenidMapping()) || {};
|
||||
|
||||
// 确保有unionid_mapping字段
|
||||
if (!currentMapping.unionid_mapping) {
|
||||
currentMapping.unionid_mapping = {};
|
||||
}
|
||||
|
||||
// 合并新的映射数据
|
||||
currentMapping.unionid_mapping = {
|
||||
...currentMapping.unionid_mapping,
|
||||
...mappingData,
|
||||
};
|
||||
|
||||
// 保存更新后的映射数据
|
||||
const openidMapping = JSON.stringify(currentMapping);
|
||||
await this.createOrUpdate({
|
||||
configKey: 'openidMapping',
|
||||
configVal: openidMapping,
|
||||
configDescribe: '账号迁移-OpenID和UnionID映射关系',
|
||||
});
|
||||
|
||||
this.globalConfigs.openidMapping = openidMapping;
|
||||
Logger.log(
|
||||
`保存了${Object.keys(mappingData).length}个UnionID映射关系`,
|
||||
'GlobalConfigService',
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
Logger.error(`保存UnionID映射关系失败: ${error.message}`, 'GlobalConfigService');
|
||||
throw new HttpException('保存UnionID映射关系失败', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
55
service/src/modules/mailer/mailer.service.ts
Normal file
55
service/src/modules/mailer/mailer.service.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import * as nodemailer from 'nodemailer';
|
||||
import { GlobalConfigService } from '../globalConfig/globalConfig.service';
|
||||
|
||||
@Injectable()
|
||||
export class MailerService {
|
||||
constructor(private globalConfigService: GlobalConfigService) {}
|
||||
|
||||
async sendMail(options: { to: string; context: any }): Promise<void> {
|
||||
try {
|
||||
const configs = await this.globalConfigService.getConfigs([
|
||||
'MAILER_HOST',
|
||||
'MAILER_PORT',
|
||||
'MAILER_USER',
|
||||
'MAILER_PASS',
|
||||
'MAILER_SECURE',
|
||||
'siteName',
|
||||
'siteUrl',
|
||||
]);
|
||||
|
||||
// 直接使用字符串拼接构建HTML邮件内容
|
||||
const html = `
|
||||
<div style="font-family: Helvetica, Arial, sans-serif; max-width: 500px; margin: auto; padding: 40px; background-color: #ffffff; border-radius: 12px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);">
|
||||
<h2 style="text-align: center; color: #111; font-weight: 400;">验证您的邮箱</h2>
|
||||
<hr style="border: none; border-top: 1px solid #eaeaea; margin: 30px 0;">
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<span style="display: inline-block; font-size: 42px; font-weight: 700; padding: 10px 20px; background-color: #f5f5f5; border-radius: 10px;">${options.context.code}</span>
|
||||
</div>
|
||||
<p style="font-size: 16px; color: #111; text-align: center; line-height: 1.5;">此验证码将在 10 分钟后失效,非本人操作请忽略。</p>
|
||||
<hr style="border: none; border-top: 1px solid #eaeaea; margin: 30px 0;">
|
||||
<p style="font-size: 14px; color: #999; text-align: center;">点击访问:<a href="${configs.siteUrl}" style="color: #007AFF; text-decoration: none;">${configs.siteName}</a></p>
|
||||
</div>`;
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: configs.MAILER_HOST,
|
||||
port: configs.MAILER_PORT,
|
||||
secure: configs.MAILER_SECURE === '1' ? true : false,
|
||||
auth: {
|
||||
user: configs.MAILER_USER,
|
||||
pass: configs.MAILER_PASS,
|
||||
},
|
||||
});
|
||||
|
||||
await transporter.sendMail({
|
||||
from: configs.MAILER_USER,
|
||||
to: options.to,
|
||||
subject: `验证码${options.context.code}`,
|
||||
html: html,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('error: ', error);
|
||||
throw new HttpException('邮件发送失败!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
service/src/modules/models/dto/queryModel.dto.ts
Normal file
33
service/src/modules/models/dto/queryModel.dto.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class QueryModelDto {
|
||||
@ApiProperty({ example: 1, description: '页码', required: true })
|
||||
page: number;
|
||||
|
||||
@ApiProperty({ example: 10, description: '数量', required: true })
|
||||
size: number;
|
||||
|
||||
@ApiProperty({ example: 1, description: '模型类型', required: true })
|
||||
keyType: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'dsadgadaorjoqm',
|
||||
description: '模型key',
|
||||
required: true,
|
||||
})
|
||||
key: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: true,
|
||||
description: '是否开启当前key对应的模型',
|
||||
required: true,
|
||||
})
|
||||
status: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'gpt-3.5',
|
||||
description: '当前key绑定的模型是多少 需要调用的模型',
|
||||
required: true,
|
||||
})
|
||||
model: string;
|
||||
}
|
||||
19
service/src/modules/models/dto/queryModelType.dto.ts
Normal file
19
service/src/modules/models/dto/queryModelType.dto.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class QueryModelTypeDto {
|
||||
@ApiProperty({ example: 1, description: '页码', required: true })
|
||||
page: number;
|
||||
|
||||
@ApiProperty({ example: 10, description: '数量', required: true })
|
||||
size: number;
|
||||
|
||||
@ApiProperty({ example: 1, description: '模型类型', required: true })
|
||||
keyType: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: true,
|
||||
description: '是否开启当前key对应的模型',
|
||||
required: true,
|
||||
})
|
||||
status: boolean;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user