Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
U
upgrade-data-crawler-be
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ThinhNC
upgrade-data-crawler-be
Commits
adc75aa5
Commit
adc75aa5
authored
Sep 10, 2026
by
ThinhNC
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(system-config): sync configs with .env and add 7-day retention cron jobs
parent
ded020b6
Changes
14
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
480 additions
and
176 deletions
+480
-176
sync-system-configs.ts
scripts/sync-system-configs.ts
+1
-0
cron.constant.ts
src/common/constants/cron.constant.ts
+13
-3
system-config.constant.ts
src/common/constants/system-config.constant.ts
+84
-26
swagger.json
src/docs/swagger.json
+146
-136
crawl-export.dto.ts
src/modules/crawl-exports/crawl-export.dto.ts
+1
-0
cron.service.test.ts
src/modules/cron/__tests__/cron.service.test.ts
+29
-0
cron.repository.ts
src/modules/cron/cron.repository.ts
+51
-0
cron.service.ts
src/modules/cron/cron.service.ts
+80
-2
export.service.test.ts
src/modules/exports/__tests__/export.service.test.ts
+5
-0
export.service.ts
src/modules/exports/export.service.ts
+12
-0
system-config.controller.ts
src/modules/system-config/system-config.controller.ts
+17
-0
system-config.repository.ts
src/modules/system-config/system-config.repository.ts
+12
-6
system-config.route.ts
src/modules/system-config/system-config.route.ts
+8
-0
system-config.service.ts
src/modules/system-config/system-config.service.ts
+21
-3
No files found.
scripts/sync-system-configs.ts
View file @
adc75aa5
...
...
@@ -12,6 +12,7 @@ async function main() {
await
prisma
.
systemConfig
.
upsert
({
where
:
{
key
:
item
.
key
},
update
:
{
value
:
item
.
value
as
any
,
description
:
item
.
description
??
null
,
category
:
item
.
category
,
isPublic
:
item
.
isPublic
,
...
...
src/common/constants/cron.constant.ts
View file @
adc75aa5
export
const
CRON_JOB_NAMES
=
{
CLEANUP_AUDIT_LOGS
:
"cleanup-audit-logs"
,
CLEANUP_EXPORTS
:
"cleanup-exports"
,
CLEANUP_UNCONFIRMED_UPLOADS
:
"cleanup-unconfirmed-uploads"
,
CLEANUP_EXPIRED_TOKENS
:
"cleanup-expired-tokens"
,
DAILY_SUMMARY_DIGEST
:
"daily-summary-digest"
,
...
...
@@ -24,7 +25,9 @@ export const CRON_QUEUE_NAME = "cron-scheduler-queue" as const;
export
const
CRON_SYSTEM_CONFIG_KEY
=
"CRON_JOB_STATUSES"
as
const
;
export
const
DEFAULT_AUDIT_LOG_RETENTION_DAYS
=
30
;
export
const
DEFAULT_AUDIT_LOG_RETENTION_DAYS
=
7
;
export
const
DEFAULT_EXPORT_RETENTION_DAYS
=
7
;
export
const
DEFAULT_UNCONFIRMED_UPLOAD_MAX_AGE_HOURS
=
24
;
...
...
@@ -37,13 +40,20 @@ export interface CronScheduleConfig {
export
const
DEFAULT_CRON_SCHEDULES
:
Record
<
CronJobName
,
CronScheduleConfig
>
=
{
[
CRON_JOB_NAMES
.
CLEANUP_AUDIT_LOGS
]:
{
cron
:
"0 2 * * *"
,
description
:
"Dọn dẹp các bản ghi nhật ký kiểm toán
cũ hơn số ngày quy định
"
,
description
:
"Dọn dẹp các bản ghi nhật ký kiểm toán
(audit logs) cũ hơn 7 ngày
"
,
defaultParams
:
{
retentionDays
:
DEFAULT_AUDIT_LOG_RETENTION_DAYS
,
},
},
[
CRON_JOB_NAMES
.
CLEANUP_
UNCONFIRMED_UPLOAD
S
]:
{
[
CRON_JOB_NAMES
.
CLEANUP_
EXPORT
S
]:
{
cron
:
"0 3 * * *"
,
description
:
"Dọn dẹp các tệp xuất dữ liệu (exports) và bản ghi hết hạn hoặc cũ hơn 7 ngày"
,
defaultParams
:
{
retentionDays
:
DEFAULT_EXPORT_RETENTION_DAYS
,
},
},
[
CRON_JOB_NAMES
.
CLEANUP_UNCONFIRMED_UPLOADS
]:
{
cron
:
"0 4 * * *"
,
description
:
"Quét và dọn dẹp các tệp tin tải lên mồ côi hoặc xuất file tạm quá hạn"
,
defaultParams
:
{
maxAgeHours
:
DEFAULT_UNCONFIRMED_UPLOAD_MAX_AGE_HOURS
,
...
...
src/common/constants/system-config.constant.ts
View file @
adc75aa5
import
"dotenv/config"
;
export
const
SYSTEM_CONFIG_CATEGORY
=
{
GENERAL
:
"GENERAL"
,
FEATURE_FLAG
:
"FEATURE_FLAG"
,
...
...
@@ -40,7 +42,7 @@ export const DEFAULT_SYSTEM_CONFIGS: readonly DefaultSystemConfigItem[] = [
},
{
key
:
"app.frontend_url"
,
value
:
"http://localhost:3000"
,
value
:
process
.
env
.
FRONTEND_URL
||
"http://localhost:3000"
,
category
:
SYSTEM_CONFIG_CATEGORY
.
GENERAL
,
isPublic
:
true
,
description
:
"Địa chỉ Web Frontend điều hướng liên kết và callback xác thực (FRONTEND_URL)"
,
...
...
@@ -61,85 +63,120 @@ export const DEFAULT_SYSTEM_CONFIGS: readonly DefaultSystemConfigItem[] = [
},
{
key
:
"crawler.max_pages_default"
,
value
:
100
,
value
:
parseInt
(
process
.
env
.
MAX_CRAWL_PAGES
||
"100"
,
10
)
,
category
:
SYSTEM_CONFIG_CATEGORY
.
GENERAL
,
isPublic
:
false
,
description
:
"Số lượng trang cào tối đa mặc định cho một tác vụ cào dữ liệu (MAX_CRAWL_PAGES)"
,
},
{
key
:
"crawler.max_depth_default"
,
value
:
3
,
value
:
parseInt
(
process
.
env
.
MAX_CRAWL_DEPTH
||
"3"
,
10
)
,
category
:
SYSTEM_CONFIG_CATEGORY
.
GENERAL
,
isPublic
:
false
,
description
:
"Độ sâu liên kết tối đa mặc định khi thu thập dữ liệu web (MAX_CRAWL_DEPTH)"
,
},
{
key
:
"crawler.timeout_ms"
,
value
:
60000
,
value
:
parseInt
(
process
.
env
.
FIRECRAWL_REQUEST_TIMEOUT_MS
||
"60000"
,
10
)
,
category
:
SYSTEM_CONFIG_CATEGORY
.
GENERAL
,
isPublic
:
false
,
description
:
"Thời gian chờ tối đa cho mỗi yêu cầu HTTP thu thập trang (FIRECRAWL_REQUEST_TIMEOUT_MS)"
,
},
{
key
:
"crawler.worker_concurrency"
,
value
:
3
,
value
:
parseInt
(
process
.
env
.
WORKER_CONCURRENCY
||
"3"
,
10
)
,
category
:
SYSTEM_CONFIG_CATEGORY
.
GENERAL
,
isPublic
:
false
,
description
:
"Số lượng luồng worker chạy đồng thời xử lý hàng đợi cào dữ liệu (WORKER_CONCURRENCY)"
,
},
{
key
:
"crawler.job_timeout_ms"
,
value
:
300000
,
value
:
parseInt
(
process
.
env
.
WORKER_JOB_TIMEOUT_MS
||
"300000"
,
10
)
,
category
:
SYSTEM_CONFIG_CATEGORY
.
GENERAL
,
isPublic
:
false
,
description
:
"Thời gian chạy tối đa cho toàn bộ một tiến trình cào trước khi timeout (WORKER_JOB_TIMEOUT_MS)"
,
},
{
key
:
"crawler.max_stalled_count"
,
value
:
1
,
value
:
parseInt
(
process
.
env
.
WORKER_MAX_STALLED_COUNT
||
"1"
,
10
)
,
category
:
SYSTEM_CONFIG_CATEGORY
.
GENERAL
,
isPublic
:
false
,
description
:
"Số lần tối đa cho phép worker khôi phục tác vụ cào khi bị treo (WORKER_MAX_STALLED_COUNT)"
,
},
{
key
:
"storage.default_driver"
,
value
:
"s3"
,
value
:
process
.
env
.
STORAGE_DRIVER
||
"s3"
,
category
:
SYSTEM_CONFIG_CATEGORY
.
GENERAL
,
isPublic
:
false
,
description
:
"Driver lưu trữ tệp xuất dữ liệu mặc định: 's3' hoặc 'local' (STORAGE_DRIVER)"
,
},
{
key
:
"storage.export_dir"
,
value
:
"storage/exports"
,
value
:
process
.
env
.
STORAGE_EXPORT_DIR
||
"storage/exports"
,
category
:
SYSTEM_CONFIG_CATEGORY
.
GENERAL
,
isPublic
:
false
,
description
:
"Thư mục lưu trữ tạm thời các gói nén ZIP trên máy chủ (STORAGE_EXPORT_DIR)"
,
},
{
key
:
"storage.s3.bucket"
,
value
:
"data-crawler-exports"
,
value
:
process
.
env
.
S3_BUCKET
||
"data-crawler-exports"
,
category
:
SYSTEM_CONFIG_CATEGORY
.
GENERAL
,
isPublic
:
false
,
description
:
"Tên bucket lưu trữ các tệp xuất dữ liệu cào dạng nén ZIP (S3_BUCKET)"
,
},
{
key
:
"storage.s3.region"
,
value
:
"us-
east-1"
,
value
:
process
.
env
.
S3_REGION
||
"ap-south
east-1"
,
category
:
SYSTEM_CONFIG_CATEGORY
.
GENERAL
,
isPublic
:
false
,
description
:
"Vùng máy chủ lưu trữ dữ liệu tệp nén S3 (S3_REGION)"
,
},
{
key
:
"storage.s3.endpoint"
,
value
:
"http://127.0.0.1:9000"
,
value
:
process
.
env
.
S3_ENDPOINT
||
""
,
category
:
SYSTEM_CONFIG_CATEGORY
.
GENERAL
,
isPublic
:
false
,
description
:
"Địa chỉ Endpoint kết nối dịch vụ lưu trữ AWS S3 hoặc Supabase Storage (S3_ENDPOINT)"
,
},
{
key
:
"storage.s3.force_path_style"
,
value
:
process
.
env
.
S3_FORCE_PATH_STYLE
!==
undefined
?
process
.
env
.
S3_FORCE_PATH_STYLE
===
"true"
:
true
,
category
:
SYSTEM_CONFIG_CATEGORY
.
GENERAL
,
isPublic
:
false
,
description
:
"Địa chỉ Endpoint kết nối dịch vụ lưu trữ AWS S3 hoặc MinIO (S3_ENDPOINT)"
,
description
:
"Bật chế độ Path-Style Access cho AWS S3 hoặc Supabase Storage (S3_FORCE_PATH_STYLE)"
,
},
{
key
:
"retention.audit_logs_days"
,
value
:
7
,
category
:
SYSTEM_CONFIG_CATEGORY
.
GENERAL
,
isPublic
:
false
,
description
:
"Số ngày lưu trữ tối đa cho bản ghi nhật ký kiểm toán (Audit Logs) trước khi tự động dọn dẹp (ngày)"
,
},
{
key
:
"retention.exports_days"
,
value
:
7
,
category
:
SYSTEM_CONFIG_CATEGORY
.
GENERAL
,
isPublic
:
false
,
description
:
"Số ngày lưu trữ tối đa cho các tệp và bản ghi xuất dữ liệu (Crawl Exports) trước khi tự động xóa (ngày)"
,
},
// ==========================================
// 2. FEATURE_FLAG (Cờ bật/tắt tính năng động)
// ==========================================
{
key
:
"feature.cron.cleanup_audit_logs.enabled"
,
value
:
true
,
category
:
SYSTEM_CONFIG_CATEGORY
.
FEATURE_FLAG
,
isPublic
:
false
,
description
:
"Cờ tính năng: Cho phép tự động dọn dẹp các bản ghi nhật ký kiểm toán (Audit Logs) cũ hơn 7 ngày"
,
},
{
key
:
"feature.cron.cleanup_exports.enabled"
,
value
:
true
,
category
:
SYSTEM_CONFIG_CATEGORY
.
FEATURE_FLAG
,
isPublic
:
false
,
description
:
"Cờ tính năng: Cho phép tự động dọn dẹp các tệp và bản ghi xuất dữ liệu (Crawl Exports) hết hạn"
,
},
{
key
:
"feature.registration.enabled"
,
value
:
true
,
...
...
@@ -216,21 +253,42 @@ export const DEFAULT_SYSTEM_CONFIGS: readonly DefaultSystemConfigItem[] = [
},
{
key
:
"integration.firecrawl.api_key"
,
value
:
"fc-ae80a8acc69a4f42a8bca0e80370077e
"
,
value
:
process
.
env
.
FIRECRAWL_API_KEY
||
"
"
,
category
:
SYSTEM_CONFIG_CATEGORY
.
INTEGRATION
,
isPublic
:
false
,
description
:
"Khóa Firecrawl API Key điều khiển engine cào DOM (
xoay tua & đổi động mà không cần restart
)"
,
description
:
"Khóa Firecrawl API Key điều khiển engine cào DOM (
FIRECRAWL_API_KEY
)"
,
},
{
key
:
"integration.firecrawl.base_url"
,
value
:
"https://api.firecrawl.dev"
,
value
:
process
.
env
.
FIRECRAWL_BASE_URL
||
"https://api.firecrawl.dev"
,
category
:
SYSTEM_CONFIG_CATEGORY
.
INTEGRATION
,
isPublic
:
false
,
description
:
"Địa chỉ API gốc của dịch vụ Firecrawl thu thập DOM (FIRECRAWL_BASE_URL)"
,
},
{
key
:
"integration.smtp.host"
,
value
:
process
.
env
.
SMTP_HOST
||
"smtp.gmail.com"
,
category
:
SYSTEM_CONFIG_CATEGORY
.
INTEGRATION
,
isPublic
:
false
,
description
:
"Máy chủ SMTP gửi email thông báo hệ thống (SMTP_HOST)"
,
},
{
key
:
"integration.smtp.port"
,
value
:
parseInt
(
process
.
env
.
SMTP_PORT
||
"587"
,
10
),
category
:
SYSTEM_CONFIG_CATEGORY
.
INTEGRATION
,
isPublic
:
false
,
description
:
"Cổng kết nối máy chủ SMTP (SMTP_PORT)"
,
},
{
key
:
"integration.smtp.user"
,
value
:
process
.
env
.
SMTP_USER
||
"nctmdt@gmail.com"
,
category
:
SYSTEM_CONFIG_CATEGORY
.
INTEGRATION
,
isPublic
:
false
,
description
:
"Tài khoản người dùng SMTP gửi email (SMTP_USER)"
,
},
{
key
:
"integration.smtp.from_name"
,
value
:
"Data Crawler
Support
"
,
value
:
"Data Crawler"
,
category
:
SYSTEM_CONFIG_CATEGORY
.
INTEGRATION
,
isPublic
:
true
,
description
:
"Tên người gửi hiển thị trong các email hệ thống (SMTP_FROM)"
,
...
...
@@ -262,63 +320,63 @@ export const DEFAULT_SYSTEM_CONFIGS: readonly DefaultSystemConfigItem[] = [
// ==========================================
{
key
:
"jwt.access_expires_in"
,
value
:
"1d"
,
value
:
process
.
env
.
JWT_ACCESS_EXPIRES_IN
||
"1d"
,
category
:
SYSTEM_CONFIG_CATEGORY
.
SECURITY
,
isPublic
:
false
,
description
:
"Thời gian hết hạn của JWT Access Token cho phiên đăng nhập (JWT_ACCESS_EXPIRES_IN)"
,
},
{
key
:
"jwt.refresh_expires_in"
,
value
:
"7d"
,
value
:
process
.
env
.
JWT_REFRESH_EXPIRES_IN
||
"7d"
,
category
:
SYSTEM_CONFIG_CATEGORY
.
SECURITY
,
isPublic
:
false
,
description
:
"Thời gian hết hạn của Refresh Token cho phiên đăng nhập (JWT_REFRESH_EXPIRES_IN)"
,
},
{
key
:
"quota.user_max_pages"
,
value
:
100
,
value
:
parseInt
(
process
.
env
.
USER_MAX_PAGES
||
"100"
,
10
)
,
category
:
SYSTEM_CONFIG_CATEGORY
.
SECURITY
,
isPublic
:
false
,
description
:
"Hạn ngạch số trang cào tối đa mặc định cho tài khoản người dùng thông thường (USER_MAX_PAGES)"
,
},
{
key
:
"quota.user_max_jobs_per_day"
,
value
:
10
,
value
:
parseInt
(
process
.
env
.
USER_MAX_JOBS_PER_DAY
||
"10"
,
10
)
,
category
:
SYSTEM_CONFIG_CATEGORY
.
SECURITY
,
isPublic
:
false
,
description
:
"Hạn ngạch số tác vụ cào tối đa trong một ngày cho mỗi tài khoản (USER_MAX_JOBS_PER_DAY)"
,
},
{
key
:
"quota.user_max_concurrent_jobs"
,
value
:
3
,
value
:
parseInt
(
process
.
env
.
USER_MAX_CONCURRENT_JOBS
||
"3"
,
10
)
,
category
:
SYSTEM_CONFIG_CATEGORY
.
SECURITY
,
isPublic
:
false
,
description
:
"Hạn ngạch số tác vụ cào được phép chạy song song cho mỗi tài khoản (USER_MAX_CONCURRENT_JOBS)"
,
},
{
key
:
"quota.user_max_pages_per_month"
,
value
:
1000
,
value
:
parseInt
(
process
.
env
.
USER_MAX_PAGES_PER_MONTH
||
"1000"
,
10
)
,
category
:
SYSTEM_CONFIG_CATEGORY
.
SECURITY
,
isPublic
:
false
,
description
:
"Hạn ngạch số trang cào tối đa trong một tháng cho mỗi tài khoản (USER_MAX_PAGES_PER_MONTH)"
,
},
{
key
:
"quota.user_max_jobs_per_month"
,
value
:
100
,
value
:
parseInt
(
process
.
env
.
USER_MAX_JOBS_PER_MONTH
||
"100"
,
10
)
,
category
:
SYSTEM_CONFIG_CATEGORY
.
SECURITY
,
isPublic
:
false
,
description
:
"Hạn ngạch số tác vụ cào tối đa trong một tháng cho mỗi tài khoản (USER_MAX_JOBS_PER_MONTH)"
,
},
{
key
:
"rate_limit.window_ms"
,
value
:
900000
,
value
:
parseInt
(
process
.
env
.
RATE_LIMIT_WINDOW_MS
||
"900000"
,
10
)
,
category
:
SYSTEM_CONFIG_CATEGORY
.
SECURITY
,
isPublic
:
false
,
description
:
"Chu kỳ tính giới hạn yêu cầu API (ms, tương đương 15 phút) (RATE_LIMIT_WINDOW_MS)"
,
},
{
key
:
"rate_limit.max_requests"
,
value
:
1000
,
value
:
parseInt
(
process
.
env
.
RATE_LIMIT_MAX
||
"1000"
,
10
)
,
category
:
SYSTEM_CONFIG_CATEGORY
.
SECURITY
,
isPublic
:
false
,
description
:
"Số lượng yêu cầu API tối đa được phép trong một chu kỳ giới hạn (RATE_LIMIT_MAX)"
,
...
...
src/docs/swagger.json
View file @
adc75aa5
...
...
@@ -12,6 +12,142 @@
}
],
"paths"
:
{
"/health/liveness"
:
{
"get"
:
{
"description"
:
"Endpoint kiểm tra xem ứng dụng còn phản hồi hay không (dành cho Kubernetes / Docker health check)."
,
"responses"
:
{
"200"
:
{
"description"
:
"Ứng dụng hoạt động bình thường"
,
"content"
:
{
"application/json"
:
{
"schema"
:
{
"type"
:
"object"
,
"properties"
:
{
"status"
:
{
"type"
:
"string"
,
"example"
:
"ok"
},
"uptimeSeconds"
:
{
"type"
:
"integer"
,
"example"
:
3600
},
"timestamp"
:
{
"type"
:
"string"
,
"format"
:
"date-time"
},
"nodeVersion"
:
{
"type"
:
"string"
,
"example"
:
"v22.14.0"
}
}
}
}
}
}
},
"tags"
:
[
"Health"
],
"summary"
:
"Kiểm tra liveness của service"
}
},
"/health/readiness"
:
{
"get"
:
{
"description"
:
"Endpoint kiểm tra kết nối tới cơ sở dữ liệu PostgreSQL và hàng đợi Redis."
,
"responses"
:
{
"200"
:
{
"description"
:
"Hệ thống sẵn sàng tiếp nhận request"
,
"content"
:
{
"application/json"
:
{
"schema"
:
{
"type"
:
"object"
,
"properties"
:
{
"status"
:
{
"type"
:
"string"
,
"example"
:
"ready"
},
"checks"
:
{
"type"
:
"object"
,
"properties"
:
{
"database"
:
{
"type"
:
"object"
,
"properties"
:
{
"status"
:
{
"type"
:
"string"
,
"example"
:
"up"
},
"latencyMs"
:
{
"type"
:
"integer"
,
"example"
:
5
}
}
},
"redis"
:
{
"type"
:
"object"
,
"properties"
:
{
"status"
:
{
"type"
:
"string"
,
"example"
:
"up"
},
"latencyMs"
:
{
"type"
:
"integer"
,
"example"
:
2
}
}
}
}
},
"timestamp"
:
{
"type"
:
"string"
,
"format"
:
"date-time"
}
}
}
}
}
},
"503"
:
{
"description"
:
"Hệ thống chưa sẵn sàng, dịch vụ phụ trợ gặp lỗi"
}
},
"tags"
:
[
"Health"
],
"summary"
:
"Kiểm tra readiness của service (PostgreSQL & Redis)"
}
},
"/health/metrics"
:
{
"get"
:
{
"description"
:
"Trả về thông tin chi tiết về bộ nhớ RAM tiến trình, thời gian uptime và trạng thái các hàng đợi BullMQ."
,
"responses"
:
{
"200"
:
{
"description"
:
"Lấy metrics thành công"
,
"content"
:
{
"application/json"
:
{
"schema"
:
{
"type"
:
"object"
,
"properties"
:
{
"memory"
:
{
"type"
:
"object"
},
"uptime"
:
{
"type"
:
"number"
},
"queues"
:
{
"type"
:
"object"
}
}
}
}
}
}
},
"tags"
:
[
"Health"
],
"summary"
:
"Xem thông số metrics hệ thống và hàng đợi"
}
},
"/auth/login"
:
{
"post"
:
{
"description"
:
"Xác thực email và mật khẩu để nhận Access Token và Refresh Token."
,
...
...
@@ -2095,6 +2231,16 @@
"summary"
:
"Bật/tắt nhanh Feature Flag"
}
},
"/system/configs/sync-env"
:
{
"post"
:
{
"description"
:
""
,
"responses"
:
{
"default"
:
{
"description"
:
""
}
}
}
},
"/cron/jobs"
:
{
"get"
:
{
"description"
:
""
,
...
...
@@ -5150,142 +5296,6 @@
],
"summary"
:
"Xóa template trích xuất"
}
},
"/health/liveness"
:
{
"get"
:
{
"tags"
:
[
"Health"
],
"summary"
:
"Kiểm tra liveness của service"
,
"description"
:
"Endpoint kiểm tra xem ứng dụng còn phản hồi hay không (dành cho Kubernetes / Docker health check)."
,
"responses"
:
{
"200"
:
{
"description"
:
"Ứng dụng hoạt động bình thường"
,
"content"
:
{
"application/json"
:
{
"schema"
:
{
"type"
:
"object"
,
"properties"
:
{
"status"
:
{
"type"
:
"string"
,
"example"
:
"ok"
},
"uptimeSeconds"
:
{
"type"
:
"integer"
,
"example"
:
3600
},
"timestamp"
:
{
"type"
:
"string"
,
"format"
:
"date-time"
},
"nodeVersion"
:
{
"type"
:
"string"
,
"example"
:
"v22.14.0"
}
}
}
}
}
}
}
}
},
"/health/readiness"
:
{
"get"
:
{
"tags"
:
[
"Health"
],
"summary"
:
"Kiểm tra readiness của service (PostgreSQL & Redis)"
,
"description"
:
"Endpoint kiểm tra kết nối tới cơ sở dữ liệu PostgreSQL và hàng đợi Redis."
,
"responses"
:
{
"200"
:
{
"description"
:
"Hệ thống sẵn sàng tiếp nhận request"
,
"content"
:
{
"application/json"
:
{
"schema"
:
{
"type"
:
"object"
,
"properties"
:
{
"status"
:
{
"type"
:
"string"
,
"example"
:
"ready"
},
"checks"
:
{
"type"
:
"object"
,
"properties"
:
{
"database"
:
{
"type"
:
"object"
,
"properties"
:
{
"status"
:
{
"type"
:
"string"
,
"example"
:
"up"
},
"latencyMs"
:
{
"type"
:
"integer"
,
"example"
:
5
}
}
},
"redis"
:
{
"type"
:
"object"
,
"properties"
:
{
"status"
:
{
"type"
:
"string"
,
"example"
:
"up"
},
"latencyMs"
:
{
"type"
:
"integer"
,
"example"
:
2
}
}
}
}
},
"timestamp"
:
{
"type"
:
"string"
,
"format"
:
"date-time"
}
}
}
}
}
},
"503"
:
{
"description"
:
"Hệ thống chưa sẵn sàng, dịch vụ phụ trợ gặp lỗi"
}
}
}
},
"/health/metrics"
:
{
"get"
:
{
"tags"
:
[
"Health"
],
"summary"
:
"Xem thông số metrics hệ thống và hàng đợi"
,
"description"
:
"Trả về thông tin chi tiết về bộ nhớ RAM tiến trình, thời gian uptime và trạng thái các hàng đợi BullMQ."
,
"responses"
:
{
"200"
:
{
"description"
:
"Lấy metrics thành công"
,
"content"
:
{
"application/json"
:
{
"schema"
:
{
"type"
:
"object"
,
"properties"
:
{
"memory"
:
{
"type"
:
"object"
},
"uptime"
:
{
"type"
:
"number"
},
"queues"
:
{
"type"
:
"object"
}
}
}
}
}
}
}
}
}
},
"components"
:
{
...
...
src/modules/crawl-exports/crawl-export.dto.ts
View file @
adc75aa5
...
...
@@ -10,6 +10,7 @@ export interface CreateCrawlExportDto {
filePath
:
string
;
fileSize
?:
number
;
mimeType
?:
string
;
expiredAt
?:
Date
|
null
;
}
export
interface
UpdateCrawlExportDto
{
...
...
src/modules/cron/__tests__/cron.service.test.ts
View file @
adc75aa5
...
...
@@ -16,8 +16,13 @@ describe("CronService", () => {
let
mockStorageService
:
jest
.
Mocked
<
IStorageService
>
;
let
mockMailService
:
jest
.
Mocked
<
MailService
>
;
let
mockQueueService
:
jest
.
Mocked
<
CronQueueService
>
;
let
mockConfigService
:
{
get
:
jest
.
Mock
};
beforeEach
(()
=>
{
mockConfigService
=
{
get
:
jest
.
fn
().
mockImplementation
((
_key
:
string
,
defaultVal
:
unknown
)
=>
Promise
.
resolve
(
defaultVal
)),
};
mockRepository
=
{
getJobStatuses
:
jest
.
fn
().
mockResolvedValue
({
[
CRON_JOB_NAMES
.
CLEANUP_AUDIT_LOGS
]:
true
,
...
...
@@ -43,6 +48,10 @@ describe("CronService", () => {
deletedCount
:
2
,
filePaths
:
[
"exports/test1.zip"
,
"exports/test2.zip"
],
}),
cleanupOldExports
:
jest
.
fn
().
mockResolvedValue
({
deletedCount
:
3
,
filePaths
:
[
"exports/old1.zip"
,
"exports/old2.zip"
,
"exports/old3.zip"
],
}),
getActiveAvatarUrls
:
jest
.
fn
().
mockResolvedValue
([]),
getDigestStats
:
jest
.
fn
().
mockResolvedValue
({
newUsers
:
5
,
...
...
@@ -85,6 +94,7 @@ describe("CronService", () => {
mockStorageService
,
mockMailService
,
mockQueueService
,
mockConfigService
as
any
,
);
});
...
...
@@ -180,6 +190,25 @@ describe("CronService", () => {
);
});
it
(
"should execute cleanup-exports and delete old export files"
,
async
()
=>
{
const
result
=
await
cronService
.
triggerJob
(
CRON_JOB_NAMES
.
CLEANUP_EXPORTS
,
{
retentionDays
:
7
},
);
expect
(
result
.
success
).
toBe
(
true
);
expect
(
mockRepository
.
cleanupOldExports
).
toHaveBeenCalled
();
expect
(
mockStorageService
.
deleteFile
).
toHaveBeenCalledWith
(
"exports/old1.zip"
);
expect
(
mockStorageService
.
deleteFile
).
toHaveBeenCalledWith
(
"exports/old2.zip"
);
expect
(
mockStorageService
.
deleteFile
).
toHaveBeenCalledWith
(
"exports/old3.zip"
);
expect
(
result
.
data
).
toEqual
(
expect
.
objectContaining
({
cleanedExportsCount
:
3
,
retentionDays
:
7
,
}),
);
});
it
(
"should execute cleanup-unconfirmed-uploads and delete expired export files"
,
async
()
=>
{
const
result
=
await
cronService
.
triggerJob
(
CRON_JOB_NAMES
.
CLEANUP_UNCONFIRMED_UPLOADS
,
...
...
src/modules/cron/cron.repository.ts
View file @
adc75aa5
...
...
@@ -194,6 +194,57 @@ export class CronRepository {
};
}
/**
* Dọn dẹp các tệp xuất dữ liệu CrawlExport cũ hơn số ngày quy định (createdAt < cutoffDate)
* hoặc đã hết hạn (expiredAt < now)
*/
async
cleanupOldExports
(
cutoffDate
:
Date
,
now
:
Date
=
new
Date
(),
):
Promise
<
{
deletedCount
:
number
;
filePaths
:
string
[]
}
>
{
const
expiredExports
=
await
prisma
.
crawlExport
.
findMany
({
where
:
{
OR
:
[
{
expiredAt
:
{
not
:
null
,
lt
:
now
,
},
},
{
createdAt
:
{
lt
:
cutoffDate
,
},
},
],
},
select
:
{
id
:
true
,
filePath
:
true
,
},
});
if
(
expiredExports
.
length
===
0
)
{
return
{
deletedCount
:
0
,
filePaths
:
[]
};
}
const
ids
=
expiredExports
.
map
((
e
)
=>
e
.
id
);
const
filePaths
=
expiredExports
.
map
((
e
)
=>
e
.
filePath
).
filter
(
Boolean
);
await
prisma
.
crawlExport
.
deleteMany
({
where
:
{
id
:
{
in
:
ids
,
},
},
});
return
{
deletedCount
:
ids
.
length
,
filePaths
,
};
}
/**
* Lấy danh sách đường dẫn avatar đang được người dùng sử dụng
*/
...
...
src/modules/cron/cron.service.ts
View file @
adc75aa5
...
...
@@ -10,6 +10,7 @@ import {
CRON_JOB_STATUS
,
CronJobName
,
DEFAULT_AUDIT_LOG_RETENTION_DAYS
,
DEFAULT_EXPORT_RETENTION_DAYS
,
DEFAULT_CRON_SCHEDULES
,
DEFAULT_UNCONFIRMED_UPLOAD_MAX_AGE_HOURS
,
}
from
"../../common/constants/cron.constant"
;
...
...
@@ -26,6 +27,10 @@ import {
getZonedDateParts
,
createUtcDateFromZonedParts
,
}
from
"../../common/helpers/schedule-calculator.helper"
;
import
{
SystemConfigService
,
systemConfigService
,
}
from
"../system-config/system-config.service"
;
export
class
CronService
{
constructor
(
...
...
@@ -33,6 +38,7 @@ export class CronService {
private
readonly
storageService
:
IStorageService
=
StorageFactory
.
getStorageService
(),
private
readonly
mailService
:
MailService
=
new
MailService
(),
private
readonly
queueService
:
CronQueueService
=
cronQueue
,
private
readonly
configService
:
SystemConfigService
=
systemConfigService
,
)
{}
/**
...
...
@@ -179,6 +185,13 @@ export class CronService {
break
;
}
case
CRON_JOB_NAMES
.
CLEANUP_EXPORTS
:
{
executionData
=
await
this
.
executeCleanupExports
(
params
as
{
retentionDays
?:
number
}
|
undefined
,
);
break
;
}
case
CRON_JOB_NAMES
.
CLEANUP_UNCONFIRMED_UPLOADS
:
{
executionData
=
await
this
.
executeCleanupUnconfirmedUploads
(
params
as
{
maxAgeHours
?:
number
}
|
undefined
,
...
...
@@ -262,8 +275,23 @@ export class CronService {
retentionDays
:
number
;
cutoffDate
:
string
;
}
>
{
const
retentionDays
=
params
?.
retentionDays
??
DEFAULT_AUDIT_LOG_RETENTION_DAYS
;
const
isFeatureEnabled
=
await
this
.
configService
.
get
<
boolean
>
(
"feature.cron.cleanup_audit_logs.enabled"
,
true
,
);
if
(
!
isFeatureEnabled
&&
params
?.
retentionDays
===
undefined
)
{
return
{
deletedCount
:
0
,
retentionDays
:
0
,
cutoffDate
:
new
Date
().
toISOString
(),
};
}
const
defaultRetention
=
await
this
.
configService
.
get
<
number
>
(
"retention.audit_logs_days"
,
DEFAULT_AUDIT_LOG_RETENTION_DAYS
,
);
const
retentionDays
=
params
?.
retentionDays
??
defaultRetention
;
const
cutoffDate
=
new
Date
(
Date
.
now
()
-
retentionDays
*
24
*
60
*
60
*
1000
,
);
...
...
@@ -277,6 +305,56 @@ export class CronService {
};
}
/**
* Tác vụ: Xóa các bản ghi CrawlExport và tệp tin xuất dữ liệu cũ hơn số ngày quy định
*/
async
executeCleanupExports
(
params
?:
{
retentionDays
?:
number
}):
Promise
<
{
cleanedExportsCount
:
number
;
retentionDays
:
number
;
cutoffDate
:
string
;
}
>
{
const
isFeatureEnabled
=
await
this
.
configService
.
get
<
boolean
>
(
"feature.cron.cleanup_exports.enabled"
,
true
,
);
if
(
!
isFeatureEnabled
&&
params
?.
retentionDays
===
undefined
)
{
return
{
cleanedExportsCount
:
0
,
retentionDays
:
0
,
cutoffDate
:
new
Date
().
toISOString
(),
};
}
const
defaultRetention
=
await
this
.
configService
.
get
<
number
>
(
"retention.exports_days"
,
DEFAULT_EXPORT_RETENTION_DAYS
,
);
const
retentionDays
=
params
?.
retentionDays
??
defaultRetention
;
const
cutoffDate
=
new
Date
(
Date
.
now
()
-
retentionDays
*
24
*
60
*
60
*
1000
,
);
const
{
deletedCount
,
filePaths
}
=
await
this
.
repository
.
cleanupOldExports
(
cutoffDate
,
new
Date
());
for
(
const
filePath
of
filePaths
)
{
try
{
await
this
.
storageService
.
deleteFile
(
filePath
);
}
catch
(
fileErr
)
{
console
.
warn
(
`[Cron Cleanup] Could not delete export file
${
filePath
}
:`
,
fileErr
,
);
}
}
return
{
cleanedExportsCount
:
deletedCount
,
retentionDays
,
cutoffDate
:
cutoffDate
.
toISOString
(),
};
}
/**
* Tác vụ: Quét và dọn dẹp các tệp tin tải lên mồ côi và export đã hết hạn
*/
...
...
src/modules/exports/__tests__/export.service.test.ts
View file @
adc75aa5
import
{
CrawlJob
,
CrawlPage
}
from
"@prisma/client"
;
import
{
ExportService
}
from
"../export.service"
;
import
{
JsonExportService
}
from
"../json-export.service"
;
import
{
systemConfigService
}
from
"../../system-config/system-config.service"
;
describe
(
"ExportService"
,
()
=>
{
beforeEach
(()
=>
{
jest
.
spyOn
(
systemConfigService
,
"get"
).
mockResolvedValue
(
7
);
});
afterEach
(()
=>
{
jest
.
restoreAllMocks
();
});
...
...
src/modules/exports/export.service.ts
View file @
adc75aa5
...
...
@@ -14,6 +14,8 @@ import {
ExportType
,
}
from
"../../common/constants/export-type.constant"
;
import
{
JOB_STATUS
}
from
"../../common/constants/job-status.constant"
;
import
{
DEFAULT_EXPORT_RETENTION_DAYS
}
from
"../../common/constants/cron.constant"
;
import
{
systemConfigService
}
from
"../system-config/system-config.service"
;
const
EXPORT_SERVICES
:
Record
<
ExportType
,
new
()
=>
IExportService
>
=
{
[
EXPORT_TYPE
.
JSON
]:
JsonExportService
,
...
...
@@ -46,6 +48,15 @@ export class ExportService {
const
service
=
new
ServiceClass
();
const
result
=
await
service
.
export
(
fullJob
);
const
retentionDays
=
await
systemConfigService
.
get
<
number
>
(
"retention.exports_days"
,
DEFAULT_EXPORT_RETENTION_DAYS
,
);
const
expiredAt
=
new
Date
(
Date
.
now
()
+
retentionDays
*
24
*
60
*
60
*
1000
,
);
const
exportRecord
=
await
this
.
exportRepository
.
create
({
jobId
:
job
.
id
,
exportType
,
...
...
@@ -53,6 +64,7 @@ export class ExportService {
filePath
:
result
.
filePath
,
fileSize
:
result
.
fileSize
,
mimeType
:
result
.
mimeType
,
expiredAt
,
});
const
completedExportRecord
=
await
this
.
exportRepository
.
update
(
...
...
src/modules/system-config/system-config.controller.ts
View file @
adc75aa5
...
...
@@ -146,4 +146,21 @@ export class SystemConfigController {
next
(
error
);
}
};
syncFromEnv
=
async
(
req
:
Request
,
res
:
Response
,
next
:
NextFunction
,
):
Promise
<
void
>
=>
{
try
{
const
result
=
await
this
.
service
.
syncFromEnv
();
res
.
json
({
success
:
true
,
data
:
result
,
message
:
"Đã đồng bộ toàn bộ giá trị cấu hình từ .env thành công"
,
});
}
catch
(
error
)
{
next
(
error
);
}
};
}
src/modules/system-config/system-config.repository.ts
View file @
adc75aa5
...
...
@@ -104,14 +104,20 @@ export class SystemConfigRepository {
});
}
async
ensureDefault
(
item
:
DefaultSystemConfigItem
)
{
async
ensureDefault
(
item
:
DefaultSystemConfigItem
,
syncValue
=
true
)
{
const
updatePayload
:
Prisma
.
SystemConfigUpdateInput
=
{
description
:
item
.
description
??
null
,
category
:
item
.
category
,
isPublic
:
item
.
isPublic
,
};
if
(
syncValue
)
{
updatePayload
.
value
=
item
.
value
as
Prisma
.
InputJsonValue
;
}
return
prisma
.
systemConfig
.
upsert
({
where
:
{
key
:
item
.
key
},
update
:
{
description
:
item
.
description
??
null
,
category
:
item
.
category
,
isPublic
:
item
.
isPublic
,
},
update
:
updatePayload
,
create
:
{
key
:
item
.
key
,
value
:
item
.
value
as
Prisma
.
InputJsonValue
,
...
...
src/modules/system-config/system-config.route.ts
View file @
adc75aa5
...
...
@@ -76,4 +76,12 @@ router.delete(
controller
.
delete
,
);
// 8. POST /api/v1/system/configs/sync-env (Sync configs from .env into DB)
router
.
post
(
"/configs/sync-env"
,
authMiddleware
,
requirePermission
(
PERMISSIONS
.
SYSTEM_CONFIG_MANAGE
),
controller
.
syncFromEnv
,
);
export
default
router
;
src/modules/system-config/system-config.service.ts
View file @
adc75aa5
...
...
@@ -388,12 +388,12 @@ export class SystemConfigService {
}
/**
* Khởi tạo
cấu hình mặc định nếu chưa tồn tại trong
Database
* Khởi tạo
và đồng bộ cấu hình mặc định từ biến môi trường vào
Database
*/
async
ensureDefaultConfigs
():
Promise
<
void
>
{
async
ensureDefaultConfigs
(
syncValues
=
true
):
Promise
<
void
>
{
for
(
const
item
of
DEFAULT_SYSTEM_CONFIGS
)
{
try
{
await
this
.
repository
.
ensureDefault
(
item
);
await
this
.
repository
.
ensureDefault
(
item
,
syncValues
);
}
catch
(
err
)
{
console
.
warn
(
`[SystemConfig] ensureDefault error for
${
item
.
key
}
:`
,
...
...
@@ -403,6 +403,24 @@ export class SystemConfigService {
}
this
.
clearLocalCache
();
}
/**
* Đồng bộ toàn bộ giá trị cấu hình từ biến môi trường (.env) vào Database
*/
async
syncFromEnv
():
Promise
<
{
syncedCount
:
number
;
keys
:
string
[]
}
>
{
const
keys
:
string
[]
=
[];
for
(
const
item
of
DEFAULT_SYSTEM_CONFIGS
)
{
try
{
await
this
.
repository
.
ensureDefault
(
item
,
true
);
keys
.
push
(
item
.
key
);
}
catch
(
err
)
{
console
.
warn
(
`[SystemConfig] syncFromEnv error for
${
item
.
key
}
:`
,
err
);
}
}
this
.
clearLocalCache
();
await
this
.
publishInvalidation
();
return
{
syncedCount
:
keys
.
length
,
keys
};
}
}
export
const
systemConfigService
=
new
SystemConfigService
();
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment