diff --git a/server/api/admin/cultivatecontent/cultivate_content.go b/server/api/admin/cultivatecontent/cultivate_content.go index d0cfae7..fcf687d 100644 --- a/server/api/admin/cultivatecontent/cultivate_content.go +++ b/server/api/admin/cultivatecontent/cultivate_content.go @@ -19,6 +19,21 @@ type ReviewReq struct { sysin.CultivateContentReviewInp } type ReviewRes struct{} +type BatchReviewReq struct { + g.Meta `path:"/cultivate/content/batch-review" method:"post" tags:"养成管理" summary:"批量审核或下架动态评论"` + sysin.CultivateContentBatchReviewInp +} +type BatchReviewRes struct { + *sysin.CultivateContentBatchReviewModel +} + +type ViewReq struct { + g.Meta `path:"/cultivate/content/view" method:"get" tags:"养成管理" summary:"获取内容审核详情"` + sysin.CultivateContentViewInp +} +type ViewRes struct { + *sysin.CultivateContentViewModel +} type ReportListReq struct { g.Meta `path:"/cultivate/reports/list" method:"get" tags:"养成管理" summary:"获取举报列表"` sysin.CultivateReportListInp @@ -32,6 +47,13 @@ type ReportHandleReq struct { sysin.CultivateReportHandleInp } type ReportHandleRes struct{} +type ReportViewReq struct { + g.Meta `path:"/cultivate/reports/view" method:"get" tags:"养成管理" summary:"获取举报详情"` + sysin.CultivateReportViewInp +} +type ReportViewRes struct { + *sysin.CultivateReportViewModel +} type LogListReq struct { g.Meta `path:"/cultivate/logs/list" method:"get" tags:"养成管理" summary:"获取养成管理操作日志"` sysin.CultivateOperationLogListInp @@ -40,3 +62,8 @@ type LogListRes struct { form.PageRes List []*sysin.CultivateOperationLogListModel `json:"list"` } +type DashboardReq struct { + g.Meta `path:"/cultivate/dashboard" method:"get" tags:"养成管理" summary:"获取养成管理统计"` + sysin.CultivateDashboardInp +} +type DashboardRes struct{ *sysin.CultivateDashboardModel } diff --git a/server/internal/controller/admin/sys/cultivate_content.go b/server/internal/controller/admin/sys/cultivate_content.go index 6841c32..873ecd8 100644 --- a/server/internal/controller/admin/sys/cultivate_content.go +++ b/server/internal/controller/admin/sys/cultivate_content.go @@ -11,6 +11,14 @@ var CultivateContent = cCultivateContent{} type cCultivateContent struct{} +func (*cCultivateContent) Dashboard(ctx context.Context, req *cultivatecontent.DashboardReq) (res *cultivatecontent.DashboardRes, err error) { + data, err := service.SysCultivateContent().Dashboard(ctx, &req.CultivateDashboardInp) + if err != nil { + return nil, err + } + return &cultivatecontent.DashboardRes{CultivateDashboardModel: data}, nil +} + func (*cCultivateContent) List(ctx context.Context, req *cultivatecontent.ListReq) (res *cultivatecontent.ListRes, err error) { list, total, err := service.SysCultivateContent().List(ctx, &req.CultivateContentListInp) if err != nil { @@ -23,10 +31,24 @@ func (*cCultivateContent) List(ctx context.Context, req *cultivatecontent.ListRe res.PageRes.Pack(req, total) return } +func (*cCultivateContent) View(ctx context.Context, req *cultivatecontent.ViewReq) (res *cultivatecontent.ViewRes, err error) { + data, err := service.SysCultivateContent().View(ctx, &req.CultivateContentViewInp) + if err != nil { + return nil, err + } + return &cultivatecontent.ViewRes{CultivateContentViewModel: data}, nil +} func (*cCultivateContent) Review(ctx context.Context, req *cultivatecontent.ReviewReq) (res *cultivatecontent.ReviewRes, err error) { err = service.SysCultivateContent().Review(ctx, &req.CultivateContentReviewInp) return } +func (*cCultivateContent) BatchReview(ctx context.Context, req *cultivatecontent.BatchReviewReq) (res *cultivatecontent.BatchReviewRes, err error) { + data, err := service.SysCultivateContent().BatchReview(ctx, &req.CultivateContentBatchReviewInp) + if err != nil { + return nil, err + } + return &cultivatecontent.BatchReviewRes{CultivateContentBatchReviewModel: data}, nil +} func (*cCultivateContent) ReportList(ctx context.Context, req *cultivatecontent.ReportListReq) (res *cultivatecontent.ReportListRes, err error) { list, total, err := service.SysCultivateContent().ReportList(ctx, &req.CultivateReportListInp) if err != nil { @@ -43,6 +65,13 @@ func (*cCultivateContent) ReportHandle(ctx context.Context, req *cultivateconten err = service.SysCultivateContent().ReportHandle(ctx, &req.CultivateReportHandleInp) return } +func (*cCultivateContent) ReportView(ctx context.Context, req *cultivatecontent.ReportViewReq) (res *cultivatecontent.ReportViewRes, err error) { + data, err := service.SysCultivateContent().ReportView(ctx, &req.CultivateReportViewInp) + if err != nil { + return nil, err + } + return &cultivatecontent.ReportViewRes{CultivateReportViewModel: data}, nil +} func (*cCultivateContent) LogList(ctx context.Context, req *cultivatecontent.LogListReq) (res *cultivatecontent.LogListRes, err error) { list, total, err := service.SysCultivateContent().LogList(ctx, &req.CultivateOperationLogListInp) if err != nil { diff --git a/server/internal/logic/sys/cultivate_content.go b/server/internal/logic/sys/cultivate_content.go index 7ed2994..e9ed0af 100644 --- a/server/internal/logic/sys/cultivate_content.go +++ b/server/internal/logic/sys/cultivate_content.go @@ -4,6 +4,8 @@ import ( "context" "database/sql" "errors" + "fmt" + "net/url" "strconv" "strings" "time" @@ -13,8 +15,10 @@ import ( "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" @@ -26,6 +30,46 @@ 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" { @@ -80,14 +124,108 @@ func (s *sSysCultivateContent) List(ctx context.Context, in *sysin.CultivateCont 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) - return dao.FeedPost.Transaction(ctx, func(txCtx context.Context, tx gdb.TX) error { + 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 { @@ -223,7 +361,8 @@ func writeContentAudit(ctx context.Context, tx gdb.TX, tenantID, operatorID int6 return err } columns := dao.ContentAuditLog.Columns() - _, err = tx.Model(dao.ContentAuditLog.Table()).Data(g.Map{columns.Id: id, columns.TenantId: tenantID, columns.ObjectType: objectType, columns.ObjectId: objectID, columns.AuditSource: "manual", columns.StatusBefore: before, columns.StatusAfter: after, columns.ResultCode: result, columns.RiskLabels: "[]", columns.OperatorId: operatorID, columns.OccurredAt: now, columns.CreatedAt: now}).Insert() + 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 } @@ -248,14 +387,75 @@ func (s *sSysCultivateContent) ReportList(ctx context.Context, in *sysin.Cultiva } list = make([]*sysin.CultivateReportListModel, 0, len(rows)) for _, row := range rows { - list = append(list, &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}) + 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) - return dao.FeedReport.Transaction(ctx, func(txCtx context.Context, tx gdb.TX) error { + 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) @@ -277,11 +477,30 @@ func (s *sSysCultivateContent) ReportHandle(ctx context.Context, in *sysin.Culti 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) { @@ -296,6 +515,12 @@ func (s *sSysCultivateContent) LogList(ctx context.Context, in *sysin.CultivateO 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]) } @@ -322,3 +547,171 @@ func jsonValue(value *gjson.Json) any { } 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 +} diff --git a/server/internal/logic/sys/cultivate_flower.go b/server/internal/logic/sys/cultivate_flower.go index 46175b9..61dabb4 100644 --- a/server/internal/logic/sys/cultivate_flower.go +++ b/server/internal/logic/sys/cultivate_flower.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "net/url" "strings" "time" @@ -11,11 +12,14 @@ import ( "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gctx" "hotgo/internal/dao" "hotgo/internal/library/contexts" + "hotgo/internal/library/imadmin" "hotgo/internal/library/imtenant" + "hotgo/internal/library/location" "hotgo/internal/model/entity" "hotgo/internal/model/input/sysin" "hotgo/internal/service" @@ -86,6 +90,9 @@ func (s *sSysCultivateFlower) View(ctx context.Context, in *sysin.CultivateFlowe func (s *sSysCultivateFlower) Edit(ctx context.Context, in *sysin.CultivateFlowerEditInp) (err error) { tenantID := cultivateTenantID(ctx) + if err = validateCultivateAssets(ctx, tenantID, in); err != nil { + return err + } operatorID := contexts.GetUserId(ctx) now := time.Now().UTC() version := "cat_" + now.Format("20060102_150405") @@ -151,6 +158,59 @@ func (s *sSysCultivateFlower) Edit(ctx context.Context, in *sysin.CultivateFlowe }) } +func validateCultivateAssets(ctx context.Context, tenantID int64, in *sysin.CultivateFlowerEditInp) error { + seen := make(map[string]struct{}) + fileIDs := make([]string, 0, 1+len(in.StageRules)*2) + add := func(fileID string) { + fileID = strings.TrimSpace(fileID) + if fileID == "" { + return + } + if _, ok := seen[fileID]; ok { + return + } + seen[fileID] = struct{}{} + fileIDs = append(fileIDs, fileID) + } + add(in.IconFileId) + for _, rule := range in.StageRules { + add(rule.ImageFileId) + add(rule.AnimationFileId) + } + if len(fileIDs) == 0 { + return nil + } + tenant, err := resolveImAdminTenant(ctx, tenantID) + if err != nil { + return err + } + path := "/internal/cultivate/tenants/" + url.PathEscape(tenant.Code) + "/files/validate" + for offset := 0; offset < len(fileIDs); offset += 20 { + end := offset + 20 + if end > len(fileIDs) { + end = len(fileIDs) + } + var out struct { + Items []struct { + FileId string `json:"file_id"` + ReviewStatus string `json:"review_status"` + } `json:"items"` + } + if err = imadmin.DefaultClient(ctx).InternalPost(ctx, path, map[string]any{"user_id": 0, "file_ids": fileIDs[offset:end]}, &out); err != nil { + return gerror.Wrap(err, "校验花种素材失败") + } + for _, item := range out.Items { + if item.ReviewStatus == "risky" { + return gerror.Newf("素材 %s 审核未通过", item.FileId) + } + if item.ReviewStatus == "pending" { + return gerror.Newf("素材 %s 尚在审核中", item.FileId) + } + } + } + return nil +} + func (s *sSysCultivateFlower) Status(ctx context.Context, in *sysin.CultivateFlowerStatusInp) error { tenantID := cultivateTenantID(ctx) operatorID := contexts.GetUserId(ctx) @@ -185,7 +245,12 @@ func writeCultivateOperation(ctx context.Context, tx gdb.TX, tenantID, operatorI } beforeJSON, _ := json.Marshal(before) afterJSON, _ := json.Marshal(after) + ipAddress, userAgent := "", "" + if request := ghttp.RequestFromCtx(ctx); request != nil { + ipAddress = location.GetClientIp(request) + userAgent = request.UserAgent() + } c := dao.AdminOperationLog.Columns() - _, e = tx.Model(dao.AdminOperationLog.Table()).Data(g.Map{c.Id: id, c.TenantId: tenantID, c.OperatorId: operatorID, c.OperationType: operation, c.TargetType: targetType, c.TargetId: targetID, c.RequestId: gctx.CtxId(ctx), c.BeforeSnapshot: gjson.New(beforeJSON).MustToJsonString(), c.AfterSnapshot: gjson.New(afterJSON).MustToJsonString(), c.Reason: reason, c.IpAddress: "", c.UserAgent: "", c.CreatedAt: now}).Insert() + _, e = tx.Model(dao.AdminOperationLog.Table()).Data(g.Map{c.Id: id, c.TenantId: tenantID, c.OperatorId: operatorID, c.OperationType: operation, c.TargetType: targetType, c.TargetId: targetID, c.RequestId: gctx.CtxId(ctx), c.BeforeSnapshot: gjson.New(beforeJSON).MustToJsonString(), c.AfterSnapshot: gjson.New(afterJSON).MustToJsonString(), c.Reason: reason, c.IpAddress: ipAddress, c.UserAgent: userAgent, c.CreatedAt: now}).Insert() return e } diff --git a/server/internal/model/input/sysin/cultivate_content.go b/server/internal/model/input/sysin/cultivate_content.go index 10946a1..2b483cf 100644 --- a/server/internal/model/input/sysin/cultivate_content.go +++ b/server/internal/model/input/sysin/cultivate_content.go @@ -50,6 +50,48 @@ type CultivateContentListModel struct { UpdatedAt *gtime.Time `json:"updatedAt"` } +type CultivateContentViewInp struct { + ObjectType string `json:"objectType" v:"required|in:feed_post,feed_comment#内容类型不能为空|内容类型不正确"` + Id string `json:"id" v:"required#内容ID不能为空"` + ObjectId uint64 `json:"-"` +} + +func (in *CultivateContentViewInp) Filter(context.Context) error { + id, err := strconv.ParseUint(strings.TrimSpace(in.Id), 10, 64) + if err != nil || id == 0 { + return gerror.New("内容ID不正确") + } + in.ObjectId = id + return nil +} + +type CultivateMediaModel struct { + Id string `json:"id"` + FileId string `json:"fileId"` + ThumbnailFileId string `json:"thumbnailFileId"` + MediaType string `json:"mediaType"` + AuditStatus string `json:"auditStatus"` + SortNo uint `json:"sortNo"` +} +type CultivateAuditModel struct { + Id string `json:"id"` + AuditSource string `json:"auditSource"` + StatusBefore string `json:"statusBefore"` + StatusAfter string `json:"statusAfter"` + ResultCode string `json:"resultCode"` + RiskLabels any `json:"riskLabels"` + OperatorId string `json:"operatorId"` + OccurredAt *gtime.Time `json:"occurredAt"` +} +type CultivateContentViewModel struct { + CultivateContentListModel + AuthorNickname string `json:"authorNickname"` + AuthorAvatarFileId string `json:"authorAvatarFileId"` + Media []*CultivateMediaModel `json:"media"` + Audits []*CultivateAuditModel `json:"audits"` + RelatedReports []*CultivateReportListModel `json:"relatedReports"` +} + type CultivateContentReviewInp struct { ObjectType string `json:"objectType" v:"required|in:feed_post,feed_comment#内容类型不能为空|内容类型不正确"` Id string `json:"id" v:"required#内容ID不能为空"` @@ -58,6 +100,36 @@ type CultivateContentReviewInp struct { ObjectId uint64 `json:"-"` } +type CultivateContentBatchReviewInp struct { + ObjectType string `json:"objectType" v:"required|in:feed_post,feed_comment#内容类型不能为空|内容类型不正确"` + Ids []string `json:"ids" v:"required#请选择内容"` + Action string `json:"action" v:"required|in:approve,reject,take_down,restore#审核动作不能为空|审核动作不正确"` + Reason string `json:"reason"` +} + +func (in *CultivateContentBatchReviewInp) Filter(ctx context.Context) error { + if len(in.Ids) == 0 || len(in.Ids) > 100 { + return gerror.New("单次批量处理数量必须为1至100条") + } + in.Reason = strings.TrimSpace(in.Reason) + if (in.Action == "reject" || in.Action == "take_down") && in.Reason == "" { + return gerror.New("拒绝或下架时必须填写原因") + } + for _, id := range in.Ids { + item := &CultivateContentReviewInp{ObjectType: in.ObjectType, Id: id, Action: in.Action, Reason: in.Reason} + if err := item.Filter(ctx); err != nil { + return err + } + } + return nil +} + +type CultivateContentBatchReviewModel struct { + SuccessCount int `json:"successCount"` + FailedCount int `json:"failedCount"` + Failures map[string]string `json:"failures"` +} + func (in *CultivateContentReviewInp) Filter(context.Context) error { id, err := strconv.ParseUint(strings.TrimSpace(in.Id), 10, 64) if err != nil || id == 0 { @@ -105,11 +177,36 @@ type CultivateReportListModel struct { CreatedAt *gtime.Time `json:"createdAt"` } +type CultivateReportViewInp struct { + Id string `json:"id" v:"required#举报ID不能为空"` + ReportId uint64 `json:"-"` +} + +func (in *CultivateReportViewInp) Filter(context.Context) error { + id, err := strconv.ParseUint(strings.TrimSpace(in.Id), 10, 64) + if err != nil || id == 0 { + return gerror.New("举报ID不正确") + } + in.ReportId = id + return nil +} + +type CultivateReportViewModel struct { + CultivateReportListModel + ReporterNickname string `json:"reporterNickname"` + ReportedUserNickname string `json:"reportedUserNickname"` + TargetContent *CultivateContentListModel `json:"targetContent"` + History []*CultivateReportListModel `json:"history"` +} + type CultivateReportHandleInp struct { - Id string `json:"id" v:"required#举报ID不能为空"` - Action string `json:"action" v:"required|in:processing,resolve,reject#处理动作不能为空|处理动作不正确"` - HandleResult string `json:"handleResult"` - ReportId uint64 `json:"-"` + Id string `json:"id" v:"required#举报ID不能为空"` + Action string `json:"action" v:"required|in:processing,resolve,reject#处理动作不能为空|处理动作不正确"` + HandleResult string `json:"handleResult"` + ContentAction string `json:"contentAction" v:"in:none,take_down#内容处置不正确"` + UserAction string `json:"userAction" v:"in:none,freeze,ban#用户处置不正确"` + Reason string `json:"reason"` + ReportId uint64 `json:"-"` } func (in *CultivateReportHandleInp) Filter(context.Context) error { @@ -119,9 +216,24 @@ func (in *CultivateReportHandleInp) Filter(context.Context) error { } in.ReportId = id in.HandleResult = strings.TrimSpace(in.HandleResult) + in.ContentAction = strings.TrimSpace(in.ContentAction) + in.UserAction = strings.TrimSpace(in.UserAction) + in.Reason = strings.TrimSpace(in.Reason) + if in.ContentAction == "" { + in.ContentAction = "none" + } + if in.UserAction == "" { + in.UserAction = "none" + } if in.Action != "processing" && in.HandleResult == "" { return gerror.New("办结举报时必须填写处理结果") } + if (in.ContentAction != "none" || in.UserAction != "none") && in.Reason == "" { + return gerror.New("联动处置时必须填写处置原因") + } + if in.Action != "resolve" && (in.ContentAction != "none" || in.UserAction != "none") { + return gerror.New("仅确认处理时可执行内容或用户处置") + } return nil } @@ -130,9 +242,38 @@ type CultivateOperationLogListInp struct { OperatorId string `json:"operatorId"` OperationType string `json:"operationType"` TargetType string `json:"targetType"` + TargetId string `json:"targetId"` + RequestId string `json:"requestId"` CreatedAt []*gtime.Time `json:"createdAt"` } +type CultivateDashboardInp struct{} + +func (*CultivateDashboardInp) Filter(context.Context) error { return nil } + +type CultivateDashboardModel struct { + PlantCount int64 `json:"plantCount"` + GrowingCount int64 `json:"growingCount"` + BloomCount int64 `json:"bloomCount"` + HarvestedCount int64 `json:"harvestedCount"` + RemovedCount int64 `json:"removedCount"` + Active7d int64 `json:"active7d"` + Active30d int64 `json:"active30d"` + PostCount int64 `json:"postCount"` + PublicPostCount int64 `json:"publicPostCount"` + FriendsPostCount int64 `json:"friendsPostCount"` + PrivatePostCount int64 `json:"privatePostCount"` + CommentCount int64 `json:"commentCount"` + LikeCount int64 `json:"likeCount"` + TakenDownCount int64 `json:"takenDownCount"` + PendingAuditCount int64 `json:"pendingAuditCount"` + ApprovedAuditCount int64 `json:"approvedAuditCount"` + RejectedAuditCount int64 `json:"rejectedAuditCount"` + PendingReportCount int64 `json:"pendingReportCount"` + ProcessingReportCount int64 `json:"processingReportCount"` + ProcessedReportCount int64 `json:"processedReportCount"` +} + func (*CultivateOperationLogListInp) Filter(context.Context) error { return nil } type CultivateOperationLogListModel struct { diff --git a/server/internal/model/input/sysin/cultivate_content_test.go b/server/internal/model/input/sysin/cultivate_content_test.go new file mode 100644 index 0000000..43aeeb5 --- /dev/null +++ b/server/internal/model/input/sysin/cultivate_content_test.go @@ -0,0 +1,27 @@ +package sysin + +import ( + "context" + "testing" +) + +func TestCultivateContentBatchReviewFilter(t *testing.T) { + in := &CultivateContentBatchReviewInp{ObjectType: "feed_post", Ids: []string{"123"}, Action: "take_down", Reason: "违规内容"} + if err := in.Filter(context.Background()); err != nil { + t.Fatalf("Filter returned error: %v", err) + } +} + +func TestCultivateContentBatchReviewFilterRequiresReason(t *testing.T) { + in := &CultivateContentBatchReviewInp{ObjectType: "feed_post", Ids: []string{"123"}, Action: "reject"} + if err := in.Filter(context.Background()); err == nil { + t.Fatal("expected missing reason error") + } +} + +func TestCultivateReportHandleFilterGovernanceRules(t *testing.T) { + in := &CultivateReportHandleInp{Id: "123", Action: "processing", UserAction: "freeze", Reason: "恶意行为"} + if err := in.Filter(context.Background()); err == nil { + t.Fatal("expected governance action to require resolve") + } +} diff --git a/server/internal/service/sys.go b/server/internal/service/sys.go index 0ff1412..5c8ca57 100644 --- a/server/internal/service/sys.go +++ b/server/internal/service/sys.go @@ -179,9 +179,13 @@ type ( Status(ctx context.Context, in *sysin.CultivateFlowerStatusInp) (err error) } ISysCultivateContent interface { + Dashboard(ctx context.Context, in *sysin.CultivateDashboardInp) (*sysin.CultivateDashboardModel, error) List(ctx context.Context, in *sysin.CultivateContentListInp) (list []*sysin.CultivateContentListModel, totalCount int, err error) + View(ctx context.Context, in *sysin.CultivateContentViewInp) (*sysin.CultivateContentViewModel, error) Review(ctx context.Context, in *sysin.CultivateContentReviewInp) error + BatchReview(ctx context.Context, in *sysin.CultivateContentBatchReviewInp) (*sysin.CultivateContentBatchReviewModel, error) ReportList(ctx context.Context, in *sysin.CultivateReportListInp) (list []*sysin.CultivateReportListModel, totalCount int, err error) + ReportView(ctx context.Context, in *sysin.CultivateReportViewInp) (*sysin.CultivateReportViewModel, error) ReportHandle(ctx context.Context, in *sysin.CultivateReportHandleInp) error LogList(ctx context.Context, in *sysin.CultivateOperationLogListInp) (list []*sysin.CultivateOperationLogListModel, totalCount int, err error) } diff --git a/server/storage/data/generate/cultivate_menu.sql b/server/storage/data/generate/cultivate_menu.sql index 5a9c457..e3737f5 100644 --- a/server/storage/data/generate/cultivate_menu.sql +++ b/server/storage/data/generate/cultivate_menu.sql @@ -3,9 +3,14 @@ SET NAMES utf8mb4; SET @now := NOW(); INSERT INTO hg_admin_menu (pid,level,tree,title,name,path,icon,type,redirect,permissions,permission_name,component,always_show,active_menu,is_root,is_frame,frame_src,keep_alive,hidden,affix,sort,remark,status,created_at,updated_at) -SELECT 0,1,'','养成管理','cultivateManager','/cultivate','ExperimentOutlined',1,'/cultivate/flowers','','','LAYOUT',1,'',0,0,'',0,0,0,124,'后院养成与物友动态管理',1,@now,@now +SELECT 0,1,'','养成管理','cultivateManager','/cultivate','ExperimentOutlined',1,'/cultivate/dashboard','','','LAYOUT',1,'',0,0,'',0,0,0,124,'后院养成与物友动态管理',1,@now,@now WHERE NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name='cultivateManager'); SET @cultivateId := (SELECT id FROM hg_admin_menu WHERE name='cultivateManager' LIMIT 1); +UPDATE hg_admin_menu SET redirect='/cultivate/dashboard', updated_at=@now WHERE id=@cultivateId; + +INSERT INTO hg_admin_menu (pid,level,tree,title,name,path,icon,type,redirect,permissions,permission_name,component,always_show,active_menu,is_root,is_frame,frame_src,keep_alive,hidden,affix,sort,remark,status,created_at,updated_at) +SELECT @cultivateId,2,CONCAT('tr_',@cultivateId,' '),'数据概览','cultivateDashboard','dashboard','',2,'','/cultivate/dashboard','','/cultivate/dashboard/index',1,'cultivateDashboard',0,0,'',1,0,0,50,'养成与物友治理核心指标',1,@now,@now +WHERE NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name='cultivateDashboard'); INSERT INTO hg_admin_menu (pid,level,tree,title,name,path,icon,type,redirect,permissions,permission_name,component,always_show,active_menu,is_root,is_frame,frame_src,keep_alive,hidden,affix,sort,remark,status,created_at,updated_at) SELECT @cultivateId,2,CONCAT('tr_',@cultivateId,' '),'花种配置','cultivateFlowers','flowers','',2,'','/cultivate/flowers/list','','/cultivate/flowers/index',1,'cultivateFlowers',0,0,'',1,0,0,40,'花种、成长阶段、每日限额与素材配置',1,@now,@now @@ -33,6 +38,14 @@ INSERT INTO hg_admin_menu (pid,level,tree,title,name,path,icon,type,redirect,per SELECT @contentId,3,CONCAT('tr_',@cultivateId,' tr_',@contentId,' '),'审核内容','cultivateContentReview','','',3,'','/cultivate/content/review','','',1,'',0,0,'',0,1,0,20,'',1,@now,@now WHERE NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name='cultivateContentReview'); +INSERT INTO hg_admin_menu (pid,level,tree,title,name,path,icon,type,redirect,permissions,permission_name,component,always_show,active_menu,is_root,is_frame,frame_src,keep_alive,hidden,affix,sort,remark,status,created_at,updated_at) +SELECT @contentId,3,CONCAT('tr_',@cultivateId,' tr_',@contentId,' '),'批量审核内容','cultivateContentBatchReview','','',3,'','/cultivate/content/batch-review','','',1,'',0,0,'',0,1,0,10,'',1,@now,@now +WHERE NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name='cultivateContentBatchReview'); + +INSERT INTO hg_admin_menu (pid,level,tree,title,name,path,icon,type,redirect,permissions,permission_name,component,always_show,active_menu,is_root,is_frame,frame_src,keep_alive,hidden,affix,sort,remark,status,created_at,updated_at) +SELECT @contentId,3,CONCAT('tr_',@cultivateId,' tr_',@contentId,' '),'查看内容详情','cultivateContentView','','',3,'','/cultivate/content/view','','',1,'',0,0,'',0,1,0,30,'',1,@now,@now +WHERE NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name='cultivateContentView'); + INSERT INTO hg_admin_menu (pid,level,tree,title,name,path,icon,type,redirect,permissions,permission_name,component,always_show,active_menu,is_root,is_frame,frame_src,keep_alive,hidden,affix,sort,remark,status,created_at,updated_at) SELECT @cultivateId,2,CONCAT('tr_',@cultivateId,' '),'举报处理','cultivateReports','reports','',2,'','/cultivate/reports/list','','/cultivate/reports/index',1,'cultivateReports',0,0,'',1,0,0,20,'物友内容与用户举报处理',1,@now,@now WHERE NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name='cultivateReports'); @@ -42,6 +55,10 @@ INSERT INTO hg_admin_menu (pid,level,tree,title,name,path,icon,type,redirect,per SELECT @reportsId,3,CONCAT('tr_',@cultivateId,' tr_',@reportsId,' '),'处理举报','cultivateReportsHandle','','',3,'','/cultivate/reports/handle','','',1,'',0,0,'',0,1,0,20,'',1,@now,@now WHERE NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name='cultivateReportsHandle'); +INSERT INTO hg_admin_menu (pid,level,tree,title,name,path,icon,type,redirect,permissions,permission_name,component,always_show,active_menu,is_root,is_frame,frame_src,keep_alive,hidden,affix,sort,remark,status,created_at,updated_at) +SELECT @reportsId,3,CONCAT('tr_',@cultivateId,' tr_',@reportsId,' '),'查看举报详情','cultivateReportsView','','',3,'','/cultivate/reports/view','','',1,'',0,0,'',0,1,0,30,'',1,@now,@now +WHERE NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name='cultivateReportsView'); + INSERT INTO hg_admin_menu (pid,level,tree,title,name,path,icon,type,redirect,permissions,permission_name,component,always_show,active_menu,is_root,is_frame,frame_src,keep_alive,hidden,affix,sort,remark,status,created_at,updated_at) SELECT @cultivateId,2,CONCAT('tr_',@cultivateId,' '),'操作日志','cultivateLogs','logs','',2,'','/cultivate/logs/list','','/cultivate/logs/index',1,'cultivateLogs',0,0,'',1,0,0,10,'养成管理操作审计日志',1,@now,@now WHERE NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name='cultivateLogs'); diff --git a/web/src/api/cultivate/content.ts b/web/src/api/cultivate/content.ts index ef1a63f..1a86a8f 100644 --- a/web/src/api/cultivate/content.ts +++ b/web/src/api/cultivate/content.ts @@ -1,7 +1,29 @@ import { http } from '@/utils/http/axios'; -export function ContentList(params) { return http.request({ url: '/cultivate/content/list', method: 'get', params }); } -export function ContentReview(params) { return http.request({ url: '/cultivate/content/review', method: 'POST', params }); } -export function ReportList(params) { return http.request({ url: '/cultivate/reports/list', method: 'get', params }); } -export function ReportHandle(params) { return http.request({ url: '/cultivate/reports/handle', method: 'POST', params }); } -export function OperationLogList(params) { return http.request({ url: '/cultivate/logs/list', method: 'get', params }); } +export function ContentList(params) { + return http.request({ url: '/cultivate/content/list', method: 'get', params }); +} +export function ContentView(params) { + return http.request({ url: '/cultivate/content/view', method: 'get', params }); +} +export function ContentReview(params) { + return http.request({ url: '/cultivate/content/review', method: 'POST', params }); +} +export function ContentBatchReview(params) { + return http.request({ url: '/cultivate/content/batch-review', method: 'POST', params }); +} +export function ReportList(params) { + return http.request({ url: '/cultivate/reports/list', method: 'get', params }); +} +export function ReportView(params) { + return http.request({ url: '/cultivate/reports/view', method: 'get', params }); +} +export function ReportHandle(params) { + return http.request({ url: '/cultivate/reports/handle', method: 'POST', params }); +} +export function OperationLogList(params) { + return http.request({ url: '/cultivate/logs/list', method: 'get', params }); +} +export function CultivateDashboard() { + return http.request({ url: '/cultivate/dashboard', method: 'get' }); +} diff --git a/web/src/views/cultivate/content/index.vue b/web/src/views/cultivate/content/index.vue index 1918026..43b37a7 100644 --- a/web/src/views/cultivate/content/index.vue +++ b/web/src/views/cultivate/content/index.vue @@ -1,37 +1,236 @@ + diff --git a/web/src/views/cultivate/dashboard/index.vue b/web/src/views/cultivate/dashboard/index.vue new file mode 100644 index 0000000..0c614ec --- /dev/null +++ b/web/src/views/cultivate/dashboard/index.vue @@ -0,0 +1,51 @@ + + + diff --git a/web/src/views/cultivate/logs/model.ts b/web/src/views/cultivate/logs/model.ts index c261243..6767d9e 100644 --- a/web/src/views/cultivate/logs/model.ts +++ b/web/src/views/cultivate/logs/model.ts @@ -1,12 +1,44 @@ import { ref } from 'vue'; import { FormSchema } from '@/components/Form'; export const schemas = ref([ - { field: 'operatorId', component: 'NInput', label: '操作人ID', componentProps: { placeholder: '请输入操作人ID' } }, - { field: 'operationType', component: 'NInput', label: '操作类型', componentProps: { placeholder: '如 content、report、flower' } }, - { field: 'targetType', component: 'NInput', label: '对象类型', componentProps: { placeholder: '请输入对象类型' } }, + { + field: 'operatorId', + component: 'NInput', + label: '操作人ID', + componentProps: { placeholder: '请输入操作人ID' }, + }, + { + field: 'operationType', + component: 'NInput', + label: '操作类型', + componentProps: { placeholder: '如 content、report、flower' }, + }, + { + field: 'targetType', + component: 'NInput', + label: '对象类型', + componentProps: { placeholder: '请输入对象类型' }, + }, + { + field: 'targetId', + component: 'NInput', + label: '对象ID', + componentProps: { placeholder: '请输入对象ID' }, + }, + { + field: 'requestId', + component: 'NInput', + label: '请求ID', + componentProps: { placeholder: '请输入请求ID' }, + }, ]); export const columns = [ - { title: '日志ID', key: 'id', width: 190 }, { title: '操作人', key: 'operatorId', width: 150 }, { title: '操作类型', key: 'operationType', width: 160 }, - { title: '对象类型', key: 'targetType', width: 130 }, { title: '对象ID', key: 'targetId', width: 190 }, { title: '原因/结果', key: 'reason', width: 260, ellipsis: { tooltip: true } }, - { title: '请求ID', key: 'requestId', width: 220 }, { title: '操作时间', key: 'createdAt', width: 180 }, + { title: '日志ID', key: 'id', width: 190 }, + { title: '操作人', key: 'operatorId', width: 150 }, + { title: '操作类型', key: 'operationType', width: 160 }, + { title: '对象类型', key: 'targetType', width: 130 }, + { title: '对象ID', key: 'targetId', width: 190 }, + { title: '原因/结果', key: 'reason', width: 260, ellipsis: { tooltip: true } }, + { title: '请求ID', key: 'requestId', width: 220 }, + { title: '操作时间', key: 'createdAt', width: 180 }, ]; diff --git a/web/src/views/cultivate/reports/index.vue b/web/src/views/cultivate/reports/index.vue index e6d1fd6..a84d4a2 100644 --- a/web/src/views/cultivate/reports/index.vue +++ b/web/src/views/cultivate/reports/index.vue @@ -1,25 +1,214 @@ +