/auto-searchadmin/src/views/auto-search/api/internal/keyword/keyword, keyword_result)projects, project_details, thông qua ConfirmFire)keyword_result.Project mới trong MySQL (giống hệt như tạo thủ công trên Dashboard1), liên kết với chiến dịch mặc định, và kích hoạt ngay lập tức.google-search — Theo dõi Google (engine value = 1)coccoc-search — Theo dõi CocCoc (engine value = 2)server-performance — Giám sát hiệu năng node Worker (chỉ một số user được phép)location — Quản lý danh sách địa điểm địa lý phục vụ proxy routing[Admin click "Thêm từ khóa"]
|
v
[CreateModal.vue] --> POST /api/keywords
|
v
[handler/create_keyword.go] --> service.ProcessKeywords()
|
v
[service/create_keyword.go]
- Kiểm tra từ khóa đã tồn tại chưa (repository.FindByKeyword)
- Tạo mới vào MongoDB (repository.CreateMany)
- Nếu status = 1 (Enable), gọi ngay SearchKeyword()
|
v
[service/search_keyword.go] -- createTaskSearchKeyword()
- Lấy random Location từ MySQL bảng locations
- Với mỗi keyword × engine × device × deviceOS → tạo 1 task
- Đẩy tất cả task vào keyword_pool_task (MongoDB)
|
v
[Worker Node] nhận task từ pool
- Mở trình duyệt với Proxy (lấy qua env.WebhookProxyGetProxyUrl)
- Tìm kiếm từ khóa trên Engine tương ứng
- Bóc tách kết quả Ads (.googleadservices.com link) và Search
- POST kết quả về Webhook URL: env.WebhookSearchKeywordUrl/{keyword_id}
|
v
[Backend nhận webhook] --> Lưu vào MongoDB collection keyword_result
|
v
[Frontend polling mỗi 10 giây]
GET /api/keywords?page=1&limit=200&engine=1
--> Hiển thị danh sách domain theo từng keyword
|
v
[Admin chọn domain, nhấn "Xác nhận bắn"]
POST /api/keywords/confirm-fire
|
v
[ConfirmFire Service] --> Tạo Project trong MySQL
--> Cập nhật confirm_status = true trong MongoDBadmin/src/views/auto-search/IndexView.vue<RouterView>. Nó không tự fetch dữ liệu mà để mỗi sub-tab tự quản lý vòng đời của mình.#09484a0d (xanh đen mờ). Khi active, tab đổi sang nền #0a4e4f và chữ trắng.Server Performance chỉ render nếu:tabChange với payload là route.name hiện tại mỗi khi route thay đổi. Sự kiện này được các component cha (layout) dùng để pause/resume interval refresh khi người dùng chuyển tab.admin/src/views/auto-search/components/GoogleAds.vueautoSearchStore.select2h === 0 — Hiển thị danh sách keyword dạng NCollapse.autoSearchStore.select2h === 1 — Ẩn danh sách chính, hiển thị component GoogleAds2h (xem kết quả 2 giờ gần nhất theo định dạng khác).onBeforeMount):state.isPending = true.autoSearchStore.engine = 1 (đánh dấu đang ở tab Google).autoSearchStore.isFetching2h = true.autoSearchStore.fetchDataGoogleAds() và autoSearchStore.fetchLocation().state.isPending = false.onUnmounted):autoSearchStore.$reset() — Xóa toàn bộ state về giá trị mặc định.pause() — Dừng interval refresh ngay lập tức.useIntervalFn):urlParams.page hoặc urlParams.limit tồn tại hoặc engine là 1:fetchDataGoogleAds(true) — true là tham số silent, nghĩa là không hiển thị loading spinner, làm mới ngầm.data.every(item => !item.is_running). Nếu không còn keyword nào đang chạy thì gọi pause() để dừng interval (tránh tải server vô ích).is_running, đặt item.is_loading = false và state.isLoading[item.id] = false.A + D + S (được detect qua useMagicKeys() của VueUse) sẽ toggle biến autoSearchStore.isFilterCC. Khi bật, hệ thống hiển thị thêm cột phân loại engine trong bảng kết quả, hữu ích khi debug cross-engine data.listDataGoogle.data thay đổi:expandedNames = tất cả ID của keyword (mở hết NCollapse cha).expandedChildren = tất cả ID của từng history_keyword entry (mở hết NCollapse con).CreateModal — Mở khi stateModal.isShowCreateModal = true.CreateProjectModal — Mở khi stateModal.isShowCreateProjectModal = true.UpdateModal — Mở khi stateModal.isShowUpdateModal = true. Khi mở, autoSearchStore.idEditKeyword phải đã được set bằng ID keyword cần sửa.SmartImportModal — Controlled bởi biến local isShowSmartImportModal (không qua store).admin/src/views/auto-search/components/CocCocAds.vueGoogleAds.vue, nhưng có một số điểm khác biệt quan trọng:autoSearchStore.engine = 2 thay vì 1.fetchDataCocCocAds() thay vì fetchDataGoogleAds().fetchDataCocCocAds(true) thay vì fetchDataGoogleAds(true).CocCocAdsKeywordItem thay vì GoogleAdsKeywordItem.SmartImportModal — Tab CocCoc không hỗ trợ Smart Import.onChangeFilter dùng useDebounceFn với delay 500ms, trong khi GoogleAds dùng useGoogleAdsFilters composable riêng.admin/src/views/auto-search/components/CreateModal.vueonInputNames split theo \n, filter dòng rỗng, lưu vào formData.keyword dưới dạng string[].formData.keyword là mảng để backend có thể tạo hàng loạt từ khóa trong một request.result_num):engine):1 = Google, 2 = Coccoc.device):0 = Desktop (Window)1 = Desktop (Macbook)2 = Mobile (Android)3 = Mobile (IOS)device và device_os của MongoDB. Trước khi gửi, code chuyển đổi như sau:device chỉ có thể là [0], [1], hoặc [0, 1]. device_os chứa phân loại OS theo từng device type.sort):status):1 = Hoạt động, 0 = Không hoạt động.formDataCreate từ formData.apiAutoSearch.create(formDataCreate) — POST về /keywords.success, đóng modal.showMessageError(error).admin/src/views/auto-search/components/UpdateModal.vueonMounted): Tự động gọi fetchData() → apiAutoSearch.getDetail(autoSearchStore.idEditKeyword) để load dữ liệu hiện tại.apiAutoSearch.update(autoSearchStore.idEditKeyword, formDataUpdate) — PUT về /keywords/{id}.admin/src/views/auto-search/components/SmartImportModal.vuekill (Google Kill) hoặc home (Home Traffic).is_kill của project:kill → is_kill = truehome → is_kill = falsetotal_process của mỗi project tạo ra.handleImport:chạy trên desktop
- mb88 https://mb88.net/
- w88 https://w88.com/
chạy trên mobile
- bet88 https://bet88.me/totalTraffic.campaign_id lấy từ useCampaignStore().campaignId. Nếu store này chưa có giá trị (user chưa chọn campaign nào), fallback cứng về 11.organic_engine luôn là 0 (Google VN), không tùy chỉnh được trong modal này.time_on_page cố định 5-10 giây — rất thấp, chỉ phù hợp click Ads thuần túy.admin/src/views/auto-search/stores/useStore.tsautoSearchStore quản lý toàn bộ state của module.limit mặc định là 200, tức là API luôn trả về tối đa 200 keyword trong một request.admin/src/services/api/autoSearch.ts/keywords| Method | Endpoint | Mô tả | Payload/Params |
|---|---|---|---|
| GET | /keywords | Lấy danh sách keyword kèm kết quả | { page, limit, keywords, status } |
| GET | /campaign/fired/gg | Lấy thông tin campaign Google dùng để Fire | - |
| GET | /campaign/fired/cc | Lấy thông tin campaign CocCoc dùng để Fire | - |
| GET | /keywords/{id} | Lấy chi tiết một keyword | - |
| POST | /keywords | Tạo keyword mới | { keyword[], engine[], device[], device_os, result_num, status, sort } |
| PUT | /keywords/{id} | Cập nhật keyword | Tương tự Create |
| PUT | /keywords/update-status/{id} | Cập nhật chỉ trạng thái | `{ status: 0 |
| POST | /keywords/confirm-fire | Tạo project từ kết quả scan | { keyword_id, domain, type_data, device, device_os, ... } |
| GET | /keywords/{id}/results/ | Xem lịch sử kết quả scan của keyword | { date, device, device_os, url } |
| POST | /keywords/reset/{id} | Xóa toàn bộ kết quả scan, chạy lại | - |
| POST | /keywords/reset-all | Reset tất cả keyword | - |
| DELETE | /keywords/{id} | Xóa keyword | - |
| GET | /keywords/export-excel | Xuất Excel báo cáo | - |
PUT /keyword-result/update-search-data/{id} → Cập nhật trạng thái của một domain cụ thể trong kết quả (không thuộc prefix /keywords).api/internal/keyword/handler/handler.go/keywords:keywordService và activityLogService.update_status.go)::id từ URL path, parse thành primitive.ObjectID.dto.UpdateStatusRequest { Status int }.validate.Validate(dto).handler.keywordService.FindByID(objectId).handler.keywordService.UpdateStatus(dto.Status, objectId).get_keyword_results_by_keyword_id.go):date (format 2006-01-02)device (int)device_os (int)url (string) — lọc kết quả theo URL cụ thể[]KeywordSearchDataAdsResult, mỗi phần tử chứa SearchData với cả Ads và Search results.api/internal/keyword/service/create_keyword.goProcessKeywords:dto.CreateKeyword và userID, trả về:keywordExist []string — danh sách từ khóa đã tồn tại, không tạo lại.newKeywords []keyword.Keyword — danh sách object sẵn sàng để insert MongoDB.params.Keyword:repository.FindByKeyword(keyword).keyword.Keyword object với primitive.NewObjectID() và strings.TrimSpace(keyword).Create:saveNewKeywords + trigger SearchKeyword:SearchKeyword được gọi đồng bộ (không goroutine), nên response có thể hơi chậm khi tạo nhiều keyword cùng lúc vì phải tạo hết task trước khi return.api/internal/keyword/service/search_keyword.goSearchKeyword(keywords_param ...keyword.Keyword):repository.GetAllActiveKeywords()). Đây là mode được dùng bởi Cron Job định kỳ (e.g. mỗi 30 phút hệ thống scan lại tất cả keyword).locationRepository.FindRandom()). Location bao gồm GeoLocation Lat/Lng dùng để fake vị trí GPS và routing proxy theo vùng.createTaskSearchKeyword() để tạo danh sách task.keywordPoolTaskService.CreateMany(tasks) để đẩy task vào pool MongoDB.createTaskSearchKeyword:engine × device × deviceOS:/webhookSearchKeyword/{keyword_id}.api/internal/keyword/service/get_list.gorepository.GetList(dto, paging) → Lấy danh sách keyword từ MongoDB.keywordResultRepository.GetSearchDataByKeywordIDs(keywords) → Một query batch để lấy tất cả kết quả scan gần nhất cho mọi keyword, tránh N+1 query.searchData thống nhất:KeywordsResponse cho mỗi keyword bao gồm keyword.IsRunning (để UI biết có cần hiện spinner không).Type trong SearchResult:ConfirmStatus:true = Domain này đã được Admin bấm "Xác nhận bắn" và một Project đã tồn tại. UI sẽ hiển thị nhãn "Đã xác nhận" thay vì nút "Bắn".api/internal/keyword/service/confirm_fire.goapi/internal/keyword/service/confirm_fire_config.gogetCampaignConfig:setup_fire (MySQL) và biến môi trường:configTotalProcess là số traffic mặc định PER PROJECT, lấy từ biến môi trường. Số này có thể bị điều chỉnh xuống trong hàm tính toán traffic nếu đã có project khác đang chạy.isProjectRunning:api/internal/keyword/service/confirm_fire_projects.gogetRunningProjects(campaign):campaign.Projects.Status == 1, truy vấn chi tiết từ MySQL (projectRepository.GetOne).State != State_Dead.project.Project đầy đủ và append vào kết quả.runningProjects này được dùng trong bước tính toán traffic để phân bổ lại.api/internal/keyword/service/confirm_fire_traffic.goconfigTotalProcess, hệ thống tự động phân bổ lại traffic cho tất cả project trong cùng campaign để tổng traffic không vượt quá pool.calculateTotalProcess:configTotalProcess = 1000 (biến môi trường).TotalProcess = 500 mỗi.newSort <= maxRunningSort.ceil(1000 / (2+1)) = 334.getMaxRunningSort:Sort lớn nhất trong số các project đang chạy để làm ngưỡng so sánh.api/internal/keyword/service/confirm_fire_creation.gocreateNewProject:campaign.Project với toàn bộ thông tin cần thiết:OnlyAds = true — Project từ Confirm Fire luôn là "Click Ads" mode. Không có cách nào tạo Home Traffic hay Kill Traffic qua Confirm Fire.IsKill = false — Tương tự.TrafficType = 1 — Luôn là Search traffic (không phải Direct/Home).createProjectDetailForNewProject:project_detail record cho giờ hiện tại:TotalProcess = 0 ban đầu. Cron Job tạo task sẽ cập nhật nó sau. Mục đích tạo ngay là để Dashboard1 không bị thiếu record khi query trong cùng ngày.confirm_status = true cho domain đó trong MongoDB để UI hiện "Đã xác nhận".api/internal/keyword/repository/repository.gogo.mongodb.org/mongo-driver/mongo).GetList(dto, paging) — Find với filter status/engine, sort, skip/limit.FindByKeyword(keyword) — FindOne để check duplicate.CreateMany(keywords) — InsertMany.GetKeywordByID(id) — FindOne theo ObjectID.GetAllActiveKeywords() — Find tất cả keyword có status = 1.api/internal/keyword/keyword.goKeyword (Lưu trong MongoDB collection "keyword"):KeywordsResponse (Trả về API, có kèm results):KeywordSearchDataResult (Một batch kết quả theo device+engine):Server Performance bị ẩn với user thông thường. Danh sách user được phép xem hard-code trong IndexView.vue:userAllow.includes(auth.profile?.name) || auth.profile?.email === 'tron@tron.com'google-search, coccoc-search, location) không có hạn chế.PUT /keywords/update-status/{id}.status = 1 trong MongoDB.SearchKeyword(). Hệ thống chỉ bắn task vào lần scan Cron Job tiếp theo (hoặc khi gọi Reset).newSort > maxRunningSort:calculateTotalProcess trả về 0.TotalProcess = 0.isProjectRunning kiểm tra TotalProcess > 0).TotalProcess thủ công cho project đó.setup_fire không được cấu hình đúng:getCampaignConfig trả về campaignID = 0.ConfirmFire trả về lỗi "campaign not found" ngay ở bước đầu.setup_fire trong MySQL và cấu hình CampaignGGID và CampaignCCID.Skipped: No URL found in "..."| Giá trị | Tên | Phạm vi | Ghi chú |
|---|---|---|---|
| 0 | Google VN | google.com.vn | Kết quả địa phương, phù hợp SEO Việt Nam |
| 1 | Google INT | google.com | Kết quả quốc tế |
| 2 | CocCoc | coccoc.com | Dùng riêng cho tab CocCoc Ads |
| Giá trị | Tên | DeviceOS Desktop | DeviceOS Mobile |
|---|---|---|---|
| 0 | Desktop | 0=Windows, 1=MacOS | - |
| 1 | Mobile | - | 0=Android, 1=iOS |
| UI Option | UI Value | device[] | device_os.desktop | device_os.mobile |
|---|---|---|---|---|
| Desktop (Window) | 0 | [0] | [0] | - |
| Desktop (Macbook) | 1 | [0] | [1] | - |
| Mobile (Android) | 2 | [1] | - | [0] |
| Mobile (IOS) | 3 | [1] | - | [1] |
| Window + Macbook + Android | 0,1,2 | [0,1] | [0,1] | [0] |
| Trường | Giá trị cố định | Lý do |
|---|---|---|
only_ads | true | Luôn là Click Ads mode |
is_kill | false | ConfirmFire không hỗ trợ Kill mode |
traffic_type | 1 | Luôn là Search (có từ khóa) |
status | 1 | Chạy ngay lập tức |
state | State_Unknown | Chưa biết trạng thái domain |