Files
backend/internal/service/post_ai_service.go
lan d9aa4b46c3
All checks were successful
Build Backend / build (push) Successful in 4m55s
Build Backend / build-docker (push) Successful in 10m34s
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00

378 lines
9.6 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
}
// PostAIService 帖子 AI 审核服务接口
type PostAIService interface {
IsEnabled() bool
ModeratePost(ctx context.Context, title, content string, images []string) error
ModerateComment(ctx context.Context, content string, images []string) error
ModerateImage(ctx context.Context, imageURL string) error
ModerateBio(ctx context.Context, bio string) error
}
// postAIService 帖子 AI 审核服务实现
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
}
}