1. Bussiness | Logic
Traffic Tool
  • Traffic Tool Docs
    • Tổng Quan
    • project
      • admin
        • Hướng dẫn sử dung
          • Đăng Nhập & Xác Thực Hai Bước
          • Thống Kê Hệ Thống
          • Thống Kê Hàng Ngày
          • Tìm Kiếm Tự Động
          • Trung Tâm Báo Cáo
          • Quản Lý Tài Khoản
          • Quản Lý Server
          • Quản Lý Proxy
          • Quản Lý Gói Proxy
          • Quản Lý Nhà Cung Cấp Proxy
          • Quản Lý Backup Proxy
          • Quản Lý Tài Khoản Google
      • api + tool
        • Hướng dẫn sử dụng
          • User Guide
          • Setup Guide
        • Bussiness | Logic
          • 1. Thống kê hàng ngày (Daily Statistics)
          • 2. Thống kê hệ thống (System Statistics)
          • 3. Tìm kiếm tự động (Automated Discovery)
          • 4. Trung tâm Báo cáo (Report Center)
          • 5. Quản lý tài khoản (Account Management)
          • 6. Quản lý Hệ thống (System Management)
          • 7. Quản lý tài khoản Google (Google Account Management)
        • Architecture
          • Database Schema
          • System Architecture
          • Code Structure
          • Logs and Monitoring
          • Environment & Configuration
        • Deployment
          • Local
          • Staging
          • Product
  • Traffic Backend API
    • 🔑 Identity & Session
      • Đăng nhập hệ thống (Login)
      • Đăng ký tài khoản mới (Public)
      • Lấy danh sách người dùng (Phân trang)
      • Admin tạo người dùng mới
      • Lấy thông tin cá nhân hiện tại
      • Khởi tạo bảo mật 2FA
      • Xác thực mã OTP
      • Chi tiết người dùng theo ID
      • Cập nhật thông tin người dùng
      • Xóa tài khoản người dùng
      • Đổi mật khẩu
      • Kiểm tra Cấu hình Thông báo Telegram Toàn hệ thống
      • Bật/Tắt Thông báo Telegram Toàn cục
    • 📁 Campaign Management
      • Danh sách Chiến dịch Toàn cầu
      • Khởi tạo Chiến dịch Mới
      • Chi tiết Chiến dịch
      • Cập nhật Chiến dịch
      • Xóa Chiến dịch
      • Tắt/Mở Chiến dịch (Hàng loạt)
      • Dữ liệu Hình mẫu SEO (Negative SEO)
    • ⚙️ Project Mechanics
      • Danh sách Dự án (Projects List)
      • Khởi tạo Kịch bản Mô phỏng
      • Lấy chi tiết cấu hình Dự án
      • Cập nhật Kịch bản chạy
      • Xóa Dự án
      • Chỉnh sửa Hàng loạt (Bulk)
      • Bật/Tắt Dự án
    • 👤 Profile Management
      • Danh sách Vân tay số (Profiles Data)
      • Tạo Hồ sơ Đơn lẻ (Tạo Vân tay mới)
      • Nhập kho Tài khoản Email (Bulk Import)
      • Chi tiết Session/Cookies
      • Chỉnh sửa Hồ sơ/Ghi chú
      • Xóa Vân tay số và Dữ liệu Local
      • Mở khóa Captcha/Trạng thái Blocked
      • Tra cứu Kho Profile Khả dụng
      • Báo cáo Sức khỏe Kho Tài Khoản
    • 🦾 Worker Interface
      • Đăng ký Khởi tạo Node (Handshake)
      • Nhịp Tim Khảo Sát Tình Trạng (Heartbeat)
      • Nhận Cấu hình Bypass & Hệ thống
      • Kéo (PULL) Nhiệm vụ Traffic SEO
      • Báo cáo Sự cố Node (Crash Report)
    • 📊 Report: Execution
      • Kéo Công việc Báo cáo (Worker Pull)
      • Quản lý Giám sát Nhiệm vụ (Task Dashboard)
      • Bắn Lại Báo Cáo Thất Bại (Manual Retry)
    • 📊 Report: Discovery
      • Danh sách Tên miền Chờ Xử Lý (Discovery Pool)
      • Nhập Mục Tiêu Thủ Công (Manual Insert)
      • Kích hoạt Heuristic Scanner (Cào tự động)
      • Chi tiết Bằng Chứng (Evidence Data)
      • Dán nhãn Vi Phạm / Cập nhật Screenshot
      • Loại Bỏ Mục Tiêu
      • Duyệt Yêu Cầu (Approve to Execution)
    • 📊 Report: Platforms Configuration
      • Truy vấn Danh sách Nền tảng Đối tác (Vendor)
      • Tạo Nền tảng Vendor Báo cáo Mới
      • Bật/Tắt Trang báo cáo theo Loại
      • Cập nhật Metadata Trang
      • Xóa Trang Báo Cáo
      • Lấy Cài đặt LLM cho Vendor cụ thể
      • Cập nhật Cài đặt Tạo mẫu LLM
      • Lấy Cấu hình Schema Biểu mẫu cho Vendor
      • Cập nhật Yêu cầu Trường Biểu mẫu
    • 📊 Report: Email Automation
      • Danh sách SMTP/Mailer Server
      • Thêm kết nối SMTP Mới
      • Nhật ký Nhiệm vụ Gửi Mail (Email Log)
      • Phân tích Tỉ lệ Chuyển đổi (Email Delivery Analytics)
    • 🌐 Global Proxies
      • Danh sách Kho Proxy Phân trang (Proxy Pool)
      • Thêm mới Tuyến IP (Bulk Import)
      • Cập nhật Thông tin máy chủ Proxy
      • Xóa Proxy (Thu hồi tài nguyên)
      • Kích hoạt Xoay vòng (Rotate IP) Cưỡng bức
    • 📈 System Intelligence
      • Báo cáo Luồng Bảo mật Hợp nhất (Colossal Report)
      • Bản Đồ Lưu Lượng GeoIP (Heatmap)
    • 🪝 Webhook Integrations
      • Callback Hoàn tất Gói Lưu lượng SEO (Traffic Node)
      • Callback Thông báo Hoàn tất Bắn Report AI
    • Schemas
      • AppError
      • LoginRequest
      • UserResponseDto
      • UserModel
      • CreateUserRequest
      • UpdateUserRequest
      • PagingInfo
      • ProfileModel
      • CreateProfileRequest
      • BulkImportProfileReq
      • UpdateProfileRequest
      • CampaignModel
      • CampaignConfigs
      • CreateCampaignRequest
      • UpdateCampaignRequest
      • BulkUpdateCampaignStatusReq
      • NegativeSeoData
      • CreateProjectRequest
      • UpdateProjectRequest
      • ProjectModel
      • BulkUpdateProjectRequest
      • ProjectAttribute
      • WorkerHandshakeRequest
      • WorkerHandshakeResponse
      • WorkerHeartbeatPayload
      • WorkerGlobalSettings
      • TaskPullRequest
      • WorkerFatalLog
      • TaskModel
      • TaskTrafficType
      • TaskUpdateDto
      • ReportTaskModel
      • ReportTaskResult
      • PCReportResponse
      • SummaryStats
      • DailyStats
      • BrandStats
      • ProfileStats
      • ReportTaskItem
      • ReportSiteItem
      • CreateReportSiteRequest
      • GPTConfig
      • ResponseConfig
      • FieldConfig
      • ReportDomainsResponse
      • ReportDomainItem
      • AddReportDomainRequest
      • UpdateReportDomainRequest
      • BulkUpdateDashboardRequest
      • ProxyModel
      • CreateProxyRequest
      • UpdateProxyRequest
      • SMTPServerConfig
      • EmailTaskLog
      • GeoLocation
  • Traffic Tools V2 API
    • Tasks
      • Lọc và Truy vấn Nhật ký Phiên Duyệt lẻ
      • Get all tasks
      • Nạp Kết Quả Chạy của Puppeteer (Update Callback)
      • Xóa toàn bộ Tasks lẻ
      • Get all tasks
      • Xóa một Task (ID)
    • Group task
      • Đẩy nhóm công việc Traffic theo lô (Batch Pipeline Task Creation)
      • Lấy sơ đồ trạng thái hàng đợi Group Tasks
      • Endpoint /api/task-traffics/
      • Clear/Flush Hàng đợi Group Task (Kill Queue)
      • Endpoint /api/task-traffics/
      • Xóa 1 nhóm nhiệm vụ (Theo ObjectID Mongoose)
      • Parse chuỗi Proxy
    • Test
      • Kiểm tra Proxy hoạt động ngầm
      • Endpoint /api/test
      • Kho Fingerprint Thử Nghiệm
      • Endpoint /api/test/account-stats
      • Endpoint /api/test/session-recommendation
      • Endpoint /api/test/check-browser
      • Endpoint /api/test/clear-browser-sessions
    • Proxies
      • Lấy kho Proxy server
      • Khai báo Pool Tuyến Proxy mới
      • Get all proxies
      • Get all proxies
      • Xóa toàn bộ Proxy
      • Get all proxies
      • Xóa Proxy theo ID
      • Reset lại số đếm tiến trình (Cron Proxy Thread)
    • Report Platforms
      • Submit report to multiple platforms
      • Submit report to specific platform
      • Get available platforms
      • Get platform statistics
      • Health check
    • Reports
      • Create a new report
      • Create a new report
      • Get report by ID
      • Get report by ID
      • Get report statistics
      • Get report groups
      • Get report groups
      • Get report group
      • Add report to group
      • Bulk add reports to group
      • Get report group statistics
      • Get analytics data
      • Get platform success rates
      • Get reports by platform
      • Get reports by group
      • Create test reports in bulk
    • Test Report
      • Test Microsoft report submission
      • Test AdGuard report submission
      • Test Google Ads report submission
      • Test Spamhaus report submission
      • Test ESET report submission
      • Test multi-platform report submission
      • Get available platforms
    • ESET Test
      • Simple test
      • Minimal POST test
      • Generate ESET report content
      • Get sample test data
      • Test form filling
      • Test full form submission
    • Spamhaus Test
      • Simple test
      • Minimal POST test
      • Generate Spamhaus report content
      • Get sample test data
      • Test Spamhaus form filling
      • Test full Spamhaus form submission
    • Report Scheduler
      • Get report scheduler statistics
      • Process ad detection result
      • Submit a scheduled report
      • Start report scheduler
      • Stop report scheduler
      • Get scheduler status
      • Process scheduled reports
      • Test browser session (non-headless)
      • Test report submission (non-headless)
    • Comprehensive Analytics
      • Get comprehensive dashboard analytics
      • Get overview statistics
      • Get brand-wise statistics
      • Get daily statistics
      • Get platform success rates
      • Get account performance
      • Get summary report
    • Search
      • Ra lệnh Máy Lọc Dữ Liệu TOP SEO Hàng Loại (Scraper Engine)
      • Đếm tổng số dòng tìm kiếm (Length)
    • Report Tasks
      • Tạo chiến dịch Report (Auto Spawn 14 tasks)
      • Tạo chiến dịch Report Form hàng loạt (Bulk Abuse Report)
      • Create Direct Report (Từ API hệ thống thay vì Frontend)
      • Tạo Nhiệm Vụ Sinh Email Khiếu nại (Automated Bulk Mail Spam)
      • Worker Pull - Lấy task Báo cáo đang Pending
      • Worker Trả kết quả (Report Status)
      • Danh mục Platfoms & Providers
      • Dashboard Statistic Báo Cáo
      • Force Run Cronjob
    • Monitor
      • Xuất dữ liệu đo đếm Hardware (Telemetry OS Watchdog)
    • Schemas
      • CreateTask
      • UpdateTask
      • CreateTaskGroupRequest
      • CreateProxy
      • CreateSearch
      • ReportTaskCreation
      • CreateEmail
      • CreateMultipleSearchRequest
      • CreateMultipleReportRequest
      • UpdateReportStatusRequest
      • TestProxyRequest
  1. Bussiness | Logic

