# 您每年的欧盟DSA合规工作流程是怎样的？

**URL:** <https://meta.discourse.org/t/whats-your-workflow-for-the-annual-eu-dsa-compliance/396390>\
**Category:** Data & reporting\
**Created:** [2026年二月18日 13:16 UTC](https://meta.discourse.org/t/whats-your-workflow-for-the-annual-eu-dsa-compliance/396390 "2026-02-18T13:16:18Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![sps](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sps/32/380186_2.png) [@sps](https://meta.discourse.org/u/sps)\
**Post date:** [2026年二月18日 13:16 UTC](https://meta.discourse.org/t/whats-your-workflow-for-the-annual-eu-dsa-compliance/396390/1 "2026-02-18T13:16:18Z")

</div>

大家好，

我最近一直在与一位客户合作，为他们准备年度报告以符合欧盟《数字服务法案》（DSA）的要求，这让我意识到，对于在欧盟运营的 Discourse 社区的许多版主和管理员来说，这很可能是一项重复性工作。

鉴于报告流程是重复且注重细节的，我很好奇大家是如何处理的。

- 你们为准备准确的年度报告开发了哪些工作流程？
- 你们是依赖特定的插件、自定义脚本、数据探索器查询还是外部工具？
- 你们是否实施了任何自动化来简化数据收集或报告？
- Discourse 是否提供任何内置功能可以有意义地简化或自动化 DSA 报告流程的某些部分？

我注意到 Discourse 本身会发布结构良好的 DSA 透明度报告（例如：[https://discourse.org/dsa-2025），这作为参考非常有帮助。](https://discourse.org/dsa-2025%EF%BC%89%EF%BC%8C%E8%BF%99%E4%BD%9C%E4%B8%BA%E5%8F%82%E8%80%83%E9%9D%9E%E5%B8%B8%E6%9C%89%E5%B8%AE%E5%8A%A9%E3%80%82)

我非常想听听不同的社区是如何应对这一挑战的。

期待从大家的集体经验中学习。

提前感谢！

---

<div class="post-metadata">

**Author:** ![HAWK](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/hawk/32/86627_2.png) [@HAWK](https://meta.discourse.org/u/HAWK)\
**Post date:** [2026年二月18日 19:45 UTC](https://meta.discourse.org/t/whats-your-workflow-for-the-annual-eu-dsa-compliance/396390/4 "2026-02-18T19:45:48Z")

</div>

> [@sps](#):
>
> 我看到 Discourse 本身会发布结构良好的 DSA 透明度报告（例如：[数字服务法案 2025 (DSA) 透明度报告 | Discourse - 文明讨论](https://discourse.org/dsa-2025)），这对于参考非常有帮助。

我上周第一次创建了该报告。使用数据资源管理器（[Data Explorer](https://meta.discourse.org/t/32566?silent=true)）来完成它非常快速和简单。以下是我使用的查询（功劳归于 @SaraDev 编写它们）：

### DSA 报告

```sql
-- [params]
-- date :start_date = 2020-01-01
-- date :end_date = 2026-01-01

WITH flag_data AS (
    SELECT
        r.id AS flag_id,
        p.id AS post_id,
        p.topic_id,
        p.raw AS flagged_item_text,
        p.user_id AS post_author_id,
        fu.username AS flagged_by_username,
        r.created_at AS flagged_date,
        r.type AS flag_type,
        r.reviewable_by_moderator AS flag_source,
        r.payload AS flag_reason,
        r.status AS review_status,
        rs.reviewed_by_id,
        rs.reviewed_at,
        rs.score AS review_score,
        ru.username AS reviewed_by_username,
        p.deleted_at AS post_deleted_at,
        u.silenced_till AS user_silenced_till,
        u.suspended_till AS user_suspended_till,
        p.hidden_at AS post_hidden_at,
        pa.post_action_type_id
    FROM
        reviewables r
    LEFT JOIN posts p ON r.target_id = p.id AND r.target_type = 'Post'
    LEFT JOIN users fu ON r.created_by_id = fu.id
    LEFT JOIN reviewable_scores rs ON rs.reviewable_id = r.id
    LEFT JOIN users ru ON rs.reviewed_by_id = ru.id
    LEFT JOIN users u ON p.user_id = u.id
    LEFT JOIN post_actions pa ON pa.post_id = p.id
    WHERE
        r.created_at BETWEEN :start_date AND :end_date
        AND r.status = 1 -- 仅包含已同意并已采取行动的标记
),
flag_types AS (
    SELECT
        3 AS post_action_type_id, 'Off-topic' AS flag_type_name
    UNION ALL
    SELECT
        4 AS post_action_type_id, 'Inappropriate' AS flag_type_name
    UNION ALL
    SELECT
        6 AS post_action_type_id, 'Notify_user' AS flag_type_name
    UNION ALL
    SELECT
        7 AS post_action_type_id, 'Notify_moderators' AS flag_type_name
    UNION ALL
    SELECT
        8 AS post_action_type_id, 'Spam' AS flag_type_name
    UNION ALL
    SELECT
        10 AS post_action_type_id, 'Illegal' AS flag_type_name
),
median_time_to_act AS (
    SELECT
        CASE
            WHEN ft.flag_type_name IS NOT NULL THEN ft.flag_type_name
            WHEN fd.flag_type IN (
                'ReviewableAkismetPost',
                'ReviewableFlaggedPost',
                'ReviewableChatMessage',
                'ReviewablePost',
                'ReviewableQueuedPost'
            ) THEN 'something_else'
            ELSE fd.flag_type
        END AS flag_type,
        ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM (fd.reviewed_at - fd.flagged_date))) / 60) AS median_time_minutes
    FROM
        flag_data fd
    LEFT JOIN flag_types ft 
        ON fd.post_action_type_id = ft.post_action_type_id
    WHERE
        fd.reviewed_at IS NOT NULL
        AND (
            ft.flag_type_name IS NOT NULL -- 包含已映射的标记类型
            OR fd.flag_type IN (
                'ReviewableAkismetPost',
                'ReviewableUser',
                'ReviewableFlaggedPost',
                'ReviewableChatMessage',
                'ReviewablePost',
                'ReviewableQueuedPost'
            ) -- 包含特定标记类型以获得 NULL 结果
        )
    GROUP BY
        CASE
            WHEN ft.flag_type_name IS NOT NULL THEN ft.flag_type_name
            WHEN fd.flag_type IN (
                'ReviewableAkismetPost',
                'ReviewableFlaggedPost',
                'ReviewableChatMessage',
                'ReviewablePost',
                'ReviewableQueuedPost'
            ) THEN 'something_else'
            ELSE fd.flag_type
        END
)
SELECT
    CASE
        WHEN ft.flag_type_name IS NOT NULL THEN ft.flag_type_name
        WHEN fd.flag_type IN (
            'ReviewableAkismetPost',
            'ReviewableFlaggedPost',
            'ReviewableChatMessage',
            'ReviewablePost',
            'ReviewableQueuedPost'
        ) THEN 'something_else'
        ELSE fd.flag_type
    END AS Type,
    COUNT(CASE WHEN fd.flagged_by_username NOT IN ('spam_scanner_bot', 'system') THEN 1 END) AS Reported,
    COUNT(CASE WHEN fd.flagged_by_username IN ('spam_scanner_bot', 'system') THEN 1 END) AS Automated,
    COUNT(*) AS Total,
    COALESCE(mta.median_time_minutes, 0) AS "Median time to act (minutes)",
    COUNT(CASE WHEN fd.user_silenced_till IS NOT NULL THEN 1 END) AS "User silenced",
    COUNT(CASE WHEN fd.user_suspended_till IS NOT NULL THEN 1 END) AS "User deleted",
    COUNT(CASE WHEN fd.post_deleted_at IS NOT NULL THEN 1 END) AS "Post deleted",
    COUNT(CASE WHEN fd.post_hidden_at IS NOT NULL THEN 1 END) AS "Post hidden"

FROM
    flag_data fd
LEFT JOIN flag_types ft 
    ON fd.post_action_type_id = ft.post_action_type_id
LEFT JOIN median_time_to_act mta 
    ON CASE
        WHEN ft.flag_type_name IS NOT NULL THEN ft.flag_type_name
        WHEN fd.flag_type IN (
            'ReviewableAkismetPost',
            'ReviewableFlaggedPost',
            'ReviewableChatMessage',
            'ReviewablePost',
            'ReviewableQueuedPost'
        ) THEN 'something_else'
        ELSE fd.flag_type
    END = mta.flag_type
WHERE
    ft.flag_type_name IS NOT NULL -- 包含已映射的标记类型
    OR fd.flag_type IN (
        'ReviewableAkismetPost',
        'ReviewableUser',
        'ReviewableFlaggedPost',
        'ReviewableChatMessage',
        'ReviewablePost',
        'ReviewableQueuedPost'
    ) -- 包含特定标记类型以获得 NULL 结果
GROUP BY
    CASE
        WHEN ft.flag_type_name IS NOT NULL THEN ft.flag_type_name
        WHEN fd.flag_type IN (
            'ReviewableAkismetPost',
            'ReviewableFlaggedPost',
            'ReviewableChatMessage',
            'ReviewablePost',
            'ReviewableQueuedPost'
        ) THEN 'something_else'
        ELSE fd.flag_type
    END,
    mta.median_time_minutes
ORDER BY
    Total DESC

```

### 已采取的版主操作

```sql
-- [params]
-- date :start_date = 2024-01-01
-- date :end_date = 2025-01-01
-- null int :action_type

SELECT 
    uh.acting_user_id,
    uh.action AS action_type,
    CASE uh.action
        WHEN 1 THEN 'delete_user'
        WHEN 10 THEN 'suspend_user'
        WHEN 11 THEN 'unsuspend_user'
        WHEN 17 THEN 'delete_post'
        WHEN 18 THEN 'delete_topic'
        WHEN 25 THEN 'reviewed_post'
        WHEN 30 THEN 'silence_user'
        WHEN 31 THEN 'unsilence_user'
        WHEN 39 THEN 'deactivate_user'
        WHEN 41 THEN 'lock_trust_level'
        WHEN 42 THEN 'unlock_trust_level'
        WHEN 47 THEN 'notified_about_get_a_room'
        WHEN 49 THEN 'post_locked'
        WHEN 50 THEN 'post_unlocked'
        WHEN 56 THEN 'post_approved'
        WHEN 60 THEN 'removed_silence_user'
        WHEN 61 THEN 'removed_suspend_user'
        WHEN 62 THEN 'removed_unsilence_user'
        WHEN 63 THEN 'removed_unsuspend_user'
        WHEN 64 THEN 'post_rejected'
        WHEN 69 THEN 'approve_user'
        WHEN 95 THEN 'post_staff_note_create'
        WHEN 96 THEN 'post_staff_note_destroy'
        ELSE 'unknown_action'
    END AS action_name,
    uh.target_user_id AS user_id,
    uh.subject AS "Subject", -- 操作的主题
    uh.created_at AS "When", -- 操作的时间戳
    uh.details AS "Details", -- 操作的附加详情
    uh.context AS "Context", -- 操作的上下文
    uh.previous_value,
    uh.new_value,
    u.suspended_till,
    u.silenced_till,
    uh.topic_id AS topic_id,
    uh.post_id AS post_id,
    uh.category_id AS category_id,
    uh.custom_type
FROM 
    user_histories uh
LEFT JOIN 
    users u ON uh.target_user_id = u.id
WHERE 
    uh.created_at BETWEEN :start_date AND :end_date
    AND uh.action IN (
        1, -- delete_user
        10, -- suspend_user
        11, -- unsuspend_user
        17, -- delete_post
        18, -- delete_topic
        25, -- reviewed_post
        30, -- silence_user
        31, -- unsilence_user
        39, -- deactivate_user
        41, -- lock_trust_level
        42, -- unlock_trust_level
        47, -- notified_about_get_a_room
        49, -- post_locked
        50, -- post_unlocked
        56, -- post_approved
        60, -- removed_silence_user
        61, -- removed_suspend_user
        62, -- removed_unsilence_user
        63, -- removed_unsuspend_user
        64, -- post_rejected
        69, -- approve_user
        95, -- post_staff_note_create
        96 -- post_staff_note_destroy
    )
    AND (:action_type IS NULL OR uh.action = :action_type)
ORDER BY 
    uh.created_at DESC

```

以及有关发出的警告：[https://meta.discourse.org/admin/reports/moderator\_warning\_private\_messages?end\_date=2024-12-31&mode=table&start\_date=2024-01-01](https://meta.discourse.org/admin/reports/moderator_warning_private_messages?end_date=2024-12-31&mode=table&start_date=2024-01-01)

---

<div class="post-metadata">

**Author:** ![sps](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sps/32/380186_2.png) [@sps](https://meta.discourse.org/u/sps)\
**Post date:** [2026年二月18日 20:34 UTC](https://meta.discourse.org/t/whats-your-workflow-for-the-annual-eu-dsa-compliance/396390/5 "2026-02-18T20:34:12Z")

</div>

嘿 @HAWK

这超级有帮助。感谢分享。

> **我有几个后续问题。**
>
> 1. 关于 DSA 报告查询，开始日期是 2020 年，而已发布的 DSA 报告列出的期间是 2025 年 1 月 1 日至 12 月 31 日。这与此查询或 Discourse 的工作方式有关吗？
> 
> > [@HAWK](#):
> >
> > ```plaintext
> > -- [params]
> > -- date :start_date = 2020-01-01
> > -- date :end_date = 2026-01-01
> > 
> > ```
> 
> 1. 版主操作查询是否也考虑了未列出的主题？我们的实例启用了 [Discourse AI - AI triage](https://meta.discourse.org/t/discourse-ai-ai-triage/281227)，其操作之一是自动取消列出超出范围的主题。

---

<div class="post-metadata">

**Author:** ![HAWK](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/hawk/32/86627_2.png) [@HAWK](https://meta.discourse.org/u/HAWK)\
**Post date:** [2026年二月18日 20:39 UTC](https://meta.discourse.org/t/whats-your-workflow-for-the-annual-eu-dsa-compliance/396390/6 "2026-02-18T20:39:32Z")

</div>

> [@sps](#):
>
> 在 DSA 报告查询中，开始日期是 2020 年，而已发布的 DSA 报告列出的期间是 2025 年 1 月 1 日至 12 月 31 日。这与此查询或 Discourse 的工作方式有关吗？

当您在 DE 中运行它时，会有字段来设置参数。这些只是默认值。

> [@sps](#):
>
> 版主操作查询是否也考虑了未列出的主题？我们在实例上启用了 [Discourse AI - AI triage](https://meta.discourse.org/t/discourse-ai-ai-triage/281227)，其操作之一是自动取消列出超出范围的主题。

是的，它还会记录由 `@system` 用户执行的操作。

---

<div class="post-metadata">

**Author:** ![pfaffman](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/pfaffman/32/120154_2.png) [@pfaffman](https://meta.discourse.org/u/pfaffman)\
**Post date:** [2026年二月18日 21:01 UTC](https://meta.discourse.org/t/whats-your-workflow-for-the-annual-eu-dsa-compliance/396390/7 "2026-02-18T21:01:23Z")

</div>

好的。你应该把它们添加到库存查询中，并在标题中加入一些明显的 DSA。
