mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 03:15:04 +08:00
525 lines
18 KiB
Go
525 lines
18 KiB
Go
package services
|
||
|
||
import (
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/ctwj/urldb/db/entity"
|
||
"github.com/ctwj/urldb/utils"
|
||
"github.com/silenceper/wechat/v2/cache"
|
||
"github.com/silenceper/wechat/v2/officialaccount"
|
||
"github.com/silenceper/wechat/v2/officialaccount/config"
|
||
"github.com/silenceper/wechat/v2/officialaccount/message"
|
||
)
|
||
|
||
// loadConfig 加载微信配置
|
||
func (s *WechatBotServiceImpl) loadConfig() error {
|
||
configs, err := s.systemConfigRepo.GetOrCreateDefault()
|
||
if err != nil {
|
||
return fmt.Errorf("加载配置失败: %v", err)
|
||
}
|
||
|
||
utils.Info("[WECHAT] 从数据库加载到 %d 个配置项", len(configs))
|
||
|
||
// 初始化默认值
|
||
s.config.Enabled = false
|
||
s.config.AppID = ""
|
||
s.config.AppSecret = ""
|
||
s.config.Token = ""
|
||
s.config.EncodingAesKey = ""
|
||
s.config.WelcomeMessage = "欢迎关注老九网盘资源库!发送关键词即可搜索资源。"
|
||
s.config.AutoReplyEnabled = true
|
||
s.config.SearchLimit = 5
|
||
|
||
for _, config := range configs {
|
||
switch config.Key {
|
||
case entity.ConfigKeyWechatBotEnabled:
|
||
s.config.Enabled = config.Value == "true"
|
||
utils.Info("[WECHAT:CONFIG] 加载配置 %s = %s (Enabled: %v)", config.Key, config.Value, s.config.Enabled)
|
||
case entity.ConfigKeyWechatAppId:
|
||
s.config.AppID = config.Value
|
||
utils.Info("[WECHAT:CONFIG] 加载配置 %s = [HIDDEN]", config.Key)
|
||
case entity.ConfigKeyWechatAppSecret:
|
||
s.config.AppSecret = config.Value
|
||
utils.Info("[WECHAT:CONFIG] 加载配置 %s = [HIDDEN]", config.Key)
|
||
case entity.ConfigKeyWechatToken:
|
||
s.config.Token = config.Value
|
||
utils.Info("[WECHAT:CONFIG] 加载配置 %s = [HIDDEN]", config.Key)
|
||
case entity.ConfigKeyWechatEncodingAesKey:
|
||
s.config.EncodingAesKey = config.Value
|
||
utils.Info("[WECHAT:CONFIG] 加载配置 %s = [HIDDEN]", config.Key)
|
||
case entity.ConfigKeyWechatWelcomeMessage:
|
||
if config.Value != "" {
|
||
s.config.WelcomeMessage = config.Value
|
||
}
|
||
utils.Info("[WECHAT:CONFIG] 加载配置 %s = %s", config.Key, config.Value)
|
||
case entity.ConfigKeyWechatAutoReplyEnabled:
|
||
s.config.AutoReplyEnabled = config.Value == "true"
|
||
utils.Info("[WECHAT:CONFIG] 加载配置 %s = %s (AutoReplyEnabled: %v)", config.Key, config.Value, s.config.AutoReplyEnabled)
|
||
case entity.ConfigKeyWechatSearchLimit:
|
||
if config.Value != "" {
|
||
limit, err := strconv.Atoi(config.Value)
|
||
if err == nil && limit > 0 {
|
||
s.config.SearchLimit = limit
|
||
}
|
||
}
|
||
utils.Info("[WECHAT:CONFIG] 加载配置 %s = %s (SearchLimit: %d)", config.Key, config.Value, s.config.SearchLimit)
|
||
}
|
||
}
|
||
|
||
utils.Info("[WECHAT:SERVICE] 微信公众号机器人配置加载完成: Enabled=%v, AutoReplyEnabled=%v",
|
||
s.config.Enabled, s.config.AutoReplyEnabled)
|
||
return nil
|
||
}
|
||
|
||
// Start 启动微信公众号机器人服务
|
||
func (s *WechatBotServiceImpl) Start() error {
|
||
if s.isRunning {
|
||
utils.Info("[WECHAT:SERVICE] 微信公众号机器人服务已经在运行中")
|
||
return nil
|
||
}
|
||
|
||
// 加载配置
|
||
if err := s.loadConfig(); err != nil {
|
||
return fmt.Errorf("加载配置失败: %v", err)
|
||
}
|
||
|
||
if !s.config.Enabled || s.config.AppID == "" || s.config.AppSecret == "" {
|
||
utils.Info("[WECHAT:SERVICE] 微信公众号机器人未启用或配置不完整")
|
||
return nil
|
||
}
|
||
|
||
// 创建微信客户端
|
||
cfg := &config.Config{
|
||
AppID: s.config.AppID,
|
||
AppSecret: s.config.AppSecret,
|
||
Token: s.config.Token,
|
||
EncodingAESKey: s.config.EncodingAesKey,
|
||
Cache: cache.NewMemory(),
|
||
}
|
||
s.wechatClient = officialaccount.NewOfficialAccount(cfg)
|
||
|
||
s.isRunning = true
|
||
utils.Info("[WECHAT:SERVICE] 微信公众号机器人服务已启动")
|
||
return nil
|
||
}
|
||
|
||
// Stop 停止微信公众号机器人服务
|
||
func (s *WechatBotServiceImpl) Stop() error {
|
||
if !s.isRunning {
|
||
return nil
|
||
}
|
||
|
||
s.isRunning = false
|
||
utils.Info("[WECHAT:SERVICE] 微信公众号机器人服务已停止")
|
||
return nil
|
||
}
|
||
|
||
// IsRunning 检查微信公众号机器人服务是否正在运行
|
||
func (s *WechatBotServiceImpl) IsRunning() bool {
|
||
return s.isRunning
|
||
}
|
||
|
||
// ReloadConfig 重新加载微信公众号机器人配置
|
||
func (s *WechatBotServiceImpl) ReloadConfig() error {
|
||
utils.Info("[WECHAT:SERVICE] 开始重新加载配置...")
|
||
|
||
// 重新加载配置
|
||
if err := s.loadConfig(); err != nil {
|
||
utils.Error("[WECHAT:SERVICE] 重新加载配置失败: %v", err)
|
||
return fmt.Errorf("重新加载配置失败: %v", err)
|
||
}
|
||
|
||
utils.Info("[WECHAT:SERVICE] 配置重新加载完成: Enabled=%v, AutoReplyEnabled=%v",
|
||
s.config.Enabled, s.config.AutoReplyEnabled)
|
||
return nil
|
||
}
|
||
|
||
// GetRuntimeStatus 获取微信公众号机器人运行时状态
|
||
func (s *WechatBotServiceImpl) GetRuntimeStatus() map[string]interface{} {
|
||
status := map[string]interface{}{
|
||
"is_running": s.IsRunning(),
|
||
"config_loaded": s.config != nil,
|
||
"app_id": s.config.AppID,
|
||
}
|
||
|
||
return status
|
||
}
|
||
|
||
// GetConfig 获取当前配置
|
||
func (s *WechatBotServiceImpl) GetConfig() *WechatBotConfig {
|
||
return s.config
|
||
}
|
||
|
||
// HandleMessage 处理微信消息
|
||
func (s *WechatBotServiceImpl) HandleMessage(msg *message.MixMessage) (interface{}, error) {
|
||
utils.Info("[WECHAT:MESSAGE] 收到消息: FromUserName=%s, MsgType=%s, Event=%s, Content=%s",
|
||
msg.FromUserName, msg.MsgType, msg.Event, msg.Content)
|
||
|
||
switch msg.MsgType {
|
||
case message.MsgTypeText:
|
||
return s.handleTextMessage(msg)
|
||
case message.MsgTypeEvent:
|
||
return s.handleEventMessage(msg)
|
||
default:
|
||
return nil, nil // 不处理其他类型消息
|
||
}
|
||
}
|
||
|
||
// handleTextMessage 处理文本消息
|
||
func (s *WechatBotServiceImpl) handleTextMessage(msg *message.MixMessage) (interface{}, error) {
|
||
utils.Debug("[WECHAT:MESSAGE] 处理文本消息 - AutoReplyEnabled: %v", s.config.AutoReplyEnabled)
|
||
if !s.config.AutoReplyEnabled {
|
||
utils.Info("[WECHAT:MESSAGE] 自动回复未启用")
|
||
return nil, nil
|
||
}
|
||
|
||
keyword := strings.TrimSpace(msg.Content)
|
||
utils.Info("[WECHAT:MESSAGE] 搜索关键词: '%s'", keyword)
|
||
|
||
// 检查是否是分页命令
|
||
if keyword == "上一页" || keyword == "prev" {
|
||
return s.handlePrevPage(string(msg.FromUserName))
|
||
}
|
||
|
||
if keyword == "下一页" || keyword == "next" {
|
||
return s.handleNextPage(string(msg.FromUserName))
|
||
}
|
||
|
||
// 检查是否是获取命令(例如:获取 1, 获取2等)
|
||
if strings.HasPrefix(keyword, "获取") || strings.HasPrefix(keyword, "get") {
|
||
return s.handleGetResource(string(msg.FromUserName), keyword)
|
||
}
|
||
|
||
if keyword == "" {
|
||
utils.Info("[WECHAT:MESSAGE] 关键词为空,返回提示消息")
|
||
return message.NewText("请输入搜索关键词"), nil
|
||
}
|
||
|
||
// 检查搜索关键词是否包含违禁词
|
||
cleanWords, err := utils.GetForbiddenWordsFromConfig(func() (string, error) {
|
||
return s.systemConfigRepo.GetConfigValue(entity.ConfigKeyForbiddenWords)
|
||
})
|
||
if err != nil {
|
||
utils.Error("获取违禁词配置失败: %v", err)
|
||
cleanWords = []string{} // 如果获取失败,使用空列表
|
||
}
|
||
|
||
// 检查关键词是否包含违禁词
|
||
if len(cleanWords) > 0 {
|
||
containsForbidden, matchedWords := utils.CheckContainsForbiddenWords(keyword, cleanWords)
|
||
if containsForbidden {
|
||
utils.Info("[WECHAT:MESSAGE] 搜索关键词包含违禁词: %v", matchedWords)
|
||
return message.NewText("您的搜索关键词包含违禁内容,不予处理"), nil
|
||
}
|
||
}
|
||
|
||
// 搜索资源
|
||
utils.Debug("[WECHAT:MESSAGE] 开始搜索资源,限制数量: %d", s.config.SearchLimit)
|
||
resources, err := s.SearchResources(keyword)
|
||
if err != nil {
|
||
utils.Error("[WECHAT:SEARCH] 搜索失败: %v", err)
|
||
return message.NewText("搜索服务暂时不可用,请稍后重试"), nil
|
||
}
|
||
|
||
utils.Info("[WECHAT:MESSAGE] 搜索完成,找到 %d 个资源", len(resources))
|
||
if len(resources) == 0 {
|
||
utils.Info("[WECHAT:MESSAGE] 未找到相关资源,返回提示消息")
|
||
return message.NewText(fmt.Sprintf("未找到关键词\"%s\"相关的资源,请尝试其他关键词", keyword)), nil
|
||
}
|
||
|
||
// 创建搜索会话并保存第一页结果
|
||
s.searchSessionManager.CreateSession(string(msg.FromUserName), keyword, resources, 4)
|
||
pageResources := s.searchSessionManager.GetCurrentPageResources(string(msg.FromUserName))
|
||
|
||
// 格式化第一页搜索结果
|
||
resultText := s.formatSearchResultsWithPagination(keyword, pageResources, string(msg.FromUserName))
|
||
utils.Info("[WECHAT:MESSAGE] 格式化搜索结果,返回文本长度: %d", len(resultText))
|
||
return message.NewText(resultText), nil
|
||
}
|
||
|
||
// handlePrevPage 处理上一页命令
|
||
func (s *WechatBotServiceImpl) handlePrevPage(userID string) (interface{}, error) {
|
||
session := s.searchSessionManager.GetSession(userID)
|
||
if session == nil {
|
||
return message.NewText("没有找到搜索记录,请先进行搜索"), nil
|
||
}
|
||
|
||
if !s.searchSessionManager.HasPrevPage(userID) {
|
||
return message.NewText("已经是第一页了"), nil
|
||
}
|
||
|
||
prevResources := s.searchSessionManager.PrevPage(userID)
|
||
if prevResources == nil {
|
||
return message.NewText("获取上一页失败"), nil
|
||
}
|
||
|
||
currentPage, totalPages, _, _ := s.searchSessionManager.GetPageInfo(userID)
|
||
resultText := s.formatPageResources(session.Keyword, prevResources, currentPage, totalPages, userID)
|
||
return message.NewText(resultText), nil
|
||
}
|
||
|
||
// handleNextPage 处理下一页命令
|
||
func (s *WechatBotServiceImpl) handleNextPage(userID string) (interface{}, error) {
|
||
session := s.searchSessionManager.GetSession(userID)
|
||
if session == nil {
|
||
return message.NewText("没有找到搜索记录,请先进行搜索"), nil
|
||
}
|
||
|
||
if !s.searchSessionManager.HasNextPage(userID) {
|
||
return message.NewText("已经是最后一页了"), nil
|
||
}
|
||
|
||
nextResources := s.searchSessionManager.NextPage(userID)
|
||
if nextResources == nil {
|
||
return message.NewText("获取下一页失败"), nil
|
||
}
|
||
|
||
currentPage, totalPages, _, _ := s.searchSessionManager.GetPageInfo(userID)
|
||
resultText := s.formatPageResources(session.Keyword, nextResources, currentPage, totalPages, userID)
|
||
return message.NewText(resultText), nil
|
||
}
|
||
|
||
// handleGetResource 处理获取资源命令
|
||
func (s *WechatBotServiceImpl) handleGetResource(userID, command string) (interface{}, error) {
|
||
session := s.searchSessionManager.GetSession(userID)
|
||
if session == nil {
|
||
return message.NewText("没有找到搜索记录,请先进行搜索"), nil
|
||
}
|
||
|
||
// 检查是否只输入了"获取"或"get",没有指定编号
|
||
if command == "获取" || command == "get" {
|
||
return message.NewText("📌 请输入要获取的资源编号\n\n💡 提示:回复\"获取 1\"或\"get 1\"获取第一个资源的详细信息"), nil
|
||
}
|
||
|
||
// 解析命令,例如:"获取 1" 或 "get 2"
|
||
// 支持"获取4"这种没有空格的格式
|
||
var index int
|
||
_, err := fmt.Sscanf(command, "获取%d", &index)
|
||
if err != nil {
|
||
_, err = fmt.Sscanf(command, "获取 %d", &index)
|
||
if err != nil {
|
||
_, err = fmt.Sscanf(command, "get%d", &index)
|
||
if err != nil {
|
||
_, err = fmt.Sscanf(command, "get %d", &index)
|
||
if err != nil {
|
||
return message.NewText("❌ 命令格式错误\n\n📌 正确格式:\n • 获取 1\n • get 1\n • 获取1\n • get1"), nil
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if index < 1 || index > len(session.Resources) {
|
||
return message.NewText(fmt.Sprintf("❌ 资源编号超出范围\n\n📌 请输入 1-%d 之间的数字\n💡 提示:回复\"获取 %d\"获取第%d个资源", len(session.Resources), index, index)), nil
|
||
}
|
||
|
||
// 获取指定资源
|
||
resource := session.Resources[index-1]
|
||
|
||
// 格式化资源详细信息(美化输出)
|
||
var result strings.Builder
|
||
// result.WriteString(fmt.Sprintf("📌 资源详情\n\n"))
|
||
|
||
// 标题
|
||
result.WriteString(fmt.Sprintf("📌 标题: %s\n", resource.Title))
|
||
|
||
// 描述
|
||
if resource.Description != "" {
|
||
result.WriteString(fmt.Sprintf("\n📝 描述:\n %s\n", resource.Description))
|
||
}
|
||
|
||
// 文件大小
|
||
if resource.FileSize != "" {
|
||
result.WriteString(fmt.Sprintf("\n📊 大小: %s\n", resource.FileSize))
|
||
}
|
||
|
||
// 作者
|
||
if resource.Author != "" {
|
||
result.WriteString(fmt.Sprintf("\n👤 作者: %s\n", resource.Author))
|
||
}
|
||
|
||
// 分类
|
||
if resource.Category.Name != "" {
|
||
result.WriteString(fmt.Sprintf("\n📂 分类: %s\n", resource.Category.Name))
|
||
}
|
||
|
||
// 标签
|
||
if len(resource.Tags) > 0 {
|
||
result.WriteString("\n🏷️ 标签: ")
|
||
var tags []string
|
||
for _, tag := range resource.Tags {
|
||
tags = append(tags, tag.Name)
|
||
}
|
||
result.WriteString(fmt.Sprintf("%s\n", strings.Join(tags, " ")))
|
||
}
|
||
|
||
// 链接(美化)
|
||
if resource.SaveURL != "" {
|
||
result.WriteString(fmt.Sprintf("\n📥 转存链接:\n %s", resource.SaveURL))
|
||
} else if resource.URL != "" {
|
||
result.WriteString(fmt.Sprintf("\n🔗 资源链接:\n %s", resource.URL))
|
||
}
|
||
|
||
// 添加操作提示
|
||
result.WriteString(fmt.Sprintf("\n\n💡 提示:回复\"获取 %d\"可再次查看此资源", index))
|
||
|
||
return message.NewText(result.String()), nil
|
||
}
|
||
|
||
// formatSearchResultsWithPagination 格式化带分页的搜索结果
|
||
func (s *WechatBotServiceImpl) formatSearchResultsWithPagination(keyword string, resources []entity.Resource, userID string) string {
|
||
currentPage, totalPages, _, _ := s.searchSessionManager.GetPageInfo(userID)
|
||
return s.formatPageResources(keyword, resources, currentPage, totalPages, userID)
|
||
}
|
||
|
||
// formatPageResources 格式化页面资源
|
||
// 根据用户需求,搜索结果中不显示资源链接,只显示标题和描述
|
||
func (s *WechatBotServiceImpl) formatPageResources(keyword string, resources []entity.Resource, currentPage, totalPages int, userID string) string {
|
||
var result strings.Builder
|
||
result.WriteString(fmt.Sprintf("🔍 搜索\"%s\"的结果(第%d/%d页):\n\n", keyword, currentPage, totalPages))
|
||
|
||
for i, resource := range resources {
|
||
// 构建当前资源的文本表示
|
||
var resourceText strings.Builder
|
||
|
||
// 计算全局索引(当前页的第i个资源在整个结果中的位置)
|
||
globalIndex := (currentPage-1)*4 + i + 1
|
||
resourceText.WriteString(fmt.Sprintf("%d. 📌 %s\n", globalIndex, resource.Title))
|
||
|
||
if resource.Description != "" {
|
||
// 限制描述长度以避免消息过长(正确处理中文字符)
|
||
desc := resource.Description
|
||
// 将字符串转换为 rune 切片以正确处理中文字符
|
||
runes := []rune(desc)
|
||
if len(runes) > 50 {
|
||
desc = string(runes[:50]) + "..."
|
||
}
|
||
resourceText.WriteString(fmt.Sprintf(" 📝 %s\n", desc))
|
||
}
|
||
|
||
// 添加标签显示(格式:🏷️标签,空格,再接别的标签)
|
||
if len(resource.Tags) > 0 {
|
||
var tags []string
|
||
for _, tag := range resource.Tags {
|
||
tags = append(tags, "🏷️"+tag.Name)
|
||
}
|
||
// 限制标签数量以避免消息过长
|
||
if len(tags) > 5 {
|
||
tags = tags[:5]
|
||
}
|
||
resourceText.WriteString(fmt.Sprintf(" %s\n", strings.Join(tags, " ")))
|
||
}
|
||
|
||
resourceText.WriteString(fmt.Sprintf(" 👉 回复\"获取 %d\"查看详细信息\n", globalIndex))
|
||
resourceText.WriteString("\n")
|
||
|
||
// 预计算添加当前资源后的消息长度
|
||
tempMessage := result.String() + resourceText.String()
|
||
|
||
// 添加分页提示和预留空间
|
||
if currentPage > 1 || currentPage < totalPages {
|
||
tempMessage += "💡 提示:回复\""
|
||
if currentPage > 1 && currentPage < totalPages {
|
||
tempMessage += "上一页\"或\"下一页"
|
||
} else if currentPage > 1 {
|
||
tempMessage += "上一页"
|
||
} else {
|
||
tempMessage += "下一页"
|
||
}
|
||
tempMessage += "\"翻页\n"
|
||
}
|
||
|
||
// 检查添加当前资源后是否会超过微信限制
|
||
tempRunes := []rune(tempMessage)
|
||
if len(tempRunes) > 550 {
|
||
result.WriteString("💡 内容较多,请翻页查看更多\n")
|
||
break
|
||
}
|
||
|
||
// 如果不会超过限制,则添加当前资源到结果中
|
||
result.WriteString(resourceText.String())
|
||
}
|
||
|
||
// 添加分页提示
|
||
var pageTips []string
|
||
if currentPage > 1 {
|
||
pageTips = append(pageTips, "上一页")
|
||
}
|
||
if currentPage < totalPages {
|
||
pageTips = append(pageTips, "下一页")
|
||
}
|
||
|
||
if len(pageTips) > 0 {
|
||
result.WriteString(fmt.Sprintf("💡 提示:回复\"%s\"翻页\n", strings.Join(pageTips, "\"或\"")))
|
||
}
|
||
|
||
// 确保消息不超过微信限制(正确处理中文字符)
|
||
message := result.String()
|
||
// 将字符串转换为 rune 切片以正确处理中文字符
|
||
runes := []rune(message)
|
||
if len(runes) > 600 {
|
||
// 如果还是超过限制,截断消息(微信建议不超过600个字符)
|
||
message = string(runes[:597]) + "..."
|
||
}
|
||
|
||
return message
|
||
}
|
||
|
||
// handleEventMessage 处理事件消息
|
||
func (s *WechatBotServiceImpl) handleEventMessage(msg *message.MixMessage) (interface{}, error) {
|
||
if msg.Event == message.EventSubscribe {
|
||
// 新用户关注
|
||
return message.NewText(s.config.WelcomeMessage), nil
|
||
}
|
||
return nil, nil
|
||
}
|
||
|
||
// SearchResources 搜索资源
|
||
func (s *WechatBotServiceImpl) SearchResources(keyword string) ([]entity.Resource, error) {
|
||
// 使用统一搜索函数(包含Meilisearch优先搜索和违禁词处理)
|
||
return UnifiedSearchResources(keyword, s.config.SearchLimit, s.systemConfigRepo, s.resourceRepo)
|
||
}
|
||
|
||
// formatSearchResults 格式化搜索结果
|
||
func (s *WechatBotServiceImpl) formatSearchResults(keyword string, resources []entity.Resource) string {
|
||
var result strings.Builder
|
||
result.WriteString(fmt.Sprintf("🔍 搜索\"%s\"的结果(共%d条):\n\n", keyword, len(resources)))
|
||
|
||
for i, resource := range resources {
|
||
result.WriteString(fmt.Sprintf("%d. %s\n", i+1, resource.Title))
|
||
if resource.Cover != "" {
|
||
result.WriteString(fmt.Sprintf(" \n", resource.Cover))
|
||
}
|
||
if resource.Description != "" {
|
||
desc := resource.Description
|
||
if len(desc) > 50 {
|
||
desc = desc[:50] + "..."
|
||
}
|
||
result.WriteString(fmt.Sprintf(" %s\n", desc))
|
||
}
|
||
if resource.SaveURL != "" {
|
||
result.WriteString(fmt.Sprintf(" 转存链接:%s\n", resource.SaveURL))
|
||
} else if resource.URL != "" {
|
||
result.WriteString(fmt.Sprintf(" 资源链接:%s\n", resource.URL))
|
||
}
|
||
result.WriteString("\n")
|
||
}
|
||
|
||
result.WriteString("💡 提示:回复资源编号可获取详细信息")
|
||
return result.String()
|
||
}
|
||
|
||
// SendWelcomeMessage 发送欢迎消息(预留接口,实际通过事件处理)
|
||
func (s *WechatBotServiceImpl) SendWelcomeMessage(openID string) error {
|
||
// 实际上欢迎消息是通过关注事件自动发送的
|
||
// 这里提供一个手动发送的接口
|
||
if !s.isRunning || s.wechatClient == nil {
|
||
return fmt.Errorf("微信客户端未初始化")
|
||
}
|
||
|
||
// 注意:Customer API 需要额外的权限,这里仅作示例
|
||
// 实际应用中可能需要使用模板消息或其他方式
|
||
return nil
|
||
}
|