- Add GET /api/v1/course-management/practices/:practiceId/detail with full question items - Add migration 000040 for sub-module content inactive purge tracking - Hierarchy queries, sqlc gen, config/app purge job, swagger refresh Made-with: Cursor
306 lines
8.9 KiB
Go
306 lines
8.9 KiB
Go
package httpserver
|
|
|
|
import (
|
|
dbgen "Yimaru-Backend/gen/db"
|
|
"Yimaru-Backend/internal/config"
|
|
activitylogservice "Yimaru-Backend/internal/services/activity_log"
|
|
"Yimaru-Backend/internal/services/arifpay"
|
|
"Yimaru-Backend/internal/services/assessment"
|
|
"Yimaru-Backend/internal/services/authentication"
|
|
cloudconvertservice "Yimaru-Backend/internal/services/cloudconvert"
|
|
"Yimaru-Backend/internal/services/course_management"
|
|
minioservice "Yimaru-Backend/internal/services/minio"
|
|
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
|
notificationservice "Yimaru-Backend/internal/services/notification"
|
|
"Yimaru-Backend/internal/services/questions"
|
|
ratingsservice "Yimaru-Backend/internal/services/ratings"
|
|
rbacservice "Yimaru-Backend/internal/services/rbac"
|
|
"Yimaru-Backend/internal/services/recommendation"
|
|
"Yimaru-Backend/internal/services/subscriptions"
|
|
"Yimaru-Backend/internal/services/team"
|
|
vimeoservice "Yimaru-Backend/internal/services/vimeo"
|
|
|
|
"Yimaru-Backend/internal/services/settings"
|
|
"Yimaru-Backend/internal/services/transaction"
|
|
"Yimaru-Backend/internal/services/user"
|
|
jwtutil "Yimaru-Backend/internal/web_server/jwt"
|
|
customvalidator "Yimaru-Backend/internal/web_server/validator"
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/bytedance/sonic"
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
type App struct {
|
|
assessmentSvc *assessment.Service
|
|
courseSvc *course_management.Service
|
|
questionsSvc *questions.Service
|
|
subscriptionsSvc *subscriptions.Service
|
|
arifpaySvc *arifpay.ArifpayService
|
|
issueReportingSvc *issuereporting.Service
|
|
vimeoSvc *vimeoservice.Service
|
|
teamSvc *team.Service
|
|
activityLogSvc *activitylogservice.Service
|
|
cloudConvertSvc *cloudconvertservice.Service
|
|
minioSvc *minioservice.Service
|
|
ratingSvc *ratingsservice.Service
|
|
fiber *fiber.App
|
|
recommendationSvc recommendation.RecommendationService
|
|
cfg *config.Config
|
|
logger *slog.Logger
|
|
NotidicationStore *notificationservice.Service
|
|
port int
|
|
settingSvc *settings.Service
|
|
authSvc *authentication.Service
|
|
userSvc *user.Service
|
|
transactionSvc *transaction.Service
|
|
validator *customvalidator.CustomValidator
|
|
JwtConfig jwtutil.JwtConfig
|
|
Logger *slog.Logger
|
|
mongoLoggerSvc *zap.Logger
|
|
analyticsDB *dbgen.Queries
|
|
rbacSvc *rbacservice.Service
|
|
stopPurgeWorker context.CancelFunc
|
|
stopInactiveSubModuleContentPurge context.CancelFunc
|
|
}
|
|
|
|
func NewApp(
|
|
assessmentSvc *assessment.Service,
|
|
courseSvc *course_management.Service,
|
|
questionsSvc *questions.Service,
|
|
subscriptionsSvc *subscriptions.Service,
|
|
arifpaySvc *arifpay.ArifpayService,
|
|
issueReportingSvc *issuereporting.Service,
|
|
vimeoSvc *vimeoservice.Service,
|
|
teamSvc *team.Service,
|
|
activityLogSvc *activitylogservice.Service,
|
|
cloudConvertSvc *cloudconvertservice.Service,
|
|
minioSvc *minioservice.Service,
|
|
ratingSvc *ratingsservice.Service,
|
|
port int, validator *customvalidator.CustomValidator,
|
|
settingSvc *settings.Service,
|
|
authSvc *authentication.Service,
|
|
logger *slog.Logger,
|
|
JwtConfig jwtutil.JwtConfig,
|
|
userSvc *user.Service,
|
|
transactionSvc *transaction.Service,
|
|
notidicationStore *notificationservice.Service,
|
|
recommendationSvc recommendation.RecommendationService,
|
|
cfg *config.Config,
|
|
mongoLoggerSvc *zap.Logger,
|
|
analyticsDB *dbgen.Queries,
|
|
rbacSvc *rbacservice.Service,
|
|
) *App {
|
|
app := fiber.New(fiber.Config{
|
|
CaseSensitive: true,
|
|
DisableHeaderNormalizing: true,
|
|
JSONEncoder: sonic.Marshal,
|
|
JSONDecoder: sonic.Unmarshal,
|
|
BodyLimit: 500 * 1024 * 1024, // 500 MB
|
|
})
|
|
|
|
app.Use(cors.New(cors.Config{
|
|
AllowOrigins: "*",
|
|
AllowMethods: "GET,POST,PUT,PATCH,DELETE,OPTIONS",
|
|
AllowHeaders: "Content-Type,Authorization,platform",
|
|
// AllowCredentials: true,
|
|
}))
|
|
|
|
app.Static("/static", "./static")
|
|
|
|
s := &App{
|
|
assessmentSvc: assessmentSvc,
|
|
courseSvc: courseSvc,
|
|
questionsSvc: questionsSvc,
|
|
subscriptionsSvc: subscriptionsSvc,
|
|
arifpaySvc: arifpaySvc,
|
|
vimeoSvc: vimeoSvc,
|
|
teamSvc: teamSvc,
|
|
activityLogSvc: activityLogSvc,
|
|
cloudConvertSvc: cloudConvertSvc,
|
|
minioSvc: minioSvc,
|
|
ratingSvc: ratingSvc,
|
|
issueReportingSvc: issueReportingSvc,
|
|
fiber: app,
|
|
port: port,
|
|
settingSvc: settingSvc,
|
|
authSvc: authSvc,
|
|
validator: validator,
|
|
logger: logger,
|
|
JwtConfig: JwtConfig,
|
|
userSvc: userSvc,
|
|
transactionSvc: transactionSvc,
|
|
NotidicationStore: notidicationStore,
|
|
Logger: logger,
|
|
recommendationSvc: recommendationSvc,
|
|
cfg: cfg,
|
|
mongoLoggerSvc: mongoLoggerSvc,
|
|
analyticsDB: analyticsDB,
|
|
rbacSvc: rbacSvc,
|
|
}
|
|
|
|
s.initAppRoutes()
|
|
|
|
return s
|
|
}
|
|
|
|
func (a *App) Run() error {
|
|
a.startAccountDeletionPurgeWorker()
|
|
defer a.stopAccountDeletionPurgeWorker()
|
|
a.startInactiveSubModuleContentPurgeWorker()
|
|
defer a.stopInactiveSubModuleContentPurgeWorker()
|
|
return a.fiber.Listen(fmt.Sprintf(":%d", a.port))
|
|
}
|
|
|
|
func (a *App) startAccountDeletionPurgeWorker() {
|
|
if a.cfg == nil || !a.cfg.AccountDeletionPurgeEnabled {
|
|
a.logger.Info("account deletion purge worker disabled")
|
|
return
|
|
}
|
|
|
|
interval := a.cfg.AccountDeletionPurgeInterval
|
|
if interval <= 0 {
|
|
interval = time.Hour
|
|
}
|
|
|
|
batchSize := a.cfg.AccountDeletionPurgeBatchSize
|
|
if batchSize <= 0 {
|
|
batchSize = 100
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
a.stopPurgeWorker = cancel
|
|
|
|
a.logger.Info(
|
|
"starting account deletion purge worker",
|
|
"interval", interval.String(),
|
|
"batch_size", batchSize,
|
|
)
|
|
|
|
go func() {
|
|
// Run once on startup so stale due rows are cleaned quickly.
|
|
a.runAccountDeletionPurgeOnce(ctx, batchSize)
|
|
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
a.logger.Info("account deletion purge worker stopped")
|
|
return
|
|
case <-ticker.C:
|
|
a.runAccountDeletionPurgeOnce(ctx, batchSize)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (a *App) stopAccountDeletionPurgeWorker() {
|
|
if a.stopPurgeWorker != nil {
|
|
a.stopPurgeWorker()
|
|
}
|
|
}
|
|
|
|
func (a *App) runAccountDeletionPurgeOnce(ctx context.Context, batchSize int32) {
|
|
deletedCount, err := a.userSvc.PurgeDueUserDeletions(ctx, batchSize)
|
|
if err != nil {
|
|
a.logger.Error("account deletion purge run failed", "error", err)
|
|
return
|
|
}
|
|
|
|
if deletedCount > 0 {
|
|
a.logger.Info("account deletion purge run completed", "deleted_count", deletedCount, "batch_size", batchSize)
|
|
}
|
|
}
|
|
|
|
func (a *App) startInactiveSubModuleContentPurgeWorker() {
|
|
if a.cfg == nil || !a.cfg.InactiveSubModuleContentPurgeEnabled {
|
|
a.logger.Info("inactive submodule content purge worker disabled")
|
|
return
|
|
}
|
|
|
|
interval := a.cfg.InactiveSubModuleContentPurgeInterval
|
|
if interval <= 0 {
|
|
interval = 24 * time.Hour
|
|
}
|
|
|
|
retentionDays := a.cfg.InactiveSubModuleContentRetentionDays
|
|
if retentionDays < 1 {
|
|
retentionDays = 30
|
|
}
|
|
retention := time.Duration(retentionDays) * 24 * time.Hour
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
a.stopInactiveSubModuleContentPurge = cancel
|
|
|
|
a.logger.Info(
|
|
"starting inactive submodule content purge worker",
|
|
"interval", interval.String(),
|
|
"retention_days", retentionDays,
|
|
)
|
|
|
|
go func() {
|
|
a.runInactiveSubModuleContentPurgeOnce(ctx, retention)
|
|
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
a.logger.Info("inactive submodule content purge worker stopped")
|
|
return
|
|
case <-ticker.C:
|
|
a.runInactiveSubModuleContentPurgeOnce(ctx, retention)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (a *App) stopInactiveSubModuleContentPurgeWorker() {
|
|
if a.stopInactiveSubModuleContentPurge != nil {
|
|
a.stopInactiveSubModuleContentPurge()
|
|
}
|
|
}
|
|
|
|
func (a *App) runInactiveSubModuleContentPurgeOnce(ctx context.Context, retention time.Duration) {
|
|
cutoff := time.Now().Add(-retention)
|
|
cutoffParam := pgtype.Timestamptz{Time: cutoff, Valid: true}
|
|
|
|
nLessons, err := a.analyticsDB.PurgeInactiveSubModuleLessonsBefore(ctx, cutoffParam)
|
|
if err != nil {
|
|
a.logger.Error("purge inactive submodule lessons failed", "error", err)
|
|
return
|
|
}
|
|
|
|
nPractices, err := a.analyticsDB.PurgeInactiveSubModulePracticesBefore(ctx, cutoffParam)
|
|
if err != nil {
|
|
a.logger.Error("purge inactive submodule practices failed", "error", err)
|
|
return
|
|
}
|
|
|
|
nCapstones, err := a.analyticsDB.PurgeInactiveSubModuleCapstonesBefore(ctx, cutoffParam)
|
|
if err != nil {
|
|
a.logger.Error("purge inactive submodule capstones failed", "error", err)
|
|
return
|
|
}
|
|
|
|
if nLessons > 0 || nPractices > 0 || nCapstones > 0 {
|
|
a.logger.Info(
|
|
"inactive submodule content purge run completed",
|
|
"lessons_deleted", nLessons,
|
|
"practice_question_sets_deleted", nPractices,
|
|
"capstone_question_sets_deleted", nCapstones,
|
|
"cutoff", cutoff.UTC().Format(time.RFC3339),
|
|
)
|
|
}
|
|
}
|