2. Thống kê hệ thống (System Statistics)

ĐẶC TẢ KỸ THUẬT: THỐNG KÊ HỆ THỐNG (DASHBOARD1)

Route: /dashboard
Source Frontend: admin/src/views/dashboard1/
Source Backend: api/internal/campaign/
Database: MySQL (bảng campaigns, projects, project_details)


MỤC LỤC

  • I. Tổng quan chức năng và ba loại chiến dịch
  • II. Sự khác biệt cốt lõi giữa ba tab (flag comparison)
  • III. Kiến trúc giao diện (IndexView)
  • IV. Tab Click Quảng Cáo (ClickAds.vue)
    • IV.1. Lifecycle & Init
    • IV.2. Bộ lọc ngày tháng
    • IV.3. Component CampaignItem
    • IV.4. Component ListGroup — Bảng dự án
    • IV.5. Các cột trong bảng (useDataTableColumns)
    • IV.6. Các Modal: Create/Update Campaign & Project
    • IV.7. Pinia Store (clickAdsStore)
  • V. Tab Negative SEO (NegativeSeo.vue)
    • V.1. Điểm khác biệt so với Click Ads
    • V.2. Cấu trúc dữ liệu NegativeSeo.Item
    • V.3. Pinia Store (negativeStore)
  • VI. Tab Traffic Tự Nhiên (TrafficNatural.vue)
  • VII. Backend: Danh mục Endpoint
  • VIII. Backend: GetListWithPagination — query phức tạp nhất
    • VIII.1. Query không có date range
    • VIII.2. Query có date range
    • VIII.3. Logic buildStatusStateFilter
  • IX. Backend: GetNegativeSeoData và GetHomeTrafficData
    • IX.1. SQL Negative SEO — các flag bắt buộc
    • IX.2. SQL Home Traffic — các flag bắt buộc
    • IX.3. Handler timeout 180 giây
  • X. Backend: GetListAndExport — Xuất dữ liệu
  • XI. Bảng tra cứu: Flag project theo từng tab
  • XII. Điều gì xảy ra khi cấu hình sai flag
  • XIII. Luồng tạo Project (Create Modal)
  • XIV. Thao tác hàng loạt (Bulk Actions)
  • XV. Cột Traffic: total_success / total_process

I. Tổng quan chức năng và ba loại chiến dịch

Module Thống kê Hệ thống (Dashboard1) là nơi quản lý và giám sát toàn bộ các chiến dịch traffic đang chạy trong hệ thống. Nó chia làm ba tab tương ứng với ba mục tiêu nghiệp vụ khác nhau, mỗi tab được lọc bởi một tổ hợp flag khác nhau ở tầng database.

Ba tab:

  1. Click Quảng Cáo — Theo dõi các dự án có mục tiêu click vào quảng cáo Google Ads / CocCoc Ads của đối thủ.
  2. Negative SEO — Theo dõi các dự án đang tấn công website đối thủ bằng cách bơm traffic "kill" làm méo số liệu.
  3. Traffic Tự Nhiên — Theo dõi các dự án đang bơm traffic tự nhiên vào domain của khách hàng để tăng tín hiệu SEO.

Mỗi tab truy vấn một tập hợp con khác nhau của cùng một bảng projects, phân biệt bằng các cột flag: only_ads, is_kill, traffic_type.


II. Sự khác biệt cốt lõi giữa ba tab (flag comparison)

Đây là điểm quan trọng nhất của toàn bộ module. Ba tab không phải ba loại campaign khác nhau — tất cả đều dùng chung bảng projects. Sự phân loại chỉ dựa vào flag của từng project.

TrườngClick Quảng CáoNegative SEOTraffic Tự Nhiên
only_adstruefalsefalse
is_killfalsetruefalse
traffic_type111
organic_engine0 hoặc 10 hoặc 10 hoặc 1
Mục tiêuClick vào AdsPhá traffic đối thủTăng organic traffic
Hiển thị ở tabGetlist (/campaign/)/get-negative-seo-data/get-home-traffic-data

Giải thích cụ thể:

  • only_ads = true, is_kill = false: Worker sẽ tìm kiếm từ khóa, cuộn đến phần Ads được đánh dấu "Sponsored", click vào đúng URL của domain target. Đây là loại tốn tiền nhất cho đối thủ.

  • only_ads = false, is_kill = true: Worker click ngẫu nhiên vào nhiều link tìm kiếm, kể cả link đối thủ, để tạo ra lượng traffic không có giá trị chuyển đổi. Mục đích làm tăng tỷ lệ bounce, giảm Quality Score của Ads đối thủ.

  • only_ads = false, is_kill = false: Worker mô phỏng người dùng thật, tìm kiếm từ khóa, click vào kết quả organic của domain target, cuộn trang, ở lại đủ lâu. Mục đích gửi tín hiệu tích cực đến Google để tăng rank SEO.


III. Kiến trúc giao diện (IndexView)

File: admin/src/views/dashboard1/IndexView.vue

IndexView là container đơn giản với ba RouterLink và một <RouterView>. Nó không có state, không fetch data. Mỗi sub-tab tự quản lý toàn bộ vòng đời của mình.

Thanh tab pill navigation dùng CSS class .tab-item với background-color: #0a4e4f khi active. Không có kiểm soát phân quyền theo user ở tab này — tất cả user đều thấy cả ba tab.

Routes tương ứng:

  • RouteName.dashboard.click_ads → ClickAds.vue
  • RouteName.dashboard.negative_seo → NegativeSeo.vue
  • RouteName.dashboard.traffic_natural → TrafficNatural.vue

IV. Tab Click Quảng Cáo (ClickAds.vue)

File: admin/src/views/dashboard1/click-ads/ClickAds.vue

IV.1. Lifecycle & Init

Khi component mount (onMounted):

  1. Đặt urlParams.start_date và urlParams.end_date từ dateRange hiện tại (mặc định = ngày hôm nay).
  2. Đặt state.isPending = true.
  3. Gọi song song fetchData() và locationStore.fetchData() (để load danh sách địa điểm cho modal tạo project).
  4. Khi cả hai hoàn tất: state.isPending = false.

Khi component unmount (onUnmounted):

  • Gọi clickAdsStore.$reset() — xóa toàn bộ state về giá trị mặc định ban đầu.

Hàm fetchData:

const fetchData = async () => {
  await clickAdsStore.fetchData()
  if (clickAdsStore.listData?.data) {
    // Lưu bản sao deep clone để làm dữ liệu gốc cho filter
    defaultCampaigns.value = JSON.parse(JSON.stringify(clickAdsStore.listData.data))
    
    // Khôi phục lại các filter đã lưu trong store cho từng campaign
    clickAdsStore.listData.data.forEach((item) => {
      const savedFilters = clickAdsStore.campaignFilters[item.id]
      if (savedFilters?.dateRange !== null) {
        filtering.onChangeFilterProject(item.id, 'dateRange', savedFilters.dateRange)
      }
      if (savedFilters?.status?.length > 0) {
        filtering.onChangeFilterProject(item.id, 'status', savedFilters.status)
      }
    })
  }
}

Quan trọng: Sau khi fetch, hệ thống luôn khôi phục lại các filter trước đó (tìm kiếm theo domain, lọc theo status) cho từng campaign item. Điều này giúp người dùng không mất filter khi dữ liệu được refresh.

IV.2. Bộ lọc ngày tháng

dateRange là ref<[number, number] | null> với giá trị mặc định là ngày hôm nay:

const now = new Date()
const dateRange = ref<[number, number] | null>([now.getTime(), now.getTime()])

Khi dateRange thay đổi (watch):

  • Nếu ngày mới vượt quá ngày hôm nay: Tự động reset về ngày hôm nay (hệ thống không cho phép xem dữ liệu tương lai).
  • Ngược lại: Gọi fetchData() với date range mới.

Hàm onChangeFilter:

const onChangeFilter = (newValue: [number, number] | null) => {
  if (!newValue) {
    clickAdsStore.urlParams.start_date = null
    clickAdsStore.urlParams.end_date = null
    return
  }
  clickAdsStore.urlParams.start_date = dateFns.format(dateRange.value[0], 'yyyy-MM-dd')
  clickAdsStore.urlParams.end_date = dateFns.format(dateRange.value[1], 'yyyy-MM-dd')
}

Params gửi lên API có format yyyy-MM-dd (ví dụ: 2026-03-21).

IV.3. Component CampaignItem

File: admin/src/views/dashboard1/click-ads/components/CampaignItem.vue

Hiển thị một Campaign với toàn bộ danh sách project bên dưới.

Header của CampaignItem:

  • Tên chiến dịch (campaign.name) — Truncate ở 160px, hover show tooltip đầy đủ.
  • Tổng tên miền đã bắn — campaign.projects.length (tổng số project trong campaign, không phân trang).
  • Tổng traffic đã bắn — Computed từ useCampaignOperations.totalTraffic(item).
  • Nút "Bật tất cả" — Gọi apiProjects.bulkUpdate({ ids, fields: { status: 1 } }) với tất cả project ID trong campaign.
  • Nút "Tắt tất cả" — Tương tự nhưng status: 0. Cả hai đều hiển thị window.$dialog.warning để xác nhận trước.
  • Nút "Thêm từ khóa" — Emit create-project lên ClickAds.vue, set clickAdsStore.campaignId = campaign.id.
  • Input tìm kiếm — Nhập text, nhấn Enter hoặc click icon search để filter theo domain/keyword trong campaign.
  • Select Trạng thái — Filter multiple, mảng giá trị number[].
  • Nút Xuất file — Emit export, gọi API xuất Excel cho campaign cụ thể.

Hàm handleToggleAll:

const handleToggleAll = (status: number) => {
  window.$dialog.warning({
    title: `Xác nhận ${status === 1 ? 'Bật' : 'Tắt'} tất cả`,
    content: `Bạn có chắc chắn muốn ... ${props.campaign.projects.length} dự án?`,
    onPositiveClick: async () => {
      const ids = props.campaign.projects.map((p) => p.id)
      await apiProjects.bulkUpdate({ ids, fields: { status } })
      await clickAdsStore.fetchData()  // Refresh toàn bộ trang
    }
  })
}

IV.4. Component ListGroup — Bảng dự án

File: admin/src/views/dashboard1/click-ads/components/ListGroup.vue

Đây là bảng dữ liệu thực sự hiển thị các project trong một campaign.

Pagination phía client:

  • currentPage và pageSize (mặc định 20) là ref local — không yêu cầu thêm API call khi chuyển trang.
  • paginatedData = computed(() => projects.value.slice((currentPage-1)*pageSize, currentPage*pageSize))
  • Khi projects.length thay đổi: Reset về trang 1 và xóa selection.

Bulk Selection:

  • Sử dụng composable useBulkSelection<Task.Item>.
  • Cho phép chọn nhiều dòng và:
    • Bật đã chọn: apiProjects.bulkUpdate({ ids, fields: { status: 1 } })
    • Tắt đã chọn: apiProjects.bulkUpdate({ ids, fields: { status: 0 } })
    • Xóa đã chọn: apiProjects.bulkDelete(uniqueIds) — lấy project_id từ task, dedup bằng Set.

Lưu ý về ID trong bulk delete: Bảng hiển thị Task.Item, mỗi task có project_id. Khi xóa, hệ thống lấy project_id từ các task đã chọn, loại trùng lặp bằng [...new Set(ids)], sau đó gọi bulkDelete.

IV.5. Các cột trong bảng (useDataTableColumns)

