Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
Y
yd-ai
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
xingmin
yd-ai
Commits
9e9c9872
Commit
9e9c9872
authored
Apr 23, 2026
by
zhangxingmin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
push
parent
4fdc510f
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
105 additions
and
52 deletions
+105
-52
yd-ai-api/src/main/java/com/yd/ai/api/AiApiApplication.java
+2
-0
yd-ai-api/src/main/java/com/yd/ai/api/config/AsyncConfig.java
+23
-0
yd-ai-api/src/main/java/com/yd/ai/api/controller/ApiAiStreamController.java
+37
-8
yd-ai-api/src/main/java/com/yd/ai/api/service/ApiAiStreamService.java
+4
-1
yd-ai-api/src/main/java/com/yd/ai/api/service/impl/ApiAiStreamServiceImpl.java
+39
-43
No files found.
yd-ai-api/src/main/java/com/yd/ai/api/AiApiApplication.java
View file @
9e9c9872
...
...
@@ -5,10 +5,12 @@ import org.mybatis.spring.annotation.MapperScan;
import
org.springframework.boot.autoconfigure.SpringBootApplication
;
import
org.springframework.boot.builder.SpringApplicationBuilder
;
import
org.springframework.cloud.openfeign.EnableFeignClients
;
import
org.springframework.scheduling.annotation.EnableAsync
;
@SpringBootApplication
(
scanBasePackages
=
"com.yd"
)
@MapperScan
(
"com.yd.**.dao"
)
@EnableFeignClients
(
basePackages
=
"com.yd"
)
@EnableAsync
// 开启异步执行支持
public
class
AiApiApplication
{
public
static
void
main
(
String
[]
args
)
{
...
...
yd-ai-api/src/main/java/com/yd/ai/api/config/AsyncConfig.java
0 → 100644
View file @
9e9c9872
package
com
.
yd
.
ai
.
api
.
config
;
import
org.springframework.context.annotation.Bean
;
import
org.springframework.context.annotation.Configuration
;
import
org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
;
import
java.util.concurrent.Executor
;
@Configuration
public
class
AsyncConfig
{
@Bean
(
name
=
"aiStreamExecutor"
)
public
Executor
taskExecutor
()
{
ThreadPoolTaskExecutor
executor
=
new
ThreadPoolTaskExecutor
();
executor
.
setCorePoolSize
(
5
);
// 核心线程数
executor
.
setMaxPoolSize
(
10
);
// 最大线程数
executor
.
setQueueCapacity
(
100
);
// 队列容量
executor
.
setThreadNamePrefix
(
"ai-stream-"
);
// 线程名前缀
executor
.
initialize
();
return
executor
;
}
}
\ No newline at end of file
yd-ai-api/src/main/java/com/yd/ai/api/controller/ApiAiStreamController.java
View file @
9e9c9872
package
com
.
yd
.
ai
.
api
.
controller
;
import
com.yd.ai.api.service.ApiAiStreamService
;
import
com.yd.common.utils.RedisUtil
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.http.
MediaType
;
import
org.springframework.http.
ResponseEntity
;
import
org.springframework.web.bind.annotation.*
;
import
reactor.core.publisher.Flux
;
import
java.util.HashMap
;
import
java.util.Map
;
import
java.util.UUID
;
@RestController
@RequestMapping
(
"/api/ai"
)
...
...
@@ -13,14 +17,38 @@ public class ApiAiStreamController {
@Autowired
private
ApiAiStreamService
apiAiStreamService
;
@Autowired
private
RedisUtil
redisUtil
;
private
static
final
String
REDIS_KEY_PREFIX
=
"ai:stream:"
;
private
static
final
long
EXPIRE_SECONDS
=
300
;
// 5分钟
/**
*
调用大模型接口获取AI回答(流式)
*
启动流式生成,返回 sessionId 供前端轮询
*/
@CrossOrigin
(
origins
=
"*"
)
@GetMapping
(
value
=
"/stream-sse"
,
produces
=
MediaType
.
TEXT_EVENT_STREAM_VALUE
)
public
Flux
<
String
>
streamChatSse
(
@RequestParam
String
question
)
{
return
apiAiStreamService
.
streamChatWithSensitiveCheck
(
question
)
.
onErrorResume
(
e
->
Flux
.
just
(
"系统繁忙,请稍后重试"
));
@PostMapping
(
"/start-stream"
)
public
ResponseEntity
<
Map
<
String
,
String
>>
startStream
(
@RequestParam
String
question
)
{
String
sessionId
=
UUID
.
randomUUID
().
toString
();
// 异步执行流式生成,不阻塞主线程
apiAiStreamService
.
generateAndStore
(
sessionId
,
question
);
Map
<
String
,
String
>
result
=
new
HashMap
<>();
result
.
put
(
"sessionId"
,
sessionId
);
return
ResponseEntity
.
ok
(
result
);
}
/**
* 轮询获取生成内容
*/
@GetMapping
(
"/stream-content"
)
public
ResponseEntity
<
Map
<
String
,
Object
>>
getStreamContent
(
@RequestParam
String
sessionId
)
{
String
redisKey
=
REDIS_KEY_PREFIX
+
sessionId
;
String
content
=
redisUtil
.
getCacheObject
(
redisKey
);
String
doneKey
=
redisKey
+
":done"
;
String
done
=
redisUtil
.
getCacheObject
(
doneKey
);
Map
<
String
,
Object
>
result
=
new
HashMap
<>();
result
.
put
(
"content"
,
content
!=
null
?
content
:
""
);
result
.
put
(
"finished"
,
"true"
.
equals
(
done
));
return
ResponseEntity
.
ok
(
result
);
}
}
\ No newline at end of file
yd-ai-api/src/main/java/com/yd/ai/api/service/ApiAiStreamService.java
View file @
9e9c9872
package
com
.
yd
.
ai
.
api
.
service
;
import
org.springframework.scheduling.annotation.Async
;
import
reactor.core.publisher.Flux
;
import
reactor.core.publisher.Mono
;
public
interface
ApiAiStreamService
{
Flux
<
String
>
streamChatWithSensitiveCheck
(
String
question
);
@Async
(
"aiStreamExecutor"
)
void
generateAndStore
(
String
sessionId
,
String
question
);
}
\ No newline at end of file
yd-ai-api/src/main/java/com/yd/ai/api/service/impl/ApiAiStreamServiceImpl.java
View file @
9e9c9872
...
...
@@ -13,24 +13,26 @@ import com.yd.auth.core.dto.AuthUserDto;
import
com.yd.auth.core.utils.SecurityUtil
;
import
com.yd.common.enums.ResultCode
;
import
com.yd.common.exception.BusinessException
;
import
com.yd.common.utils.RedisUtil
;
import
com.yd.notice.feign.client.ApiNotificationTaskFeignClient
;
import
com.yd.notice.feign.request.ApiSendRequest
;
import
io.reactivex.rxjava3.core.Flowable
;
import
io.reactivex.rxjava3.schedulers.Schedulers
;
// 使用 RxJava3 的 Schedulers
import
io.reactivex.rxjava3.schedulers.Schedulers
;
import
lombok.extern.slf4j.Slf4j
;
import
org.reactivestreams.Publisher
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.scheduling.annotation.Async
;
import
org.springframework.stereotype.Service
;
import
reactor.core.publisher.Flux
;
import
java.util.Arrays
;
import
java.util.concurrent.TimeUnit
;
@Slf4j
@Service
public
class
ApiAiStreamServiceImpl
implements
ApiAiStreamService
{
public
static
final
String
RESET
=
"\u001B[0m"
;
public
static
final
String
CYAN
=
"\u001B[36m"
;
public
static
final
String
GREEN
=
"\u001B[32m"
;
@Autowired
private
RedisUtil
redisUtil
;
@Autowired
private
ApiSensitiveWordDetailService
apiSensitiveWordDetailService
;
...
...
@@ -38,22 +40,27 @@ public class ApiAiStreamServiceImpl implements ApiAiStreamService {
@Autowired
private
ApiNotificationTaskFeignClient
apiNotificationTaskFeignClient
;
private
static
final
String
REDIS_KEY_PREFIX
=
"ai:stream:"
;
private
static
final
int
EXPIRE_SECONDS
=
300
;
// 改为 int 类型,与 RedisUtil 匹配
/**
*
流式对话(SSE),保留敏感词检测逻辑
*
异步生成内容并存入 Redis
*/
@Override
public
Flux
<
String
>
streamChatWithSensitiveCheck
(
String
question
)
{
// 1. 敏感词校验(与原非流式方法完全一致)
@Async
(
"aiStreamExecutor"
)
public
void
generateAndStore
(
String
sessionId
,
String
question
)
{
String
redisKey
=
REDIS_KEY_PREFIX
+
sessionId
;
// 1. 敏感词校验
try
{
apiSensitiveWordDetailService
.
checkWord
(
question
);
}
catch
(
BusinessException
e
)
{
int
code
=
e
.
getCode
();
String
finalContent
;
if
(
code
==
ResultCode
.
SENSITIVE_WORDS_EXIST
.
getCode
())
{
log
.
info
(
"检测到禁用敏感词,返回提示语"
);
return
Flux
.
just
(
"抱歉,您输入的内容包含敏感词汇,无法为您提供服务。请调整后重新提问。"
);
finalContent
=
"抱歉,您输入的内容包含敏感词汇,无法为您提供服务。请调整后重新提问。"
;
}
else
if
(
code
==
ResultCode
.
SENSITIVE_TZ_WORDS_EXIST
.
getCode
())
{
log
.
info
(
"检测到通知类型敏感词,发送企业微信通知"
);
// 发送通知
AuthUserDto
authUserDto
=
SecurityUtil
.
getCurrentLoginUser
();
String
userName
=
authUserDto
.
getUsername
();
String
params
=
"{\"customerName\":\""
+
userName
+
"\"}"
;
...
...
@@ -63,10 +70,13 @@ public class ApiAiStreamServiceImpl implements ApiAiStreamService {
request
.
setReceiver
(
"zxm|Sweet"
);
request
.
setParams
(
params
);
apiNotificationTaskFeignClient
.
send
(
request
);
// 返回特殊标记,前端识别后展示产品列表
return
Flux
.
just
(
"__SENSITIVE_NOTIFICATION__"
);
finalContent
=
"__SENSITIVE_NOTIFICATION__"
;
}
else
{
finalContent
=
"系统错误"
;
}
throw
e
;
redisUtil
.
setCacheObject
(
redisKey
,
finalContent
,
EXPIRE_SECONDS
,
TimeUnit
.
SECONDS
);
redisUtil
.
setCacheObject
(
redisKey
+
":done"
,
"true"
,
EXPIRE_SECONDS
,
TimeUnit
.
SECONDS
);
return
;
}
// 2. 正常调用大模型流式接口
...
...
@@ -85,52 +95,37 @@ public class ApiAiStreamServiceImpl implements ApiAiStreamService {
.
model
(
"qwen3-max"
)
// 使用可用的模型
.
messages
(
Arrays
.
asList
(
systemMsg
,
userMsg
))
.
resultFormat
(
GenerationParam
.
ResultFormat
.
MESSAGE
)
.
incrementalOutput
(
true
)
// 必须开启流式
.
incrementalOutput
(
true
)
.
build
();
return
Flux
.
create
(
sink
->
{
Publisher
<
GenerationResult
>
publisher
=
null
;
try
{
publisher
=
gen
.
streamCall
(
param
);
}
catch
(
NoApiKeyException
|
InputRequiredException
e
)
{
log
.
error
(
"流式调用初始化失败"
,
e
);
sink
.
error
(
e
);
return
;
}
try
{
Publisher
<
GenerationResult
>
publisher
=
gen
.
streamCall
(
param
);
Flowable
<
GenerationResult
>
flowable
=
Flowable
.
fromPublisher
(
publisher
)
.
subscribeOn
(
Schedulers
.
io
());
// 用于累计完整文本的 StringBuilder
StringBuilder
fullContent
=
new
StringBuilder
();
flowable
.
s
ubscribe
(
flowable
.
blockingS
ubscribe
(
result
->
{
String
delta
=
result
.
getOutput
().
getChoices
().
get
(
0
).
getMessage
().
getContent
();
// 拼接到累计文本中
fullContent
.
append
(
delta
);
//
将当前完整内容发送给前端
sink
.
next
(
fullContent
.
toString
()
);
//
实时写入 Redis
redisUtil
.
setCacheObject
(
redisKey
,
fullContent
.
toString
(),
EXPIRE_SECONDS
,
TimeUnit
.
SECONDS
);
},
error
->
{
log
.
error
(
"流式调用出错"
,
error
);
sink
.
error
(
error
);
redisUtil
.
setCacheObject
(
redisKey
,
"系统繁忙,请稍后重试"
,
EXPIRE_SECONDS
,
TimeUnit
.
SECONDS
);
redisUtil
.
setCacheObject
(
redisKey
+
":done"
,
"true"
,
EXPIRE_SECONDS
,
TimeUnit
.
SECONDS
);
},
()
->
{
log
.
info
(
"流式输出完成,
总长度: {}"
,
fullContent
.
length
());
sink
.
complete
(
);
log
.
info
(
"流式输出完成,
sessionId: {}, 总长度: {}"
,
sessionId
,
fullContent
.
length
());
redisUtil
.
setCacheObject
(
redisKey
+
":done"
,
"true"
,
EXPIRE_SECONDS
,
TimeUnit
.
SECONDS
);
}
);
});
}
private
static
void
printWithStyle
(
String
text
)
{
for
(
char
ch
:
text
.
toCharArray
())
{
if
(
ch
==
'|'
||
ch
==
'-'
||
ch
==
'='
)
{
System
.
out
.
print
(
CYAN
+
ch
+
RESET
);
}
else
{
System
.
out
.
print
(
GREEN
+
ch
+
RESET
);
}
}
catch
(
NoApiKeyException
|
InputRequiredException
e
)
{
log
.
error
(
"流式调用初始化失败"
,
e
);
redisUtil
.
setCacheObject
(
redisKey
,
"系统配置错误"
,
EXPIRE_SECONDS
,
TimeUnit
.
SECONDS
);
redisUtil
.
setCacheObject
(
redisKey
+
":done"
,
"true"
,
EXPIRE_SECONDS
,
TimeUnit
.
SECONDS
);
}
}
}
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment