企微通知功能对接
This commit is contained in:
@@ -52,8 +52,8 @@ mcp:
|
|||||||
headers: {}
|
headers: {}
|
||||||
|
|
||||||
wxWork:
|
wxWork:
|
||||||
# 是否启用企业微信登录能力。
|
# 是否启用企业微信能力。
|
||||||
# false 时不会初始化企业微信 SDK,/api/auth/wxwork/* 接口不可用。
|
# false 时不会初始化企业微信 SDK,登录、客服回调、应用通知都不可用。
|
||||||
enabled: false
|
enabled: false
|
||||||
# 企业微信公司 ID。
|
# 企业微信公司 ID。
|
||||||
# 例如:wwxxxxxxxxxxxxxxxx,来自企业微信管理后台。
|
# 例如:wwxxxxxxxxxxxxxxxx,来自企业微信管理后台。
|
||||||
@@ -82,3 +82,19 @@ wxWork:
|
|||||||
# 企业微信消息加解密 EncodingAESKey。
|
# 企业微信消息加解密 EncodingAESKey。
|
||||||
# 当前登录流程未使用,保留给消息回调等场景。
|
# 当前登录流程未使用,保留给消息回调等场景。
|
||||||
encodingAESKey: ""
|
encodingAESKey: ""
|
||||||
|
|
||||||
|
notify:
|
||||||
|
# 是否启用企业微信应用消息通知。
|
||||||
|
enabled: false
|
||||||
|
# 默认接收通知的成员ID列表;如业务目标用户已绑定企业微信身份,会优先发给目标用户。
|
||||||
|
toUsers: []
|
||||||
|
# 默认接收通知的部门ID列表。
|
||||||
|
toParties: []
|
||||||
|
# 默认接收通知的标签ID列表。
|
||||||
|
toTags: []
|
||||||
|
# 是否发送保密消息。
|
||||||
|
safe: false
|
||||||
|
# 是否开启重复消息检查。
|
||||||
|
enableDuplicateCheck: true
|
||||||
|
# 重复消息检查时间窗口,单位秒。
|
||||||
|
duplicateCheckInterval: 1800
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ type Config struct {
|
|||||||
WxWork WxWorkConfig `yaml:"wxWork"`
|
WxWork WxWorkConfig `yaml:"wxWork"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WxWorkNotifyConfig struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
ToUsers []string `yaml:"toUsers"`
|
||||||
|
ToParties []string `yaml:"toParties"`
|
||||||
|
ToTags []string `yaml:"toTags"`
|
||||||
|
Safe bool `yaml:"safe"`
|
||||||
|
EnableDuplicateCheck bool `yaml:"enableDuplicateCheck"`
|
||||||
|
DuplicateCheckInterval int `yaml:"duplicateCheckInterval"`
|
||||||
|
}
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
Port int `yaml:"port"`
|
Port int `yaml:"port"`
|
||||||
}
|
}
|
||||||
@@ -140,6 +150,8 @@ type WxWorkConfig struct {
|
|||||||
// EncodingAESKey 为企业微信消息加解密密钥。
|
// EncodingAESKey 为企业微信消息加解密密钥。
|
||||||
// 当前登录流程未使用,保留给消息回调等场景。
|
// 当前登录流程未使用,保留给消息回调等场景。
|
||||||
EncodingAESKey string `yaml:"encodingAESKey"`
|
EncodingAESKey string `yaml:"encodingAESKey"`
|
||||||
|
// Notify 为企业微信应用消息通知配置。
|
||||||
|
Notify WxWorkNotifyConfig `yaml:"notify"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load(path string) (*Config, error) {
|
func Load(path string) (*Config, error) {
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ func (s *conversationDispatchService) DispatchPendingConversation(conversation *
|
|||||||
"requested_team_ids", report.RequestedTeamIDs,
|
"requested_team_ids", report.RequestedTeamIDs,
|
||||||
)
|
)
|
||||||
WsService.PublishConversationChanged(dispatched, enums.IMRealtimeEventConversationAssigned)
|
WsService.PublishConversationChanged(dispatched, enums.IMRealtimeEventConversationAssigned)
|
||||||
|
WxWorkNotifyService.NotifyConversationAssigned(dispatched.ID, dispatched.CurrentAssigneeID, "自动分配")
|
||||||
return dispatched, nil
|
return dispatched, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ func (s *conversationService) AssignConversation(req request.AssignConversationR
|
|||||||
if conversation := s.Get(req.ConversationID); conversation != nil {
|
if conversation := s.Get(req.ConversationID); conversation != nil {
|
||||||
WsService.PublishConversationChanged(conversation, enums.IMRealtimeEventConversationAssigned)
|
WsService.PublishConversationChanged(conversation, enums.IMRealtimeEventConversationAssigned)
|
||||||
}
|
}
|
||||||
|
WxWorkNotifyService.NotifyConversationAssigned(req.ConversationID, req.AssigneeID, req.Reason)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,6 +286,7 @@ func (s *conversationService) TransferConversation(conversationID, toUserID int6
|
|||||||
if conversation := s.Get(conversationID); conversation != nil {
|
if conversation := s.Get(conversationID); conversation != nil {
|
||||||
WsService.PublishConversationChanged(conversation, enums.IMRealtimeEventConversationTransferred)
|
WsService.PublishConversationChanged(conversation, enums.IMRealtimeEventConversationTransferred)
|
||||||
}
|
}
|
||||||
|
WxWorkNotifyService.NotifyConversationAssigned(conversationID, toUserID, reason)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -831,7 +831,9 @@ func (s *ticketService) CreateTicket(req request.CreateTicketRequest, operator *
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return s.Get(ticket.ID), nil
|
current := s.Get(ticket.ID)
|
||||||
|
WxWorkNotifyService.NotifyTicketCreated(ticket.ID)
|
||||||
|
return current, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ticketService) CreateFromConversation(req request.CreateTicketFromConversationRequest, operator *dto.AuthPrincipal) (*models.Ticket, error) {
|
func (s *ticketService) CreateFromConversation(req request.CreateTicketFromConversationRequest, operator *dto.AuthPrincipal) (*models.Ticket, error) {
|
||||||
@@ -981,9 +983,13 @@ func (s *ticketService) LinkTicketCustomer(ticketID, customerID int64, operator
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ticketService) AssignTicket(req request.AssignTicketRequest, operator *dto.AuthPrincipal) error {
|
func (s *ticketService) AssignTicket(req request.AssignTicketRequest, operator *dto.AuthPrincipal) error {
|
||||||
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
||||||
return s.assignTicketTx(ctx.Tx, req, operator)
|
return s.assignTicketTx(ctx.Tx, req, operator)
|
||||||
})
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
WxWorkNotifyService.NotifyTicketAssigned(req.TicketID, req.ToUserID, req.Reason)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ticketService) assignTicketTx(tx *gorm.DB, req request.AssignTicketRequest, operator *dto.AuthPrincipal) error {
|
func (s *ticketService) assignTicketTx(tx *gorm.DB, req request.AssignTicketRequest, operator *dto.AuthPrincipal) error {
|
||||||
|
|||||||
@@ -0,0 +1,339 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"cs-agent/internal/models"
|
||||||
|
"cs-agent/internal/pkg/config"
|
||||||
|
"cs-agent/internal/pkg/enums"
|
||||||
|
"cs-agent/internal/repositories"
|
||||||
|
"cs-agent/internal/wxwork"
|
||||||
|
|
||||||
|
"github.com/mlogclub/simple/sqls"
|
||||||
|
wxmessage "github.com/silenceper/wechat/v2/work/message"
|
||||||
|
)
|
||||||
|
|
||||||
|
var WxWorkNotifyService = newWxWorkNotifyService()
|
||||||
|
|
||||||
|
type wxWorkMessageSender interface {
|
||||||
|
SendText(request wxmessage.SendTextRequest) (*wxmessage.SendResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type wxWorkNotifyRecipients struct {
|
||||||
|
ToUsers []string
|
||||||
|
ToParties []string
|
||||||
|
ToTags []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type wxWorkNotifyService struct {
|
||||||
|
senderFactory func() (wxWorkMessageSender, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWxWorkNotifyService() *wxWorkNotifyService {
|
||||||
|
return &wxWorkNotifyService{
|
||||||
|
senderFactory: func() (wxWorkMessageSender, error) {
|
||||||
|
if !wxwork.Enabled() || wxwork.GetWorkCli() == nil {
|
||||||
|
return nil, fmt.Errorf("wxwork is not enabled")
|
||||||
|
}
|
||||||
|
return wxwork.GetWorkCli().GetMessage(), nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *wxWorkNotifyService) NotifyConversationAssigned(conversationID, assigneeID int64, reason string) {
|
||||||
|
if conversationID <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
conversation := ConversationService.Get(conversationID)
|
||||||
|
if conversation == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.sendToAssigneeOrDefault(assigneeID, "会话分配提醒", s.buildConversationAssignedBody(conversation, assigneeID, reason)); err != nil {
|
||||||
|
slog.Warn("send wxwork conversation assignment notify failed",
|
||||||
|
"conversation_id", conversationID,
|
||||||
|
"assignee_id", assigneeID,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *wxWorkNotifyService) NotifyTicketCreated(ticketID int64) {
|
||||||
|
if ticketID <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ticket := TicketService.Get(ticketID)
|
||||||
|
if ticket == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.sendToAssigneeOrDefault(ticket.CurrentAssigneeID, "工单创建提醒", s.buildTicketCreatedBody(ticket)); err != nil {
|
||||||
|
slog.Warn("send wxwork ticket created notify failed",
|
||||||
|
"ticket_id", ticketID,
|
||||||
|
"assignee_id", ticket.CurrentAssigneeID,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *wxWorkNotifyService) NotifyTicketAssigned(ticketID, assigneeID int64, reason string) {
|
||||||
|
if ticketID <= 0 || assigneeID <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ticket := TicketService.Get(ticketID)
|
||||||
|
if ticket == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.sendToAssigneeOrDefault(assigneeID, "工单指派提醒", s.buildTicketAssignedBody(ticket, assigneeID, reason)); err != nil {
|
||||||
|
slog.Warn("send wxwork ticket assigned notify failed",
|
||||||
|
"ticket_id", ticketID,
|
||||||
|
"assignee_id", assigneeID,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *wxWorkNotifyService) Enabled() bool {
|
||||||
|
if !wxwork.Enabled() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return config.Current().WxWork.Notify.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *wxWorkNotifyService) sendToAssigneeOrDefault(assigneeID int64, title, body string) error {
|
||||||
|
if !s.Enabled() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
recipients := s.resolveRecipientsByUserIDs([]int64{assigneeID})
|
||||||
|
if recipients.empty() {
|
||||||
|
recipients = s.defaultRecipients()
|
||||||
|
}
|
||||||
|
if recipients.empty() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.sendText(title, body, recipients)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *wxWorkNotifyService) sendText(title, body string, recipients wxWorkNotifyRecipients) error {
|
||||||
|
if !s.Enabled() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
content := s.buildTextContent(title, body)
|
||||||
|
if content == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sender, err := s.senderFactory()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cfg := config.Current().WxWork
|
||||||
|
req := wxmessage.SendTextRequest{
|
||||||
|
SendRequestCommon: &wxmessage.SendRequestCommon{
|
||||||
|
ToUser: strings.Join(recipients.ToUsers, "|"),
|
||||||
|
ToParty: strings.Join(recipients.ToParties, "|"),
|
||||||
|
ToTag: strings.Join(recipients.ToTags, "|"),
|
||||||
|
AgentID: strings.TrimSpace(cfg.AgentID),
|
||||||
|
Safe: boolToInt(cfg.Notify.Safe),
|
||||||
|
EnableDuplicateCheck: boolToInt(cfg.Notify.EnableDuplicateCheck),
|
||||||
|
DuplicateCheckInterval: s.normalizeDuplicateCheckInterval(cfg.Notify.DuplicateCheckInterval),
|
||||||
|
},
|
||||||
|
Text: wxmessage.TextField{Content: content},
|
||||||
|
}
|
||||||
|
_, err = sender.SendText(req)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *wxWorkNotifyService) resolveRecipientsByUserIDs(userIDs []int64) wxWorkNotifyRecipients {
|
||||||
|
userIDs = uniqueInt64s(userIDs)
|
||||||
|
if len(userIDs) == 0 {
|
||||||
|
return wxWorkNotifyRecipients{}
|
||||||
|
}
|
||||||
|
cfg := config.Current().WxWork
|
||||||
|
identities := repositories.UserIdentityRepository.Find(sqls.DB(), sqls.NewCnd().
|
||||||
|
Eq("provider", enums.ThirdProviderWxWork).
|
||||||
|
Eq("provider_corp_id", strings.TrimSpace(cfg.CorpID)).
|
||||||
|
Eq("status", enums.StatusOk).
|
||||||
|
In("user_id", userIDs).
|
||||||
|
Asc("id"))
|
||||||
|
recipients := wxWorkNotifyRecipients{}
|
||||||
|
for i := range identities {
|
||||||
|
if receiver := strings.TrimSpace(identities[i].ProviderUserID); receiver != "" {
|
||||||
|
recipients.ToUsers = append(recipients.ToUsers, receiver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recipients.ToUsers = uniqueStrings(recipients.ToUsers)
|
||||||
|
return recipients
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *wxWorkNotifyService) defaultRecipients() wxWorkNotifyRecipients {
|
||||||
|
cfg := config.Current().WxWork.Notify
|
||||||
|
return wxWorkNotifyRecipients{
|
||||||
|
ToUsers: uniqueStrings(cfg.ToUsers),
|
||||||
|
ToParties: uniqueStrings(cfg.ToParties),
|
||||||
|
ToTags: uniqueStrings(cfg.ToTags),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *wxWorkNotifyService) buildConversationAssignedBody(conversation *models.Conversation, assigneeID int64, reason string) string {
|
||||||
|
if conversation == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
lines := []string{
|
||||||
|
fmt.Sprintf("会话ID: #%d", conversation.ID),
|
||||||
|
fmt.Sprintf("会话主题: %s", defaultIfBlank(conversation.Subject, "-")),
|
||||||
|
fmt.Sprintf("接入渠道: %s", enums.GetExternalSourceLabel(conversation.ExternalSource)),
|
||||||
|
fmt.Sprintf("当前状态: %s", enums.GetIMConversationStatusLabel(conversation.Status)),
|
||||||
|
fmt.Sprintf("处理人: %s", s.resolveUserLabel(assigneeID)),
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(reason) != "" {
|
||||||
|
lines = append(lines, fmt.Sprintf("分配原因: %s", strings.TrimSpace(reason)))
|
||||||
|
}
|
||||||
|
lines = append(lines, fmt.Sprintf("时间: %s", time.Now().Format("2006-01-02 15:04:05")))
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *wxWorkNotifyService) buildTicketCreatedBody(ticket *models.Ticket) string {
|
||||||
|
if ticket == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
lines := []string{
|
||||||
|
fmt.Sprintf("工单号: %s", defaultIfBlank(ticket.TicketNo, fmt.Sprintf("#%d", ticket.ID))),
|
||||||
|
fmt.Sprintf("工单标题: %s", defaultIfBlank(ticket.Title, "-")),
|
||||||
|
fmt.Sprintf("工单来源: %s", defaultIfBlank(string(ticket.Source), "-")),
|
||||||
|
fmt.Sprintf("当前状态: %s", enums.GetTicketStatusLabel(ticket.Status)),
|
||||||
|
}
|
||||||
|
if ticket.CurrentAssigneeID > 0 {
|
||||||
|
lines = append(lines, fmt.Sprintf("处理人: %s", s.resolveUserLabel(ticket.CurrentAssigneeID)))
|
||||||
|
}
|
||||||
|
lines = append(lines, fmt.Sprintf("时间: %s", time.Now().Format("2006-01-02 15:04:05")))
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *wxWorkNotifyService) buildTicketAssignedBody(ticket *models.Ticket, assigneeID int64, reason string) string {
|
||||||
|
if ticket == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
lines := []string{
|
||||||
|
fmt.Sprintf("工单号: %s", defaultIfBlank(ticket.TicketNo, fmt.Sprintf("#%d", ticket.ID))),
|
||||||
|
fmt.Sprintf("工单标题: %s", defaultIfBlank(ticket.Title, "-")),
|
||||||
|
fmt.Sprintf("当前状态: %s", enums.GetTicketStatusLabel(ticket.Status)),
|
||||||
|
fmt.Sprintf("处理人: %s", s.resolveUserLabel(assigneeID)),
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(reason) != "" {
|
||||||
|
lines = append(lines, fmt.Sprintf("指派原因: %s", strings.TrimSpace(reason)))
|
||||||
|
}
|
||||||
|
lines = append(lines, fmt.Sprintf("时间: %s", time.Now().Format("2006-01-02 15:04:05")))
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *wxWorkNotifyService) resolveUserLabel(userID int64) string {
|
||||||
|
if userID <= 0 {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
user := UserService.Get(userID)
|
||||||
|
if user == nil {
|
||||||
|
return fmt.Sprintf("用户#%d", userID)
|
||||||
|
}
|
||||||
|
if nickname := strings.TrimSpace(user.Nickname); nickname != "" {
|
||||||
|
return nickname
|
||||||
|
}
|
||||||
|
if username := strings.TrimSpace(user.Username); username != "" {
|
||||||
|
return username
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("用户#%d", userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *wxWorkNotifyService) buildTextContent(title, body string) string {
|
||||||
|
title = strings.TrimSpace(title)
|
||||||
|
body = strings.TrimSpace(body)
|
||||||
|
switch {
|
||||||
|
case title == "" && body == "":
|
||||||
|
return ""
|
||||||
|
case title == "":
|
||||||
|
return truncateRunes(body, 1024)
|
||||||
|
case body == "":
|
||||||
|
return truncateRunes(title, 1024)
|
||||||
|
default:
|
||||||
|
return truncateRunes(title+"\n\n"+body, 1024)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *wxWorkNotifyService) normalizeDuplicateCheckInterval(value int) int {
|
||||||
|
if value <= 0 {
|
||||||
|
return 1800
|
||||||
|
}
|
||||||
|
if value > 14400 {
|
||||||
|
return 14400
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r wxWorkNotifyRecipients) empty() bool {
|
||||||
|
return len(r.ToUsers) == 0 && len(r.ToParties) == 0 && len(r.ToTags) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func uniqueStrings(values []string) []string {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ret := make([]string, 0, len(values))
|
||||||
|
seen := make(map[string]struct{}, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
item := strings.TrimSpace(value)
|
||||||
|
if item == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[item]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[item] = struct{}{}
|
||||||
|
ret = append(ret, item)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func uniqueInt64s(values []int64) []int64 {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ret := make([]int64, 0, len(values))
|
||||||
|
seen := make(map[int64]struct{}, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
if value <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[value]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[value] = struct{}{}
|
||||||
|
ret = append(ret, value)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateRunes(value string, max int) string {
|
||||||
|
if max <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
runes := []rune(strings.TrimSpace(value))
|
||||||
|
if len(runes) <= max {
|
||||||
|
return string(runes)
|
||||||
|
}
|
||||||
|
return string(runes[:max])
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultIfBlank(value, fallback string) string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolToInt(v bool) int {
|
||||||
|
if v {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"cs-agent/internal/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWxWorkNotifyBuildTextContent(t *testing.T) {
|
||||||
|
svc := newWxWorkNotifyService()
|
||||||
|
got := svc.buildTextContent("工单提醒", "这是一条测试消息")
|
||||||
|
if got != "工单提醒\n\n这是一条测试消息" {
|
||||||
|
t.Fatalf("unexpected content: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWxWorkNotifyDefaultRecipients(t *testing.T) {
|
||||||
|
config.SetCurrent(&config.Config{
|
||||||
|
WxWork: config.WxWorkConfig{
|
||||||
|
Notify: config.WxWorkNotifyConfig{
|
||||||
|
Enabled: true,
|
||||||
|
ToUsers: []string{" user_a ", "user_a", ""},
|
||||||
|
ToParties: []string{"2", "2"},
|
||||||
|
ToTags: []string{"tag-1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
svc := newWxWorkNotifyService()
|
||||||
|
recipients := svc.defaultRecipients()
|
||||||
|
if len(recipients.ToUsers) != 1 || recipients.ToUsers[0] != "user_a" {
|
||||||
|
t.Fatalf("unexpected users: %#v", recipients.ToUsers)
|
||||||
|
}
|
||||||
|
if len(recipients.ToParties) != 1 || recipients.ToParties[0] != "2" {
|
||||||
|
t.Fatalf("unexpected parties: %#v", recipients.ToParties)
|
||||||
|
}
|
||||||
|
if len(recipients.ToTags) != 1 || recipients.ToTags[0] != "tag-1" {
|
||||||
|
t.Fatalf("unexpected tags: %#v", recipients.ToTags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWxWorkNotifyNormalizeDuplicateCheckInterval(t *testing.T) {
|
||||||
|
svc := newWxWorkNotifyService()
|
||||||
|
if got := svc.normalizeDuplicateCheckInterval(0); got != 1800 {
|
||||||
|
t.Fatalf("expected default interval 1800, got %d", got)
|
||||||
|
}
|
||||||
|
if got := svc.normalizeDuplicateCheckInterval(20000); got != 14400 {
|
||||||
|
t.Fatalf("expected capped interval 14400, got %d", got)
|
||||||
|
}
|
||||||
|
if got := svc.normalizeDuplicateCheckInterval(600); got != 600 {
|
||||||
|
t.Fatalf("expected interval 600, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user