File: admin/src/views/dashboard1/click-ads/composables/useDataTableColumns.ts

CộtKeyMô tảSortable
Selection-Checkbox chọn nhiều, fixed left-
STTidSố thứ tự (tính theo currentPage×pageSize)-
Tên miềnlinksLấy links[0], sort alphabeticalCó
Từ khóasearch_keywordTruncate + tooltipCó
Vị trílatest_rankRank hiện tại, sort numericCó
Ngày tạocreated_atFormat dateTime, sort by timestampCó
Thời gian chếtstate_timeThời điểm project chuyển trạng tháiCó
Tình trạngdomain_typeRender badge màu theo state-
Traffictotal_processHiển thị total_success / total_processCó
Chi tiếtactionsNút action (edit, chart,...), fixed right-
Trạng tháidomain_typeToggle on/off-

Cột Traffic:

render: (row: any) => {
  return h('span', { class: 'font-medium' }, [
    h('span', { class: 'text-[#18A058]' }, $n(row.total_success)),  // Màu xanh
    ' / ',
    h('span', {}, $n(row.total_process))                              // Màu đen
  ])
}

Ý nghĩa: total_success là số lượt traffic đã thực hiện thành công, total_process là budget tổng được phân bổ cho project.

Cột Tình trạng:
Hiển thị badge với màu sắc dựa theo state và status của project:

  • state = State_Kill (1) → Badge "Giết" màu đỏ
  • state = State_Dead (2) → Badge "Đã chết" màu xám
  • status = 0, state = State_Unknown → Badge "Không hoạt động"
  • status = 1, state = State_Unknown → Badge "Hoạt động" màu xanh

IV.6. Các Modal: Create/Update Campaign & Project

Trong ClickAds.vue có 6 modal:

  1. CreateCampaign — Tạo campaign mới.
  2. UpdateCampaign — Chỉnh sửa campaign. Mở bằng cách click icon edit ở CampaignItem, set clickAdsStore.campaignId = id.
  3. CreateModal — Tạo project mới. Đây là modal dùng chung admin/src/views/task/components/CreateModal.vue. Khi mở, clickAdsStore.campaignId phải đã được set.
  4. UpdateModal — Chỉnh sửa project. Tương tự dùng chung với task module.
  5. TrafficChart — Hiển thị biểu đồ traffic theo giờ của một project. Dữ liệu lấy từ clickAdsStore.trafficChartData.
  6. ChartDetailRanking — Biểu đồ ranking theo ngày, kết hợp với đường chết (deathTime).

Mở ChartDetailRanking:

  • stateModal.isShowChartDetailRanking = true
  • Cần có đủ data trong trafficRankingData:
    • dates: Mảng ngày trên trục X.
    • ranking: Vị trí SEO theo ngày.
    • deathTime: Thời điểm project bị đánh dấu Dead.
    • total_success: Mảng tổng traffic thành công theo ngày.

IV.7. Pinia Store (clickAdsStore)

File: admin/src/views/dashboard1/click-ads/stores/clickAdsStore.ts

State quan trọng:

{
  listData: Pagination<Campaign.Item> | undefined,   // Dữ liệu hiển thị chính
  listDataFilter: Pagination<Campaign.Item> | undefined, // Dữ liệu sau khi filter client-side
  urlParams: {
    page: 1,
    limit: 20,          // Số campaign mỗi trang
    keywords: '',       // Tìm kiếm theo tên campaign
    start_date: null,   // yyyy-MM-dd
    end_date: null
  },
  campaignId: null,     // Campaign đang được chọn (để tạo/sửa)
  campaignFilters: {},  // Lưu filter của từng campaign: { [campaignId]: { dateRange, status } }
  campaignFiltersSearch: {}, // Lưu search text của từng campaign: { [campaignId]: { searchText } }
  trafficRankingData: {
    dates: [],
    ranking: [],
    deathTime: [],
    total_success: []
  },
  state: {
    isFetching: true,
    isFetchingTrafficChart: false,
    isFetchingTrafficChartDetail: false
  }
}

campaignFilters và campaignFiltersSearch:
Đây là cơ chế lưu trữ filter riêng cho từng campaign item. Mỗi campaign có thể có một bộ filter độc lập. Khi user filter theo status ở Campaign A, Campaign B giữ nguyên trạng thái filter của mình.


V. Tab Negative SEO (NegativeSeo.vue)

File: admin/src/views/dashboard1/negative-seo/NegativeSeo.vue

V.1. Điểm khác biệt so với Click Ads

Negative SEO có kiến trúc UI hoàn toàn khác với Click Ads:

  • Click Ads: Tổ chức theo Campaign → Project (mỗi campaign là một card, trong đó có bảng project).
  • Negative SEO: Tổ chức theo Domain → Keyword (mỗi domain là một NCollapse cha, trong đó có danh sách các keyword đang bắn vào domain đó).

Sở dĩ cấu trúc khác vì trong Negative SEO, quan tâm chính không phải campaign nào đang chạy, mà là domain nào đang bị tấn công.

API endpoint cũng khác: /campaign/get-negative-seo-data thay vì /campaign/ (list campaign).

V.2. Cấu trúc dữ liệu NegativeSeo.Item

Mỗi item trong negativeStore.listData là một domain đang bị tấn công:

interface NegativeSeo.Item {
  domain: string          // Domain đối thủ (lấy từ links[0] của project)
  data_keywords: NegativeSeo.dataKeywords[]  // Danh sách keyword đang bắn vào domain này
}

interface NegativeSeo.dataKeywords {
  project_id: number      // ID project MySQL
  campaign_id: number
  search_keyword: string  // Từ khóa tìm kiếm
  ranking: number         // Vị trí tốt nhất tìm thấy (MIN(pd.rank))
  total_process: number   // SUM(pd.total_success) — traffic đã bắn
  keyword_created: string // Ngày tạo project
  status: number
  state: number
}

Query SQL tạo ra dữ liệu này (từ get_negative_seo.go):

SELECT
  p.id as project_id,
  p.campaign_id,
  p.search_keyword,
  REPLACE(JSON_EXTRACT(p.links, '$[0]'), '"', '') AS domain,
  MIN(pd.rank) as ranking,
  COALESCE(SUM(pd.total_success), 0) as total_process,
  p.created_at as keyword_created,
  p.status,
  p.state
FROM projects p
JOIN project_details pd ON pd.project_id = p.id
WHERE pd.date >= ? AND pd.date <= ?
  AND p.organic_engine IN (0, 1)
  AND p.only_ads = false
  AND p.is_kill = true        -- ← Flag bắt buộc để vào tab này
  AND p.traffic_type = 1
GROUP BY p.id
ORDER BY p.created_at ASC

Backend sau đó gọi service để group kết quả theo domain, không phải theo project_id.

Cột domain được lấy từ JSON trong MySQL:

REPLACE(JSON_EXTRACT(p.links, '$[0]'), '"', '') AS domain

Cột links lưu dạng JSON array ["https://domain.com"]. JSON_EXTRACT lấy phần tử đầu tiên, REPLACE loại bỏ dấu nháy kép.

V.3. Pinia Store (negativeStore)

File: admin/src/views/dashboard1/negative-seo/stores/negativeStore.ts

Khác với clickAdsStore, store này không có client-side filter phức tạp. Hàm fetchData đơn giản hơn:

async fetchData() {
  this.state.isFetching = true
  const res = await apiNegativeSeo.getList({ ...this.urlParams })
  const dataRes = res.data.data
  
  // Thiết lập default expanded cho NCollapse
  this.collapseDefault.parent = dataRes.map((item) => item.domain) // Key theo domain
  this.collapseDefault.childrend = dataRes.flatMap((item) =>
    (item.data_keywords ?? []).map((kw) => kw.project_id)
  )
  
  this.listData = dataRes
}

Lưu ý collapseDefault.childrend lưu project_id (number), trong khi Click Ads lưu id (number). Đây là sự không nhất quán trong naming nhưng không ảnh hưởng runtime.


VI. Tab Traffic Tự Nhiên (TrafficNatural.vue)

File: admin/src/views/dashboard1/traffic-natural/TrafficNatural.vue

Cấu trúc gần như giống hệt Negative SEO — tổ chức theo Domain, dùng NCollapse. Các điểm khác biệt duy nhất:

  1. Store: useTrafficNaturalStore (tách biệt hoàn toàn với negativeStore).
  2. API endpoint: /campaign/get-home-traffic-data.
  3. Flag SQL khác: is_kill = false thay vì true.

SQL query tương ứng (từ get_home_traffic.go):

WHERE p.organic_engine IN (0, 1)
  AND p.only_ads = false   -- Giống Negative SEO
  AND p.is_kill = false    -- ← Điểm khác biệt duy nhất với Negative SEO
  AND p.traffic_type = 1

Có nghĩa là: Nếu một project được tạo với only_ads=false, is_kill=false thì sẽ hiện ở tab Traffic Tự Nhiên. Nếu đổi is_kill=true thì project đó sẽ chuyển sang tab Negative SEO (không còn hiện ở Traffic Tự Nhiên nữa).


VII. Backend: Danh mục Endpoint

File: api/internal/campaign/handler/handler.go
Base route: /campaign (prefix)

MethodPathHandlerMô tả
GET/campaign/GetListWithPaginationDữ liệu tab Click Ads
GET/campaign/get-negative-seo-dataGetNegativeSeoDataDữ liệu tab Negative SEO
GET/campaign/get-home-traffic-dataGetHomeTrafficDataDữ liệu tab Traffic Tự Nhiên
GET/campaign/exportExportCampaignXuất Excel Click Ads
GET/campaign/export-negative-seo-dataExportNegativeSeoDataXuất Excel Negative SEO (tất cả)
GET/campaign/export-negative-seo-data-by-idsExportNegativeSeoByIdsXuất Excel Negative SEO theo ID
GET/campaign/export-home-traffic-dataExportHomeTrafficXuất Excel Home Traffic
GET/campaign/export-home-traffic-data-by-idsExportHomeTrafficByIdsXuất theo ID
GET/campaign/summary-dataSummaryDataTổng hợp số liệu
POST/campaign/CreateTạo campaign
GET/campaign/:idGetDetailChi tiết campaign
PUT/campaign/:idUpdateCập nhật campaign
DELETE/campaign/:idDeleteXóa campaign
GET/campaign/fired/:typeGetFiredLấy campaign dùng cho ConfirmFire (type: gg/cc)
GET/campaign/get-allGetListLấy tất cả campaign (không phân trang)

VIII. Backend: GetListWithPagination — query phức tạp nhất

File: api/internal/campaign/repository/get_list_with_pagination.go

Đây là query phức tạp nhất trong toàn bộ hệ thống, phục vụ tab Click Ads.

Tổng quan cấu trúc:

  1. Lấy campaign list (phân trang, filter theo keywords).
  2. Đếm tổng record cho pagination.
  3. Tùy theo có hay không có start_date/end_date, thực hiện hai nhánh query khác nhau.

VIII.1. Query không có date range

Khi params.StartDate == nil || params.EndDate == nil:

WITH ProjectStats AS (
  SELECT
    project_id,
    SUM(total_success) as total_success,
    COUNT(*) as detail_count
  FROM project_details
  GROUP BY project_id          -- Tính tổng toàn thời gian
)
SELECT
  p.*,
  COALESCE(ps.total_success, 0) as total_success
FROM projects p
LEFT JOIN ProjectStats ps ON ps.project_id = p.id
WHERE p.campaign_id IN (...)   -- Chỉ lấy project thuộc các campaign hiện tại
  {statusFilter}               -- Filter thêm theo status/state nếu có
ORDER BY p.id DESC

Sau đó, với mỗi project, lấy toàn bộ project_details không filter ngày.

VIII.2. Query có date range

Khi có start_date và end_date:

WITH ProjectStats AS (
  SELECT
    project_id,
    SUM(total_success) as total_success
  FROM project_details
  WHERE date BETWEEN CAST(? AS DATE) AND CAST(? AS DATE)  -- Chỉ tính trong khoảng ngày
  GROUP BY project_id
)
SELECT p.*, COALESCE(ps.total_success, 0) as total_success
FROM projects p
LEFT JOIN ProjectStats ps ON ps.project_id = p.id
WHERE p.campaign_id IN (...)
  AND (
    ps.project_id IS NOT NULL      -- Có data trong khoảng ngày này
    OR p.status = 1                -- Hoặc đang chạy
    OR (DATE(p.created_at) >= ? AND DATE(p.created_at) <= ?)  -- Hoặc được tạo trong khoảng
  )
  {statusFilter}
ORDER BY p.id DESC

Điều kiện OR quan trọng: Một project hiển thị nếu thỏa ít nhất một trong:

  • Có traffic được ghi nhận trong khoảng ngày đã chọn.
  • Đang ở trạng thái active (status=1) — để không bị ẩn dù chưa có traffic ngày hôm nay.
  • Được tạo trong khoảng ngày đó — để project mới tạo luôn hiển thị.

Sau khi lấy project, query project_details batch theo project_id IN (?) và filter thêm date BETWEEN ? AND ?.

Cơ chế Map dữ liệu:

// Map details vào projects
detailsByProjectID := make(map[uint][]campaign.ProjectDetails)
for _, detail := range allDetails {
  detailsByProjectID[detail.ProjectID] = append(...)
}
for i := range projects {
  projects[i].ProjectDetails = detailsByProjectID[projects[i].ID]
}

// Map projects vào campaigns
projectsByCampaignID := make(map[uint][]campaign.Project)
for _, p := range projects {
  projectsByCampaignID[p.CampaignID] = append(...)
}
for i, c := range records {
  records[i].Projects = projectsByCampaignID[c.ID]
}

Đây là pattern aggregate bằng Go Map thay vì SQL JOIN để tránh row explosion.

VIII.3. Logic buildStatusStateFilter

func buildStatusStateFilter(statusStr, stateStr string) string {
  // statusStr có thể là: "1" hoặc "0,1,2,3" (nhiều giá trị cách nhau dấu phẩy)
  // Validate từng giá trị phải là số nguyên (chống injection)
  // Kết quả: "AND (p.status = 0 OR p.status = 1) AND (p.state = 2)"
}

Các giá trị status hợp lệ: 0 (dừng), 1 (chạy).
Các giá trị state hợp lệ: 0 (unknown), 1 (kill), 2 (dead).

Ở Frontend, status options trong ClickAds.vue:

const statusOptions = [
  { label: 'Hoạt động', value: 1 },
  { label: 'Không hoạt động', value: 0 },
  { label: 'Giết', value: 2 },     // state = 1 (Kill)
  { label: 'Đã chết', value: 3 },  // state = 2 (Dead)
  { label: 'Chưa rõ', value: 4 }   // state = 0 (Unknown)
]

IX. Backend: GetNegativeSeoData và GetHomeTrafficData

Files: api/internal/campaign/handler/negative_seo_data.go và get_home_traffic.go

IX.1. SQL Negative SEO — các flag bắt buộc

// File: api/internal/campaign/repository/get_negative_seo.go
query = query.Where("p.organic_engine IN (0, 1)")
query = query.Where("p.only_ads = ?", false)
query = query.Where("p.is_kill = ?", true)    // BẮT BUỘC phải true
query = query.Where("p.traffic_type = ?", 1)
query = query.Group("p.id")
query = query.Order("p.created_at ASC")

IX.2. SQL Home Traffic — các flag bắt buộc

// File: api/internal/campaign/repository/get_home_traffic.go
query = query.Where("p.organic_engine IN (0, 1)")
query = query.Where("p.only_ads = ?", false)
query = query.Where("p.is_kill = ?", false)   // BẮT BUỘC phải false
query = query.Where("p.traffic_type = ?", 1)
query = query.Group("p.id")
query = query.Order("p.created_at ASC")

