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
Expand all
Show 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() {
...
@@ -12,6 +12,7 @@ async function main() {
await
prisma
.
systemConfig
.
upsert
({
await
prisma
.
systemConfig
.
upsert
({
where
:
{
key
:
item
.
key
},
where
:
{
key
:
item
.
key
},
update
:
{
update
:
{
value
:
item
.
value
as
any
,
description
:
item
.
description
??
null
,
description
:
item
.
description
??
null
,
category
:
item
.
category
,
category
:
item
.
category
,
isPublic
:
item
.
isPublic
,
isPublic
:
item
.
isPublic
,
...
...
src/common/constants/cron.constant.ts
View file @
adc75aa5
export
const
CRON_JOB_NAMES
=
{
export
const
CRON_JOB_NAMES
=
{
CLEANUP_AUDIT_LOGS
:
"cleanup-audit-logs"
,
CLEANUP_AUDIT_LOGS
:
"cleanup-audit-logs"
,
CLEANUP_EXPORTS
:
"cleanup-exports"
,
CLEANUP_UNCONFIRMED_UPLOADS
:
"cleanup-unconfirmed-uploads"
,
CLEANUP_UNCONFIRMED_UPLOADS
:
"cleanup-unconfirmed-uploads"
,
CLEANUP_EXPIRED_TOKENS
:
"cleanup-expired-tokens"
,
CLEANUP_EXPIRED_TOKENS
:
"cleanup-expired-tokens"
,
DAILY_SUMMARY_DIGEST
:
"daily-summary-digest"
,
DAILY_SUMMARY_DIGEST
:
"daily-summary-digest"
,
...
@@ -24,7 +25,9 @@ export const CRON_QUEUE_NAME = "cron-scheduler-queue" as const;
...
@@ -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
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
;
export
const
DEFAULT_UNCONFIRMED_UPLOAD_MAX_AGE_HOURS
=
24
;
...
@@ -37,13 +40,20 @@ export interface CronScheduleConfig {
...
@@ -37,13 +40,20 @@ export interface CronScheduleConfig {
export
const
DEFAULT_CRON_SCHEDULES
:
Record
<
CronJobName
,
CronScheduleConfig
>
=
{
export
const
DEFAULT_CRON_SCHEDULES
:
Record
<
CronJobName
,
CronScheduleConfig
>
=
{
[
CRON_JOB_NAMES
.
CLEANUP_AUDIT_LOGS
]:
{
[
CRON_JOB_NAMES
.
CLEANUP_AUDIT_LOGS
]:
{
cron
:
"0 2 * * *"
,
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
:
{
defaultParams
:
{
retentionDays
:
DEFAULT_AUDIT_LOG_RETENTION_DAYS
,
retentionDays
:
DEFAULT_AUDIT_LOG_RETENTION_DAYS
,
},
},
},
},
[
CRON_JOB_NAMES
.
CLEANUP_
UNCONFIRMED_UPLOAD
S
]:
{
[
CRON_JOB_NAMES
.
CLEANUP_
EXPORT
S
]:
{
cron
:
"0 3 * * *"
,
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"
,
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
:
{
defaultParams
:
{
maxAgeHours
:
DEFAULT_UNCONFIRMED_UPLOAD_MAX_AGE_HOURS
,
maxAgeHours
:
DEFAULT_UNCONFIRMED_UPLOAD_MAX_AGE_HOURS
,
...
...
src/common/constants/system-config.constant.ts
View file @
adc75aa5
This diff is collapsed.
Click to expand it.
src/docs/swagger.json
View file @
adc75aa5
...
@@ -12,6 +12,142 @@
...
@@ -12,6 +12,142 @@
}
}
],
],
"paths"
:
{
"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"
:
{
"/auth/login"
:
{
"post"
:
{
"post"
:
{
"description"
:
"Xác thực email và mật khẩu để nhận Access Token và Refresh Token."
,
"description"
:
"Xác thực email và mật khẩu để nhận Access Token và Refresh Token."
,
...
@@ -2095,6 +2231,16 @@
...
@@ -2095,6 +2231,16 @@
"summary"
:
"Bật/tắt nhanh Feature Flag"
"summary"
:
"Bật/tắt nhanh Feature Flag"
}
}
},
},
"/system/configs/sync-env"
:
{
"post"
:
{
"description"
:
""
,
"responses"
:
{
"default"
:
{
"description"
:
""
}
}
}
},
"/cron/jobs"
:
{
"/cron/jobs"
:
{
"get"
:
{
"get"
:
{
"description"
:
""
,
"description"
:
""
,
...
@@ -5150,142 +5296,6 @@
...
@@ -5150,142 +5296,6 @@
],
],
"summary"
:
"Xóa template trích xuất"
"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"
:
{
"components"
:
{
...
...
src/modules/crawl-exports/crawl-export.dto.ts
View file @
adc75aa5
...
@@ -10,6 +10,7 @@ export interface CreateCrawlExportDto {
...
@@ -10,6 +10,7 @@ export interface CreateCrawlExportDto {
filePath
:
string
;
filePath
:
string
;
fileSize
?:
number
;
fileSize
?:
number
;
mimeType
?:
string
;
mimeType
?:
string
;
expiredAt
?:
Date
|
null
;
}
}
export
interface
UpdateCrawlExportDto
{
export
interface
UpdateCrawlExportDto
{
...
...
src/modules/cron/__tests__/cron.service.test.ts
View file @
adc75aa5
...
@@ -16,8 +16,13 @@ describe("CronService", () => {
...
@@ -16,8 +16,13 @@ describe("CronService", () => {
let
mockStorageService
:
jest
.
Mocked
<
IStorageService
>
;
let
mockStorageService
:
jest
.
Mocked
<
IStorageService
>
;
let
mockMailService
:
jest
.
Mocked
<
MailService
>
;
let
mockMailService
:
jest
.
Mocked
<
MailService
>
;
let
mockQueueService
:
jest
.
Mocked
<
CronQueueService
>
;
let
mockQueueService
:
jest
.
Mocked
<
CronQueueService
>
;
let
mockConfigService
:
{
get
:
jest
.
Mock
};
beforeEach
(()
=>
{
beforeEach
(()
=>
{
mockConfigService
=
{
get
:
jest
.
fn
().
mockImplementation
((
_key
:
string
,
defaultVal
:
unknown
)
=>
Promise
.
resolve
(
defaultVal
)),
};
mockRepository
=
{
mockRepository
=
{
getJobStatuses
:
jest
.
fn
().
mockResolvedValue
({
getJobStatuses
:
jest
.
fn
().
mockResolvedValue
({
[
CRON_JOB_NAMES
.
CLEANUP_AUDIT_LOGS
]:
true
,
[
CRON_JOB_NAMES
.
CLEANUP_AUDIT_LOGS
]:
true
,
...
@@ -43,6 +48,10 @@ describe("CronService", () => {
...
@@ -43,6 +48,10 @@ describe("CronService", () => {
deletedCount
:
2
,
deletedCount
:
2
,
filePaths
:
[
"exports/test1.zip"
,
"exports/test2.zip"
],
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
([]),
getActiveAvatarUrls
:
jest
.
fn
().
mockResolvedValue
([]),
getDigestStats
:
jest
.
fn
().
mockResolvedValue
({
getDigestStats
:
jest
.
fn
().
mockResolvedValue
({
newUsers
:
5
,
newUsers
:
5
,
...
@@ -85,6 +94,7 @@ describe("CronService", () => {
...
@@ -85,6 +94,7 @@ describe("CronService", () => {
mockStorageService
,
mockStorageService
,
mockMailService
,
mockMailService
,
mockQueueService
,
mockQueueService
,
mockConfigService
as
any
,
);
);
});
});
...
@@ -180,6 +190,25 @@ describe("CronService", () => {
...
@@ -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
()
=>
{
it
(
"should execute cleanup-unconfirmed-uploads and delete expired export files"
,
async
()
=>
{
const
result
=
await
cronService
.
triggerJob
(
const
result
=
await
cronService
.
triggerJob
(
CRON_JOB_NAMES
.
CLEANUP_UNCONFIRMED_UPLOADS
,
CRON_JOB_NAMES
.
CLEANUP_UNCONFIRMED_UPLOADS
,
...
...
src/modules/cron/cron.repository.ts
View file @
adc75aa5
...
@@ -194,6 +194,57 @@ export class CronRepository {
...
@@ -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
* 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 {
...
@@ -10,6 +10,7 @@ import {
CRON_JOB_STATUS
,
CRON_JOB_STATUS
,
CronJobName
,
CronJobName
,
DEFAULT_AUDIT_LOG_RETENTION_DAYS
,
DEFAULT_AUDIT_LOG_RETENTION_DAYS
,
DEFAULT_EXPORT_RETENTION_DAYS
,
DEFAULT_CRON_SCHEDULES
,
DEFAULT_CRON_SCHEDULES
,
DEFAULT_UNCONFIRMED_UPLOAD_MAX_AGE_HOURS
,
DEFAULT_UNCONFIRMED_UPLOAD_MAX_AGE_HOURS
,
}
from
"../../common/constants/cron.constant"
;
}
from
"../../common/constants/cron.constant"
;
...
@@ -26,6 +27,10 @@ import {
...
@@ -26,6 +27,10 @@ import {
getZonedDateParts
,
getZonedDateParts
,
createUtcDateFromZonedParts
,
createUtcDateFromZonedParts
,
}
from
"../../common/helpers/schedule-calculator.helper"
;
}
from
"../../common/helpers/schedule-calculator.helper"
;
import
{
SystemConfigService
,
systemConfigService
,
}
from
"../system-config/system-config.service"
;
export
class
CronService
{
export
class
CronService
{
constructor
(
constructor
(
...
@@ -33,6 +38,7 @@ export class CronService {
...
@@ -33,6 +38,7 @@ export class CronService {
private
readonly
storageService
:
IStorageService
=
StorageFactory
.
getStorageService
(),
private
readonly
storageService
:
IStorageService
=
StorageFactory
.
getStorageService
(),
private
readonly
mailService
:
MailService
=
new
MailService
(),
private
readonly
mailService
:
MailService
=
new
MailService
(),
private
readonly
queueService
:
CronQueueService
=
cronQueue
,
private
readonly
queueService
:
CronQueueService
=
cronQueue
,
private
readonly
configService
:
SystemConfigService
=
systemConfigService
,
)
{}
)
{}
/**
/**
...
@@ -179,6 +185,13 @@ export class CronService {
...
@@ -179,6 +185,13 @@ export class CronService {
break
;
break
;
}
}
case
CRON_JOB_NAMES
.
CLEANUP_EXPORTS
:
{
executionData
=
await
this
.
executeCleanupExports
(
params
as
{
retentionDays
?:
number
}
|
undefined
,
);
break
;
}
case
CRON_JOB_NAMES
.
CLEANUP_UNCONFIRMED_UPLOADS
:
{
case
CRON_JOB_NAMES
.
CLEANUP_UNCONFIRMED_UPLOADS
:
{
executionData
=
await
this
.
executeCleanupUnconfirmedUploads
(
executionData
=
await
this
.
executeCleanupUnconfirmedUploads
(
params
as
{
maxAgeHours
?:
number
}
|
undefined
,
params
as
{
maxAgeHours
?:
number
}
|
undefined
,
...
@@ -262,8 +275,23 @@ export class CronService {
...
@@ -262,8 +275,23 @@ export class CronService {
retentionDays
:
number
;
retentionDays
:
number
;
cutoffDate
:
string
;
cutoffDate
:
string
;
}
>
{
}
>
{
const
retentionDays
=
const
isFeatureEnabled
=
await
this
.
configService
.
get
<
boolean
>
(
params
?.
retentionDays
??
DEFAULT_AUDIT_LOG_RETENTION_DAYS
;
"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
(
const
cutoffDate
=
new
Date
(
Date
.
now
()
-
retentionDays
*
24
*
60
*
60
*
1000
,
Date
.
now
()
-
retentionDays
*
24
*
60
*
60
*
1000
,
);
);
...
@@ -277,6 +305,56 @@ export class CronService {
...
@@ -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
* 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
{
CrawlJob
,
CrawlPage
}
from
"@prisma/client"
;
import
{
ExportService
}
from
"../export.service"
;
import
{
ExportService
}
from
"../export.service"
;
import
{
JsonExportService
}
from
"../json-export.service"
;
import
{
JsonExportService
}
from
"../json-export.service"
;
import
{
systemConfigService
}
from
"../../system-config/system-config.service"
;
describe
(
"ExportService"
,
()
=>
{
describe
(
"ExportService"
,
()
=>
{
beforeEach
(()
=>
{
jest
.
spyOn
(
systemConfigService
,
"get"
).
mockResolvedValue
(
7
);
});
afterEach
(()
=>
{
afterEach
(()
=>
{
jest
.
restoreAllMocks
();
jest
.
restoreAllMocks
();
});
});
...
...
src/modules/exports/export.service.ts
View file @
adc75aa5
...
@@ -14,6 +14,8 @@ import {
...
@@ -14,6 +14,8 @@ import {
ExportType
,
ExportType
,
}
from
"../../common/constants/export-type.constant"
;
}
from
"../../common/constants/export-type.constant"
;
import
{
JOB_STATUS
}
from
"../../common/constants/job-status.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
>
=
{
const
EXPORT_SERVICES
:
Record
<
ExportType
,
new
()
=>
IExportService
>
=
{
[
EXPORT_TYPE
.
JSON
]:
JsonExportService
,
[
EXPORT_TYPE
.
JSON
]:
JsonExportService
,
...
@@ -46,6 +48,15 @@ export class ExportService {
...
@@ -46,6 +48,15 @@ export class ExportService {
const
service
=
new
ServiceClass
();
const
service
=
new
ServiceClass
();
const
result
=
await
service
.
export
(
fullJob
);
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
({
const
exportRecord
=
await
this
.
exportRepository
.
create
({
jobId
:
job
.
id
,
jobId
:
job
.
id
,
exportType
,
exportType
,
...
@@ -53,6 +64,7 @@ export class ExportService {
...
@@ -53,6 +64,7 @@ export class ExportService {
filePath
:
result
.
filePath
,
filePath
:
result
.
filePath
,
fileSize
:
result
.
fileSize
,
fileSize
:
result
.
fileSize
,
mimeType
:
result
.
mimeType
,
mimeType
:
result
.
mimeType
,
expiredAt
,
});
});
const
completedExportRecord
=
await
this
.
exportRepository
.
update
(
const
completedExportRecord
=
await
this
.
exportRepository
.
update
(
...
...
src/modules/system-config/system-config.controller.ts
View file @
adc75aa5
...
@@ -146,4 +146,21 @@ export class SystemConfigController {
...
@@ -146,4 +146,21 @@ export class SystemConfigController {
next
(
error
);
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 {
...
@@ -104,14 +104,20 @@ export class SystemConfigRepository {
});
});
}
}
async
ensureDefault
(
item
:
DefaultSystemConfigItem
)
{
async
ensureDefault
(
item
:
DefaultSystemConfigItem
,
syncValue
=
true
)
{
return
prisma
.
systemConfig
.
upsert
({
const
updatePayload
:
Prisma
.
SystemConfigUpdateInput
=
{
where
:
{
key
:
item
.
key
},
update
:
{
description
:
item
.
description
??
null
,
description
:
item
.
description
??
null
,
category
:
item
.
category
,
category
:
item
.
category
,
isPublic
:
item
.
isPublic
,
isPublic
:
item
.
isPublic
,
},
};
if
(
syncValue
)
{
updatePayload
.
value
=
item
.
value
as
Prisma
.
InputJsonValue
;
}
return
prisma
.
systemConfig
.
upsert
({
where
:
{
key
:
item
.
key
},
update
:
updatePayload
,
create
:
{
create
:
{
key
:
item
.
key
,
key
:
item
.
key
,
value
:
item
.
value
as
Prisma
.
InputJsonValue
,
value
:
item
.
value
as
Prisma
.
InputJsonValue
,
...
...
src/modules/system-config/system-config.route.ts
View file @
adc75aa5
...
@@ -76,4 +76,12 @@ router.delete(
...
@@ -76,4 +76,12 @@ router.delete(
controller
.
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
;
export
default
router
;
src/modules/system-config/system-config.service.ts
View file @
adc75aa5
...
@@ -388,12 +388,12 @@ export class SystemConfigService {
...
@@ -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
)
{
for
(
const
item
of
DEFAULT_SYSTEM_CONFIGS
)
{
try
{
try
{
await
this
.
repository
.
ensureDefault
(
item
);
await
this
.
repository
.
ensureDefault
(
item
,
syncValues
);
}
catch
(
err
)
{
}
catch
(
err
)
{
console
.
warn
(
console
.
warn
(
`[SystemConfig] ensureDefault error for
${
item
.
key
}
:`
,
`[SystemConfig] ensureDefault error for
${
item
.
key
}
:`
,
...
@@ -403,6 +403,24 @@ export class SystemConfigService {
...
@@ -403,6 +403,24 @@ export class SystemConfigService {
}
}
this
.
clearLocalCache
();
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
();
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