package sys import ( "context" "database/sql" "errors" "fmt" "net/url" "strconv" "strings" "time" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "hotgo/internal/consts" "hotgo/internal/dao" "hotgo/internal/library/contexts" "hotgo/internal/library/imadmin" "hotgo/internal/library/imtenant" "hotgo/internal/model/entity" "hotgo/internal/model/input/sysin" "hotgo/internal/service" ) type sSysCultivateContent struct{} func NewSysCultivateContent() *sSysCultivateContent { return &sSysCultivateContent{} } func init() { service.RegisterSysCultivateContent(NewSysCultivateContent()) } func (s *sSysCultivateContent) Dashboard(ctx context.Context, _ *sysin.CultivateDashboardInp) (*sysin.CultivateDashboardModel, error) { tenantID := cultivateTenantID(ctx) out := new(sysin.CultivateDashboardModel) db := g.DB("cultivate") queries := []struct { target *int64 sql string args []any }{ {&out.PlantCount, "SELECT COUNT(*) FROM cv_user_flower WHERE tenant_id=?", []any{tenantID}}, {&out.GrowingCount, "SELECT COUNT(*) FROM cv_user_flower WHERE tenant_id=? AND status='growing'", []any{tenantID}}, {&out.BloomCount, "SELECT COUNT(*) FROM cv_user_flower WHERE tenant_id=? AND status='bloomed'", []any{tenantID}}, {&out.HarvestedCount, "SELECT COUNT(*) FROM cv_user_flower WHERE tenant_id=? AND status='harvested'", []any{tenantID}}, {&out.RemovedCount, "SELECT COUNT(*) FROM cv_user_flower WHERE tenant_id=? AND status='removed'", []any{tenantID}}, {&out.Active7d, "SELECT COUNT(DISTINCT user_id) FROM cv_garden_active_day WHERE tenant_id=? AND biz_date>=DATE_SUB(CURDATE(),INTERVAL 6 DAY)", []any{tenantID}}, {&out.Active30d, "SELECT COUNT(DISTINCT user_id) FROM cv_garden_active_day WHERE tenant_id=? AND biz_date>=DATE_SUB(CURDATE(),INTERVAL 29 DAY)", []any{tenantID}}, {&out.PostCount, "SELECT COUNT(*) FROM cv_feed_post WHERE tenant_id=? AND status='published'", []any{tenantID}}, {&out.PublicPostCount, "SELECT COUNT(*) FROM cv_feed_post WHERE tenant_id=? AND status='published' AND visibility='public'", []any{tenantID}}, {&out.FriendsPostCount, "SELECT COUNT(*) FROM cv_feed_post WHERE tenant_id=? AND status='published' AND visibility='friends'", []any{tenantID}}, {&out.PrivatePostCount, "SELECT COUNT(*) FROM cv_feed_post WHERE tenant_id=? AND status='published' AND visibility='private'", []any{tenantID}}, {&out.CommentCount, "SELECT COUNT(*) FROM cv_feed_comment WHERE tenant_id=? AND status='published'", []any{tenantID}}, {&out.LikeCount, "SELECT COALESCE(SUM(like_count),0) FROM cv_feed_post WHERE tenant_id=?", []any{tenantID}}, {&out.TakenDownCount, "SELECT COUNT(*) FROM cv_feed_post WHERE tenant_id=? AND status='taken_down'", []any{tenantID}}, {&out.PendingAuditCount, "SELECT (SELECT COUNT(*) FROM cv_feed_post WHERE tenant_id=? AND status='reviewing')+(SELECT COUNT(*) FROM cv_feed_comment WHERE tenant_id=? AND status='reviewing')", []any{tenantID, tenantID}}, {&out.ApprovedAuditCount, "SELECT COUNT(*) FROM cv_content_audit_log WHERE tenant_id=? AND status_after='published'", []any{tenantID}}, {&out.RejectedAuditCount, "SELECT COUNT(*) FROM cv_content_audit_log WHERE tenant_id=? AND status_after='rejected'", []any{tenantID}}, {&out.PendingReportCount, "SELECT COUNT(*) FROM cv_feed_report WHERE tenant_id=? AND status='submitted'", []any{tenantID}}, {&out.ProcessingReportCount, "SELECT COUNT(*) FROM cv_feed_report WHERE tenant_id=? AND status='processing'", []any{tenantID}}, {&out.ProcessedReportCount, "SELECT COUNT(*) FROM cv_feed_report WHERE tenant_id=? AND status IN ('resolved','rejected')", []any{tenantID}}, } for _, q := range queries { value, err := db.GetValue(ctx, q.sql, q.args...) if err != nil { return nil, gerror.Wrap(err, "获取养成统计失败") } *q.target = value.Int64() } return out, nil } func (s *sSysCultivateContent) List(ctx context.Context, in *sysin.CultivateContentListInp) (list []*sysin.CultivateContentListModel, totalCount int, err error) { tenantID := cultivateTenantID(ctx) if in.ObjectType == "feed_comment" { columns := dao.FeedComment.Columns() mod := dao.FeedComment.Ctx(ctx).Where(columns.TenantId, tenantID) if in.AuthorId != "" { mod = mod.Where(columns.AuthorId, in.AuthorId) } if in.Status != "" { mod = mod.Where(columns.Status, in.Status) } if in.Keyword != "" { mod = mod.WhereLike(columns.ContentText, "%"+strings.TrimSpace(in.Keyword)+"%") } if len(in.CreatedAt) == 2 { mod = mod.WhereBetween(columns.CreatedAt, in.CreatedAt[0], in.CreatedAt[1]) } var rows []*entity.FeedComment err = mod.Page(in.Page, in.PerPage).OrderDesc(columns.Id).ScanAndCount(&rows, &totalCount, false) if err != nil { return nil, 0, gerror.Wrap(err, "获取评论审核列表失败") } list = make([]*sysin.CultivateContentListModel, 0, len(rows)) for _, row := range rows { list = append(list, &sysin.CultivateContentListModel{Id: strconv.FormatUint(row.Id, 10), ObjectType: "feed_comment", PostId: strconv.FormatUint(row.PostId, 10), AuthorId: strconv.FormatUint(row.AuthorId, 10), ContentText: row.ContentText, Status: row.Status, AuditStatus: row.AuditStatus, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}) } return } columns := dao.FeedPost.Columns() mod := dao.FeedPost.Ctx(ctx).Where(columns.TenantId, tenantID) if in.AuthorId != "" { mod = mod.Where(columns.AuthorId, in.AuthorId) } if in.Status != "" { mod = mod.Where(columns.Status, in.Status) } if in.Keyword != "" { mod = mod.WhereLike(columns.ContentText, "%"+strings.TrimSpace(in.Keyword)+"%") } if len(in.CreatedAt) == 2 { mod = mod.WhereBetween(columns.CreatedAt, in.CreatedAt[0], in.CreatedAt[1]) } var rows []*entity.FeedPost err = mod.Page(in.Page, in.PerPage).OrderDesc(columns.Id).ScanAndCount(&rows, &totalCount, false) if err != nil { return nil, 0, gerror.Wrap(err, "获取动态审核列表失败") } list = make([]*sysin.CultivateContentListModel, 0, len(rows)) for _, row := range rows { list = append(list, &sysin.CultivateContentListModel{Id: strconv.FormatUint(row.Id, 10), ObjectType: "feed_post", AuthorId: strconv.FormatUint(row.AuthorId, 10), ContentText: row.ContentText, FlowerCode: row.FlowerCode, Visibility: row.Visibility, Status: row.Status, AuditStatus: row.AuditStatus, RejectReason: row.RejectReasonCode, LikeCount: row.LikeCount, CommentCount: row.CommentCount, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}) } return } func (s *sSysCultivateContent) View(ctx context.Context, in *sysin.CultivateContentViewInp) (*sysin.CultivateContentViewModel, error) { tenantID := cultivateTenantID(ctx) base := &sysin.CultivateContentViewModel{Media: []*sysin.CultivateMediaModel{}, Audits: []*sysin.CultivateAuditModel{}, RelatedReports: []*sysin.CultivateReportListModel{}} if in.ObjectType == "feed_comment" { c := dao.FeedComment.Columns() var row entity.FeedComment if err := dao.FeedComment.Ctx(ctx).Where(g.Map{c.Id: in.ObjectId, c.TenantId: tenantID}).Scan(&row); err != nil { return nil, err } if row.Id == 0 { return nil, gerror.New("评论不存在") } base.CultivateContentListModel = sysin.CultivateContentListModel{Id: in.Id, ObjectType: in.ObjectType, PostId: strconv.FormatUint(row.PostId, 10), AuthorId: strconv.FormatUint(row.AuthorId, 10), ContentText: row.ContentText, Status: row.Status, AuditStatus: row.AuditStatus, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt} } else { c := dao.FeedPost.Columns() var row entity.FeedPost if err := dao.FeedPost.Ctx(ctx).Where(g.Map{c.Id: in.ObjectId, c.TenantId: tenantID}).Scan(&row); err != nil { return nil, err } if row.Id == 0 { return nil, gerror.New("动态不存在") } base.CultivateContentListModel = sysin.CultivateContentListModel{Id: in.Id, ObjectType: in.ObjectType, AuthorId: strconv.FormatUint(row.AuthorId, 10), ContentText: row.ContentText, FlowerCode: row.FlowerCode, Visibility: row.Visibility, Status: row.Status, AuditStatus: row.AuditStatus, RejectReason: row.RejectReasonCode, LikeCount: row.LikeCount, CommentCount: row.CommentCount, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt} mc := dao.FeedMedia.Columns() var media []*entity.FeedMedia if err := dao.FeedMedia.Ctx(ctx).Where(g.Map{mc.TenantId: tenantID, mc.PostId: in.ObjectId}).OrderAsc(mc.SortNo).Scan(&media); err != nil { return nil, err } for _, m := range media { base.Media = append(base.Media, &sysin.CultivateMediaModel{Id: strconv.FormatUint(m.Id, 10), FileId: m.FileId, ThumbnailFileId: m.ThumbnailFileId, MediaType: m.MediaType, AuditStatus: m.AuditStatus, SortNo: m.SortNo}) } } ac := dao.ContentAuditLog.Columns() var audits []*entity.ContentAuditLog if err := dao.ContentAuditLog.Ctx(ctx).Where(g.Map{ac.TenantId: tenantID, ac.ObjectType: in.ObjectType, ac.ObjectId: in.ObjectId}).OrderDesc(ac.Id).Scan(&audits); err != nil { return nil, err } for _, a := range audits { base.Audits = append(base.Audits, &sysin.CultivateAuditModel{Id: strconv.FormatUint(a.Id, 10), AuditSource: a.AuditSource, StatusBefore: a.StatusBefore, StatusAfter: a.StatusAfter, ResultCode: a.ResultCode, RiskLabels: jsonValue(a.RiskLabels), OperatorId: optionalUint(a.OperatorId), OccurredAt: a.OccurredAt}) } rc := dao.FeedReport.Columns() var reports []*entity.FeedReport if err := dao.FeedReport.Ctx(ctx).Where(g.Map{rc.TenantId: tenantID, rc.ObjectType: in.ObjectType, rc.ObjectId: in.Id}).OrderDesc(rc.Id).Scan(&reports); err == nil { for _, r := range reports { base.RelatedReports = append(base.RelatedReports, reportModel(r)) } } profiles, _ := cultivateUserProfiles(ctx, tenantID, []string{base.AuthorId}) if p := profiles[base.AuthorId]; p != nil { base.AuthorNickname = p.Nickname base.AuthorAvatarFileId = p.AvatarFileId } return base, nil } func (s *sSysCultivateContent) Review(ctx context.Context, in *sysin.CultivateContentReviewInp) error { tenantID, operatorID := cultivateTenantID(ctx), contexts.GetUserId(ctx) var authorID uint64 err := dao.FeedPost.Transaction(ctx, func(txCtx context.Context, tx gdb.TX) error { if in.ObjectType == "feed_comment" { var row entity.FeedComment c := dao.FeedComment.Columns() if e := tx.Model(dao.FeedComment.Table()).Where(g.Map{c.Id: in.ObjectId, c.TenantId: tenantID}).Scan(&row); e != nil { return e } authorID = row.AuthorId return s.reviewComment(txCtx, tx, tenantID, operatorID, in) } var row entity.FeedPost c := dao.FeedPost.Columns() if e := tx.Model(dao.FeedPost.Table()).Where(g.Map{c.Id: in.ObjectId, c.TenantId: tenantID}).Scan(&row); e != nil { return e } authorID = row.AuthorId return s.reviewPost(txCtx, tx, tenantID, operatorID, in) }) if err != nil { return err } if authorID > 0 { publishCultivateNotice(ctx, tenantID, authorID, fmt.Sprintf("admin.content.%s.%s.%s", in.ObjectType, in.Id, in.Action), "cultivate.content.reviewed", "内容审核结果", cultivateReviewSummary(in.Action), map[string]any{"type": in.ObjectType, "object_type": in.ObjectType, "object_id": in.Id}) } return nil } func (s *sSysCultivateContent) BatchReview(ctx context.Context, in *sysin.CultivateContentBatchReviewInp) (*sysin.CultivateContentBatchReviewModel, error) { result := &sysin.CultivateContentBatchReviewModel{Failures: make(map[string]string)} for _, id := range in.Ids { item := &sysin.CultivateContentReviewInp{ObjectType: in.ObjectType, Id: id, Action: in.Action, Reason: in.Reason} if err := item.Filter(ctx); err != nil { result.FailedCount++ result.Failures[id] = err.Error() continue } if err := s.Review(ctx, item); err != nil { result.FailedCount++ result.Failures[id] = err.Error() continue } result.SuccessCount++ } return result, nil } func (s *sSysCultivateContent) reviewPost(ctx context.Context, tx gdb.TX, tenantID, operatorID int64, in *sysin.CultivateContentReviewInp) error { columns := dao.FeedPost.Columns() var before entity.FeedPost err := tx.Model(dao.FeedPost.Table()).Where(g.Map{columns.Id: in.ObjectId, columns.TenantId: tenantID}).LockUpdate().Scan(&before) if err != nil && !errors.Is(err, sql.ErrNoRows) { return err } if before.Id == 0 { return gerror.New("动态不存在") } now := time.Now().UTC() data := g.Map{columns.UpdatedAt: now, columns.Version: gdb.Raw(columns.Version + "+1")} afterStatus := before.Status switch in.Action { case "approve": if before.Status != "reviewing" && before.Status != "rejected" { return gerror.New("仅待审核或已拒绝动态可审核通过") } afterStatus = "published" data[columns.Status] = afterStatus data[columns.AuditStatus] = "approved" data[columns.RejectReasonCode] = "" if before.PublishedAt == nil { data[columns.PublishedAt] = now } case "reject": if before.Status != "reviewing" { return gerror.New("仅待审核动态可拒绝") } afterStatus = "rejected" data[columns.Status] = afterStatus data[columns.AuditStatus] = "rejected" data[columns.RejectReasonCode] = in.Reason case "take_down": if before.Status != "published" { return gerror.New("仅已发布动态可下架") } afterStatus = "taken_down" data[columns.Status] = afterStatus data[columns.TakenDownAt] = now data[columns.RejectReasonCode] = in.Reason case "restore": if before.Status != "taken_down" { return gerror.New("仅已下架动态可恢复") } afterStatus = "published" data[columns.Status] = afterStatus data[columns.TakenDownAt] = nil data[columns.RejectReasonCode] = "" } if _, err = tx.Model(dao.FeedPost.Table()).Where(columns.Id, before.Id).Data(data).Update(); err != nil { return gerror.Wrap(err, "更新动态审核状态失败") } if err = writeContentAudit(ctx, tx, tenantID, operatorID, "feed_post", before.Id, before.Status, afterStatus, in.Action, now); err != nil { return err } return writeCultivateOperation(ctx, tx, tenantID, operatorID, "content."+in.Action, "feed_post", in.Id, before, data, in.Reason, now) } func (s *sSysCultivateContent) reviewComment(ctx context.Context, tx gdb.TX, tenantID, operatorID int64, in *sysin.CultivateContentReviewInp) error { columns := dao.FeedComment.Columns() var before entity.FeedComment err := tx.Model(dao.FeedComment.Table()).Where(g.Map{columns.Id: in.ObjectId, columns.TenantId: tenantID}).LockUpdate().Scan(&before) if err != nil && !errors.Is(err, sql.ErrNoRows) { return err } if before.Id == 0 { return gerror.New("评论不存在") } now := time.Now().UTC() data := g.Map{columns.UpdatedAt: now} afterStatus := before.Status counterDelta := 0 switch in.Action { case "approve": if before.Status != "reviewing" && before.Status != "rejected" { return gerror.New("仅待审核或已拒绝评论可审核通过") } afterStatus = "published" data[columns.Status] = afterStatus data[columns.AuditStatus] = "approved" data[columns.PublishedAt] = now counterDelta = 1 case "reject": if before.Status != "reviewing" { return gerror.New("仅待审核评论可拒绝") } afterStatus = "rejected" data[columns.Status] = afterStatus data[columns.AuditStatus] = "rejected" data[columns.DeleteReason] = in.Reason case "take_down": if before.Status != "published" { return gerror.New("仅已发布评论可下架") } afterStatus = "taken_down" data[columns.Status] = afterStatus data[columns.DeleteReason] = in.Reason counterDelta = -1 case "restore": if before.Status != "taken_down" { return gerror.New("仅已下架评论可恢复") } afterStatus = "published" data[columns.Status] = afterStatus data[columns.DeleteReason] = "" counterDelta = 1 } if _, err = tx.Model(dao.FeedComment.Table()).Where(columns.Id, before.Id).Data(data).Update(); err != nil { return gerror.Wrap(err, "更新评论审核状态失败") } if counterDelta != 0 { postColumns := dao.FeedPost.Columns() expression := "`comment_count` + 1" if counterDelta < 0 { expression = "GREATEST(`comment_count` - 1, 0)" } if _, err = tx.Model(dao.FeedPost.Table()).Where(g.Map{postColumns.Id: before.PostId, postColumns.TenantId: tenantID}).Data(g.Map{postColumns.CommentCount: gdb.Raw(expression), postColumns.UpdatedAt: now}).Update(); err != nil { return gerror.Wrap(err, "更新动态评论数失败") } } if err = writeContentAudit(ctx, tx, tenantID, operatorID, "feed_comment", before.Id, before.Status, afterStatus, in.Action, now); err != nil { return err } return writeCultivateOperation(ctx, tx, tenantID, operatorID, "content."+in.Action, "feed_comment", in.Id, before, data, in.Reason, now) } func writeContentAudit(ctx context.Context, tx gdb.TX, tenantID, operatorID int64, objectType string, objectID uint64, before, after, result string, now time.Time) error { id, err := imtenant.NextSnowflakeID(ctx) if err != nil { return err } columns := dao.ContentAuditLog.Columns() requestID := fmt.Sprintf("manual-%s-%d-%d", objectType, objectID, id) _, err = tx.Model(dao.ContentAuditLog.Table()).Data(g.Map{columns.Id: id, columns.TenantId: tenantID, columns.ObjectType: objectType, columns.ObjectId: objectID, columns.AuditRequestId: requestID, columns.AuditSource: "manual", columns.StatusBefore: before, columns.StatusAfter: after, columns.ResultCode: result, columns.RiskLabels: "[]", columns.OperatorId: operatorID, columns.OccurredAt: now, columns.CreatedAt: now}).Insert() return err } func (s *sSysCultivateContent) ReportList(ctx context.Context, in *sysin.CultivateReportListInp) (list []*sysin.CultivateReportListModel, totalCount int, err error) { columns := dao.FeedReport.Columns() mod := dao.FeedReport.Ctx(ctx).Where(columns.TenantId, cultivateTenantID(ctx)) if in.ObjectType != "" { mod = mod.Where(columns.ObjectType, in.ObjectType) } if in.Status != "" { mod = mod.Where(columns.Status, in.Status) } if in.ReporterId != "" { mod = mod.Where(columns.ReporterId, in.ReporterId) } if len(in.CreatedAt) == 2 { mod = mod.WhereBetween(columns.CreatedAt, in.CreatedAt[0], in.CreatedAt[1]) } var rows []*entity.FeedReport if err = mod.Page(in.Page, in.PerPage).OrderDesc(columns.Id).ScanAndCount(&rows, &totalCount, false); err != nil { return nil, 0, gerror.Wrap(err, "获取举报列表失败") } list = make([]*sysin.CultivateReportListModel, 0, len(rows)) for _, row := range rows { list = append(list, reportModel(row)) } return } func reportModel(row *entity.FeedReport) *sysin.CultivateReportListModel { return &sysin.CultivateReportListModel{Id: strconv.FormatUint(row.Id, 10), ReporterId: strconv.FormatUint(row.ReporterId, 10), ReportedUserId: strconv.FormatUint(row.ReportedUserId, 10), ObjectType: row.ObjectType, ObjectId: row.ObjectId, ReasonCode: row.ReasonCode, Description: row.Description, EvidenceFileIds: row.EvidenceFileIds, SourcePage: row.SourcePage, Status: row.Status, HandlerId: optionalUint(row.HandlerId), HandleResult: row.HandleResult, HandledAt: row.HandledAt, CreatedAt: row.CreatedAt} } func (s *sSysCultivateContent) ReportView(ctx context.Context, in *sysin.CultivateReportViewInp) (*sysin.CultivateReportViewModel, error) { tenantID := cultivateTenantID(ctx) c := dao.FeedReport.Columns() var row entity.FeedReport if err := dao.FeedReport.Ctx(ctx).Where(g.Map{c.Id: in.ReportId, c.TenantId: tenantID}).Scan(&row); err != nil { return nil, err } if row.Id == 0 { return nil, gerror.New("举报记录不存在") } out := &sysin.CultivateReportViewModel{CultivateReportListModel: *reportModel(&row), History: []*sysin.CultivateReportListModel{}} profiles, _ := cultivateUserProfiles(ctx, tenantID, []string{out.ReporterId, out.ReportedUserId}) if p := profiles[out.ReporterId]; p != nil { out.ReporterNickname = p.Nickname } if p := profiles[out.ReportedUserId]; p != nil { out.ReportedUserNickname = p.Nickname } var history []*entity.FeedReport if err := dao.FeedReport.Ctx(ctx).Where(g.Map{c.TenantId: tenantID, c.ReportedUserId: row.ReportedUserId}).OrderDesc(c.Id).Limit(20).Scan(&history); err == nil { for _, item := range history { out.History = append(out.History, reportModel(item)) } } if row.ObjectType == "feed_post" || row.ObjectType == "feed_comment" { view, err := s.View(ctx, &sysin.CultivateContentViewInp{ObjectType: row.ObjectType, Id: row.ObjectId, ObjectId: mustUint(row.ObjectId)}) if err == nil { out.TargetContent = &view.CultivateContentListModel } } return out, nil } func (s *sSysCultivateContent) ReportHandle(ctx context.Context, in *sysin.CultivateReportHandleInp) error { tenantID, operatorID := cultivateTenantID(ctx), contexts.GetUserId(ctx) columns := dao.FeedReport.Columns() var report entity.FeedReport if err := dao.FeedReport.Ctx(ctx).Where(g.Map{columns.Id: in.ReportId, columns.TenantId: tenantID}).Scan(&report); err != nil { return err } if report.Id == 0 { return gerror.New("举报记录不存在") } if report.Status == "resolved" || report.Status == "rejected" { return gerror.New("举报已办结,不能重复处理") } if in.ContentAction == "take_down" { if err := validateReportedContentCanTakeDown(ctx, tenantID, &report); err != nil { return err } } if in.UserAction != "none" && report.ReportedUserId == 0 { return gerror.New("被举报用户不存在,无法执行用户处置") } if in.UserAction != "none" { if err := governCultivateUser(ctx, tenantID, report.ReportedUserId, in.UserAction, in.Reason); err != nil { return gerror.Wrap(err, "用户联动处置失败") } } err := dao.FeedReport.Transaction(ctx, func(txCtx context.Context, tx gdb.TX) error { columns := dao.FeedReport.Columns() var before entity.FeedReport err := tx.Model(dao.FeedReport.Table()).Where(g.Map{columns.Id: in.ReportId, columns.TenantId: tenantID}).LockUpdate().Scan(&before) if err != nil && !errors.Is(err, sql.ErrNoRows) { return err } if before.Id == 0 { return gerror.New("举报记录不存在") } if before.Status == "resolved" || before.Status == "rejected" { return gerror.New("举报已办结,不能重复处理") } now := time.Now().UTC() status := in.Action if status == "resolve" { status = "resolved" } data := g.Map{columns.Status: status, columns.HandlerId: operatorID, columns.HandleResult: in.HandleResult, columns.UpdatedAt: now} if status == "resolved" || status == "rejected" { data[columns.HandledAt] = now } if in.ContentAction == "take_down" { if err = takeDownReportedContent(txCtx, tx, tenantID, operatorID, &before, in.Reason, now); err != nil { return err } } if _, err = tx.Model(dao.FeedReport.Table()).Where(columns.Id, before.Id).Data(data).Update(); err != nil { return gerror.Wrap(err, "更新举报处理状态失败") } return writeCultivateOperation(txCtx, tx, tenantID, operatorID, "report."+in.Action, "feed_report", in.Id, before, data, in.HandleResult, now) }) if err != nil { return err } if in.Action == "resolve" || in.Action == "reject" { summary := "你的举报已处理,感谢你帮助维护社区环境" if in.Action == "reject" { summary = "你的举报已完成核查,暂未发现违规" } publishCultivateNotice(ctx, tenantID, report.ReporterId, "admin.report."+in.Id, "cultivate.report.handled", "举报处理结果", summary, map[string]any{"type": "report", "report_id": in.Id}) } if in.ContentAction != "none" || in.UserAction != "none" { publishCultivateNotice(ctx, tenantID, report.ReportedUserId, "admin.report.target."+in.Id, "cultivate.governance.applied", "社区治理通知", "你的内容或账号已根据社区规则进行处置", map[string]any{"type": "report", "report_id": in.Id}) } return nil } func (s *sSysCultivateContent) LogList(ctx context.Context, in *sysin.CultivateOperationLogListInp) (list []*sysin.CultivateOperationLogListModel, totalCount int, err error) { columns := dao.AdminOperationLog.Columns() mod := dao.AdminOperationLog.Ctx(ctx).Where(columns.TenantId, cultivateTenantID(ctx)) if in.OperatorId != "" { mod = mod.Where(columns.OperatorId, in.OperatorId) } if in.OperationType != "" { mod = mod.WhereLike(columns.OperationType, "%"+strings.TrimSpace(in.OperationType)+"%") } if in.TargetType != "" { mod = mod.Where(columns.TargetType, in.TargetType) } if in.TargetId != "" { mod = mod.Where(columns.TargetId, strings.TrimSpace(in.TargetId)) } if in.RequestId != "" { mod = mod.Where(columns.RequestId, strings.TrimSpace(in.RequestId)) } if len(in.CreatedAt) == 2 { mod = mod.WhereBetween(columns.CreatedAt, in.CreatedAt[0], in.CreatedAt[1]) } var rows []*entity.AdminOperationLog if err = mod.Page(in.Page, in.PerPage).OrderDesc(columns.Id).ScanAndCount(&rows, &totalCount, false); err != nil { return nil, 0, gerror.Wrap(err, "获取操作日志失败") } list = make([]*sysin.CultivateOperationLogListModel, 0, len(rows)) for _, row := range rows { list = append(list, &sysin.CultivateOperationLogListModel{Id: strconv.FormatUint(row.Id, 10), OperatorId: strconv.FormatUint(row.OperatorId, 10), OperationType: row.OperationType, TargetType: row.TargetType, TargetId: row.TargetId, RequestId: row.RequestId, BeforeSnapshot: jsonValue(row.BeforeSnapshot), AfterSnapshot: jsonValue(row.AfterSnapshot), Reason: row.Reason, CreatedAt: row.CreatedAt}) } return } func optionalUint(value uint64) string { if value == 0 { return "" } return strconv.FormatUint(value, 10) } func jsonValue(value *gjson.Json) any { if value == nil { return nil } return value } type cultivateProfile struct { UserId int64 `json:"user_id"` Nickname string `json:"nickname"` AvatarFileId string `json:"avatar_file_id"` } func cultivateUserProfiles(ctx context.Context, tenantID int64, userIDs []string) (map[string]*cultivateProfile, error) { result := make(map[string]*cultivateProfile) ids := make([]int64, 0, len(userIDs)) for _, value := range userIDs { id, err := strconv.ParseInt(value, 10, 64) if err == nil && id > 0 { ids = append(ids, id) } } if len(ids) == 0 { return result, nil } tenant, err := resolveImAdminTenant(ctx, tenantID) if err != nil { return nil, err } var out struct { Items []*cultivateProfile `json:"items"` } path := "/internal/cultivate/tenants/" + url.PathEscape(tenant.Code) + "/users/batch" if err = imadmin.DefaultClient(ctx).InternalPost(ctx, path, map[string]any{"user_ids": ids}, &out); err != nil { return nil, err } for _, item := range out.Items { result[strconv.FormatInt(item.UserId, 10)] = item } return result, nil } func publishCultivateNotice(ctx context.Context, tenantID int64, targetUserID uint64, eventID, eventType, title, summary string, action map[string]any) { if targetUserID == 0 { return } tenant, err := resolveImAdminTenant(ctx, tenantID) if err == nil { path := "/internal/cultivate/tenants/" + url.PathEscape(tenant.Code) + "/events/publish" payload := map[string]any{"event_id": eventID, "event_type": eventType, "actor_id": contexts.GetUserId(ctx), "target_user_id": targetUserID, "title": title, "summary": summary, "action": action} err = imadmin.DefaultClient(ctx).InternalPost(ctx, path, payload, nil) } if err != nil { g.Log().Warningf(ctx, "养成通知投递失败: %v", err) } } func cultivateReviewSummary(action string) string { switch action { case "approve": return "你的内容已审核通过" case "reject": return "你的内容未通过审核,请修改后重试" case "take_down": return "你的内容已根据社区规则下架" case "restore": return "你的内容已恢复展示" default: return "你的内容审核状态已更新" } } func mustUint(value string) uint64 { id, _ := strconv.ParseUint(value, 10, 64) return id } func governCultivateUser(ctx context.Context, tenantID int64, userID uint64, action, reason string) error { tenant, err := resolveImAdminTenant(ctx, tenantID) if err != nil { return err } eventType := consts.ImAdminCommandUserFreeze if action == "ban" { eventType = consts.ImAdminCommandUserBan } path := "/internal/admin-query/tenants/" + url.PathEscape(tenant.Code) + "/users/" + strconv.FormatUint(userID, 10) + "/action" payload := map[string]any{"event_type": eventType, "actor_user_id": strconv.FormatInt(contexts.GetUserId(ctx), 10), "reason": reason} return imadmin.DefaultClient(ctx).InternalPost(ctx, path, payload, nil) } func takeDownReportedContent(ctx context.Context, tx gdb.TX, tenantID, operatorID int64, report *entity.FeedReport, reason string, now time.Time) error { id := mustUint(report.ObjectId) if id == 0 { return gerror.New("举报对象ID不正确") } if report.ObjectType == "feed_post" { columns := dao.FeedPost.Columns() var before entity.FeedPost if err := tx.Model(dao.FeedPost.Table()).Where(g.Map{columns.Id: id, columns.TenantId: tenantID}).LockUpdate().Scan(&before); err != nil { return err } if before.Id == 0 { return gerror.New("被举报动态不存在") } if before.Status != "published" { return gerror.New("仅已发布动态可联动下架") } data := g.Map{columns.Status: "taken_down", columns.TakenDownAt: now, columns.RejectReasonCode: reason, columns.UpdatedAt: now, columns.Version: gdb.Raw(columns.Version + "+1")} if _, err := tx.Model(dao.FeedPost.Table()).Where(columns.Id, id).Data(data).Update(); err != nil { return err } if err := writeContentAudit(ctx, tx, tenantID, operatorID, report.ObjectType, id, before.Status, "taken_down", "report_take_down", now); err != nil { return err } return writeCultivateOperation(ctx, tx, tenantID, operatorID, "content.report_take_down", report.ObjectType, report.ObjectId, before, data, reason, now) } if report.ObjectType == "feed_comment" { columns := dao.FeedComment.Columns() var before entity.FeedComment if err := tx.Model(dao.FeedComment.Table()).Where(g.Map{columns.Id: id, columns.TenantId: tenantID}).LockUpdate().Scan(&before); err != nil { return err } if before.Id == 0 { return gerror.New("被举报评论不存在") } if before.Status != "published" { return gerror.New("仅已发布评论可联动下架") } data := g.Map{columns.Status: "taken_down", columns.DeleteReason: reason, columns.UpdatedAt: now} if _, err := tx.Model(dao.FeedComment.Table()).Where(columns.Id, id).Data(data).Update(); err != nil { return err } postColumns := dao.FeedPost.Columns() if _, err := tx.Model(dao.FeedPost.Table()).Where(g.Map{postColumns.Id: before.PostId, postColumns.TenantId: tenantID}).Data(g.Map{postColumns.CommentCount: gdb.Raw("GREATEST(`comment_count` - 1, 0)"), postColumns.UpdatedAt: now}).Update(); err != nil { return err } if err := writeContentAudit(ctx, tx, tenantID, operatorID, report.ObjectType, id, before.Status, "taken_down", "report_take_down", now); err != nil { return err } return writeCultivateOperation(ctx, tx, tenantID, operatorID, "content.report_take_down", report.ObjectType, report.ObjectId, before, data, reason, now) } return gerror.New("该举报对象不支持内容联动下架") } func validateReportedContentCanTakeDown(ctx context.Context, tenantID int64, report *entity.FeedReport) error { id := mustUint(report.ObjectId) if id == 0 { return gerror.New("举报对象ID不正确") } switch report.ObjectType { case "feed_post": columns := dao.FeedPost.Columns() value, err := dao.FeedPost.Ctx(ctx).Where(g.Map{columns.Id: id, columns.TenantId: tenantID}).Value(columns.Status) if err != nil { return err } if value.String() != "published" { return gerror.New("仅已发布动态可联动下架") } case "feed_comment": columns := dao.FeedComment.Columns() value, err := dao.FeedComment.Ctx(ctx).Where(g.Map{columns.Id: id, columns.TenantId: tenantID}).Value(columns.Status) if err != nil { return err } if value.String() != "published" { return gerror.New("仅已发布评论可联动下架") } default: return gerror.New("该举报对象不支持内容联动下架") } return nil }