IX.3. Handler timeout 180 giây

Cả hai handler đều tạo context riêng với timeout 180 giây:

reqCtx, cancel := context.WithTimeout(baseCtx, 180*time.Second)
defer cancel()

Timeout này dài hơn nhiều so với các endpoint thông thường (thường 30-60s). Nguyên nhân: Query join projects với project_details có thể xử lý hàng triệu bản ghi khi date range rộng.

Xử lý ngày mặc định khi không có input:

  • start_date không truyền → Mặc định 2023-01-01 (lấy từ đầu hệ thống).
  • end_date không truyền → Mặc định ngày hiện tại.
  • Nếu start_date > end_date → Trả lỗi validation.

X. Backend: GetListAndExport — Xuất dữ liệu

File: api/internal/campaign/repository/get_list_and_export.go

Hàm xuất dữ liệu dùng GORM Preload thay vì Raw SQL:

query = query.Preload("User")
query = query.Preload("Projects", func(q *gorm.DB) *gorm.DB {
  subQuery := q.Preload("ProjectDetails", func(q *gorm.DB) *gorm.DB {
    if hasDateRange {
      return q.Where("project_details.date BETWEEN ? AND ?", startDate, endDate).Order("hour DESC")
    }
    return q.Where("project_details.date = ?", today).Order("hour DESC")
  })
  if hasDateRange {
    subQuery = subQuery.Where("EXISTS (...)")
  }
  return subQuery
})

Cách tiếp cận này khác với GetListWithPagination (dùng Raw SQL + Map). Preload đơn giản hơn nhưng có thể sinh ra N+1 query nếu số lượng project lớn.

Filter theo ID là campaign ID đơn lẻ để xuất file một campaign cụ thể.


XI. Bảng tra cứu: Flag project theo từng tab

Bảng này giúp xác định nhanh một project có only_ads và is_kill là bao nhiêu thì hiện ở tab nào.

only_adsis_killtraffic_typeHiển thị tại tab
truefalse1Click Quảng Cáo
falsetrue1Negative SEO
falsefalse1Traffic Tự Nhiên
truetrue1Không hợp lệ — không hiển thị ở bất kỳ tab nào
falsefalse0Không hiện ở tab nào (traffic_type=0 không được query)

Click Ads lấy data qua /campaign/ (list campaign), không filter only_ads/is_kill ở tầng repository. Toàn bộ project đều hiện trong tab Click Ads nếu thuộc về campaign đó, bất kể flag nào. Sự phân loại ở tầng frontend dựa vào việc project đó nằm trong campaign nào.


XII. Điều gì xảy ra khi cấu hình sai flag

1. Tạo project với only_ads=false, is_kill=false nhưng muốn nó vào tab Negative SEO:

  • Project sẽ hiện ở tab Traffic Tự Nhiên, không phải Negative SEO.
  • SQL get_negative_seo.go lọc is_kill = true, project này không thỏa.
  • Cách fix: Vào MySQL cập nhật is_kill = 1 cho project đó.

2. Tạo project với only_ads=true, is_kill=true:

  • Column only_ads = true → Project có thể hiện ở tab Click Ads (vì Click Ads lấy theo campaign, không filter flag).
  • Query Negative SEO: only_ads = false → Không hiện.
  • Query Home Traffic: only_ads = false → Không hiện.
  • Mâu thuẫn logic: only_ads=true nghĩa là click Ads, is_kill=true nghĩa là tấn công. Không có tab phù hợp cho trường hợp này.

3. Không set start_date và end_date khi gọi GetListWithPagination:

  • Query dùng nhánh "không có date range": Lấy toàn bộ project_details của mọi thời điểm.
  • total_success sẽ là tổng cộng dồn từ ngày đầu đến hiện tại.
  • Nếu số lượng project_details rất lớn, query sẽ rất chậm.

4. State = Dead nhưng Status = 1:

  • isProjectRunning trong service kiểm tra p.State != State_Dead AND p.TotalProcess > 0.
  • Nếu State=Dead nhưng Status=1, project vẫn visible trên UI với badge "Đã chết", nhưng Worker không tạo thêm task nào cho project này.

XIII. Luồng tạo Project (Create Modal)

Khi Admin nhấn "Thêm từ khóa" trong tab Click Ads:

  1. Frontend set clickAdsStore.campaignId = campaign.id và clickAdsStore.campaignName = campaign.name.
  2. Mở CreateModal (tại admin/src/views/task/components/CreateModal.vue).
  3. CreateModal đọc campaignId từ store để pre-set campaign dropdown.
  4. Sau khi submit form, gọi POST /tasks hoặc POST /projects để tạo project.
  5. Emit success → ClickAds.vue gọi lại fetchData().

Trong tab Negative SEO và Traffic Tự Nhiên:
Cũng dùng cùng CreateModal, nhưng store tương ứng là negativeStore hoặc trafficNaturalStore. Mỗi store có campaignId riêng để đảm bảo project được tạo đúng campaign.


XIV. Thao tác hàng loạt (Bulk Actions)

Hệ thống hỗ trợ hai cấp độ thao tác hàng loạt:

Cấp Campaign (toàn bộ project trong 1 campaign):

  • "Bật tất cả" / "Tắt tất cả" trong CampaignItem.
  • Gọi apiProjects.bulkUpdate({ ids: campaign.projects.map(p=>p.id), fields: { status } }).

Cấp Project (các project được chọn qua checkbox):

  • BulkActions bar hiện ra khi có ít nhất 1 row được check.
  • "Bật đã chọn" / "Tắt đã chọn" / "Xóa đã chọn".
  • "Bật/Tắt": apiProjects.bulkUpdate({ ids: selectedRows.map(r=>r.id), fields: { status } }).
  • "Xóa": apiProjects.bulkDelete(uniqueProjectIds).

Lưu ý về bulkDelete:

const ids = rows.map((row) => row.project_id)  // Task có project_id
const uniqueIds = [...new Set(ids)]             // Loại trùng (nhiều task cùng project)
await apiProjects.bulkDelete(uniqueIds)

XV. Cột Traffic: total_success / total_process

Đây là cột quan trọng nhất để đánh giá hiệu quả của một project.

total_process — Ngân sách traffic được phân bổ cho project. Được tính theo giờ qua project_details. Tổng hợp phụ thuộc vào date range:

  • Không có date range: ProjectStats CTE tính SUM(total_success) toàn thời gian.
  • Có date range: Chỉ tính trong khoảng ngày đã chọn.

total_success — Số lần Worker thực sự chạy thành công cho project này trong cùng khoảng thời gian.

Hiển thị: total_success / total_process với màu xanh cho success và màu đen cho process.

Nếu total_success = total_process: Project đã dùng hết ngân sách.
Nếu total_success << total_process: Project đang bị tắc — có thể do Proxy chết, Captcha, hoặc Worker node bận.
Nếu total_process = 0: Project này vừa bị tạo hoặc bị reset, chưa có budget nào.


XVI. Client-side Filtering — useCampaignFiltering

File: admin/src/views/dashboard1/click-ads/composables/useCampaignFiltering.ts

Đây là composable xử lý toàn bộ logic filter phía client cho tab Click Ads. Không gọi API khi filter — thay vào đó filter trực tiếp trên defaultCampaigns đã được clone sẵn.

Hai hàm chính:

onSearch(campaignId, value)

Tìm kiếm domain/keyword trong campaign cụ thể:

