Module: server-management
Frontend: admin/src/views/server-management/
Backend:
api/internal/server/api/internal/proxy/api/internal/proxy_packages/api/internal/proxy_providers/api/internal/backup_proxy/servers, proxies, proxy_packages, proxy_providers, backup_proxiesModule Quản Lý Hệ Thống là một module lớn, quản lý toàn bộ hệ thống cơ sở hạ tầng phục vụ việc chạy traffic, bao gồm các máy chủ (worker nodes) và mạng lưới proxy để giả mạo IP.
Module bao gồm 5 trang (tabs) chính:
| Trang | Route name | Backend Folder | Chức năng chính |
|---|---|---|---|
| Quản Lý Server | server_management.server | server | Quản lý các worker server thực thi task. |
| Quản Lý Proxy | server_management.proxy | proxy | Quản lý danh sách IP Proxy, gán với các Gói. |
| Quản Lý Gói Proxy | server_management.package | proxy_packages | Quản lý giá tiền và phân loại Proxy. |
| Quản Lý Nhà Cung Cấp | server_management.provider | proxy_providers | Nguồn cung cấp Proxy (ví dụ Proxifly). |
| Quản Lý Backup Proxy | (riêng biệt) | backup_proxy | Nguồn proxy dự phòng tự chạy Health Check. |
Kiến trúc UI chung:
IndexView.vue làm Layout chính chứa RouterLink tab menu.RouterView render các component tab con, kèm Transition fade-transform.servers/, proxies/) có mô hình chuẩn:IndexView.vue + stores/ + composables/ + components/.Frontend thư mục: server-management/servers/
Backend API route: /api/servers/
Quản lý danh sách các Worker Nodes — các máy sẽ định kỳ gọi lên hệ thống để nhận Task và thực thi (Browser-based tasks).
type UrlSearchParams = {
page: number;
limit: number;
keywords?: string; // Tên server
is_active?: number; // 1: Hoạt động, 0: Tạm dừng
};
| Cột | Binding field | Tính năng đặc biệt |
|---|---|---|
| Checkbox | selection | Chức năng Bulk Delete / Refresh. |
| Tên | name | Sort alphabet. |
| IP | ip | Địa chỉ IP tĩnh của Server. |
| Hoạt động | is_active | NSwitch inline toggle. Gọi API cập nhật ngay. |
Tính năng nổi bật: Inline Optimistic Update
Khi user gạt công tắc "Hoạt động":
row.id vào set updatingRows (hiển thị loading trên công tắc).row.is_active = value ngay trên UI.serverStore.listData để đảm bảo reactivity.apiServers.changeStatus(row.id, value).Tính năng Bulk Actions (Sử dụng composable useBulkSelection)
<BulkActions> để xóa hoặc làm mới nhiều server một lúc.apiServers.bulkDelete(ids) → Fetch lại page=1.CreateModal / UpdateModal:
useFormData(){ name: "", ip: "", is_active: true, limit_process: 10 }limit_process, current_process nhưng trên form hiện đang lưu default limit_process = 10. Các input nhập số quá trình đang bị comment out ở template.api/internal/server)Model MySQL (servers table):
type Server struct {
Name string `gorm:"size:255;not null"`
IsActive bool `gorm:"default:0"` // Type boolean nhưng query từ frontend hay truyền 0/1
IP string `gorm:"size:255;not null"`
LimitProcess int `gorm:"column:limit_process"`
CurrentProcess int `gorm:"column:current_process"`
}
Các Endpoint chính:
| Route | Handler | Mô tả |
|---|---|---|
GET / | GetListWithPagination | Phân trang, có tích hợp filter theo name và is_active. |
GET /mongo-servers | GetMongoServers | Endpoint lấy từ MongoDB cho hệ thống tracking cũ/khác. |
GET /report | GetReport | Báo cáo tỷ lệ process / success / fail của server. |
PUT /update-status/:id | ChangeStatus | Handler chuyên biệt cho Switch Toggle. |
GET /health-check/stats | GetHealthCheckStats | Báo cáo sức khỏe. |
GET /task-monitor/stats | GetTaskMonitorStats | Hiện đang trả placeholder. |
Thay đổi trạng thái (ChangeStatus):
func (h *Handler) ChangeStatus(c *fiber.Ctx) error {
id := strconv.Atoi(c.Params("id"))
var dto dto.ChangeStatus // { is_active: bool }
err = h.serverService.UpdateActive(uint(id), dto.IsActive)
return c.JSON(...)
}
Frontend thư mục: server-management/proxies/
Backend API route: /api/proxies/ (dự kiến)
Là trang phức tạp nhất, cho phép liệt kê và thao tác trên lượng lớn IPs, gán nó vào Gói và Nhà cung cấp.
Pinia useProxyStore bao gồm các dependencies dropdown:
Mỗi trang Proxy khi load phải gọi cùng lúc 3 API:
Promise.all([
proxyStore.fetchData(), // Lấy danh sách Proxy IPs
proxyStore.fetchProvidersSearch(), // Lấy list Nhà Cung Cấp đổ vào Dropdown
proxyStore.fetchPackagesSearch({ limit: 100 }), // Lấy list Gói Proxy đổ vào Dropdown
]);
Bộ Lọc Phức Tạp (UrlSearchParams):
recorded_at: Lọc theo khoảng thời gian "Ngày hết hạn" (expired_start, expired_end).proxy_provider_id: ID Provider. Khi chọn Provider → Tự động trigger fetchPackagesSearch để lấy Gói thuộc về Provider đó bằng việc reset URL Package ID.proxy_package_id: Lọc sâu theo Gói Proxy.is_active: Dropdown select "" (Tất cả), "true", "false". Note: Khởi tạo ép buộc giá trị rỗng để hiển thị All mặc định.keyword: Tìm kiếm chuỗi văn bản (thường là địa chỉ IP).Trong màn hình proxies/IndexView.vue có 3 nút:
Update proxy (UploadIcon):
apiProxies.uploadProxyv2()Thành công: X/Y providers, Z proxies.Tải file lên (UploadIcon):
ImportDomainModal (thực chất là Import Proxy IPs Modal). Cho phép admin upload file JSON/TXT để push mass IPs.Thêm mới (AddIcon):
CreateModal.Frontend thư mục: server-management/packages/
Proxy được nhóm lại thành các "Package" (gói dịch vụ). Ví dụ Gói "IP Tĩnh US", Gói "IP Xoay VN".
Màn hình Package gọi state listProvidersSearch từ Store để cho phép chọn Package thuộc Provider nào.
onBeforeMount(() => {
Promise.all([
packageStore.fetchData(),
packageStore.fetchDataProvider(), // Lấy list Providers
]);
});
Tùy chọn tạo ra một package sẽ gắn với proxy_provider_id. Điều này setup 1 relation 1-N: Provider -> N Packages -> M Proxy IPs.
Frontend thư mục: server-management/providers/
Cấp cao nhất của cấu trúc Proxy hằng bậc: Quản lý đối tác bán Proxy (Ví dụ: WebShare, Proxifly, vv).
Chủ yếu là ứng dụng CRUD chuẩn + Status Toggling.
Name, ActivestatusOptions: Hoạt động (true) / Tạm dừng (false).Frontend thư mục: server-management/backup-proxies/
Backend API route: /api/backup-proxies/
Hệ thống Backup Proxy hoạt động độc lập (nằm ngoài route con của server-management) và chủ yếu focus vào việc query external service tự động và Monitor trạng thái die/live (Health Check).
Lọc theo đặc điểm proxy tĩnh:
country: List cứng Vietnam, Thailand, Singapore, Japan, Korea, vv...is_working: Đang hoạt động / Không hoạt độngkeyword: Thường là IP hoặc Host.apiBackupProxies.importFromProxifly(). Kéo API từ service bên thứ 3 (Proxifly) đẩy thẳng vào MySQL.apiBackupProxies.healthCheck(). Bắn ping và proxy auth request tới tất cả DB Backup Proxy để check sống / chết lập tức trên Backend. Cập nhật is_working.Tất cả các thành phần về Proxy (Khác với servers nằm trong MySQL) đều được lưu trữ hoàn toàn trong MongoDB.
proxies collectiontype Proxy struct {
ID primitive.ObjectID `bson:"_id"`
Ip string `bson:"ip"`
Port int `bson:"port"`
Username string `bson:"username"`
Password string `bson:"password"`
RotateUrl ProxyRotateUrl `bson:"rotate_url"`
RotateTime int `bson:"rotate_time"` // Giây
NextRotate time.Time `bson:"next_rotate"` // Thời gian tính toán cho chu kỳ đổi
LastUsedAt time.Time `bson:"last_used_at"`
InUse int `bson:"in_use"` // Số task đang dùng
DomainUsed []TaskUseProxy `bson:"domain_used"` // Danh sách domain đã/đang dùng proxy này
DomainBlocks []string `bson:"domain_blocks"` // Danh sách domain block proxy này
IsCaptcha bool `bson:"is_captcha"`
IsError bool `bson:"is_error"`
ErrorCount int `bson:"error_count"` // Đếm chuỗi lỗi liên tiếp
IsActive bool `bson:"is_active"`
IpPublic string `bson:"ip_public"` // IP thực sự sau khi proxy đổi
Region string `bson:"region"`
DisableAutoRotate bool `bson:"disable_auto_rotate"`
IsRotating bool `bson:"is_rotating"` // Đang trong quá trình đổi (lock)
RotateServer *string `bson:"rotate_server"` // Server đang thực hiện việc đổi proxy
Area ProxyArea `bson:"area"` // global / vn
ProxyPackageID primitive.ObjectID `bson:"proxy_package_id"`
ExpiredAt *time.Time `bson:"expired_at"` // Ngày hết hạn (đồng bộ với Proxy Provider)
}
proxy_packages collectiontype ProxyPackages struct {
ID primitive.ObjectID `bson:"_id"`
Code string `bson:"code"`
Name string `bson:"name"`
Price int `bson:"price"`
Note string `bson:"note"`
ProxyProviderID *primitive.ObjectID `bson:"proxy_provider_id"` # Liên kết đến Provider
Active bool `bson:"active"`
}
proxy_providers collectionSử dụng custom struct FlexibleTime để parse thời gian từ BSON có thể lưu dưới dạng String / Int64 Timestamp / DateTime từ các tool export / import đa dạng.
type ProxyProvider struct {
ID primitive.ObjectID `bson:"_id"`
Name string `bson:"name"` // Ví dụ: KiotProxy, ProxyXoay, Tinsoft
Account string `bson:"account"`
Domain string `bson:"domain"`
Active bool `bson:"active"`
}
backup_proxies collectiontype BackupProxy struct {
ID primitive.ObjectID `json:"id" bson:"_id"`
Host string `json:"host" bson:"host"`
Port int `json:"port" bson:"port"`
Username string `json:"username" bson:"username"` // Authentication
Password string `json:"password" bson:"password"`
Country string `json:"country" bson:"country"` // Quốc gia chỉ định (VN, TH, US...)
IPPublic string `json:"ip_public" bson:"ip_public"`
IsWorking bool `json:"is_working" bson:"is_working"` // Kết quả Health Check
LastTested time.Time `json:"last_tested" bson:"last_tested"`
SuccessCount int `json:"success_count" bson:"success_count"`
FailureCount int `json:"failure_count" bson:"failure_count"` // Nếu quá 3 -> fail
Source string `json:"source" bson:"source"` // Ví dụ: "Proxifly"
}
Chức năng Rotate Proxy là phần nhân lõi nhất của module Proxy, đảm bảo IP liên tục thay đổi, tránh bị tracking hoặc captcha từ các nền tảng Search Engine, Ads.
Handler File: api/internal/proxy/cron/start_rotate.go
StartRotateV2() (Mới nhất)Mục tiêu: Tính toán xoay nhiều proxy cùng lúc bằng cách dùng các Worker Servers (để tránh IP máy chủ gốc kết nối đến Provider quá nhiều gây rate-limit hoặc chặn).
Bước thực thi:
GetManyCanRotate()). Điều kiện: is_active = true, disable_auto_rotate = false, next_rotate <= NOW(), is_rotating = false.GetActiveServers()).serverRotateCounts) của từng Server hiện tại từ DB để Load Balancing.count_current).Luồng Retry & Rotate per Proxy:
// Lặp qua danh sách cần xoay
for i := 0; i < proxyLength; i++ {
proxy := proxies[i]
for _, server := range mapServer {
// Skip server đã crash/lỗi
if server.IsError { continue }
// Tìm server có tải xoay đang dưới/bằng mức cân bằng
if server.Count <= countCurrent {
// FIRE HTTP POST sang Server (webhook callback)
result := api_proxy.CreateProxyRotate(server.Ip, body{
WebhookUrl: <Hệ_thống_gốc>/webhook/proxy-rotation-result/<Proxy_ID>,
RotateUrl: proxy.RotateUrl,
IpPublic: proxy.IpPublic,
... auth info ...
})
if !result { server.IsError = true; continue }
// Khóa (lock) proxy này báo là đang xoay
c.proxyService.UpdateOneIsRotating(proxy.ID, true, &server.Ip)
break // Done queueing proxy này
}
}
}
Nhận diện Webhook kết quả:
Sau khi Server Node xoay thành công, nó đẩy HTTP Request về lại WebhookUrl kèm theo IP Public mới.
Nếu nhận thành công:
proxy.Reset(): Đưa Error = 0, xóa list DomainBlocked.proxy.IpPublic = mới, proxy.NextRotate = NOW + rotate_time.Sử dụng Backup Proxy phòng ngừa trường hợp các provider lớn die.
Hàm MarkFailed():
func (bp *BackupProxy) MarkFailed() {
bp.FailureCount++
bp.LastTested = time.Now()
bp.UpdatedAt = time.Now()
// Failover logic - Quá ngưỡng fail -> đánh sập status
if bp.FailureCount >= 3 {
bp.IsWorking = false
}
}
Hàm MarkWorking():
Nếu Check thành công 1 lần, reset IsWorking = true ngay lập tức và tăng SuccessCount. Rất hữu ích khi các list proxies trôi nổi có thể sống lại bất ngờ.
IsActive logic Optimistic update trên frontend khiến UX rất mượt. Nhưng API change status không có cơ chế Rollback() trực tiếp khi lỗi, chỉ bắn thông báo.serverRotateCounts) giúp Server Node (worker) không bị kiệt quệ tài nguyên.IsRotating) tránh Duplicate Request lên proxy providers.Hệ thống có một cơ chế cron job riêng (cron_notificate_to_telegram.go) dùng thư viện github.com/go-co-op/gocron/v2 để tự động báo cáo các proxy/package sắp hết hạn qua Telegram (pkg.Env.ChatNotiId).
Hai luồng báo cáo chính:
monthly_expiration_report: Chạy theo pkg.Env.ScheduleMonthlyReport.weekly_expiration_report: Chạy theo pkg.Env.ScheduleWeeklyReport.Cơ chế hoạt động:
s := scheduler.StartScheduler()
s.NewJob(
gocron.CronJob(pkg.Env.ScheduleWeeklyReport, true),
gocron.NewTask(c.sendWeeklyReport),
gocron.WithName("weekly_expiration_report"),
gocron.WithSingletonMode(gocron.LimitModeWait), // Tránh spam cron đè nhau
)
Mục đích: Báo cáo cho đội ngũ vẩn hành biết trước 7 ngày, hoặc trong tháng những nhà cung cấp proxy nào sắp hết hạn/hết tiền (Zing, ProxyXoay) dựa trên hạn của Proxy Packages.
Dưới đây là chi tiết tất cả các endpoint được định nghĩa trong api/internal/server.
GET /api/servers/ (Pagination)Handler: GetListWithPagination
get_list_with_pagination.gopage, limit, keywords, is_active) -> Validate qua Go-Playground -> Tính toán PagingData -> Trả về messages.SuccessDataPagination().GET /api/servers/get-all (Danh sách đầy đủ)Handler: GetList
Dành cho các dropdown chọn server trên hệ thống mà không cần phân trang.
GET /api/servers/mongo-serversSử dụng MongoDB thay cho MySQL với những server được khởi tạo trên collection cũ phục vụ tương thích ngược.
GET /api/servers/reportData Model Trả Về: ServerReportResponse
Trả về report biểu đồ giờ của các Server, với chi tiết các lỗi nhúng trong Mongo:
Success, Fail, Waiting, Unknown, Error, NotFound, Captcha.RSuccess, RFail, RUnknown, RError, vv...GET /api/servers/health-check/stats: Trigger ping TCP đến các Worker Server.GET /api/servers/task-monitor/stats: Theo dõi phân phối Task có bị nghẽn (bottleneck) không.GET /api/servers/failure-records & failure-stats: Ghi nhận những lúc worker down hoặc timeout kết nối API.Module Proxy là phức tạp nhất do nó là tổ hợp giữa CRUD truyền thống trên MongoDB và các hệ thống bulk import + API 3rd party.
POST /api/proxies/: Tạo mới 1 proxy.GET /api/proxies/: Fetch danh sách phân trang (kèm filters proxy_provider_id, proxy_package_id, exp time...).GET /api/proxies/:id: Chi tiết 1 proxy.PUT /api/proxies/:id/change-status: Bật/Tắt hoạt động thủ công bằng NSwitch ngoài frontend (optimistic update).PUT /api/proxies/:id: Cập nhật cấu hình: Username, Password, IP Public, Rotate URL, Disable Auto Rotate.DELETE /api/proxies/:id: Xóa proxy đơn lẻ khỏi DB MongoDB.POST /api/proxies/bulk-delete: Cho phép Frontend tick nhiều rows và gọi đồng loạt để xóa theo IDs.DELETE /api/proxies/: (DeleteAll) Xóa sạch rổ proxy đang hiện hữu tùy theo package.GET /api/proxies/demo-excel: Tải file sample mẫu header để nhập proxy bằng file.GET /api/proxies/export: Export danh sách filter hiện tại ra dạng file .xlsx.POST /api/proxies/multiple: Thêm nhanh một list proxy IP:Port dạng văn bản.Lưu cấu trúc module sử dụng file rẽ nhánh riêng để maintain cho từng Provider Endpoint:
import_kiot_proxy.goimport_proxyxoay_proxy.goimport_zing_proxy.goimport_enode_proxy.goimport_m2_proxy.goLuồng Handler chung ImportAllProxies:
Được kích hoạt khi bấm "Update Proxy" ở Frontend Proxy/IndexView.vue (apiProxies.uploadProxyv2()).
// Khởi chạy hàng loạt goroutines dùng WaitGroup
var wg sync.WaitGroup
var mu sync.Mutex
// Ví dụ import Zing
h.importZing(ctx, req.Zing, &wg, &mu, results, errors, &successProviders, &totalProxies, providerDetails)
// Đợi tất cả fetch API xong hoặc Context Timeout (2 phút)
select {
case <-ctx.Done(): ...
case <-done: ...
}
Quản lý Token tĩnh / động:
importZing check Request token -> File Config -> Env Params. Tự động parse và push update/create Proxy objects vào collection proxies trong MongoDriver dựa theo danh sách mới kéo về từ API của nhà cung cấp. Thời gian import song song chỉ mất vài giây.
Mỗi store useXXXStore.ts trong các tab Proxy đều ánh xạ cấu trúc backend cực kỳ chuẩn.
useServerStore.tsChứa các biến statusOptions (Hoạt động/Tạm dừng) và typeOptions (record-domain/check-domain).
Định nghĩa Pagination Type standard của Naive UI Table:
listData: Pagination<Server.Item> | undefined;
Chức năng Validation tự dọn rác URL:
resetUrlParams() {
this.urlParams.page = zod.number().min(1).default(1).catch(1).parse(Number(this.urlParams.page))
this.urlParams.limit = zod.number().min(1).default(20).catch(20).parse(Number(this.urlParams.limit))
}
useProxyStore.tsĐây là state management khổng lồ giữ toàn bộ lookup table:
listProviders và listProvidersSearch (Mapped Options có format {label: Name, value: ID}).listPackages và listPackagesSearch: Mapping theo proxy_provider_id làm điều kiện phụ.Mỗi khi load, bảng Proxy phải pull:
apiProxies.getList với expired_start và expired_end mapping từ array [string, string] của component Naive NDatePicker.useHelper().showMessageError().useBackupProxyStore.tsMang đặc thù riêng, BackupProxy có list Option Code cứng:
countryOptions: [
{ label: "Vietnam", value: "VN" },
{ label: "Thailand", value: "TH" },
//...
];
useDataTable.tsTính STT phân trang bằng logic: (page - 1) * limit + index + 1.
Cột Hoạt động là h() function render dynamic:
render: (row: Server.Item) =>
h(NSwitch, {
value: row.is_active,
loading: updatingRows.value.has(row.id),
onUpdateValue: async (value: boolean) => {
// ... Logic call apiServers.changeStatus
},
});
Sắp xếp (Sorter) theo Client-side (localeCompare cho tên và timestamp trừ thời gian cho date).
Các module Proxy đều sử dụng NButton Action lồng bên trong div Flex Gap:
h('div', { class: 'flex gap-2 justify-center' }, [
h(NButton, { text:true, onClick: EditIcon }, [...]),
h(NButton, { text:true, onClick: DeleteIcon }, [...])
])
Dialog xóa confirm hiển thị Tên Item trực tiếp. Ví dụ: "Bạn có chắc chắn muốn xóa hệ thống KiotProxy?"
Khối System/Server có đặc trưng về Data storage rất độc đáo:
MySQL Instances: Dành cho các module tĩnh, cần ranh giới quan hệ mạnh (Foreign Keys).
servers (id, name, ip, limit_process)GORM.MongoDB Database: Dành cho mạng Proxy khổng lồ và Bulk Inserts.
TaskUseProxy - chứa Domain Used History).primitive.ObjectID linh hoạt.Vị trí của Repository sẽ kết nối song song các Dependency Injection bằng Container.Invoke.
Ví dụ tại file server Handler handler.go:
injection.Container.Invoke(func(
service *service.Service,
activityLogService *activityLogService.Service // Từ package ActivityLog lưu mysql
) {
h = &Handler{
serverService: service,
activityLogService: activityLogService,
}
})
Kỹ thuật Performance:
Để tránh Request API đập liên tục vào Backend mỗi khi người dùng gõ phím vào khung IP Search, toàn bộ các sự kiện thay đổi Form List filter đều map vào onChangeFilter.
Hàm này đính kèm hook @vueuse/core debounce:
const onChangeFilter = useDebounceFn(() => {
proxyStore.onChangePage(1);
}, 500); // Delay 0.5 giây kể từ khi kết thúc gõ.
NaiveUI Table config:
:scroll-x="1000": Đảm bảo giao diện không gãy ở màn hình tablet.:max-height="620": Thanh cuộn nội tuyến trong block table.BulkActions component tùy chỉnh ở phía trên NDataTable để count số checkbox.Trong useDataTable.ts khi thực thi API xóa/sửa với Provider API, Response của Error rất đa dạng theo cấp độ. Frontend của Traffic Tool V2 bắt buộc xử lý qua chaining optional properties phòng trừ backend trả về Null Object thay vì Error Message JSON:
const errorMessage =
error?.response?.data?.message || // Lỗi từ API Fiber
error?.message || // Lỗi Axios Network
"Có lỗi xảy ra khi cập nhật"; // Lỗi Undefined Catch
Kết quả báo cáo thành công (Import all) cũng làm tương tự do JSON parse có chèn Object Summary nếu thành công có điều kiện.
Một Proxy Service sẽ đi qua vòng đời:
proxies. Tính Default ExpiredAt.servers table mysql) ping lên API StartRotateV2, Backend lựa chọn Server rảnh nhất và giao Proxy để gọi webhook Rotate IPs.IsError = true và ngắt dòng Task.Module "Quản Lý Hệ Thống" là một điển hình của kiến trúc lưu trữ lai (Hybrid Storage) kết hợp sức mạnh của Relational Database (MySQL) và NoSQL (MongoDB).
servers table):
ID, Name, IP, Max/Current Process.proxies, proxy_packages, proxy_providers collections):
domain_used (chứa các struct history của quá trình quay website).FlexibleTime - để hứng các format time lộn xộn từ các Provider bên thứ ba mà không gây crash API (ProxyProvider.CreatedAt).Proxy)Trong api/internal/proxy/repository, thay vì dùng GORM, dự án dùng go.mongodb.org/mongo-driver.
func (r *Repository) GetOneCanRotate() (*proxy.Proxy, error) {
collection := r.Db.Collection(proxy.CollectionName)
filter := bson.M{
"is_active": true,
"disable_auto_rotate": false,
"is_rotating": false,
"next_rotate": bson.M{"$lte": time.Now()},
}
options := options.FindOne().SetSort(bson.M{"last_used_at": 1})
var result proxy.Proxy
err := collection.FindOne(context.Background(), filter, options).Decode(&result)
return &result, err
}
Điều này giúp khả năng Queue Rotate Proxy trở nên mượt mà, hạn chế Table Lock thường thấy trên MySQL.
Tính năng Bulk Delete được sử dụng rất nhiều ở cả 5 tab frontend nhằm dọn dẹp data hiệu quả.
BulkActions.vueselectedRowKeys.length."Đã chọn X mục".Delete và Refresh sẽ trigger events emit lên component cha DataTable.vue.useBulkSelection<T> lưu state và binding UI dialog Native của Naive UI. Giúp hiển thị Loading spinner trên nút Xác nhận.proxy/handler/bulk_delete.go){"ids": ["mongo_hex_id1", "mongo_hex_id2"]}.primitive.ObjectIDFromHex(id) để cast sang MongoDB format.collection.DeleteMany(ctx, bson.M{"_id": bson.M{"$in": objectIDs}})Đồng thời, đối với servers (MySQL) - API api/internal/server/handler/bulk_delete.go:
// GORM syntax cho Bulk Delete In Array
db.Where("id IN ?", ids).Delete(&Server{})
Hai công nghệ lưu trữ có 2 cách Bulk Delete khác nhau nhưng được che giấu phía sau 1 interface chung dành cho API Client (Frontend Axios).
Module Proxy cho phép Export data ra Excel theo thời gian thực (real-time generated).
File: api/internal/proxy/handler/export.go
Library: github.com/xuri/excelize/v2
Flow Export:
reqCtx có timeout (Vì export list >100.000 record cần thời gian xử lý dài).h.service.ExportProxy(reqCtx, req) lấy danh sách cấu trúc (Structs) đã filter từ DB.f := excelize.NewFile()
// Gán headers: ID, IP, Port, Username, Rotate Time...
// Tạo vòng lặp loop map dữ liệu
f.SetCellValue(sheetName, cell, value)
c.Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
c.Set("Content-Disposition", "attachment; filename=proxies_export.xlsx")
f.Write(c).Đây là phương thức Zero-Disk (Không cần write file tạm ra Server Disk), tối ưu bộ nhớ.
Sự nhất quán (Consistency) của hệ thống admin Vue.js được thể hiện rõ qua sự copy/paste layout các function ở store và view.
IndexView.vue (Cấu trúc chung)Mọi tab đều có các thành phần template cố định:
<template>
<div>
<!-- Phần 1: Header + Breadcrumb Tooltip -->
<PageHeader title="Quản lý ..." tooltip="..." />
<!-- Phần 2: Filter Panel (Sử dụng NSelect, NInput, NDatePicker) -->
<div class="mb-3 flex... gap-x-4">
<!-- Các ô filter config debounce v-model gắn vào Store URL params -->
</div>
<!-- Phần 3: Bảng Dữ Liệu Container -->
<PageBlock>
<DataTable ... />
</PageBlock>
<!-- Phần 4: Teleported Modals -->
<NModal v-model:show="store.stateModal.isShowCreateModal"> ... </NModal>
</div>
</template>
onShow watch luôn gọi lại hàm resetForm() và clear validation errors.id. Mỗi khi ID thay đổi -> Gọi fetchDataDetail() lấy dữ liệu từ DB, dùng Lodash _merge mixin vô formData Reactive State để form hiển thị giá trị.Từng tab quản lý cẩn trọng state Fetching nhờ Pinia global context:
isDisabled=true & Button Search bị khóa loading để tránh user click liên tục tạo DDoS cục bộ.state.isFetching (cho DataTable chính); state.isFetchingProviders / state.isFetchingPackages cho Select Dropdowns phụ.Bài toán: Mỗi nhà cung cấp proxy có cấu trúc API trả về hoàn toàn khác nhau.
Cách Giải Quyết OOP của Backend (Pattern Adaptor):
System tạo ra từng Handler tĩnh. importZing, importKiot, importProxifly.
Xử lý Auth Token Dẻo (Flexible Authentication Configuration):
Một điểm rất tinh tế trong Codebase (import_all_proxies.go) là tính năng fallback Token Auth theo Priority (Quyền ưu tiên):
config.ProxyProviderTokens (Cache cục bộ từ System API lưu vào RAM)..env configuration (Bảo chứng server-level).Mảng này tích hợp thêm Function maskToken() cho Logging:
"10_kys_first...10_kys_last". Giúp dev dễ check lỗi token chết mà không vi phạm Policy.Ngoài Notification Telegram Cron và StartRotate Cron, thư mục api/internal/proxy/cron còn có thành phần rất thú vị:
change_active_cron.go: Cron quét định kỳ hệ thống để kiểm tra ExpiredAt của proxy packages và proxy provider. Tự động set IsActive = false (Tạm dừng) đối với Proxy nào đã hết hạn mua gói (Thường quét vào lúc 12:00 đêm).Để maintain Module Server - Proxy Management, các Kỹ sư phần mềm cần chú ý:
Server, hãy đảm bảo sử dụng Gorm AutoMigrate cẩn thận.Proxy_Packages sẽ sinh ra các Proxy bị mất Link "Package Name/ID" nhưng chưa được dọn dẹp. Cần có Cron CleanUp Mongo./webhook/proxy-rotation-result.Proxy IP Logs ghi nhận sự thay đổi IP liên tục 5 phút / lần cho mỗi Proxy, dễ bề phình to ổ cứng DB.Router.push từ tab này sang tab kia, Component tuy Destroy nhưng Pinia Store không Reset nếu dev quên gọi store.$reset(). May mắn là onUnmounted hook đã cover tốt được case gọi $reset.Module Server & Proxy Management thể hiện năng lực kiến trúc Distributed (Phân tán) cao độ của Team xây dựng app. Việc offload (chuyển tải) nhiệm vụ Xoay Proxy/Gửi Request cho các Node/Server "con" (Worker) giúp cho API của Admin Console (Master) luôn nhẹ, giữ UI VueJS mượt mà nhưng vẫn thao túng lượng lớn Network Request background hằng ngày.
Hệ thống cung cấp một cơ chế Ràng buộc toàn diện (Validation Rules) dựa trên naive-ui cho tất cả các Form Modal trong Module Server/Proxy để ngăn lỗi từ đầu vào.
useFormData.ts (Quản Lý Server)const formData = shallowReactive<Server.Create>({
name: "",
ip: "",
is_active: true,
limit_process: 10,
});
api-worker.domain.com:51000).useFormData.ts (Quản Lý Khách Hàng/Nhà Cung Cấp)const rules: FormRules = {
domain: [
{ required: true, message: "Tên miền không được để trống", trigger: ["blur", "input"] },
{
validator: (rule, value) => {
if (!value) return true;
// Tự động strip giao thức http:// hoặc https://
const cleanedValue = value.replace(/^(https?:\/\/)/i, "");
if (value !== cleanedValue) {
formData.domain = cleanedValue;
return true;
}
// Regex chuẩn RFC-1123 dùng cho Domain name
const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/;
if (!domainRegex.test(cleanedValue)) {
return new Error("Tên miền không hợp lệ");
}
return true;
},
trigger: ["blur"],
},
],
};
url.Parse() để process.useFormData.ts (Quản Lý Gói/Packages)const formData = shallowReactive<Package.Create>({
name: "",
code: "",
price: null,
note: "",
proxy_provider_id: null,
active: true,
});
proxy_provider_id là Foreign Key mapping về MongoDB _id của bảng proxy_providers.Nhà cung cấp là NSelect map providerOptions (value: node.id). Không được để trống.POST /api/proxies/multipleSử dụng khi User import qua Modal nhập tay trên giao diện ImportDomainModal (thường là List IP Text String cực nhanh).
Cấu trúc xử lý Backend (Golang):
\n và Split theo delimiter (:, hoặc |).[]Proxy và Insert song song bằng Bulk Operation (InsertMany) của mongo-driver.GET /api/servers/health-check/statsChạy một tiến trình Ping hoặc HTTP Request từ Node Manager tới tất cả Worker IP.
Mô hình trả về (Dummy in code nhưng đây là thiết kế chuẩn):
{
"success": true,
"data": {
"server_123": { "status": "online", "latency": "23ms" },
"server_456": { "status": "offline", "timeout": true }
}
}
DELETE /api/proxies/)Đây là function DeleteAll(), không phải xóa Database, mà là xóa tất cả Proxy theo specific criteria.
bson.M{ "proxy_package_id": packageID } để chỉ định Scope cần dọn.Trong hệ thống Traffic liên tục gánh tải hàng triệu lượt search/click, việc Worker Network Issue là bình thường.
Các endpoints được thiết kế riêng cho việc gỡ lỗi:
GET /api/servers/failure-records: Liệt kê log của các Job không phản hồi từ Worker (Server down đột ngột hoặc mất kết nối).GET /api/servers/failure-stats: Thống kê tần suất Lỗi trên tổng Task theo mỗi 15-phút. Từ đó Vẽ biểu đồ Real-time cho sysadmin xử lý Reboot worker từ xa.(Note: Một số Handler này trong code source hiện trả về placeholder, nhưng kiến trúc đã chừa API cho tính năng này).
Toàn bộ Quản lý hệ thống được gộp vào 1 Main Layout, bọc trong <RouterView>.
<RouterView v-slot="{ Component, route }">
<Transition name="fade-transform" mode="out-in" appear>
<component :is="Component" :key="route.name" class="flex-1" />
</Transition>
</RouterView>
Thuộc tính mode="out-in" quy định: Tab cũ phải biến mất hoàn toàn xong Tab mới mới Fade vào. Điều này chống tình trạng giao diện nhảy giật khi 2 Data Table của Proxy và Server đồng loạt render hàng ngàn Node HTML cùng 1 DOM Tick.
Dùng Capsule shape Tabs: Đoạn SCSS sử dụng mã màu Teal backgroundColor: #0a4e4f; kèm shadow Black #000 0 0 4px đánh bật Menu Đang Hoạt Động (Active State) khỏi background #09484a0d.
<p>
Hiển thị từ {{ ((page - 1) * limit) + 1 }} đến {{ Math.min(page * limit, total) }} trên tổng {{
total }}
</p>
Đoạn Message tính toán thủ công từ Response Meta Data thay vì dùng Message built-in của naive-ui để hỗ trợ tiếng Việt có tính cá nhân hóa tốt hơn.
Hệ thống quản lý hàng chục triệu Task, do đó Database MongoDB Proxy phải luôn được Index.
Cần đảm bảo Index MongoDB cho các field sau ở production:
_id (Mặc định)is_active, is_rotating, disable_auto_rotate (Phục vụ cron GetManyCanRotate)next_rotate (Compound Index với các status, tăng tốc độ cron 10 lần).proxy_package_id (Phục vụ delete Bulk).ip (Phục vụ search Text).Thách thức của Webhook Rotation:
Vì Webhook URL của Master (bên giao proxy) đưa cho mạng Worker gọi lại vào Callback endpoint WebhookUrl: pkg.Env.WebhookHost + "/webhook/proxy-rotation-result/" + id - Có thể tiềm ẩn rủi ro Security Webhook Forgery nếu worker không gửi kèm Signature. Giải pháp tương lai là chèn thêm X-Signature-Hmac cho payload của Worker Node.
| Phương Thức | Tác Động Tới Table | Trách Nhiệm Code (Mô hình MVC -> Fiber Handlers) | Ghi Chú Bảo Mật |
|---|---|---|---|
FindByID | servers | Tìm server bằng ID -> Trả ServerResponse DTO không chứa info nhạy cảm | API Internal |
GetDetail | proxy_packages | Lấy list Providers nhúng (Nested JSON) cùng lúc để bind vào Dropdown Edit Form | Require Role |
ImportProxies | proxies | Gọi API Zing / Webshare / Tinsoft... Map JSON schema tùy chỉnh | Cần Secret Keys |
UpdateActive | MySQL/MongoDB | Lật State cờ Toggle | Fire and Forget |
NewLog | proxy_ip_log | Cập nhật Node Public IP để tracking các Blocked Domain (như Google/Facebook) | Heavy Write |
Bên cạnh useDataTable, thư mục composables của frontend còn chứa nhiều utilities cực kỳ quan trọng giúp các View trở nên gọn gàng.
useBulkSelection.ts (Global Utility)Vị trí: admin/src/composables/useBulkSelection.ts
Đây là Generics Composable (<T>) quản lý state cho mảng các checkbox của Naive UI.
Luồng hoạt động:
Set<string | number> nội bộ tên là selectedRowKeys.handleBulkDelete, handleBulkRefresh.selectedRowKeys.clear() tự động khi thực thi xong.isProcessing (boolean state) cho table bên ngoài để hiện thị spinner (như loading=$props.isProcessing).Điều này đã thay thế hàng trăm dòng boilerplate code rải rác trong IndexView.vue của cả Servers, Proxies, Packages, Providers, và Backup Proxies.
useHelper.ts (Error Catcher)Vị trí: admin/src/composables/utils.ts (Hành vi showMessageError)
Trung tâm map các loại lỗi từ HTTP Axios AxiosError sang Naive UI Message.
function showMessageError(error: any) {
let msg = "Đã có lỗi xảy ra. Vui lòng thử lại!";
// 1. Nếu backend format chuẩn JSON
if (error?.response?.data?.message) {
msg = error.response.data.message;
}
// 2. Fallback sang text lỗi thô
else if (error?.message) {
msg = error.message;
}
window.$message?.error(msg);
}
Mỗi Modal trong hệ thống sử dụng một thủ thuật v-model:show="isShow" để đồng bộ state tắt/mở kết hợp Lifecycle Hooks của Vue3.
Tại sao Form Modal lại dễ bị lỗi lưu cache nhập liệu cũ?
Vue <NModal> của Naive UI chỉ render display: none (hoặc v-if) nhưng component con bên trong vẫn giữ nguyên trạng thái Reactive Objects. Nghĩa là: nếu bạn mở CreateModal (tạo nhà cung cấp), nhập "Tinsoft" xong bấm X (Tắt modal), lần sau mở lại chữ "Tinsoft" vẫn còn.
Cách giải quyết (watch(isShow))
Các File *Modal.vue (Ví dụ: CreateModal.vue) bắt buộc có đoạn mã Cleanup:
watch(isShow, newValue => {
if (!newValue) {
// Khi Modal bị đóng
// 1. Reset manually fields (Nếu không dùng Object.assign)
formData.name = "";
formData.active = true;
// 2. Clear đỏ Validation Naive Form
formRef.value?.restoreValidation();
}
});
trigger: ['blur', 'input'] được set ở useFormData.ts:
domain báo lỗi ngay từng nhịp phím).Dù không phải cùng chung 1 DB Engine, nhưng kiến trúc chia nhỏ Micro-level này cần một sự am hiểu ánh xạ khóa ngoại thủ công (Application Level).
Một Gói (Proxy Package) thuộc về Một Nhà Cung Cấp (Proxy Provider).
proxy_provider_id trên UI.ProxyPackageResponse.ProxyProvider struct lồng ghép.Nhiều IP (Proxy) thuộc về Một Gói (Proxy Package).
Quản lý Proxy -> Import URL list -> Ánh xạ Proxy mới tạo vào proxy_package_id.
Vì nằm ở MongoDB, khi truy xuất danh sách Proxy (GetList()), Mongo Lookup Pipeline ($lookup) được dùng để pull thông tin Gói nhét vào biến proxy_package.
Pipeline sample GetList():
{
"$lookup": {
"from": "proxy_packages",
"localField": "proxy_package_id",
"foreignField": "_id",
"as": "proxy_package"
}
}
// MongoDB array output phải được `$unwind` (bung mảng) thành object để match với struct Go.
Mối Tương Tác: Proxy <-> Server:
ServerRotate *string. Ghi nhận IP Tĩnh của Server chịu trách nhiệm gửi Lệnh xoay của chính Proxy đó.CronRotate tự fail over tìm server khác nhờ cơ chế Loop Map Count.Module Proxy là một hệ thống không đáng tin cậy (Unreliable External Source). Các IP mua từ 3rd party (Ví dụ: KiotProxy) sẽ gặp tình trạng "chết IP", "Bad Gateway", "Xác thực Captcha quá nhiều lần" trên Google (khi kéo Traffic SEO).
Flow Ghi Nhận Captcha và Error Lên DB Proxy:
Worker App (Node.js/Playwright)
http://usr:pwd@ip:port.API Endpoint Của Master (Nhận Kết Quả Error)
ProxyService.IncreaseError(objectID)update := bson.M{
"$inc": bson.M{"error_count": 1},
"$set": bson.M{
"is_captcha": true,
"is_error": true,
},
}
error_count.UI Dashboard Phản Ứng Dữ Liệu:
is_error hoặc error_count cao, Proxy đó có thể được cảnh báo đỏ nếu làm tính năng Frontend Alert. UI admin có thể tracking các Proxy IP rác và tick nhiều dòng để Bulk Delete khỏi hệ thống, sau đó gọi "Upload URL mới" khôi phục số lượng IP cần thiết cho Gói.Với chức năng "Tải file lên" (Bulk Import Modal Proxy IPs), người dùng có khuynh hướng nhập file .txt lặp lại nhiều lần.
Backend POST /api/proxies/multiple và POST /api/proxies/import phải tính phương án Check trùng lắp.
Giải pháp:
proxies, IP có thể thay đổi nên khóa kiểm tra duy nhất phức tạp. Backend thường Query Array các Auth Token / Username / Hoặc RotateUrl của file xem đã tồn tại trong Database hay chưa.Port, RotateUrl và Proxy_package_id làm Identifier đối sánh.// Tuân thủ Upsert Model logic của Go-Mongo
option := options.Update().SetUpsert(true)
collection.UpdateOne(ctx, filter_tồn_tại, data, option)
Dù codebase Server Management & Proxy rất hoàn thiện, dưới góc nhìn Kiến trúc, còn điểm có thể mở rộng:
Vấn đề Locking Proxy: IsRotating hiện tại là Boolean Flag tại DB. Khi App Crash giữa chừng trong lúc Rotate, Cờ Rotating sẽ bị kẹt True vĩnh viễn (Deadlock proxy logic xoay).
IsRotating thành RotatingLockedUntil (Timestamp). Ví dụ set (NOW + 30s). Nếu hết 30s mà cờ vẫn tồn tại thì Server mặc định coi như mở khóa (giống Redis Timeout Locks).Lưu Trữ Log Proxy Phình To: Cron change_active_cron mới chỉ tạm ngưng các proxy hết hạn mua trả phí. Nhưng chưa xóa hẳn (Hard Delete). Bảng Proxy IP Logs (proxy_ip_log) tracking public IP sẽ làm MongoDB Storage cạn kiệt sau vài tháng.
db.proxy_ip_log.createIndex( { "created_at": 1 }, { expireAfterSeconds: 2592000 } ) (Tự drop log rác sau 30 ngày ngay tại tầng Driver DB, không cần Back-end Cron xử lý).Phân biệt Lựa Chọn Tối Ưu Phân Trang (Pagination Skip/Limit vs Cursor)
Skip(). Với 500,000 proxies, trang số 5000 (Skip 100000) sẽ làm Mongo quét chậm lại (O(n))._id > last_id_of_previous_page). Tuy nhiên bù lại UI của Naive Data Table mất tính năng jump trang bấm số (1..5...89..100). Hiện tại Team đánh đổi UI Index Page (Skip) cho UX dễ xài vì số Record Proxy hiện tại chưa quá tải đến hàng chục triệu.Vấn đề muôn thủa của các hệ thống định tuyến Proxy là "Chúng ta cần biết Proxy đã giữ IP nào vào thời điểm nào". Khi một người dùng report rằng Hệ thống SEO không được lên top vì Google trát cờ spam, Admin cần tra cứu xem IP đó sinh ra từ nhà mạng (Provider) nào, Proxy cụ thể là gói gì, và nằm trong múi giờ nào.
Cấu trúc bảng ProxyIPLog:
File thiết kế api/internal/proxy_ip_log/proxy_ip_log.go cung cấp collection lưu lịch sử với khối lượng khổng lồ.
type ProxyIpLog struct {
ID primitive.ObjectID `json:"id" bson:"_id"`
ProxyId string `json:"proxy_id" bson:"proxy_id"` // ID Proxy mẹ
Ip string `json:"ip" bson:"ip"` // IP Public
Supplier string `json:"supplier" bson:"supplier"` // Domain Provider (proxifly.dev, webshare.io...)
Region *string `json:"region" bson:"region"` // Mã quốc gia (VN)
CreatedAt time.Time `json:"created_at" bson:"created_at"`
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
}
Mỗi khi Cronjob StartRotateV2() nhận lệnh webhook báo rằng IP đã xoay đổi thành công (Khác với IP Public cũ), nó sẽ trigger proxyIpLogService.NewLog(proxy_id, new_ip).
Admin Panel có khả năng cung cấp nút "Xem lịch sử Lọc" ở Data Table của Proxy. Route liên kết: /api/proxy-ip-logs/.
Cả một Controller riêng (handler.go của module proxy_ip_log) hoạt động song song mà không dính tới Server chính nhằm bảo đảm hiệu suất:
func (h *Handler) GetList(c *fiber.Ctx) error {
var params dto.GetListProxyIpLog
// ... Phân trang dựa theo "proxy_id"
result, _ := h.service.GetListProxyIpLog(reqCtx, params)
return c.JSON(result)
}
Để tài liệu kỹ thuật hoàn chỉnh, không thể không nhắc đến cách Team WinAd sắp xếp mã nguồn Golang của hệ thống Giga Master theo cơ chế Domain-Driven Design (DDD) lai với Clean Architecture.
Thay vì code toàn bộ route ở một file main.go hay router.go, mỗi nhóm nghiệp vụ (như Server, Proxy, Backup Proxy) sống độc lập trong api/internal/<tên_nghiệp_vụ>/.
Bên trong mỗi folder sẽ luôn có 6 thành phần:
dto (Data Transfer Objects): Chứa mọi Request, Response Structs.handler (Bắt Request Fiber HTTP): Gọi các function Service, return JSON theo chuẩn messages.Success.repository (Thao tác DB Model): Viết query SQL/MongoDB, không xử lý business logic.service (Logic Core): Orchestrate giữa Repository này với Repository khác.cron (Tính năng Backup): Một số Domain sẽ có Cron đính kèm nếu tự động hoá.<module>.go (Struct Config): Model mapping GORM hoặc BSON.sarulabs/di (Dependency Injection)Framework Server không tự khởi tạo hàm NewService(). Bằng cách dùng Container của thư viện dependency injection:
// Từng Repos sẽ được nạp trong bootstrap/injection.go
builder.Add(di.Def{
Name: "proxy-repository",
Build: func(ctn di.Container) (interface{}, error) {
db := ctn.Get("mongodb").(*mongo.Database)
redisCache := ctn.Get("redis").(*redis.Client)
return proxyRepo.NewRepository(db, redisCache), nil
},
})
Điều này khiến cho:
api/internal/server. (Chỉ việc đẩy MockDB Repository).import cycle not allowed).import_all_proxies)Khi viết tính năng ImportAllProxies, team đã lựa chọn thiết kế Eventual Consistency.
Khi 1 Job Import (Gồm cả API lấy data từ Kiot, Zing, M2Proxy) được chạy, nó tạo ra 4 Go-Routines độc lập bằng sync.WaitGroup.
// Chạy import song song
go h.importZing(ctx, req.Zing, &wg, &mu, results...)
go h.importKiotProxy(ctx, req, &wg, &mu, results...)
go h.importM2Proxy(ctx, req, &wg, &mu, results...)
go h.importProxyXoay(ctx, req, &wg, &mu, results...)
Tính Năng Vượt Trội:
providerCtx, cancel := context.WithTimeout(ctx, 2*time.Minute). Nếu M2Proxy Die/ Sập server mất kết nối, thì hàm importM2Proxy tự hủy bộ chọn và return Error message lên map errors.mu.Lock() (Mutex) từ struct chung để truyền dữ liệu Thread-safe vào biến totalProxies và mảng errors. Chống Race Condition trong Go.