feat(backend): implement task creation and admin web

This commit is contained in:
QiuSW
2026-07-26 14:03:32 +08:00
parent 2b265c92fc
commit c5d3b215ff
58 changed files with 8773 additions and 83 deletions
+100 -3
View File
@@ -2,6 +2,7 @@ package main
import (
"context"
"database/sql"
"errors"
"log"
"net/http"
@@ -11,8 +12,13 @@ import (
"time"
"cmroubao/backend-api/internal/config"
"cmroubao/backend-api/internal/platform/assetstore"
"cmroubao/backend-api/internal/platform/database"
"cmroubao/backend-api/internal/platform/migration"
repository "cmroubao/backend-api/internal/repository/sqlite"
"cmroubao/backend-api/internal/transport/httpapi"
"cmroubao/backend-api/internal/transport/webui"
"cmroubao/backend-api/internal/usecase"
)
func main() {
@@ -43,10 +49,11 @@ func run() error {
}
}()
router, err := httpapi.NewRouter(db, func(event string) {
log.Print(event)
})
router, err := buildRouter(startupContext, cfg, db)
if err != nil {
if errors.Is(err, errPendingMigrations) {
return err
}
return errors.New("HTTP router setup failed")
}
server := httpapi.NewServer(cfg, router)
@@ -111,3 +118,93 @@ func shutdownServer(
}
return nil
}
type migrationStatusReader interface {
Status(context.Context) ([]migration.Status, error)
}
var errPendingMigrations = errors.New(
"database migrations are pending; run migrate up",
)
func buildRouter(
ctx context.Context,
cfg config.Config,
db *sql.DB,
) (http.Handler, error) {
runner, err := migration.New(db)
if err != nil {
return nil, err
}
if err := requireCurrentMigrations(ctx, runner); err != nil {
return nil, err
}
store, err := repository.New(db)
if err != nil {
return nil, err
}
files, err := assetstore.New(cfg.AssetDirectory)
if err != nil {
return nil, err
}
clock := usecase.SystemClock{}
ids := usecase.UUIDGenerator{}
assets, err := usecase.NewAssetService(store, files, clock, ids)
if err != nil {
return nil, err
}
tasks, err := usecase.NewTaskService(store, clock, ids)
if err != nil {
return nil, err
}
webService, err := webui.NewUsecaseAdapter(tasks, assets)
if err != nil {
return nil, err
}
renderer, err := webui.NewRenderer()
if err != nil {
return nil, err
}
webHandler, err := webui.NewHandler(webService, renderer)
if err != nil {
return nil, err
}
registerAdminRoutes, err := httpapi.NewAdminRouteRegistrar(
httpapi.AdminServices{
Assets: assets,
Tasks: tasks,
},
webHandler,
)
if err != nil {
return nil, err
}
return httpapi.NewRouter(httpapi.RouterDependencies{
Database: db,
RegisterAdminRoutes: registerAdminRoutes,
LogEvent: func(event string) {
log.Print(event)
},
})
}
func requireCurrentMigrations(
ctx context.Context,
reader migrationStatusReader,
) error {
statuses, err := reader.Status(ctx)
if err != nil {
return errors.New("database migration status failed")
}
if len(statuses) == 0 {
return errors.New("database has no known migrations")
}
for _, status := range statuses {
if !status.Applied {
return errPendingMigrations
}
}
return nil
}
+51
View File
@@ -1,11 +1,14 @@
package main
import (
"context"
"errors"
"net"
"net/http"
"testing"
"time"
"cmroubao/backend-api/internal/platform/migration"
)
func TestShutdownServerForceClosesAfterGracefulTimeout(t *testing.T) {
@@ -73,3 +76,51 @@ func TestShutdownServerAcceptsNormalServerClose(t *testing.T) {
t.Fatalf("shutdownServer() error = %v", err)
}
}
func TestRequireCurrentMigrationsAcceptsAppliedVersions(t *testing.T) {
reader := stubMigrationStatusReader{
statuses: []migration.Status{
{Version: 1, Applied: true},
{Version: 2, Applied: true},
},
}
if err := requireCurrentMigrations(context.Background(), reader); err != nil {
t.Fatalf("requireCurrentMigrations() error = %v", err)
}
}
func TestRequireCurrentMigrationsRejectsPendingVersion(t *testing.T) {
reader := stubMigrationStatusReader{
statuses: []migration.Status{
{Version: 1, Applied: true},
{Version: 2, Applied: false},
},
}
err := requireCurrentMigrations(context.Background(), reader)
if err == nil || err.Error() !=
"database migrations are pending; run migrate up" {
t.Fatalf("requireCurrentMigrations() error = %v", err)
}
}
func TestRequireCurrentMigrationsHidesStatusFailure(t *testing.T) {
reader := stubMigrationStatusReader{err: errors.New("database details")}
err := requireCurrentMigrations(context.Background(), reader)
if err == nil || err.Error() != "database migration status failed" {
t.Fatalf("requireCurrentMigrations() error = %v", err)
}
}
type stubMigrationStatusReader struct {
statuses []migration.Status
err error
}
func (reader stubMigrationStatusReader) Status(
context.Context,
) ([]migration.Status, error) {
return reader.statuses, reader.err
}