const onSearch = (campaignId: number, value: string | undefined) => {
  // Lưu search text vào store để preserve qua reload
  clickAdsStore.updateCampaignFilterSearch(campaignId, 'searchText', value)

  // Khôi phục từ defaultCampaigns (bản gốc chưa filter)
  campaign.projects = JSON.parse(JSON.stringify(defaultCampaign.projects))

  // Nếu không có search text và không có filter nào khác → reset
  if (!value && noOtherFilters) return

  // Filter theo search text
  if (value) {
    const searchLower = value.toLowerCase().trim()
    campaign.projects = campaign.projects.filter((project) => {
      const inKeyword = project.search_keyword?.toLowerCase().includes(searchLower)
      const inLinks = project.links?.some((link) => link.toLowerCase().includes(searchLower))
      return inKeyword || inLinks
    })
  }

  // Sau đó apply thêm status filter nếu có
  applyStatusFilter(campaign, campaignId)
}

onChangeFilterProject(campaignId, type, value)

Xử lý filter theo status/state hoặc date range:

Filter theo date range:

const dateRanges: Record<number, [number, number]> = {
  0: [-1, -1],   // Hôm qua: từ -1 ngày đến -1 ngày
  1: [0, 0],     // Hôm nay: từ hôm nay đến hôm nay
  2: [-6, 0],    // 7 ngày gần nhất: từ -6 ngày đến hôm nay
  3: [-29, 0]    // 30 ngày: từ -29 ngày đến hôm nay
}
// Dùng timestamp để tính: today.getTime() + startDays * 86400000

Filter theo status (UI value → DB value mapping):

// selectedStatuses là mảng giá trị từ statusOptions UI
// value=2 (Giết)    → project.state === 1
// value=3 (Đã chết) → project.state === 2
// value=4 (Chưa rõ) → project.state === 0
// value=0 (Không hoạt động) → project.status === 0
// value=1 (Hoạt động)       → project.status === 1

Logic filter kết hợp State + Status:
Nếu user chọn cả "Giết" (state-based) và "Hoạt động" (status-based):

  • Hai loại filter này chạy độc lập (hai if block riêng biệt, không else if).
  • Kết quả là AND: Project phải thỏa cả state filter VÀ status filter.
  • Đây có thể dẫn đến kết quả rỗng nếu chọn tổ hợp mâu thuẫn (VD: "Đã chết" + "Hoạt động").

Nguyên tắc filter chung:

  1. Luôn bắt đầu từ bản sao của defaultCampaigns (dữ liệu gốc từ server).
  2. Apply search text filter.
  3. Apply status/state filter.
  4. Gán kết quả vào campaign.projects.
  5. Lưu trạng thái filter vào store để khôi phục khi fetchData lại.

XVII. Backend: Service Layer — Grouping Negative SEO Data

File: api/internal/campaign/service/get_negative_seo.go

Service này nhận flat list từ repository (mỗi row là một project), sau đó group theo domain:

func (s *Service) GetNegativeSeoData(ctx context.Context, startDate, endDate time.Time) ([]*dto.NegativeSeoDataRes, error) {
  // Điều chỉnh endDate về cuối ngày (23:59:59) để bao gồm toàn bộ ngày cuối
  endDate = endDate.Add(time.Hour*23 + time.Minute*59 + time.Second*59)

  getNegativeSeoData, err := s.repository.GetNegativeSeoData(ctx, startDate, endDate)

  negativeSeoDataRes := make([]*dto.NegativeSeoDataRes, 0)
  for _, data := range *getNegativeSeoData {
    var foundDomain bool
    // Tìm xem domain đã tồn tại trong kết quả chưa
    for _, res := range negativeSeoDataRes {
      if res.Domain == data.Domain {
        foundDomain = true
        // Append keyword vào domain đã tồn tại
        res.DataKeywords = append(res.DataKeywords, dto.DataKeywords{
          CampaignId:     data.CampaignId,
          ProjectId:      data.ProjectId,
          SearchKeyword:  data.SearchKeyword,
          Ranking:        data.Ranking,
          TotalProcess:   data.TotalProcess,
          Status:         data.Status,
          KeywordCreated: data.KeywordCreated.Format("02/01/2006"),  // Vietnamese date format
        })
        break
      }
    }
    if !foundDomain {
      // Domain mới → tạo entry mới
      negativeSeoDataRes = append(negativeSeoDataRes, &dto.NegativeSeoDataRes{
        Domain: data.Domain,
        DataKeywords: []dto.DataKeywords{{...}},
      })
    }
  }
  return negativeSeoDataRes, nil
}

Lưu ý về grouping algorithm:

  • Thuật toán dùng nested loop (O(n²)) thay vì map. Với dữ liệu nhỏ (<1000 domain), hiệu năng vẫn ổn.
  • Format ngày là "02/01/2006" (ngày/tháng/năm) — định dạng Việt Nam, khác với Go standard "2006-01-02".

XVIII. Backend: GetFiredById — Campaign cho ConfirmFire

File: api/internal/campaign/repository/get_fired_by_id.go

Endpoint /campaign/fired/:type (type: gg hoặc cc) được Auto Search module dùng để lấy thông tin campaign mặc định khi thực hiện Confirm Fire.

Query dùng GORM Preload với subquery lấy latest_rank:

query.Preload("Projects", func(db *gorm.DB) *gorm.DB {
  return db.Table("projects p").
    Select(`
      p.id,
      p.campaign_id,
      p.name,
      p.status,
      p.search_keyword,
      JSON_UNQUOTE(JSON_EXTRACT(p.links, '$[0]')) AS links,
      (
        SELECT pd.rank
        FROM project_details pd
        WHERE pd.project_id = p.id
        ORDER BY pd.date DESC, pd.hour DESC
        LIMIT 1
      ) as latest_rank
    `).
    Where("p.status = ?", 1)  // Chỉ lấy project đang active
})

Trường latest_rank là rank mới nhất của domain theo project_details. Dùng subquery correlated thay vì JOIN để lấy đúng một record mới nhất theo date DESC, hour DESC.


XIX. Backend: useClickAdsDataActions — fetchData chi tiết

File: admin/src/views/dashboard1/click-ads/stores/actions/useClickAdsDataActions.ts

fetchData

const fetchData = async () => {
  store.state.isFetching = true
  const params: any = {
    page: String(store.urlParams.page ?? 1),
    limit: String(store.urlParams.limit ?? 20)
  }
  // Chỉ đưa params vào nếu có giá trị thực, tránh gửi null/empty
  if (store.urlParams.keywords?.trim()) params.keywords = store.urlParams.keywords
  if (store.urlParams.start_date?.trim()) params.start_date = store.urlParams.start_date
  if (store.urlParams.end_date?.trim()) params.end_date = store.urlParams.end_date

  const res = await apiCampaigns.getList(params)
  const dataRes = res.data.data
  
  // Guard page overflow: nếu current page > 1 mà không có data → về trang trước
  if (dataRes.paginate.page > 1 && !dataRes.data?.length) {
    store.onChangePage(dataRes.paginate.page - 1)
  }
  
  // Cập nhật collapseDefault để NCollapse biết cần mở rộng gì
  store.collapseDefault.parent = dataRes.data.map((c) => c.id)
  store.collapseDefault.childrend = dataRes.data.flatMap((c) =>
    (c.projects ?? []).map((task) => task.id)
  )
  
  // Mark campaign có thể xóa nếu không có project
  dataRes.data.forEach((campaign) => {
    campaign.isDelete = (campaign.projects?.length || 0) === 0
  })
  
  store.listData = dataRes
}

fetchSummaryDetail (cho ChartDetailRanking)

const fetchSummaryDetail = async (id: number) => {
  // Gọi API lấy lịch sử rank và traffic của một project
  const response = await apiDashBoard.getSummaryDetail(id, store.trafficRankingParams)
  const data = response.data.data

  const startDate = new Date(store.trafficRankingParams.start_date)
  const endDate = new Date(store.trafficRankingParams.end_date)
  const isSameDay = startDate.getDate() === endDate.getDate()
  const dates = generateTimeArray(startDate, endDate)

  store.trafficRankingData = {
    dates: dates,
    ranking: mapRankingData(dates, data, isSameDay),      // Rank theo ngày/giờ
    deathTime: mapDeathTimeData(dates, data, isSameDay),  // Thời điểm state=Dead
    total_success: mapTrafficData(dates, data, isSameDay) // Traffic thành công
  }
}

