261 lines
6.6 KiB
Go
261 lines
6.6 KiB
Go
package services
|
||
|
||
import (
|
||
"context"
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"fmt"
|
||
"goalfymax-admin/internal/models"
|
||
"goalfymax-admin/internal/oss"
|
||
"goalfymax-admin/internal/storage"
|
||
"strconv"
|
||
)
|
||
|
||
// UserFeedbackService 用户反馈服务
|
||
type UserFeedbackService struct {
|
||
storage *storage.UserFeedbackStorage
|
||
}
|
||
|
||
// NewUserFeedbackService 创建用户反馈服务实例
|
||
func NewUserFeedbackService(storage *storage.UserFeedbackStorage) *UserFeedbackService {
|
||
return &UserFeedbackService{storage: storage}
|
||
}
|
||
|
||
// List 获取用户反馈列表
|
||
func (s *UserFeedbackService) List(ctx context.Context, req *models.UserFeedbackListRequest) (*models.UserFeedbackListResponse, error) {
|
||
// 参数校验
|
||
if req.Page <= 0 {
|
||
req.Page = 1
|
||
}
|
||
if req.PageSize <= 0 {
|
||
req.PageSize = 10
|
||
}
|
||
if req.PageSize > 100 {
|
||
req.PageSize = 100
|
||
}
|
||
|
||
// 状态值校验
|
||
if req.Status != nil && (*req.Status < 0 || *req.Status > 1) {
|
||
return nil, fmt.Errorf("状态值无效")
|
||
}
|
||
|
||
// 用户ID校验
|
||
if req.UserID != nil && *req.UserID <= 0 {
|
||
return nil, fmt.Errorf("用户ID无效")
|
||
}
|
||
|
||
// 时间格式校验
|
||
if req.StartTime != "" {
|
||
if _, err := strconv.ParseInt(req.StartTime, 10, 64); err != nil {
|
||
// 尝试解析时间格式
|
||
if err := parseTimeString(req.StartTime); err != nil {
|
||
return nil, fmt.Errorf("开始时间格式无效")
|
||
}
|
||
}
|
||
}
|
||
if req.EndTime != "" {
|
||
if _, err := strconv.ParseInt(req.EndTime, 10, 64); err != nil {
|
||
// 尝试解析时间格式
|
||
if err := parseTimeString(req.EndTime); err != nil {
|
||
return nil, fmt.Errorf("结束时间格式无效")
|
||
}
|
||
}
|
||
}
|
||
|
||
// 调用存储层
|
||
feedbacks, total, err := s.storage.List(ctx, req)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("获取反馈列表失败: %w", err)
|
||
}
|
||
|
||
// 转换为带有可访问URL的返回结构
|
||
items := make([]models.UserFeedbackItem, 0, len(feedbacks))
|
||
for _, fb := range feedbacks {
|
||
var keys []string
|
||
if fb.FileKeys != "" {
|
||
_ = json.Unmarshal([]byte(fb.FileKeys), &keys)
|
||
}
|
||
|
||
// 直接下载文件内容并进行Base64编码
|
||
var fileContents []string
|
||
for _, k := range keys {
|
||
if k == "" {
|
||
continue
|
||
}
|
||
content, mimeType, err := oss.DownloadFileContent(ctx, k)
|
||
if err != nil {
|
||
// 记录错误,但继续处理其他文件
|
||
fmt.Printf("Error downloading file %s: %v\n", k, err)
|
||
continue
|
||
}
|
||
encodedContent := fmt.Sprintf("data:%s;base64,%s", mimeType, base64.StdEncoding.EncodeToString(content))
|
||
fileContents = append(fileContents, encodedContent)
|
||
}
|
||
|
||
items = append(items, models.UserFeedbackItem{
|
||
ID: fb.ID,
|
||
UserID: fb.UID,
|
||
Content: fb.Content,
|
||
FileKeys: keys,
|
||
FileContents: fileContents, // 返回Base64编码的内容
|
||
Status: fb.Status,
|
||
HandledBy: fb.HandledBy,
|
||
HandledAt: fb.HandledAt,
|
||
CreatedAt: fb.CreatedAt,
|
||
UpdatedAt: fb.UpdatedAt,
|
||
})
|
||
}
|
||
|
||
return &models.UserFeedbackListResponse{
|
||
List: items,
|
||
Total: total,
|
||
Page: req.Page,
|
||
PageSize: req.PageSize,
|
||
}, nil
|
||
}
|
||
|
||
// GetByID 根据ID获取用户反馈
|
||
func (s *UserFeedbackService) GetByID(ctx context.Context, id int64) (*models.UserFeedbackItem, error) {
|
||
if id <= 0 {
|
||
return nil, fmt.Errorf("反馈ID无效")
|
||
}
|
||
|
||
feedback, err := s.storage.GetByID(ctx, id)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("获取用户反馈失败: %w", err)
|
||
}
|
||
|
||
if feedback == nil {
|
||
return nil, fmt.Errorf("反馈不存在")
|
||
}
|
||
|
||
// 解析 file_keys
|
||
var keys []string
|
||
if feedback.FileKeys != "" {
|
||
_ = json.Unmarshal([]byte(feedback.FileKeys), &keys)
|
||
}
|
||
|
||
// 下载文件内容并进行Base64编码
|
||
var fileContents []string
|
||
for _, k := range keys {
|
||
if k == "" {
|
||
continue
|
||
}
|
||
content, mimeType, err := oss.DownloadFileContent(ctx, k)
|
||
if err != nil {
|
||
fmt.Printf("Error downloading file %s: %v\n", k, err)
|
||
continue
|
||
}
|
||
encodedContent := fmt.Sprintf("data:%s;base64,%s", mimeType, base64.StdEncoding.EncodeToString(content))
|
||
fileContents = append(fileContents, encodedContent)
|
||
}
|
||
|
||
return &models.UserFeedbackItem{
|
||
ID: feedback.ID,
|
||
UserID: feedback.UID,
|
||
Content: feedback.Content,
|
||
FileKeys: keys,
|
||
FileContents: fileContents,
|
||
Status: feedback.Status,
|
||
HandledBy: feedback.HandledBy,
|
||
HandledAt: feedback.HandledAt,
|
||
CreatedAt: feedback.CreatedAt,
|
||
UpdatedAt: feedback.UpdatedAt,
|
||
}, nil
|
||
}
|
||
|
||
// MarkHandled 切换处理状态(已处理/未处理)
|
||
func (s *UserFeedbackService) MarkHandled(ctx context.Context, id int64, handledBy int, note string) error {
|
||
if id <= 0 {
|
||
return fmt.Errorf("反馈ID无效")
|
||
}
|
||
|
||
// 检查反馈是否存在
|
||
feedback, err := s.storage.GetByID(ctx, id)
|
||
if err != nil {
|
||
return fmt.Errorf("获取反馈信息失败: %w", err)
|
||
}
|
||
|
||
if feedback == nil {
|
||
return fmt.Errorf("反馈不存在")
|
||
}
|
||
|
||
// 如果要标记为已处理,需要处理人ID
|
||
if feedback.Status == 0 && handledBy <= 0 {
|
||
return fmt.Errorf("处理人ID无效")
|
||
}
|
||
|
||
// 切换状态
|
||
if err := s.storage.MarkHandled(ctx, id, handledBy, note); err != nil {
|
||
return fmt.Errorf("切换状态失败: %w", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// Delete 删除用户反馈
|
||
func (s *UserFeedbackService) Delete(ctx context.Context, id int64) error {
|
||
if id <= 0 {
|
||
return fmt.Errorf("反馈ID无效")
|
||
}
|
||
|
||
// 检查反馈是否存在
|
||
feedback, err := s.storage.GetByID(ctx, id)
|
||
if err != nil {
|
||
return fmt.Errorf("获取反馈信息失败: %w", err)
|
||
}
|
||
|
||
if feedback == nil {
|
||
return fmt.Errorf("反馈不存在")
|
||
}
|
||
|
||
// 删除反馈
|
||
if err := s.storage.Delete(ctx, id); err != nil {
|
||
return fmt.Errorf("删除反馈失败: %w", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetStatistics 获取反馈统计信息
|
||
func (s *UserFeedbackService) GetStatistics(ctx context.Context) (map[string]interface{}, error) {
|
||
stats, err := s.storage.GetStatistics(ctx)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("获取统计信息失败: %w", err)
|
||
}
|
||
|
||
return stats, nil
|
||
}
|
||
|
||
// parseTimeString 解析时间字符串
|
||
func parseTimeString(timeStr string) error {
|
||
// 支持多种时间格式
|
||
formats := []string{
|
||
"2006-01-02 15:04:05",
|
||
"2006-01-02",
|
||
"2006/01/02 15:04:05",
|
||
"2006/01/02",
|
||
}
|
||
|
||
// 先检查是否是时间戳格式
|
||
if _, err := strconv.ParseInt(timeStr, 10, 64); err == nil {
|
||
return nil // 时间戳格式
|
||
}
|
||
|
||
// 检查其他时间格式
|
||
for _, format := range formats {
|
||
// 这里可以添加实际的时间解析逻辑,暂时跳过
|
||
_ = format
|
||
}
|
||
|
||
return fmt.Errorf("时间格式无效")
|
||
}
|
||
|
||
// hasHTTPPrefix 判断字符串是否为 http/https URL
|
||
func hasHTTPPrefix(s string) bool {
|
||
// 此函数在此服务中已不再需要,但保留以避免潜在编译错误,或者可以在此被移除
|
||
return false
|
||
}
|
||
|
||
// joinURL 已废弃,改为强制预签名
|