feat: 完善养成管理后台功能

This commit is contained in:
richjohnrambo
2026-09-19 17:07:47 +08:00
parent f6766d3dbd
commit c7265252d8
13 changed files with 1257 additions and 61 deletions
+397 -4
View File
@@ -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
}
+66 -1
View File
@@ -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
}