## 核心改进 ### TTS 模块重构 - 统一 API,仅保留 Sound(ctx, text) 方法 - 优化日志,添加 [TTS] 前缀和结构化字段 - 实现互斥等待:同时只播放一个,新请求等待旧播放完成 - 响应 context 取消:超时或断开时立即停止播放 - 移除全局 context 存储,改为参数传递 - 简化实例化:New(config) 无需传入 context ### 代码质量提升 - 修复 PlayWav/PlayMP3 的死循环 bug(context 取消时缺少 return) - 修复 standby_ctrl/pause.go 的忙循环(添加 Sleep 避免CPU 100%) - 添加关键路径错误传播(only_video.go 不再忽略播放错误) - 新增 pkg/errorsx/handler.go 统一错误处理工具 ## 代码优化 - TTS 代码从 234 行精简到 166 行(减少 29%) - 移除冗余状态管理(playing 标志、等待循环) - 利用互斥锁的阻塞特性实现优雅等待 - 保持简洁易读的代码风格 ## 行为说明 ✅ 同时只能播放一个 TTS(互斥) ✅ 新请求等待当前播放完成(不打断) ✅ 响应 context 取消(超时停止) ✅ 日志完善,便于排查问题 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
120 lines
2.7 KiB
Go
120 lines
2.7 KiB
Go
package play
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"game-driver/config/game"
|
||
"game-driver/internal/middleware"
|
||
"game-driver/internal/schema"
|
||
"game-driver/leaf"
|
||
"game-driver/pkg/card_reader"
|
||
"game-driver/pkg/channel"
|
||
"game-driver/pkg/tts"
|
||
"go.uber.org/zap"
|
||
"io/fs"
|
||
"os"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
func WaitCard(ctx context.Context) leaf.HandlerFunc {
|
||
g := (game.C).(game.ConfigWait)
|
||
|
||
reader, err := card_reader.NewReader(g.Addr)
|
||
if err != nil {
|
||
if errors.Is(err, fs.ErrNotExist) {
|
||
zap.S().Errorf("读卡器串口文件不存在: %s", g.Addr)
|
||
os.Exit(1)
|
||
}
|
||
zap.S().Panicf("读卡器串口连接失败 [%T]: %q", err, err)
|
||
}
|
||
go func() {
|
||
<-ctx.Done()
|
||
_ = reader.Close()
|
||
}()
|
||
|
||
err = reader.Init()
|
||
if err != nil {
|
||
zap.S().Panicln("读卡器初始化失败", err)
|
||
}
|
||
|
||
return func(c *leaf.Context) {
|
||
payload := leaf.Value[*schema.PlayModal](c, middleware.PayloadJSONKey)
|
||
|
||
// 读取卡片等待时间
|
||
var waitCard time.Duration
|
||
if a, ok := payload.Game["wait_card"]; ok {
|
||
if v, ok := a.(float64); ok {
|
||
waitCard = time.Duration(v)
|
||
}
|
||
}
|
||
// 卡片ID预期值
|
||
var cardId string
|
||
if a, ok := payload.Game["card_id"]; ok {
|
||
if v, ok := a.(string); ok {
|
||
cardId = v
|
||
}
|
||
}
|
||
// 卡片比对成功语音内容
|
||
var cardOk string
|
||
if a, ok := payload.Game["card_ok"]; ok {
|
||
if v, ok := a.(string); ok {
|
||
cardOk = v
|
||
}
|
||
}
|
||
// 卡片比对失败语音内容
|
||
var cardError string
|
||
if a, ok := payload.Game["card_error"]; ok {
|
||
if v, ok := a.(string); ok {
|
||
cardError = v
|
||
}
|
||
}
|
||
|
||
// 等待组
|
||
var wait sync.WaitGroup
|
||
defer wait.Wait()
|
||
|
||
// 卡片信息通道
|
||
cardInfo := channel.NewClosed[string]()
|
||
defer cardInfo.Close()
|
||
|
||
// 结束信号通道
|
||
cc, cancel := context.WithCancel(context.TODO())
|
||
defer cancel()
|
||
|
||
wait.Add(1)
|
||
go func() {
|
||
defer wait.Done()
|
||
reader.OnCardInfo(cc, func(info *card_reader.CardInfo) {
|
||
cardInfo.Send(info.ID)
|
||
})
|
||
}()
|
||
|
||
// 多次读取,直到读取到正确的卡片
|
||
for isNeed := true; isNeed; {
|
||
isNeed = false
|
||
|
||
select {
|
||
case <-c.Done():
|
||
case <-time.After(waitCard * time.Second):
|
||
case id, ok := <-cardInfo.Data(): // 等待卡片插入
|
||
if ok { // 非关闭信号
|
||
// 比对卡号是否正确,不正确则重新读取
|
||
if cardId != id {
|
||
zap.S().Infof("读取到卡片数据%q,与预期卡片数据%q不一致", id, cardId)
|
||
// 播报错误提示
|
||
tts.DefaultTTS.Sound(c, cardError)
|
||
isNeed = true
|
||
break
|
||
}
|
||
// 播报恭喜语音
|
||
tts.DefaultTTS.Sound(c, cardOk)
|
||
//TODO: 打开炫酷光效,屏幕跳转恭喜页面
|
||
zap.S().Infof("读取到卡片数据%q,开始打开炫酷光效", id)
|
||
Default(c)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|