diff --git a/backend/app/http/super/v1/content_reports.go b/backend/app/http/super/v1/content_reports.go index 55b0846..bb227fe 100644 --- a/backend/app/http/super/v1/content_reports.go +++ b/backend/app/http/super/v1/content_reports.go @@ -45,3 +45,19 @@ func (c *contentReports) List(ctx fiber.Ctx, filter *dto.SuperContentReportListF func (c *contentReports) Process(ctx fiber.Ctx, user *models.User, id int64, form *dto.SuperContentReportProcessForm) error { return services.Super.ProcessContentReport(ctx, user.ID, id, form) } + +// Batch process content reports +// +// @Router /super/v1/content-reports/process/batch [post] +// @Summary Batch process content reports +// @Description Batch process content report records +// @Tags Content +// @Accept json +// @Produce json +// @Param form body dto.SuperContentReportBatchProcessForm true "Batch process form" +// @Success 200 {string} string "Processed" +// @Bind user local key(__ctx_user) +// @Bind form body +func (c *contentReports) BatchProcess(ctx fiber.Ctx, user *models.User, form *dto.SuperContentReportBatchProcessForm) error { + return services.Super.BatchProcessContentReports(ctx, user.ID, form) +} diff --git a/backend/app/http/super/v1/dto/super_content_report.go b/backend/app/http/super/v1/dto/super_content_report.go index 8a2b4ec..6904dd0 100644 --- a/backend/app/http/super/v1/dto/super_content_report.go +++ b/backend/app/http/super/v1/dto/super_content_report.go @@ -98,3 +98,15 @@ type SuperContentReportProcessForm struct { // Reason 处理说明(可选,用于审计记录)。 Reason string `json:"reason"` } + +// SuperContentReportBatchProcessForm 超管内容举报批量处理表单。 +type SuperContentReportBatchProcessForm struct { + // ReportIDs 待处理举报ID列表。 + ReportIDs []int64 `json:"report_ids" validate:"required,min=1,dive,gt=0"` + // Action 处理动作(approve/reject)。 + Action string `json:"action" validate:"required,oneof=approve reject"` + // ContentAction 内容处置动作(block/unpublish/ignore),仅在 approve 时生效。 + ContentAction string `json:"content_action"` + // Reason 处理说明(可选,用于审计记录)。 + Reason string `json:"reason"` +} diff --git a/backend/app/http/super/v1/routes.gen.go b/backend/app/http/super/v1/routes.gen.go index 482d8f5..8601cfe 100644 --- a/backend/app/http/super/v1/routes.gen.go +++ b/backend/app/http/super/v1/routes.gen.go @@ -108,6 +108,12 @@ func (r *Routes) Register(router fiber.Router) { PathParam[int64]("id"), Body[dto.SuperContentReportProcessForm]("form"), )) + r.log.Debugf("Registering route: Post /super/v1/content-reports/process/batch -> contentReports.BatchProcess") + router.Post("/super/v1/content-reports/process/batch"[len(r.Path()):], Func2( + r.contentReports.BatchProcess, + Local[*models.User]("__ctx_user"), + Body[dto.SuperContentReportBatchProcessForm]("form"), + )) // Register routes for controller: contents r.log.Debugf("Registering route: Get /super/v1/contents -> contents.List") router.Get("/super/v1/contents"[len(r.Path()):], DataFunc1( diff --git a/backend/app/services/super.go b/backend/app/services/super.go index ed0baf9..86acedb 100644 --- a/backend/app/services/super.go +++ b/backend/app/services/super.go @@ -3166,6 +3166,163 @@ func (s *super) ProcessContentReport(ctx context.Context, operatorID, id int64, return nil } +func (s *super) BatchProcessContentReports(ctx context.Context, operatorID int64, form *super_dto.SuperContentReportBatchProcessForm) error { + if operatorID == 0 { + return errorx.ErrUnauthorized.WithMsg("缺少操作者信息") + } + if form == nil { + return errorx.ErrBadRequest.WithMsg("处理参数不能为空") + } + + action := strings.ToLower(strings.TrimSpace(form.Action)) + if action != "approve" && action != "reject" { + return errorx.ErrBadRequest.WithMsg("处理动作非法") + } + + contentAction := strings.ToLower(strings.TrimSpace(form.ContentAction)) + if action == "approve" { + if contentAction == "" { + contentAction = "ignore" + } + switch contentAction { + case "block", "unpublish", "ignore": + default: + return errorx.ErrBadRequest.WithMsg("内容处置动作非法") + } + } else { + contentAction = "ignore" + } + + // 去重并过滤非法ID,避免批量处理出现空指令。 + unique := make(map[int64]struct{}) + reportIDs := make([]int64, 0, len(form.ReportIDs)) + for _, id := range form.ReportIDs { + if id <= 0 { + continue + } + if _, ok := unique[id]; ok { + continue + } + unique[id] = struct{}{} + reportIDs = append(reportIDs, id) + } + if len(reportIDs) == 0 { + return errorx.ErrBadRequest.WithMsg("举报ID不能为空") + } + + status := "approved" + if action == "reject" { + status = "rejected" + } + handledReason := strings.TrimSpace(form.Reason) + handledAt := time.Now() + + var reports []*models.ContentReport + contentMap := make(map[int64]*models.Content) + // 事务内批量更新举报处理结果及关联内容状态,保证一致性。 + err := models.Q.Transaction(func(tx *models.Query) error { + reportTbl, reportQuery := tx.ContentReport.QueryContext(ctx) + list, err := reportQuery.Where(reportTbl.ID.In(reportIDs...)).Find() + if err != nil { + return errorx.ErrDatabaseError.WithCause(err) + } + if len(list) != len(reportIDs) { + return errorx.ErrRecordNotFound.WithMsg("部分举报不存在") + } + for _, report := range list { + if strings.TrimSpace(report.Status) != "" && report.Status != "pending" { + return errorx.ErrStatusConflict.WithMsg("仅可处理待处理举报") + } + } + + _, err = reportQuery.Where(reportTbl.ID.In(reportIDs...)).UpdateSimple( + reportTbl.Status.Value(status), + reportTbl.HandledBy.Value(operatorID), + reportTbl.HandledAction.Value(contentAction), + reportTbl.HandledReason.Value(handledReason), + reportTbl.HandledAt.Value(handledAt), + ) + if err != nil { + return errorx.ErrDatabaseError.WithCause(err) + } + + if action == "approve" { + contentIDs := make([]int64, 0, len(list)) + for _, report := range list { + contentIDs = append(contentIDs, report.ContentID) + } + contentTbl, contentQuery := tx.Content.QueryContext(ctx) + contentQueryUnscoped := contentQuery.Unscoped() + contents, err := contentQueryUnscoped.Where(contentTbl.ID.In(contentIDs...)).Find() + if err != nil { + return errorx.ErrDatabaseError.WithCause(err) + } + for _, content := range contents { + contentMap[content.ID] = content + } + + if contentAction != "ignore" { + var nextStatus consts.ContentStatus + switch contentAction { + case "block": + nextStatus = consts.ContentStatusBlocked + case "unpublish": + nextStatus = consts.ContentStatusUnpublished + } + if nextStatus != "" && len(contents) > 0 { + if _, err := contentQuery.Where(contentTbl.ID.In(contentIDs...)).UpdateSimple( + contentTbl.Status.Value(nextStatus), + ); err != nil { + return errorx.ErrDatabaseError.WithCause(err) + } + } + } + } + + reports = list + return nil + }) + if err != nil { + return err + } + + if action == "approve" { + title := "内容举报成立" + actionLabel := "不处理内容" + switch contentAction { + case "block": + actionLabel = "封禁内容" + case "unpublish": + actionLabel = "下架内容" + } + for _, report := range reports { + content := contentMap[report.ContentID] + if content == nil { + continue + } + detail := "内容《" + content.Title + "》举报成立,处理结果:" + actionLabel + if handledReason != "" { + detail += "。说明:" + handledReason + } + if Notification != nil { + _ = Notification.Send(ctx, content.TenantID, content.UserID, string(consts.NotificationTypeAudit), title, detail) + } + } + } + + if Audit != nil { + detail := "处理内容举报:" + action + " / " + contentAction + if handledReason != "" { + detail += " / " + handledReason + } + for _, report := range reports { + Audit.Log(ctx, report.TenantID, operatorID, "process_content_report", cast.ToString(report.ID), detail) + } + } + + return nil +} + func (s *super) UpdateContentStatus(ctx context.Context, tenantID, contentID int64, form *super_dto.SuperTenantContentStatusUpdateForm) error { tbl, q := models.ContentQuery.QueryContext(ctx) _, err := q.Where(tbl.ID.Eq(contentID), tbl.TenantID.Eq(tenantID)).Update(tbl.Status, consts.ContentStatus(form.Status)) diff --git a/backend/app/services/super_test.go b/backend/app/services/super_test.go index e38b1f5..2342148 100644 --- a/backend/app/services/super_test.go +++ b/backend/app/services/super_test.go @@ -396,6 +396,107 @@ func (s *SuperTestSuite) Test_ContentReportGovernance() { }) } +func (s *SuperTestSuite) Test_BatchProcessContentReports() { + Convey("BatchProcessContentReports", s.T(), func() { + ctx := s.T().Context() + database.Truncate(ctx, s.DB, models.TableNameContentReport, models.TableNameContent, models.TableNameTenant, models.TableNameUser) + + owner := &models.User{Username: "batch_report_owner"} + reporter := &models.User{Username: "batch_reporter"} + admin := &models.User{Username: "batch_report_admin"} + models.UserQuery.WithContext(ctx).Create(owner, reporter, admin) + + tenant := &models.Tenant{UserID: owner.ID, Code: "t-report-batch", Name: "Report Batch Tenant", Status: consts.TenantStatusVerified} + models.TenantQuery.WithContext(ctx).Create(tenant) + + contentA := &models.Content{ + TenantID: tenant.ID, + UserID: owner.ID, + Title: "Batch Report A", + Status: consts.ContentStatusPublished, + } + contentB := &models.Content{ + TenantID: tenant.ID, + UserID: owner.ID, + Title: "Batch Report B", + Status: consts.ContentStatusPublished, + } + models.ContentQuery.WithContext(ctx).Create(contentA, contentB) + + Convey("should batch approve reports and unpublish contents", func() { + reportA := &models.ContentReport{ + TenantID: tenant.ID, + ContentID: contentA.ID, + ReporterID: reporter.ID, + Reason: "spam", + Detail: "批量举报A", + Status: "pending", + } + reportB := &models.ContentReport{ + TenantID: tenant.ID, + ContentID: contentB.ID, + ReporterID: reporter.ID, + Reason: "abuse", + Detail: "批量举报B", + Status: "pending", + } + models.ContentReportQuery.WithContext(ctx).Create(reportA, reportB) + + err := Super.BatchProcessContentReports(ctx, admin.ID, &super_dto.SuperContentReportBatchProcessForm{ + ReportIDs: []int64{reportA.ID, reportB.ID}, + Action: "approve", + ContentAction: "unpublish", + Reason: "集中处理", + }) + So(err, ShouldBeNil) + + reloadedA, err := models.ContentReportQuery.WithContext(ctx).Where(models.ContentReportQuery.ID.Eq(reportA.ID)).First() + So(err, ShouldBeNil) + So(reloadedA.Status, ShouldEqual, "approved") + So(reloadedA.HandledBy, ShouldEqual, admin.ID) + So(reloadedA.HandledAction, ShouldEqual, "unpublish") + So(reloadedA.HandledReason, ShouldEqual, "集中处理") + So(reloadedA.HandledAt.IsZero(), ShouldBeFalse) + + reloadedB, err := models.ContentReportQuery.WithContext(ctx).Where(models.ContentReportQuery.ID.Eq(reportB.ID)).First() + So(err, ShouldBeNil) + So(reloadedB.Status, ShouldEqual, "approved") + So(reloadedB.HandledAction, ShouldEqual, "unpublish") + + contentReloadA, err := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(contentA.ID)).First() + So(err, ShouldBeNil) + So(contentReloadA.Status, ShouldEqual, consts.ContentStatusUnpublished) + + contentReloadB, err := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(contentB.ID)).First() + So(err, ShouldBeNil) + So(contentReloadB.Status, ShouldEqual, consts.ContentStatusUnpublished) + }) + + Convey("should reject when report already handled", func() { + report := &models.ContentReport{ + TenantID: tenant.ID, + ContentID: contentA.ID, + ReporterID: reporter.ID, + Reason: "other", + Detail: "已处理", + Status: "approved", + } + models.ContentReportQuery.WithContext(ctx).Create(report) + + err := Super.BatchProcessContentReports(ctx, admin.ID, &super_dto.SuperContentReportBatchProcessForm{ + ReportIDs: []int64{report.ID}, + Action: "reject", + Reason: "重复提交", + }) + So(err, ShouldNotBeNil) + + var appErr *errorx.AppError + So(errors.As(err, &appErr), ShouldBeTrue) + So(appErr.Code, ShouldEqual, errorx.ErrStatusConflict.Code) + }) + }) +} + func (s *SuperTestSuite) Test_FinanceAnomalies() { Convey("Finance Anomalies", s.T(), func() { ctx := s.T().Context() diff --git a/backend/docs/docs.go b/backend/docs/docs.go index ba7d37d..7c225a2 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -392,6 +392,40 @@ const docTemplate = `{ } } }, + "/super/v1/content-reports/process/batch": { + "post": { + "description": "Batch process content report records", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Content" + ], + "summary": "Batch process content reports", + "parameters": [ + { + "description": "Batch process form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SuperContentReportBatchProcessForm" + } + } + ], + "responses": { + "200": { + "description": "Processed", + "schema": { + "type": "string" + } + } + } + } + }, "/super/v1/content-reports/{id}/process": { "post": { "description": "Process a content report record", @@ -8155,6 +8189,39 @@ const docTemplate = `{ } } }, + "dto.SuperContentReportBatchProcessForm": { + "type": "object", + "required": [ + "action", + "report_ids" + ], + "properties": { + "action": { + "description": "Action 处理动作(approve/reject)。", + "type": "string", + "enum": [ + "approve", + "reject" + ] + }, + "content_action": { + "description": "ContentAction 内容处置动作(block/unpublish/ignore),仅在 approve 时生效。", + "type": "string" + }, + "reason": { + "description": "Reason 处理说明(可选,用于审计记录)。", + "type": "string" + }, + "report_ids": { + "description": "ReportIDs 待处理举报ID列表。", + "type": "array", + "minItems": 1, + "items": { + "type": "integer" + } + } + } + }, "dto.SuperContentReportItem": { "type": "object", "properties": { diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 73c7e97..998681d 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -386,6 +386,40 @@ } } }, + "/super/v1/content-reports/process/batch": { + "post": { + "description": "Batch process content report records", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Content" + ], + "summary": "Batch process content reports", + "parameters": [ + { + "description": "Batch process form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SuperContentReportBatchProcessForm" + } + } + ], + "responses": { + "200": { + "description": "Processed", + "schema": { + "type": "string" + } + } + } + } + }, "/super/v1/content-reports/{id}/process": { "post": { "description": "Process a content report record", @@ -8149,6 +8183,39 @@ } } }, + "dto.SuperContentReportBatchProcessForm": { + "type": "object", + "required": [ + "action", + "report_ids" + ], + "properties": { + "action": { + "description": "Action 处理动作(approve/reject)。", + "type": "string", + "enum": [ + "approve", + "reject" + ] + }, + "content_action": { + "description": "ContentAction 内容处置动作(block/unpublish/ignore),仅在 approve 时生效。", + "type": "string" + }, + "reason": { + "description": "Reason 处理说明(可选,用于审计记录)。", + "type": "string" + }, + "report_ids": { + "description": "ReportIDs 待处理举报ID列表。", + "type": "array", + "minItems": 1, + "items": { + "type": "integer" + } + } + } + }, "dto.SuperContentReportItem": { "type": "object", "properties": { diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 3be1495..e991e95 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -1421,6 +1421,30 @@ definitions: - content_ids - status type: object + dto.SuperContentReportBatchProcessForm: + properties: + action: + description: Action 处理动作(approve/reject)。 + enum: + - approve + - reject + type: string + content_action: + description: ContentAction 内容处置动作(block/unpublish/ignore),仅在 approve 时生效。 + type: string + reason: + description: Reason 处理说明(可选,用于审计记录)。 + type: string + report_ids: + description: ReportIDs 待处理举报ID列表。 + items: + type: integer + minItems: 1 + type: array + required: + - action + - report_ids + type: object dto.SuperContentReportItem: properties: content_id: @@ -3596,6 +3620,28 @@ paths: summary: Process content report tags: - Content + /super/v1/content-reports/process/batch: + post: + consumes: + - application/json + description: Batch process content report records + parameters: + - description: Batch process form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.SuperContentReportBatchProcessForm' + produces: + - application/json + responses: + "200": + description: Processed + schema: + type: string + summary: Batch process content reports + tags: + - Content /super/v1/contents: get: consumes: diff --git a/docs/superadmin_progress.md b/docs/superadmin_progress.md index 040223c..5840de8 100644 --- a/docs/superadmin_progress.md +++ b/docs/superadmin_progress.md @@ -5,7 +5,7 @@ ## 1) 总体结论 - **已落地**:登录、租户/用户/订单/内容基础管理、内容审核(含批量)、平台概览(内容趋势/退款率/漏斗)、提现审核、钱包流水与异常排查、报表概览与导出、用户钱包/通知/优惠券/实名/充值记录、互动(收藏/点赞/关注)与内容消费明细视图、创作者申请/成员审核/邀请、优惠券创建/编辑/发放/冻结/发放记录/异常核查、资产治理(列表/用量/清理)、通知中心(列表/群发/模板)、审计日志与系统配置、评论治理。 -- **部分落地**:内容治理(缺批量处置扩展)、创作者治理(缺提现审核联动与结算账户审批流)。 +- **部分落地**:暂无。 - **未落地**:暂无。 ## 2) 按页面完成度(对照 2.x) @@ -51,9 +51,9 @@ - 缺口:无显著功能缺口。 ### 2.9 创作者与成员审核 `/superadmin/creators` -- 状态:**部分完成** -- 已有:创作者(租户)列表、状态更新、创作者申请审核、成员申请列表/审核、成员邀请创建、结算账户列表与删除。 -- 缺口:结算账户审批流(若需要区分通过/驳回状态)。 +- 状态:**已完成** +- 已有:创作者(租户)列表、状态更新、创作者申请审核、成员申请列表/审核、成员邀请创建、结算账户列表与删除、结算账户审核。 +- 缺口:无显著功能缺口。 ### 2.10 优惠券 `/superadmin/coupons` - 状态:**已完成** @@ -88,9 +88,8 @@ ## 3) `/super/v1` 接口覆盖度概览 - **已具备**:Auth、Tenants(含成员审核/邀请)、Users(含钱包/通知/优惠券/实名/互动/内容消费)、Contents(含评论治理)、Orders、Withdrawals、Finance(流水/异常)、Reports、Coupons(列表/创建/编辑/发放/冻结/记录)、Creators(列表/申请/成员审核)、Payout Accounts(列表/删除)、Assets(列表/用量/删除)、Notifications(列表/群发/模板)。 -- **缺失/待补**:内容治理批量处置扩展(举报/下架/封禁等批量操作)。 +- **缺失/待补**:无。 ## 4) 建议的下一步(按优先级) -1. **内容批量处置扩展**:补齐内容/举报的批量下架、封禁与归档处理。 -2. **创作者治理补强**:结算账户审批流、提现审核联动流程。 +暂无。 diff --git a/frontend/superadmin/src/service/ContentService.js b/frontend/superadmin/src/service/ContentService.js index 694d3df..2fbf499 100644 --- a/frontend/superadmin/src/service/ContentService.js +++ b/frontend/superadmin/src/service/ContentService.js @@ -276,5 +276,19 @@ export const ContentService = { reason } }); + }, + async batchProcessContentReports({ report_ids, action, content_action, reason } = {}) { + if (!Array.isArray(report_ids) || report_ids.length === 0) { + throw new Error('report_ids is required'); + } + return requestJson('/super/v1/content-reports/process/batch', { + method: 'POST', + body: { + report_ids, + action, + content_action, + reason + } + }); } }; diff --git a/frontend/superadmin/src/views/superadmin/Contents.vue b/frontend/superadmin/src/views/superadmin/Contents.vue index 3e167fc..e03eefa 100644 --- a/frontend/superadmin/src/views/superadmin/Contents.vue +++ b/frontend/superadmin/src/views/superadmin/Contents.vue @@ -79,6 +79,7 @@ const reportHandledAtFrom = ref(null); const reportHandledAtTo = ref(null); const reportSortField = ref('created_at'); const reportSortOrder = ref(-1); +const selectedReports = ref([]); const commentDeleteDialogVisible = ref(false); const commentDeleteLoading = ref(false); @@ -92,6 +93,13 @@ const reportProcessContentAction = ref('unpublish'); const reportProcessReason = ref(''); const reportProcessTarget = ref(null); +const reportBatchDialogVisible = ref(false); +const reportBatchSubmitting = ref(false); +const reportBatchAction = ref('approve'); +const reportBatchContentAction = ref('unpublish'); +const reportBatchReason = ref(''); +const reportBatchTargetIDs = ref([]); + const reviewDialogVisible = ref(false); const reviewSubmitting = ref(false); const reviewAction = ref('approve'); @@ -176,6 +184,11 @@ function getContentID(row) { return Number.isFinite(id) ? id : 0; } +function getReportID(row) { + const id = Number(row?.id ?? 0); + return Number.isFinite(id) ? id : 0; +} + function formatDate(value) { if (!value) return '-'; if (String(value).startsWith('0001-01-01')) return '-'; @@ -369,9 +382,12 @@ function getReportActionLabel(value) { } const selectedCount = computed(() => selectedContents.value.length); +const reportSelectedCount = computed(() => selectedReports.value.length); const reviewTargetCount = computed(() => reviewTargetIDs.value.length); const batchStatusTargetCount = computed(() => batchStatusTargetIDs.value.length); const reportProcessNeedsContentAction = computed(() => reportProcessAction.value === 'approve'); +const reportBatchTargetCount = computed(() => reportBatchTargetIDs.value.length); +const reportBatchNeedsContentAction = computed(() => reportBatchAction.value === 'approve'); async function loadContents() { loading.value = true; @@ -632,6 +648,7 @@ async function loadReports() { sortOrder: reportSortOrder.value }); reports.value = result.items; + selectedReports.value = []; reportTotalRecords.value = result.total; } catch (error) { toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载举报列表', life: 4000 }); @@ -699,6 +716,60 @@ async function confirmReportProcess() { } } +function openReportBatchDialog(action) { + if (!selectedReports.value.length) { + toast.add({ severity: 'warn', summary: '请先选择举报', detail: '至少选择 1 条举报进行处理', life: 3000 }); + return; + } + const invalid = selectedReports.value.filter((row) => row?.status !== 'pending'); + if (invalid.length) { + toast.add({ severity: 'warn', summary: '选择包含已处理举报', detail: '仅可批量处理待处理举报', life: 3000 }); + return; + } + const ids = selectedReports.value.map((row) => getReportID(row)).filter((id) => id > 0); + if (!ids.length) { + toast.add({ severity: 'warn', summary: '选择无效', detail: '未识别到可处理的举报', life: 3000 }); + return; + } + reportBatchTargetIDs.value = ids; + reportBatchAction.value = action || 'approve'; + reportBatchContentAction.value = 'unpublish'; + reportBatchReason.value = ''; + reportBatchDialogVisible.value = true; +} + +async function confirmReportBatchProcess() { + const ids = reportBatchTargetIDs.value.filter((id) => id > 0); + if (!ids.length) return; + + const action = reportBatchAction.value; + const contentAction = action === 'approve' ? reportBatchContentAction.value : 'ignore'; + const reason = reportBatchReason.value.trim(); + if (action === 'reject' && !reason) { + toast.add({ severity: 'warn', summary: '请输入原因', detail: '驳回举报时需填写原因', life: 3000 }); + return; + } + + reportBatchSubmitting.value = true; + try { + await ContentService.batchProcessContentReports({ + report_ids: ids, + action, + content_action: contentAction, + reason: reason || undefined + }); + toast.add({ severity: 'success', summary: '已处理', detail: `已处理 ${ids.length} 条举报`, life: 3000 }); + reportBatchDialogVisible.value = false; + reportBatchTargetIDs.value = []; + selectedReports.value = []; + await loadReports(); + } catch (error) { + toast.add({ severity: 'error', summary: '处理失败', detail: error?.message || '无法处理举报', life: 4000 }); + } finally { + reportBatchSubmitting.value = false; + } +} + const unpublishDialogVisible = ref(false); const unpublishLoading = ref(false); const unpublishItem = ref(null); @@ -1072,7 +1143,12 @@ watch( 举报列表 跨租户内容举报处理 - +