generateTimeArray: Tạo mảng timestamp từ startDate đến endDate. Nếu isSameDay = true, tạo theo giờ (24 điểm). Nếu khác ngày, tạo theo ngày.


XX. Model dữ liệu MySQL quan trọng

Bảng projects

CộtKiểuMô tả
iduintPrimary key
campaign_iduintFK → campaigns.id
user_idintNgười tạo
namevarcharTên dự án
statustinyint0=dừng, 1=chạy
statetinyint0=unknown, 1=kill, 2=dead
state_timedatetimeThời điểm thay đổi state
dead_timedatetimeThời điểm bị dead
total_processintNgân sách traffic
traffic_typetinyint0=direct, 1=search
traffic_devicetinyint0=desktop, 1=mobile
traffic_device_ostinyint0=win/android, 1=mac/ios
organic_enginetinyint0=GGvn, 1=GG, 2=CC
search_keywordvarcharTừ khóa tìm kiếm
linksjsonMảng URL target ["https://..."]
only_adstinyint(1)Chỉ click Ads
is_killtinyint(1)Traffic Kill mode
is_traffic_fixedtinyint(1)Cố định số traffic/ngày
keyword_idvarcharMongoDB ObjectID liên kết
sortintPriority (1-5)
geolocationjson{"lat": ..., "lng": ...}
attributejsonTime frame và time_on_page config
created_atdatetime-
updated_atdatetime-

Bảng project_details

Record này được Cron Job tạo mỗi giờ cho mỗi project đang active.

CộtKiểuMô tả
iduintPrimary key
project_iduintFK → projects.id
datedateNgày (YYYY-MM-DD)
hourtinyintGiờ (0-23)
total_processintSố lượt được phân bổ trong giờ này
total_successintSố lượt thành công trong giờ này
total_failintSố lượt thất bại
rankintVị trí SEO tốt nhất scan được
created_atdatetime-

Aggregate patterns được dùng:

  • SUM(total_success) → Tổng traffic thực tế
  • MIN(rank) → Vị trí SEO tốt nhất trong khoảng thời gian
  • MAX(date), MAX(hour) → Lần cập nhật cuối

Bảng campaigns

CộtKiểuMô tả
iduint-
namevarcharTên chiến dịch (hiển thị trên UI)
descriptiontextMô tả chi tiết (tooltip)
user_idintNgười tạo
created_atdatetime-

XXI. Bảng tra cứu: Trạng thái Project

statusstateTên hiển thịMàu badgeHành động tiếp theo
10 (Unknown)Hoạt độngXanh láWorker tiếp tục tạo task
00 (Unknown)Không hoạt độngXámWorker không tạo task
11 (Kill)GiếtĐỏWorker xử lý Kill logic
12 (Dead)Đã chếtXám đậmWorker bỏ qua project này
02 (Dead)Đã chết (tắt)Xám-

State_Dead là cờ hệ thống: Worker tự động đặt state = 2 khi phát hiện domain target không còn chạy Ads/không còn trong top kết quả. Người dùng không thể set state này thủ công từ UI — chỉ có thể xóa project và tạo lại.


XXII. Giải thích dataCampaignAdd trong clickAdsStore

dataCampaignAdd: undefined as Campaign.Item | undefined

Field này được dùng để trigger re-fetch sau khi user thêm project mới vào campaign:

// Trong ClickAds.vue:
watch(
  () => clickAdsStore.dataCampaignAdd,
  (newData) => {
    if (newData && (newData.dateRange !== null || newData.status !== null || newData.searchText !== undefined)) {
      fetchData()
    }
  },
  { deep: true }
)

Khi CreateModal emit success, nó set dataCampaignAdd vào campaign vừa thêm project. Watch này phát hiện thay đổi và tự động re-fetch để UI cập nhật ngay mà không cần user refresh tay.


XXIII. Phân trang hai cấp

Module này có hai cấp phân trang với logic khác nhau hoàn toàn:

Cấp 1: Phân trang Campaign (phía server):

  • urlParams.page và urlParams.limit = 20
  • Mỗi lần chuyển trang → gọi API mới
  • Tổng campaign và số trang từ paginate.total_pages
  • Render bởi <NPagination> phía dưới danh sách campaign

Cấp 2: Phân trang Project trong Campaign (phía client):

  • currentPage và pageSize = 20 là ref local trong ListGroup.vue
  • KHÔNG gọi API khi chuyển trang — slice dữ liệu có sẵn
  • Reset về trang 1 khi projects.length thay đổi
  • Render bởi <CPagination> phía dưới bảng project

Hiển thị phụ chú: Hiển thị từ {(currentPage-1)*pageSize+1} đến {min(currentPage*pageSize, total)} trên tổng {total}


XXIV. Luồng xử lý khi Export Excel

Export toàn bộ (tab Click Ads)

  1. User click nút xuất file ở header → emit export với campaign = undefined.
  2. useCampaignOperations.exportData() gọi GET /campaign/export.
  3. Backend lấy tất cả campaign có project (với date range nếu có).
  4. Trả về JSON → Frontend convert sang XLSX bằng thư viện client-side.

Export một campaign cụ thể

  1. User click nút xuất file ở CampaignItem → emit export với campaign cụ thể.
  2. useCampaignOperations.exportCampaignData(campaign) gọi GET /campaign/export?id={campaign.id}.
  3. Backend filter id = campaign.id → Chỉ trả về 1 campaign.

Export Negative SEO

  • Toàn bộ: GET /campaign/export-negative-seo-data
  • Theo IDs: GET /campaign/export-negative-seo-data-by-ids?ids=1,2,3
    • Frontend chọn domain muốn xuất, lấy list project_id → gửi dưới dạng ids.

XXV. Các điểm dễ nhầm lẫn trong codebase

1. listDataFilter vs listData trong clickAdsStore:

  • listData — Dữ liệu gốc từ server, không được sửa đổi.
  • listDataFilter — Dữ liệu sau khi filter, được dùng để render. Tuy nhiên, thực tế trong code hiện tại, filter được thực hiện trực tiếp trên listData.data[i].projects thay vì qua listDataFilter. listDataFilter tồn tại trong state nhưng không được sử dụng đồng nhất.

2. collapseDefault.childrend (typo):
Cả clickAdsStore và negativeStore đều có coldrend thay vì children. Đây là typo nhất quán trong codebase. Không nên sửa vì đã được tham chiếu ở nhiều nơi.

3. State management khác nhau giữa ba tab:

  • Click Ads: State phức tạp, có filter riêng theo campaign, có TrafficChart, có ChartDetailRanking.
  • Negative SEO: State đơn giản, không có client-side filter.
  • Traffic Natural: Giống 100% Negative SEO về cấu trúc store, chỉ khác store instance và API.

4. only_ads không dùng để filter ở tab Click Ads:
Mặc dù only_ads = true là đặc trưng của Click Ads project, backend /campaign/ endpoint không filter theo only_ads. Tất cả project thuộc campaign đều được trả về. Điều này có nghĩa là nếu ai đó vô tình tạo project only_ads=false vào campaign "Click Ads", nó vẫn hiện trong tab này.

5. organic_engine IN (0, 1) — Tại sao không có engine 2 (CocCoc)?

  • Negative SEO và Home Traffic query đều filter organic_engine IN (0, 1) — chỉ Google.
  • CocCoc (organic_engine = 2) không hiện trong hai tab này.
  • Nguyên nhân: Hiện chưa có use case Negative SEO hoặc Home Traffic qua CocCoc, chỉ có Click Ads CocCoc được quản lý riêng trong campaign CocCoc.
Modified at 2026-03-23 05:18:31
Previous
1. Thống kê hàng ngày (Daily Statistics)
Next
3. Tìm kiếm tự động (Automated Discovery)
Built with