- Remove BOM from all Go source files - Standardize import ordering and whitespace across handlers and services - Replace PostgreSQL full-text search with ILIKE pattern matching in post and user repositories - Enhance JPush client with TLS transport, rate limit monitoring, and simplified API - Refactor PushService to include userID in device management methods - Add MaxDevicesPerUser limit and extract helper functions for push payload construction
368 lines
9.2 KiB
Go
368 lines
9.2 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"with_you/internal/pkg/openai"
|
|
"with_you/internal/pkg/tencent"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type PostModerationRejectedError struct {
|
|
Reason string
|
|
}
|
|
|
|
func (e *PostModerationRejectedError) Error() string {
|
|
if strings.TrimSpace(e.Reason) == "" {
|
|
return "post rejected by moderation"
|
|
}
|
|
return "post rejected by moderation: " + e.Reason
|
|
}
|
|
|
|
func (e *PostModerationRejectedError) UserMessage() string {
|
|
if strings.TrimSpace(e.Reason) == "" {
|
|
return "内容未通过审核,请修改后重试"
|
|
}
|
|
return strings.TrimSpace(e.Reason)
|
|
}
|
|
|
|
type PostModerationReviewError struct {
|
|
Reason string
|
|
}
|
|
|
|
func (e *PostModerationReviewError) Error() string {
|
|
if strings.TrimSpace(e.Reason) == "" {
|
|
return "post needs manual review"
|
|
}
|
|
return "post needs manual review: " + e.Reason
|
|
}
|
|
|
|
func (e *PostModerationReviewError) UserMessage() string {
|
|
if strings.TrimSpace(e.Reason) == "" {
|
|
return "内容需要人工复审,请耐心等待"
|
|
}
|
|
return strings.TrimSpace(e.Reason)
|
|
}
|
|
|
|
func (e *PostModerationReviewError) IsReview() bool {
|
|
return true
|
|
}
|
|
|
|
type CommentModerationRejectedError struct {
|
|
Reason string
|
|
}
|
|
|
|
func (e *CommentModerationRejectedError) Error() string {
|
|
if strings.TrimSpace(e.Reason) == "" {
|
|
return "comment rejected by moderation"
|
|
}
|
|
return "comment rejected by moderation: " + e.Reason
|
|
}
|
|
|
|
func (e *CommentModerationRejectedError) UserMessage() string {
|
|
if strings.TrimSpace(e.Reason) == "" {
|
|
return "评论未通过审核,请修改后重试"
|
|
}
|
|
return strings.TrimSpace(e.Reason)
|
|
}
|
|
|
|
type CommentModerationReviewError struct {
|
|
Reason string
|
|
}
|
|
|
|
func (e *CommentModerationReviewError) Error() string {
|
|
if strings.TrimSpace(e.Reason) == "" {
|
|
return "comment needs manual review"
|
|
}
|
|
return "comment needs manual review: " + e.Reason
|
|
}
|
|
|
|
func (e *CommentModerationReviewError) UserMessage() string {
|
|
if strings.TrimSpace(e.Reason) == "" {
|
|
return "评论需要人工复审,请耐心等待"
|
|
}
|
|
return strings.TrimSpace(e.Reason)
|
|
}
|
|
|
|
func (e *CommentModerationReviewError) IsReview() bool {
|
|
return true
|
|
}
|
|
|
|
type ImageModerationRejectedError struct {
|
|
Reason string
|
|
}
|
|
|
|
func (e *ImageModerationRejectedError) Error() string {
|
|
if strings.TrimSpace(e.Reason) == "" {
|
|
return "image rejected by moderation"
|
|
}
|
|
return "image rejected by moderation: " + e.Reason
|
|
}
|
|
|
|
func (e *ImageModerationRejectedError) UserMessage() string {
|
|
if strings.TrimSpace(e.Reason) == "" {
|
|
return "图片未通过审核,请更换后重试"
|
|
}
|
|
return strings.TrimSpace(e.Reason)
|
|
}
|
|
|
|
type ImageModerationReviewError struct {
|
|
Reason string
|
|
}
|
|
|
|
func (e *ImageModerationReviewError) Error() string {
|
|
if strings.TrimSpace(e.Reason) == "" {
|
|
return "image needs manual review"
|
|
}
|
|
return "image needs manual review: " + e.Reason
|
|
}
|
|
|
|
func (e *ImageModerationReviewError) UserMessage() string {
|
|
if strings.TrimSpace(e.Reason) == "" {
|
|
return "图片需要人工复审,请耐心等待"
|
|
}
|
|
return strings.TrimSpace(e.Reason)
|
|
}
|
|
|
|
func (e *ImageModerationReviewError) IsReview() bool {
|
|
return true
|
|
}
|
|
|
|
type BioModerationRejectedError struct {
|
|
Reason string
|
|
}
|
|
|
|
func (e *BioModerationRejectedError) Error() string {
|
|
if strings.TrimSpace(e.Reason) == "" {
|
|
return "bio rejected by moderation"
|
|
}
|
|
return "bio rejected by moderation: " + e.Reason
|
|
}
|
|
|
|
func (e *BioModerationRejectedError) UserMessage() string {
|
|
if strings.TrimSpace(e.Reason) == "" {
|
|
return "个性签名未通过审核,请修改后重试"
|
|
}
|
|
return strings.TrimSpace(e.Reason)
|
|
}
|
|
|
|
type BioModerationReviewError struct {
|
|
Reason string
|
|
}
|
|
|
|
func (e *BioModerationReviewError) Error() string {
|
|
if strings.TrimSpace(e.Reason) == "" {
|
|
return "bio needs manual review"
|
|
}
|
|
return "bio needs manual review: " + e.Reason
|
|
}
|
|
|
|
func (e *BioModerationReviewError) UserMessage() string {
|
|
if strings.TrimSpace(e.Reason) == "" {
|
|
return "个性签名需要人工复审,请耐心等待"
|
|
}
|
|
return strings.TrimSpace(e.Reason)
|
|
}
|
|
|
|
func (e *BioModerationReviewError) IsReview() bool {
|
|
return true
|
|
}
|
|
|
|
type PostAIService struct {
|
|
openAIClient openai.Client
|
|
tencentClient tencent.Client
|
|
strictMode bool
|
|
}
|
|
|
|
func NewPostAIService(openAIClient openai.Client, tencentClient tencent.Client, strictMode bool) *PostAIService {
|
|
return &PostAIService{
|
|
openAIClient: openAIClient,
|
|
tencentClient: tencentClient,
|
|
strictMode: strictMode,
|
|
}
|
|
}
|
|
|
|
func (s *PostAIService) IsEnabled() bool {
|
|
if s == nil {
|
|
return false
|
|
}
|
|
return (s.openAIClient != nil && s.openAIClient.IsEnabled()) ||
|
|
(s.tencentClient != nil && s.tencentClient.IsEnabled())
|
|
}
|
|
|
|
func (s *PostAIService) isOpenAIEnabled() bool {
|
|
return s != nil && s.openAIClient != nil && s.openAIClient.IsEnabled()
|
|
}
|
|
|
|
func (s *PostAIService) isTencentEnabled() bool {
|
|
return s != nil && s.tencentClient != nil && s.tencentClient.IsEnabled()
|
|
}
|
|
|
|
func (s *PostAIService) ModeratePost(ctx context.Context, title, content string, images []string) error {
|
|
if !s.IsEnabled() {
|
|
return nil
|
|
}
|
|
|
|
if s.isOpenAIEnabled() {
|
|
resp, err := s.openAIClient.ModeratePost(ctx, title, content, images)
|
|
if err != nil {
|
|
zap.L().Warn("AI post moderation failed, trying Tencent fallback",
|
|
zap.Error(err),
|
|
)
|
|
return s.moderateTextWithTencent(ctx, title+" "+content,
|
|
func(reason string) error { return &PostModerationRejectedError{Reason: reason} },
|
|
func(reason string) error { return &PostModerationReviewError{Reason: reason} },
|
|
)
|
|
}
|
|
|
|
switch resp.Result {
|
|
case openai.ModerationResultBlock:
|
|
return &PostModerationRejectedError{Reason: resp.Reason}
|
|
case openai.ModerationResultReview:
|
|
return &PostModerationReviewError{Reason: resp.Reason}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return s.moderateTextWithTencent(ctx, title+" "+content,
|
|
func(reason string) error { return &PostModerationRejectedError{Reason: reason} },
|
|
func(reason string) error { return &PostModerationReviewError{Reason: reason} },
|
|
)
|
|
}
|
|
|
|
func (s *PostAIService) ModerateComment(ctx context.Context, content string, images []string) error {
|
|
if !s.IsEnabled() {
|
|
return nil
|
|
}
|
|
|
|
if s.isOpenAIEnabled() {
|
|
resp, err := s.openAIClient.ModerateComment(ctx, content, images)
|
|
if err != nil {
|
|
zap.L().Warn("AI comment moderation failed, trying Tencent fallback",
|
|
zap.Error(err),
|
|
)
|
|
return s.moderateTextWithTencent(ctx, content,
|
|
func(reason string) error { return &CommentModerationRejectedError{Reason: reason} },
|
|
func(reason string) error { return &CommentModerationReviewError{Reason: reason} },
|
|
)
|
|
}
|
|
|
|
switch resp.Result {
|
|
case openai.ModerationResultBlock:
|
|
return &CommentModerationRejectedError{Reason: resp.Reason}
|
|
case openai.ModerationResultReview:
|
|
return &CommentModerationReviewError{Reason: resp.Reason}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return s.moderateTextWithTencent(ctx, content,
|
|
func(reason string) error { return &CommentModerationRejectedError{Reason: reason} },
|
|
func(reason string) error { return &CommentModerationReviewError{Reason: reason} },
|
|
)
|
|
}
|
|
|
|
func (s *PostAIService) ModerateImage(ctx context.Context, imageURL string) error {
|
|
if !s.isOpenAIEnabled() {
|
|
return nil
|
|
}
|
|
|
|
resp, err := s.openAIClient.ModerateImage(ctx, imageURL)
|
|
if err != nil {
|
|
if s.strictMode {
|
|
return err
|
|
}
|
|
zap.L().Warn("AI image moderation failed, fallback allow",
|
|
zap.Error(err),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
switch resp.Result {
|
|
case openai.ModerationResultBlock:
|
|
return &ImageModerationRejectedError{Reason: resp.Reason}
|
|
case openai.ModerationResultReview:
|
|
return &ImageModerationReviewError{Reason: resp.Reason}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (s *PostAIService) ModerateBio(ctx context.Context, bio string) error {
|
|
if !s.IsEnabled() {
|
|
return nil
|
|
}
|
|
|
|
if s.isOpenAIEnabled() {
|
|
resp, err := s.openAIClient.ModerateBio(ctx, bio)
|
|
if err != nil {
|
|
zap.L().Warn("AI bio moderation failed, trying Tencent fallback",
|
|
zap.Error(err),
|
|
)
|
|
return s.moderateTextWithTencent(ctx, bio,
|
|
func(reason string) error { return &BioModerationRejectedError{Reason: reason} },
|
|
func(reason string) error { return &BioModerationReviewError{Reason: reason} },
|
|
)
|
|
}
|
|
|
|
switch resp.Result {
|
|
case openai.ModerationResultBlock:
|
|
return &BioModerationRejectedError{Reason: resp.Reason}
|
|
case openai.ModerationResultReview:
|
|
return &BioModerationReviewError{Reason: resp.Reason}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return s.moderateTextWithTencent(ctx, bio,
|
|
func(reason string) error { return &BioModerationRejectedError{Reason: reason} },
|
|
func(reason string) error { return &BioModerationReviewError{Reason: reason} },
|
|
)
|
|
}
|
|
|
|
func (s *PostAIService) moderateTextWithTencent(
|
|
ctx context.Context,
|
|
text string,
|
|
rejectFactory func(string) error,
|
|
reviewFactory func(string) error,
|
|
) error {
|
|
if !s.isTencentEnabled() {
|
|
if s.strictMode {
|
|
return fmt.Errorf("no moderation service available")
|
|
}
|
|
zap.L().Warn("No moderation service available, fallback allow")
|
|
return nil
|
|
}
|
|
|
|
resp, err := s.tencentClient.TextModeration(ctx, text)
|
|
if err != nil {
|
|
if s.strictMode {
|
|
return err
|
|
}
|
|
zap.L().Warn("Tencent TMS moderation failed, fallback allow",
|
|
zap.Error(err),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
reason := resp.Label
|
|
if len(resp.Keywords) > 0 {
|
|
reason = resp.Label + ": " + strings.Join(resp.Keywords, ", ")
|
|
}
|
|
|
|
switch resp.Suggestion {
|
|
case tencent.SuggestionBlock:
|
|
return rejectFactory(reason)
|
|
case tencent.SuggestionReview:
|
|
return reviewFactory(reason)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|