feat(admin-service): 实现移动应用版本管理服务
- DDD+Hexagonal架构 - Domain层: AppVersion实体, Platform枚举, Repository接口 - Infrastructure层: Prisma集成, Repository实现 - Application层: CheckUpdate查询(供移动端), CreateVersion命令(管理员) - API层: VersionController, DTOs (request/response) - 数据库: app_versions表设计(支持Android/iOS) - 功能: 版本检查、强制更新、文件SHA-256校验 - 部署: Dockerfile, docker-compose.yml, deploy.sh脚本 - 数据库迁移: Prisma migration初始化 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
abe6d02a4c
commit
c45ed8a575
|
|
@ -0,0 +1,21 @@
|
|||
# =============================================================================
|
||||
# Admin Service - Environment Variables Example
|
||||
# =============================================================================
|
||||
|
||||
# Application
|
||||
NODE_ENV=production
|
||||
APP_PORT=3010
|
||||
API_PREFIX=api/v1
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgresql://rwa_user:rwa_secure_password@postgres:5432/rwa_admin?schema=public
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=your-jwt-secret-here
|
||||
JWT_EXPIRES_IN=7d
|
||||
|
||||
# Redis
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=9
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Prisma
|
||||
/prisma/*.db
|
||||
/prisma/*.db-journal
|
||||
|
|
@ -1,10 +1,70 @@
|
|||
# =============================================================================
|
||||
# Admin Service Dockerfile
|
||||
# =============================================================================
|
||||
# NOTE: This service is not yet implemented. Placeholder only.
|
||||
# =============================================================================
|
||||
|
||||
FROM node:20-alpine
|
||||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
RUN echo "Admin service not yet implemented" > /app/README.txt
|
||||
CMD ["cat", "/app/README.txt"]
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
COPY tsconfig*.json ./
|
||||
COPY nest-cli.json ./
|
||||
|
||||
# Copy Prisma schema
|
||||
COPY prisma ./prisma/
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci
|
||||
|
||||
# Generate Prisma client (dummy DATABASE_URL for build time only)
|
||||
RUN DATABASE_URL="postgresql://user:pass@localhost:5432/db" npx prisma generate
|
||||
|
||||
# Copy source code
|
||||
COPY src ./src
|
||||
|
||||
# Build TypeScript
|
||||
RUN npm run build
|
||||
|
||||
# Verify build output exists
|
||||
RUN ls -la dist/ && test -f dist/main.js
|
||||
|
||||
# Production stage - use Debian slim for OpenSSL compatibility
|
||||
FROM node:20-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install OpenSSL and curl for health checks
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
openssl \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install production dependencies only
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only=production
|
||||
|
||||
# Copy Prisma schema and generate client (dummy DATABASE_URL for build time only)
|
||||
COPY prisma ./prisma/
|
||||
RUN DATABASE_URL="postgresql://user:pass@localhost:5432/db" npx prisma generate
|
||||
|
||||
# Copy built files
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# Create non-root user
|
||||
RUN groupadd -g 1001 nodejs && \
|
||||
useradd -u 1001 -g nodejs nestjs
|
||||
|
||||
# Switch to non-root user
|
||||
USER nestjs
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3010
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
|
||||
CMD curl -f http://localhost:3010/api/v1/health || exit 1
|
||||
|
||||
# Start service
|
||||
CMD ["node", "dist/main.js"]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# Admin Service - Individual Deployment Script
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
SERVICE_NAME="admin-service"
|
||||
CONTAINER_NAME="rwa-admin-service"
|
||||
IMAGE_NAME="services-admin-service"
|
||||
PORT=3010
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# Get script directory
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SERVICES_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# Load environment
|
||||
if [ -f "$SERVICES_DIR/.env" ]; then
|
||||
export $(cat "$SERVICES_DIR/.env" | grep -v '^#' | xargs)
|
||||
fi
|
||||
|
||||
case "$1" in
|
||||
build)
|
||||
log_info "Building $SERVICE_NAME..."
|
||||
docker build -t "$IMAGE_NAME" "$SCRIPT_DIR"
|
||||
log_success "$SERVICE_NAME built successfully"
|
||||
;;
|
||||
|
||||
build-no-cache)
|
||||
log_info "Building $SERVICE_NAME (no cache)..."
|
||||
docker build --no-cache -t "$IMAGE_NAME" "$SCRIPT_DIR"
|
||||
log_success "$SERVICE_NAME built successfully"
|
||||
;;
|
||||
|
||||
start)
|
||||
log_info "Starting $SERVICE_NAME..."
|
||||
cd "$SERVICES_DIR"
|
||||
docker compose up -d "$SERVICE_NAME"
|
||||
log_success "$SERVICE_NAME started"
|
||||
;;
|
||||
|
||||
stop)
|
||||
log_info "Stopping $SERVICE_NAME..."
|
||||
docker stop "$CONTAINER_NAME" 2>/dev/null || true
|
||||
docker rm "$CONTAINER_NAME" 2>/dev/null || true
|
||||
log_success "$SERVICE_NAME stopped"
|
||||
;;
|
||||
|
||||
restart)
|
||||
$0 stop
|
||||
$0 start
|
||||
;;
|
||||
|
||||
logs)
|
||||
docker logs -f "$CONTAINER_NAME"
|
||||
;;
|
||||
|
||||
logs-tail)
|
||||
docker logs --tail 100 "$CONTAINER_NAME"
|
||||
;;
|
||||
|
||||
status)
|
||||
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||
log_success "$SERVICE_NAME is running"
|
||||
docker ps --filter "name=$CONTAINER_NAME" --format "table {{.Status}}\t{{.Ports}}"
|
||||
else
|
||||
log_warn "$SERVICE_NAME is not running"
|
||||
fi
|
||||
;;
|
||||
|
||||
shell)
|
||||
log_info "Entering $SERVICE_NAME container shell..."
|
||||
docker exec -it "$CONTAINER_NAME" sh
|
||||
;;
|
||||
|
||||
migrate)
|
||||
log_info "Running database migrations for $SERVICE_NAME..."
|
||||
docker exec "$CONTAINER_NAME" npx prisma migrate deploy
|
||||
log_success "Migrations completed"
|
||||
;;
|
||||
|
||||
migrate-dev)
|
||||
log_info "Running dev migrations for $SERVICE_NAME..."
|
||||
docker exec -it "$CONTAINER_NAME" npx prisma migrate dev
|
||||
log_success "Dev migrations completed"
|
||||
;;
|
||||
|
||||
clean)
|
||||
log_warn "Cleaning $SERVICE_NAME (removing container and volumes)..."
|
||||
$0 stop
|
||||
docker volume rm "${SERVICE_NAME}_node_modules" 2>/dev/null || true
|
||||
log_success "$SERVICE_NAME cleaned"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Usage: $0 {build|build-no-cache|start|stop|restart|logs|logs-tail|status|shell|migrate|migrate-dev|clean}"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " build - Build Docker image"
|
||||
echo " build-no-cache - Build Docker image without cache"
|
||||
echo " start - Start service"
|
||||
echo " stop - Stop service"
|
||||
echo " restart - Restart service"
|
||||
echo " logs - Follow logs"
|
||||
echo " logs-tail - Show last 100 lines of logs"
|
||||
echo " status - Check service status"
|
||||
echo " shell - Enter container shell"
|
||||
echo " migrate - Run database migrations (production)"
|
||||
echo " migrate-dev - Run database migrations (development)"
|
||||
echo " clean - Remove container and volumes"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"name": "admin-service",
|
||||
"version": "1.0.0",
|
||||
"description": "RWA Admin Service - Mobile App Version Management",
|
||||
"author": "RWA Team",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"prisma": {
|
||||
"schema": "prisma/schema.prisma"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate dev",
|
||||
"prisma:migrate:prod": "prisma migrate deploy",
|
||||
"prisma:studio": "prisma studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/passport": "^10.0.0",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/swagger": "^7.1.17",
|
||||
"@prisma/client": "^5.7.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/passport-jwt": "^3.0.13",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"prettier": "^3.0.0",
|
||||
"prisma": "^5.7.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-loader": "^9.4.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.1.3"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
-- CreateEnum
|
||||
CREATE TYPE "Platform" AS ENUM ('ANDROID', 'IOS');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "app_versions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"platform" "Platform" NOT NULL,
|
||||
"versionCode" INTEGER NOT NULL,
|
||||
"versionName" TEXT NOT NULL,
|
||||
"buildNumber" TEXT NOT NULL,
|
||||
"downloadUrl" TEXT NOT NULL,
|
||||
"fileSize" BIGINT NOT NULL,
|
||||
"fileSha256" TEXT NOT NULL,
|
||||
"minOsVersion" TEXT,
|
||||
"changelog" TEXT NOT NULL,
|
||||
"isForceUpdate" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"releaseDate" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdBy" TEXT NOT NULL,
|
||||
"updatedBy" TEXT,
|
||||
|
||||
CONSTRAINT "app_versions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "app_versions_platform_isEnabled_idx" ON "app_versions"("platform", "isEnabled");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "app_versions_platform_versionCode_idx" ON "app_versions"("platform", "versionCode");
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
// =============================================================================
|
||||
// Admin Service - Prisma Schema
|
||||
// =============================================================================
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// App Version Management
|
||||
// =============================================================================
|
||||
|
||||
model AppVersion {
|
||||
id String @id @default(uuid())
|
||||
platform Platform
|
||||
versionCode Int // Android: versionCode, iOS: CFBundleVersion
|
||||
versionName String // 用户可见版本号,如 "1.2.3"
|
||||
buildNumber String // 构建号
|
||||
downloadUrl String // APK/IPA 下载地址
|
||||
fileSize BigInt // 文件大小(字节)
|
||||
fileSha256 String // 文件 SHA-256 校验和
|
||||
minOsVersion String? // 最低操作系统版本要求
|
||||
changelog String // 更新日志
|
||||
isForceUpdate Boolean @default(false) // 是否强制更新
|
||||
isEnabled Boolean @default(true) // 是否启用
|
||||
releaseDate DateTime? // 发布日期
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdBy String // 创建人ID
|
||||
updatedBy String? // 更新人ID
|
||||
|
||||
@@index([platform, isEnabled])
|
||||
@@index([platform, versionCode])
|
||||
@@map("app_versions")
|
||||
}
|
||||
|
||||
enum Platform {
|
||||
ANDROID
|
||||
IOS
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { Controller, Get } from '@nestjs/common'
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger'
|
||||
|
||||
@ApiTags('Health')
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
@Get()
|
||||
@ApiOperation({ summary: '健康检查' })
|
||||
check() {
|
||||
return {
|
||||
status: 'ok',
|
||||
service: 'admin-service',
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import { Controller, Get, Post, Body, Param, Query, UseGuards } from '@nestjs/common'
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse } from '@nestjs/swagger'
|
||||
import { CheckUpdateDto } from '../dto/request/check-update.dto'
|
||||
import { CreateVersionDto } from '../dto/request/create-version.dto'
|
||||
import { UpdateCheckResultDto, VersionDto } from '../dto/response/version.dto'
|
||||
import { CheckUpdateHandler } from '@/application/queries/check-update/check-update.handler'
|
||||
import { CheckUpdateQuery } from '@/application/queries/check-update/check-update.query'
|
||||
import { CreateVersionHandler } from '@/application/commands/create-version/create-version.handler'
|
||||
import { CreateVersionCommand } from '@/application/commands/create-version/create-version.command'
|
||||
|
||||
@ApiTags('Version Management')
|
||||
@Controller('versions')
|
||||
export class VersionController {
|
||||
constructor(
|
||||
private readonly checkUpdateHandler: CheckUpdateHandler,
|
||||
private readonly createVersionHandler: CreateVersionHandler,
|
||||
) {}
|
||||
|
||||
@Get('check-update')
|
||||
@ApiOperation({ summary: '检查更新 (供移动端调用)' })
|
||||
@ApiResponse({ status: 200, type: UpdateCheckResultDto })
|
||||
async checkUpdate(@Query() dto: CheckUpdateDto): Promise<UpdateCheckResultDto> {
|
||||
const query = new CheckUpdateQuery(dto.platform, dto.currentVersionCode)
|
||||
const result = await this.checkUpdateHandler.execute(query)
|
||||
|
||||
return {
|
||||
hasUpdate: result.hasUpdate,
|
||||
isForceUpdate: result.isForceUpdate,
|
||||
latestVersion: result.latestVersion
|
||||
? {
|
||||
...result.latestVersion,
|
||||
fileSize: result.latestVersion.fileSize.toString(),
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: '创建新版本 (管理员)' })
|
||||
@ApiBearerAuth()
|
||||
@ApiResponse({ status: 201, type: VersionDto })
|
||||
async createVersion(@Body() dto: CreateVersionDto): Promise<VersionDto> {
|
||||
const command = new CreateVersionCommand(
|
||||
dto.platform,
|
||||
dto.versionCode,
|
||||
dto.versionName,
|
||||
dto.buildNumber,
|
||||
dto.downloadUrl,
|
||||
BigInt(dto.fileSize),
|
||||
dto.fileSha256,
|
||||
dto.changelog,
|
||||
dto.isForceUpdate,
|
||||
dto.minOsVersion ?? null,
|
||||
dto.releaseDate ? new Date(dto.releaseDate) : null,
|
||||
'admin', // TODO: Get from JWT token
|
||||
)
|
||||
|
||||
const version = await this.createVersionHandler.execute(command)
|
||||
|
||||
return {
|
||||
id: version.id,
|
||||
platform: version.platform,
|
||||
versionCode: version.versionCode,
|
||||
versionName: version.versionName,
|
||||
buildNumber: version.buildNumber,
|
||||
downloadUrl: version.downloadUrl,
|
||||
fileSize: version.fileSize.toString(),
|
||||
fileSha256: version.fileSha256,
|
||||
changelog: version.changelog,
|
||||
isForceUpdate: version.isForceUpdate,
|
||||
isEnabled: version.isEnabled,
|
||||
minOsVersion: version.minOsVersion,
|
||||
releaseDate: version.releaseDate,
|
||||
createdAt: version.createdAt,
|
||||
updatedAt: version.updatedAt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { ApiProperty } from '@nestjs/swagger'
|
||||
import { IsEnum, IsInt, Min } from 'class-validator'
|
||||
import { Platform } from '@/domain/enums/platform.enum'
|
||||
|
||||
export class CheckUpdateDto {
|
||||
@ApiProperty({ enum: Platform, description: '平台类型' })
|
||||
@IsEnum(Platform)
|
||||
platform: Platform
|
||||
|
||||
@ApiProperty({ description: '当前版本号', example: 100 })
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
currentVersionCode: number
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
|
||||
import { IsEnum, IsInt, IsString, IsBoolean, IsUrl, Min, IsOptional, IsDateString } from 'class-validator'
|
||||
import { Platform } from '@/domain/enums/platform.enum'
|
||||
|
||||
export class CreateVersionDto {
|
||||
@ApiProperty({ enum: Platform, description: '平台类型' })
|
||||
@IsEnum(Platform)
|
||||
platform: Platform
|
||||
|
||||
@ApiProperty({ description: '版本号', example: 101 })
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
versionCode: number
|
||||
|
||||
@ApiProperty({ description: '版本名称', example: '1.0.1' })
|
||||
@IsString()
|
||||
versionName: string
|
||||
|
||||
@ApiProperty({ description: '构建号', example: '202512021200' })
|
||||
@IsString()
|
||||
buildNumber: string
|
||||
|
||||
@ApiProperty({ description: 'APK/IPA下载地址', example: 'https://example.com/app-v1.0.1.apk' })
|
||||
@IsUrl()
|
||||
downloadUrl: string
|
||||
|
||||
@ApiProperty({ description: '文件大小(字节)', example: 52428800 })
|
||||
@IsString()
|
||||
fileSize: string
|
||||
|
||||
@ApiProperty({ description: '文件SHA-256校验和' })
|
||||
@IsString()
|
||||
fileSha256: string
|
||||
|
||||
@ApiProperty({ description: '更新日志' })
|
||||
@IsString()
|
||||
changelog: string
|
||||
|
||||
@ApiProperty({ description: '是否强制更新', default: false })
|
||||
@IsBoolean()
|
||||
isForceUpdate: boolean
|
||||
|
||||
@ApiPropertyOptional({ description: '最低操作系统版本', example: '10.0' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
minOsVersion?: string
|
||||
|
||||
@ApiPropertyOptional({ description: '发布日期', example: '2025-12-02T10:00:00Z' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
releaseDate?: string
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
|
||||
import { Platform } from '@/domain/enums/platform.enum'
|
||||
|
||||
export class VersionDto {
|
||||
@ApiProperty()
|
||||
id: string
|
||||
|
||||
@ApiProperty({ enum: Platform })
|
||||
platform: Platform
|
||||
|
||||
@ApiProperty()
|
||||
versionCode: number
|
||||
|
||||
@ApiProperty()
|
||||
versionName: string
|
||||
|
||||
@ApiProperty()
|
||||
buildNumber: string
|
||||
|
||||
@ApiProperty()
|
||||
downloadUrl: string
|
||||
|
||||
@ApiProperty()
|
||||
fileSize: string
|
||||
|
||||
@ApiProperty()
|
||||
fileSha256: string
|
||||
|
||||
@ApiProperty()
|
||||
changelog: string
|
||||
|
||||
@ApiProperty()
|
||||
isForceUpdate: boolean
|
||||
|
||||
@ApiProperty()
|
||||
isEnabled: boolean
|
||||
|
||||
@ApiPropertyOptional()
|
||||
minOsVersion: string | null
|
||||
|
||||
@ApiPropertyOptional()
|
||||
releaseDate: Date | null
|
||||
|
||||
@ApiProperty()
|
||||
createdAt: Date
|
||||
|
||||
@ApiProperty()
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export class UpdateCheckResultDto {
|
||||
@ApiProperty({ description: '是否有更新' })
|
||||
hasUpdate: boolean
|
||||
|
||||
@ApiProperty({ description: '是否强制更新' })
|
||||
isForceUpdate: boolean
|
||||
|
||||
@ApiPropertyOptional({ description: '最新版本信息' })
|
||||
latestVersion: {
|
||||
versionCode: number
|
||||
versionName: string
|
||||
downloadUrl: string
|
||||
fileSize: string
|
||||
fileSha256: string
|
||||
changelog: string
|
||||
minOsVersion: string | null
|
||||
releaseDate: Date | null
|
||||
} | null
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { Module } from '@nestjs/common'
|
||||
import { ConfigModule } from '@nestjs/config'
|
||||
import { configurations } from './config'
|
||||
import { PrismaService } from './infrastructure/persistence/prisma/prisma.service'
|
||||
import { AppVersionRepositoryImpl } from './infrastructure/persistence/repositories/app-version.repository.impl'
|
||||
import { APP_VERSION_REPOSITORY } from './domain/repositories/app-version.repository'
|
||||
import { CheckUpdateHandler } from './application/queries/check-update/check-update.handler'
|
||||
import { CreateVersionHandler } from './application/commands/create-version/create-version.handler'
|
||||
import { VersionController } from './api/controllers/version.controller'
|
||||
import { HealthController } from './api/controllers/health.controller'
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
load: configurations,
|
||||
}),
|
||||
],
|
||||
controllers: [VersionController, HealthController],
|
||||
providers: [
|
||||
PrismaService,
|
||||
{
|
||||
provide: APP_VERSION_REPOSITORY,
|
||||
useClass: AppVersionRepositoryImpl,
|
||||
},
|
||||
CheckUpdateHandler,
|
||||
CreateVersionHandler,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { Platform } from '@/domain/enums/platform.enum'
|
||||
|
||||
export class CreateVersionCommand {
|
||||
constructor(
|
||||
public readonly platform: Platform,
|
||||
public readonly versionCode: number,
|
||||
public readonly versionName: string,
|
||||
public readonly buildNumber: string,
|
||||
public readonly downloadUrl: string,
|
||||
public readonly fileSize: bigint,
|
||||
public readonly fileSha256: string,
|
||||
public readonly changelog: string,
|
||||
public readonly isForceUpdate: boolean,
|
||||
public readonly minOsVersion: string | null,
|
||||
public readonly releaseDate: Date | null,
|
||||
public readonly createdBy: string,
|
||||
) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import { Inject, Injectable, ConflictException } from '@nestjs/common'
|
||||
import { CreateVersionCommand } from './create-version.command'
|
||||
import { AppVersionRepository, APP_VERSION_REPOSITORY } from '@/domain/repositories/app-version.repository'
|
||||
import { AppVersion } from '@/domain/entities/app-version.entity'
|
||||
|
||||
@Injectable()
|
||||
export class CreateVersionHandler {
|
||||
constructor(
|
||||
@Inject(APP_VERSION_REPOSITORY)
|
||||
private readonly appVersionRepository: AppVersionRepository,
|
||||
) {}
|
||||
|
||||
async execute(command: CreateVersionCommand): Promise<AppVersion> {
|
||||
// Check if version already exists
|
||||
const existing = await this.appVersionRepository.findByPlatformAndVersionCode(
|
||||
command.platform,
|
||||
command.versionCode,
|
||||
)
|
||||
|
||||
if (existing) {
|
||||
throw new ConflictException(
|
||||
`Version ${command.versionCode} already exists for platform ${command.platform}`,
|
||||
)
|
||||
}
|
||||
|
||||
const appVersion = AppVersion.create({
|
||||
platform: command.platform,
|
||||
versionCode: command.versionCode,
|
||||
versionName: command.versionName,
|
||||
buildNumber: command.buildNumber,
|
||||
downloadUrl: command.downloadUrl,
|
||||
fileSize: command.fileSize,
|
||||
fileSha256: command.fileSha256,
|
||||
changelog: command.changelog,
|
||||
isForceUpdate: command.isForceUpdate,
|
||||
minOsVersion: command.minOsVersion,
|
||||
releaseDate: command.releaseDate,
|
||||
createdBy: command.createdBy,
|
||||
})
|
||||
|
||||
return await this.appVersionRepository.save(appVersion)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import { Inject, Injectable } from '@nestjs/common'
|
||||
import { CheckUpdateQuery } from './check-update.query'
|
||||
import { AppVersionRepository, APP_VERSION_REPOSITORY } from '@/domain/repositories/app-version.repository'
|
||||
|
||||
export interface UpdateCheckResult {
|
||||
hasUpdate: boolean
|
||||
isForceUpdate: boolean
|
||||
latestVersion: {
|
||||
versionCode: number
|
||||
versionName: string
|
||||
downloadUrl: string
|
||||
fileSize: bigint
|
||||
fileSha256: string
|
||||
changelog: string
|
||||
minOsVersion: string | null
|
||||
releaseDate: Date | null
|
||||
} | null
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CheckUpdateHandler {
|
||||
constructor(
|
||||
@Inject(APP_VERSION_REPOSITORY)
|
||||
private readonly appVersionRepository: AppVersionRepository,
|
||||
) {}
|
||||
|
||||
async execute(query: CheckUpdateQuery): Promise<UpdateCheckResult> {
|
||||
const latestVersion = await this.appVersionRepository.findLatestByPlatform(query.platform)
|
||||
|
||||
if (!latestVersion) {
|
||||
return {
|
||||
hasUpdate: false,
|
||||
isForceUpdate: false,
|
||||
latestVersion: null,
|
||||
}
|
||||
}
|
||||
|
||||
const hasUpdate = latestVersion.isNewerThan(query.currentVersionCode)
|
||||
|
||||
return {
|
||||
hasUpdate,
|
||||
isForceUpdate: hasUpdate && latestVersion.shouldForceUpdate(),
|
||||
latestVersion: hasUpdate
|
||||
? {
|
||||
versionCode: latestVersion.versionCode,
|
||||
versionName: latestVersion.versionName,
|
||||
downloadUrl: latestVersion.downloadUrl,
|
||||
fileSize: latestVersion.fileSize,
|
||||
fileSha256: latestVersion.fileSha256,
|
||||
changelog: latestVersion.changelog,
|
||||
minOsVersion: latestVersion.minOsVersion,
|
||||
releaseDate: latestVersion.releaseDate,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { Platform } from '@/domain/enums/platform.enum'
|
||||
|
||||
export class CheckUpdateQuery {
|
||||
constructor(
|
||||
public readonly platform: Platform,
|
||||
public readonly currentVersionCode: number,
|
||||
) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
export const appConfig = () => ({
|
||||
port: parseInt(process.env.APP_PORT || '3010', 10),
|
||||
env: process.env.NODE_ENV || 'development',
|
||||
apiPrefix: process.env.API_PREFIX || 'api/v1',
|
||||
})
|
||||
|
||||
export const databaseConfig = () => ({
|
||||
url: process.env.DATABASE_URL,
|
||||
})
|
||||
|
||||
export const jwtConfig = () => ({
|
||||
secret: process.env.JWT_SECRET || 'default-jwt-secret-change-in-production',
|
||||
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
|
||||
})
|
||||
|
||||
export const redisConfig = () => ({
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: parseInt(process.env.REDIS_PORT || '6379', 10),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
db: parseInt(process.env.REDIS_DB || '9', 10),
|
||||
})
|
||||
|
||||
export const configurations = [appConfig, databaseConfig, jwtConfig, redisConfig]
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import { Platform } from '../enums/platform.enum'
|
||||
|
||||
export class AppVersion {
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly platform: Platform,
|
||||
public readonly versionCode: number,
|
||||
public readonly versionName: string,
|
||||
public readonly buildNumber: string,
|
||||
public readonly downloadUrl: string,
|
||||
public readonly fileSize: bigint,
|
||||
public readonly fileSha256: string,
|
||||
public readonly changelog: string,
|
||||
public readonly isForceUpdate: boolean,
|
||||
public readonly isEnabled: boolean,
|
||||
public readonly minOsVersion: string | null,
|
||||
public readonly releaseDate: Date | null,
|
||||
public readonly createdAt: Date,
|
||||
public readonly updatedAt: Date,
|
||||
public readonly createdBy: string,
|
||||
public readonly updatedBy: string | null,
|
||||
) {}
|
||||
|
||||
static create(params: {
|
||||
platform: Platform
|
||||
versionCode: number
|
||||
versionName: string
|
||||
buildNumber: string
|
||||
downloadUrl: string
|
||||
fileSize: bigint
|
||||
fileSha256: string
|
||||
changelog: string
|
||||
isForceUpdate: boolean
|
||||
minOsVersion?: string | null
|
||||
releaseDate?: Date | null
|
||||
createdBy: string
|
||||
}): AppVersion {
|
||||
return new AppVersion(
|
||||
crypto.randomUUID(),
|
||||
params.platform,
|
||||
params.versionCode,
|
||||
params.versionName,
|
||||
params.buildNumber,
|
||||
params.downloadUrl,
|
||||
params.fileSize,
|
||||
params.fileSha256,
|
||||
params.changelog,
|
||||
params.isForceUpdate,
|
||||
true, // isEnabled = true by default
|
||||
params.minOsVersion ?? null,
|
||||
params.releaseDate ?? null,
|
||||
new Date(),
|
||||
new Date(),
|
||||
params.createdBy,
|
||||
null,
|
||||
)
|
||||
}
|
||||
|
||||
isNewerThan(currentVersionCode: number): boolean {
|
||||
return this.versionCode > currentVersionCode
|
||||
}
|
||||
|
||||
shouldForceUpdate(): boolean {
|
||||
return this.isForceUpdate && this.isEnabled
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export enum Platform {
|
||||
ANDROID = 'ANDROID',
|
||||
IOS = 'IOS',
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { AppVersion } from '../entities/app-version.entity'
|
||||
import { Platform } from '../enums/platform.enum'
|
||||
|
||||
export const APP_VERSION_REPOSITORY = Symbol('APP_VERSION_REPOSITORY')
|
||||
|
||||
export interface AppVersionRepository {
|
||||
/**
|
||||
* 保存新版本
|
||||
*/
|
||||
save(appVersion: AppVersion): Promise<AppVersion>
|
||||
|
||||
/**
|
||||
* 根据ID查找版本
|
||||
*/
|
||||
findById(id: string): Promise<AppVersion | null>
|
||||
|
||||
/**
|
||||
* 获取指定平台的最新版本
|
||||
*/
|
||||
findLatestByPlatform(platform: Platform): Promise<AppVersion | null>
|
||||
|
||||
/**
|
||||
* 获取指定平台所有启用的版本列表
|
||||
*/
|
||||
findAllByPlatform(platform: Platform, includeDisabled?: boolean): Promise<AppVersion[]>
|
||||
|
||||
/**
|
||||
* 根据平台和版本号查找
|
||||
*/
|
||||
findByPlatformAndVersionCode(platform: Platform, versionCode: number): Promise<AppVersion | null>
|
||||
|
||||
/**
|
||||
* 更新版本信息
|
||||
*/
|
||||
update(id: string, updates: Partial<AppVersion>): Promise<AppVersion>
|
||||
|
||||
/**
|
||||
* 禁用/启用版本
|
||||
*/
|
||||
toggleEnabled(id: string, isEnabled: boolean): Promise<void>
|
||||
|
||||
/**
|
||||
* 删除版本
|
||||
*/
|
||||
delete(id: string): Promise<void>
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger(PrismaService.name)
|
||||
|
||||
async onModuleInit() {
|
||||
try {
|
||||
await this.$connect()
|
||||
this.logger.log('Successfully connected to database')
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to connect to database', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.$disconnect()
|
||||
this.logger.log('Disconnected from database')
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
import { Injectable } from '@nestjs/common'
|
||||
import { AppVersionRepository } from '@/domain/repositories/app-version.repository'
|
||||
import { AppVersion } from '@/domain/entities/app-version.entity'
|
||||
import { Platform } from '@/domain/enums/platform.enum'
|
||||
import { PrismaService } from '../prisma/prisma.service'
|
||||
|
||||
@Injectable()
|
||||
export class AppVersionRepositoryImpl implements AppVersionRepository {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async save(appVersion: AppVersion): Promise<AppVersion> {
|
||||
const created = await this.prisma.appVersion.create({
|
||||
data: {
|
||||
id: appVersion.id,
|
||||
platform: appVersion.platform,
|
||||
versionCode: appVersion.versionCode,
|
||||
versionName: appVersion.versionName,
|
||||
buildNumber: appVersion.buildNumber,
|
||||
downloadUrl: appVersion.downloadUrl,
|
||||
fileSize: appVersion.fileSize,
|
||||
fileSha256: appVersion.fileSha256,
|
||||
changelog: appVersion.changelog,
|
||||
isForceUpdate: appVersion.isForceUpdate,
|
||||
isEnabled: appVersion.isEnabled,
|
||||
minOsVersion: appVersion.minOsVersion,
|
||||
releaseDate: appVersion.releaseDate,
|
||||
createdBy: appVersion.createdBy,
|
||||
updatedBy: appVersion.updatedBy,
|
||||
},
|
||||
})
|
||||
|
||||
return this.toDomain(created)
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<AppVersion | null> {
|
||||
const found = await this.prisma.appVersion.findUnique({ where: { id } })
|
||||
return found ? this.toDomain(found) : null
|
||||
}
|
||||
|
||||
async findLatestByPlatform(platform: Platform): Promise<AppVersion | null> {
|
||||
const found = await this.prisma.appVersion.findFirst({
|
||||
where: { platform, isEnabled: true },
|
||||
orderBy: { versionCode: 'desc' },
|
||||
})
|
||||
return found ? this.toDomain(found) : null
|
||||
}
|
||||
|
||||
async findAllByPlatform(platform: Platform, includeDisabled = false): Promise<AppVersion[]> {
|
||||
const results = await this.prisma.appVersion.findMany({
|
||||
where: includeDisabled ? { platform } : { platform, isEnabled: true },
|
||||
orderBy: { versionCode: 'desc' },
|
||||
})
|
||||
return results.map((r) => this.toDomain(r))
|
||||
}
|
||||
|
||||
async findByPlatformAndVersionCode(
|
||||
platform: Platform,
|
||||
versionCode: number,
|
||||
): Promise<AppVersion | null> {
|
||||
const found = await this.prisma.appVersion.findFirst({
|
||||
where: { platform, versionCode },
|
||||
})
|
||||
return found ? this.toDomain(found) : null
|
||||
}
|
||||
|
||||
async update(id: string, updates: Partial<AppVersion>): Promise<AppVersion> {
|
||||
const updated = await this.prisma.appVersion.update({
|
||||
where: { id },
|
||||
data: {
|
||||
versionName: updates.versionName,
|
||||
buildNumber: updates.buildNumber,
|
||||
downloadUrl: updates.downloadUrl,
|
||||
fileSize: updates.fileSize,
|
||||
fileSha256: updates.fileSha256,
|
||||
changelog: updates.changelog,
|
||||
isForceUpdate: updates.isForceUpdate,
|
||||
minOsVersion: updates.minOsVersion,
|
||||
releaseDate: updates.releaseDate,
|
||||
updatedBy: updates.updatedBy,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
})
|
||||
return this.toDomain(updated)
|
||||
}
|
||||
|
||||
async toggleEnabled(id: string, isEnabled: boolean): Promise<void> {
|
||||
await this.prisma.appVersion.update({
|
||||
where: { id },
|
||||
data: { isEnabled, updatedAt: new Date() },
|
||||
})
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.prisma.appVersion.delete({ where: { id } })
|
||||
}
|
||||
|
||||
private toDomain(model: any): AppVersion {
|
||||
return new AppVersion(
|
||||
model.id,
|
||||
model.platform as Platform,
|
||||
model.versionCode,
|
||||
model.versionName,
|
||||
model.buildNumber,
|
||||
model.downloadUrl,
|
||||
model.fileSize,
|
||||
model.fileSha256,
|
||||
model.changelog,
|
||||
model.isForceUpdate,
|
||||
model.isEnabled,
|
||||
model.minOsVersion,
|
||||
model.releaseDate,
|
||||
model.createdAt,
|
||||
model.updatedAt,
|
||||
model.createdBy,
|
||||
model.updatedBy,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { NestFactory } from '@nestjs/core'
|
||||
import { ValidationPipe, Logger } from '@nestjs/common'
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'
|
||||
import { AppModule } from './app.module'
|
||||
|
||||
async function bootstrap() {
|
||||
const logger = new Logger('Bootstrap')
|
||||
const app = await NestFactory.create(AppModule)
|
||||
|
||||
// Global prefix
|
||||
app.setGlobalPrefix('api/v1')
|
||||
|
||||
// Validation
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
transformOptions: { enableImplicitConversion: true },
|
||||
}),
|
||||
)
|
||||
|
||||
// CORS
|
||||
app.enableCors({
|
||||
origin: '*',
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||
credentials: true,
|
||||
})
|
||||
|
||||
// Swagger
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('Admin Service API')
|
||||
.setDescription('RWA管理服务API - 移动应用版本管理')
|
||||
.setVersion('1.0.0')
|
||||
.addBearerAuth()
|
||||
.addTag('Version Management', '版本管理')
|
||||
.addTag('Health', '健康检查')
|
||||
.build()
|
||||
const document = SwaggerModule.createDocument(app, config)
|
||||
SwaggerModule.setup('api/docs', app, document)
|
||||
|
||||
const port = parseInt(process.env.APP_PORT || '3010', 10)
|
||||
await app.listen(port)
|
||||
|
||||
logger.log(`Admin Service is running on port ${port}`)
|
||||
logger.log(`Swagger docs: http://localhost:${port}/api/docs`)
|
||||
}
|
||||
|
||||
bootstrap()
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true,
|
||||
"strictBindCallApply": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ services:
|
|||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-rwa_user}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-rwa_secure_password}
|
||||
POSTGRES_MULTIPLE_DATABASES: rwa_identity,rwa_wallet,rwa_mpc,rwa_backup,rwa_planting,rwa_referral,rwa_reward,rwa_leaderboard,rwa_reporting,rwa_authorization
|
||||
POSTGRES_MULTIPLE_DATABASES: rwa_identity,rwa_wallet,rwa_mpc,rwa_backup,rwa_planting,rwa_referral,rwa_reward,rwa_leaderboard,rwa_reporting,rwa_authorization,rwa_admin
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
|
|
@ -462,37 +462,36 @@ services:
|
|||
networks:
|
||||
- rwa-network
|
||||
|
||||
# admin-service:
|
||||
# # NOTE: Service not yet implemented - uncomment when ready
|
||||
# build:
|
||||
# context: ./admin-service
|
||||
# dockerfile: Dockerfile
|
||||
# container_name: rwa-admin-service
|
||||
# ports:
|
||||
# - "3010:3010"
|
||||
# environment:
|
||||
# - NODE_ENV=production
|
||||
# - APP_PORT=3010
|
||||
# - DATABASE_URL=postgresql://${POSTGRES_USER:-rwa_user}:${POSTGRES_PASSWORD:-rwa_secure_password}@postgres:5432/rwa_identity?schema=public
|
||||
# - JWT_SECRET=${JWT_SECRET}
|
||||
# - REDIS_HOST=redis
|
||||
# - REDIS_PORT=6379
|
||||
# - REDIS_PASSWORD=${REDIS_PASSWORD:-}
|
||||
# - REDIS_DB=9
|
||||
# depends_on:
|
||||
# postgres:
|
||||
# condition: service_healthy
|
||||
# redis:
|
||||
# condition: service_healthy
|
||||
# healthcheck:
|
||||
# test: ["CMD", "wget", "-q", "--spider", "http://localhost:3010/health"]
|
||||
# interval: 30s
|
||||
# timeout: 10s
|
||||
# retries: 3
|
||||
# start_period: 40s
|
||||
# restart: unless-stopped
|
||||
# networks:
|
||||
# - rwa-network
|
||||
admin-service:
|
||||
build:
|
||||
context: ./admin-service
|
||||
dockerfile: Dockerfile
|
||||
container_name: rwa-admin-service
|
||||
ports:
|
||||
- "3010:3010"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- APP_PORT=3010
|
||||
- DATABASE_URL=postgresql://${POSTGRES_USER:-rwa_user}:${POSTGRES_PASSWORD:-rwa_secure_password}@postgres:5432/rwa_admin?schema=public
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
|
||||
- REDIS_DB=9
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3010/api/v1/health"]
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- rwa-network
|
||||
|
||||
# ===========================================================================
|
||||
# Volumes
|
||||
|
|
|
|||
Loading…
Reference in New Issue