diff --git a/.dockerignore b/.dockerignore index 595848f..f740bfc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,6 @@ delta-force-guide-server.iml build/ +!.gitlab-ci.yml +!build/libs/*.jar .idea/ .gradle \ No newline at end of file diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 5e09721..116f12f 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -6,15 +6,17 @@ on: env: APP_NAME: delta-force-guide-server - IMAGE_REGISTRY: ${{ vars.GITLAB_REGISTRY }} - IMAGE_NAME: ${{ vars.GITLAB_IMAGE_NAME }} + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: # ================================================================ - # Job 1 — Package: build the JAR with Gradle + # Single Job: Build, Upload JAR to Release, and Push to GHCR # ================================================================ - package: + build-and-release: runs-on: ubuntu-latest + permissions: + contents: write + packages: write steps: - uses: actions/checkout@v4 @@ -28,54 +30,47 @@ jobs: - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 + # 使用 Release Tag 做为 Gradle 属性传入 - name: Build with Gradle - run: ./gradlew build - - - name: Upload JAR artifact - uses: actions/upload-artifact@v4 - with: - name: app-jar - path: build/libs/delta-force-guide-server-*.jar - retention-days: 1 - - # ================================================================ - # Job 2 — Build & push Docker image to GitHub Container Registry - # ================================================================ - build-and-push: - needs: package - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - uses: actions/checkout@v4 - - - name: Download JAR artifact - uses: actions/download-artifact@v4 - with: - name: app-jar - path: build/libs + run: ./gradlew bootJar -x test -PartefactVersion="${{ github.event.release.tag_name }}" - name: Resolve JAR file path id: jar - run: echo "file=$(ls build/libs/delta-force-guide-server-*.jar | head -1)" >> "$GITHUB_OUTPUT" + run: | + JAR_PATH=$(find build/libs -name '*.jar' | head -1) + echo "file=$JAR_PATH" >> "$GITHUB_OUTPUT" + + # 上传 JAR 包到 GitHub Release 中 + - name: Upload JAR to GitHub Release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ${{ steps.jar.outputs.file }} + asset_name: ${{ github.event.repository.name }}-${{ github.event.release.tag_name }}.jar + tag: ${{ github.event.release.tag_name }} + overwrite: true - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Log in to GitLab Container Registry + # 登录到 GitHub Container Registry (GHCR) + - name: Log in to GHCR uses: docker/login-action@v3 with: - registry: ${{ env.IMAGE_REGISTRY }} - username: ${{ vars.GITLAB_REGISTRY_USER }} - password: ${{ secrets.GITLAB_REGISTRY_PASSWORD }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + # 镜像打标签准备 - name: Generate image tags id: meta run: | - echo "version=${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT" - echo "latest=${{ env.IMAGE_NAME }}:latest" >> "$GITHUB_OUTPUT" + OWNER_LC=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') + REPO_LC=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]') + echo "tag_version=ghcr.io/$OWNER_LC/$REPO_LC:${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT" + echo "tag_latest=ghcr.io/$OWNER_LC/$REPO_LC:latest" >> "$GITHUB_OUTPUT" + # 构建并上传镜像到 GHCR - name: Build and push Docker image uses: docker/build-push-action@v6 with: @@ -84,44 +79,8 @@ jobs: build-args: JAR_FILE=${{ steps.jar.outputs.file }} push: true tags: | - ${{ steps.meta.outputs.version }} - ${{ steps.meta.outputs.latest }} + ${{ steps.meta.outputs.tag_version }} + ${{ steps.meta.outputs.tag_latest }} cache-from: type=gha cache-to: type=gha,mode=max - # ================================================================ - # Job 3 — Deploy on the target server via SSH - # ================================================================ - deploy: - needs: build-and-push - runs-on: ubuntu-latest - steps: - - name: Deploy via SSH - uses: appleboy/ssh-action@v1.2.0 - with: - host: ${{ secrets.DEPLOY_HOST }} - username: ${{ secrets.DEPLOY_USER }} - key: ${{ secrets.DEPLOY_SSH_KEY }} - script: | - set -e - - echo '=== Pulling image ===' - echo '${{ secrets.GITLAB_REGISTRY_PASSWORD }}' | docker login ${{ env.IMAGE_REGISTRY }} \ - -u ${{ vars.GITLAB_REGISTRY_USER }} --password-stdin - docker pull ${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }} - - echo '=== Stopping old container ===' - docker stop ${{ env.APP_NAME }} || true - docker rm ${{ env.APP_NAME }} || true - - echo '=== Starting new container ===' - docker run -d \ - --name ${{ env.APP_NAME }} \ - --restart unless-stopped \ - -p ${DEPLOY_PORT:-8080}:8080 \ - ${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }} - - echo '=== Cleaning up old images ===' - docker image prune -f - - echo '=== Deployment complete ===' diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d213467..fc0e018 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,16 +1,12 @@ variables: - REGISTRY: registry.onixbyte.cn - IMAGE_NAME: delta-force-guide GRADLE_OPTS: -Dorg.gradle.daemon=false DOCKER_HOST: unix:///var/run/docker.sock stages: - - build - - package - - deploy + - release -build: - stage: build +release: + stage: release image: amazoncorretto:21-alpine cache: key: gradle @@ -19,41 +15,21 @@ build: - .gradle/caches before_script: - chmod +x gradlew + - apk add --no-cache docker-cli script: - ./gradlew bootJar -x test -PartefactVersion="$CI_COMMIT_TAG" - artifacts: - paths: - - build/libs/*.jar - expire_in: 30 min - rules: - - if: $CI_COMMIT_TAG - -package: - stage: package - image: docker:27 - needs: - - build - script: - JAR_FILE=$(find build/libs -name '*.jar' | head -1) - echo "Building Docker image for tag $CI_COMMIT_TAG with JAR $JAR_FILE" - docker build + --provenance=false -f Dockerfile.ci --build-arg JAR_FILE="$JAR_FILE" - -t "$REGISTRY/$IMAGE_NAME:$CI_COMMIT_TAG" + -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG" . - - docker tag "$REGISTRY/$IMAGE_NAME:$CI_COMMIT_TAG" "$REGISTRY/$IMAGE_NAME:latest" - rules: - - if: $CI_COMMIT_TAG - -deploy: - stage: deploy - image: docker:27 - needs: - - package - script: - - echo "Pushing image $REGISTRY/$IMAGE_NAME:$CI_COMMIT_TAG" - - docker login "$REGISTRY" -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" - - docker push "$REGISTRY/$IMAGE_NAME:$CI_COMMIT_TAG" - - docker push "$REGISTRY/$IMAGE_NAME:latest" + - docker tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG" "$CI_REGISTRY_IMAGE:latest" + - echo "Pushing image $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG" + - docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" + - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG" + - docker push "$CI_REGISTRY_IMAGE:latest" rules: - if: $CI_COMMIT_TAG diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..73c61fd --- /dev/null +++ b/LICENCE @@ -0,0 +1,21 @@ +MIT Licence + +Copyright (c) 2026 OnixByte + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..13455ae --- /dev/null +++ b/README.md @@ -0,0 +1,150 @@ +# Delta Force Guide Server + +REST API backend for managing **Delta Force** game firearm builds and modifications. Provides endpoints to browse, create, and share weapon configurations including attachments, tuning setups, tags, and build reviews. + + +## Tech Stack + +| Layer | Technology | +|-----------|------------------------------------------------------------| +| Language | Java 21 (Amazon Corretto) | +| Framework | Spring Boot 3.x (Web, Security, Data JPA, Cache, Actuator) | +| Build | Gradle (Kotlin DSL) | +| Database | PostgreSQL (Flyway migrations, JSONB columns) | +| Cache | Redis (2-hour TTL) | +| Auth | Custom JWT via httpOnly cookies + BCrypt | +| API Docs | springdoc-openapi (Swagger UI, dev profile only) | +| Container | Multi-stage Docker image (Amazon Corretto 21 → Alpine) | +| CI/CD | GitHub Actions — builds on release publish | + + +## Quick Start + +### Prerequisites + +- JDK 21 (Amazon Corretto recommended) +- PostgreSQL +- Redis + +### Configure + +Copy and customise the development config: + +```bash +cp config/application-prod.yaml.example \ + config/application-dev.yaml +``` + +Fill in your datasource and Redis connection details, then place the production config at `config/`, then enable it by environment variable `SPRING_PROFILES_ACTIVE`. + +### Build + +```bash +# Compile (skip tests) +./gradlew build -x test + +# Run all tests +./gradlew test + +# Build executable JAR +./gradlew bootJar +``` + +### Run + +```bash +SPRING_PROFILES_ACTIVE=prod java -jar build/libs/delta-force-guide-server-$version.jar +``` + +Swagger UI is available at `http://localhost:8080/swagger-ui.html` when the `dev` profile is active. + +## Data Model + +- **Firearm** — weapon base stats (name, type, level, calibre, fire rate, armour/body damage, review) +- **Modification** — a build attached to a firearm, with name, code, tags (JSONB), accessories including nested tuning objects (JSONB), author notes, and video links +- **App User** — registered user (username, email) with hashed credentials via BCrypt + +Tags and accessories are stored as PostgreSQL JSONB columns using [Hypersistence Utils](https://github.com/vladmihalcea/hypersistence-utils), enabling flexible per-build metadata and filtering with the `@>` operator. + + +## Architecture + +``` +Controller → Service → Manager → Repository / Mapper + (HTTP) (logic) (@Transactional) (data access) +``` + +The call chain is strictly enforced — skipping layers is not permitted. All request/response objects are Java records with static `from()` factory methods for entity-to-DTO conversion. + + +## Docker + +```bash +# Build image +docker pull registry.onixbyte.cn/onixbyte/delta-force-guide-server:latest + +# Run container +docker run -p 8080:8080 \ + -v /path/to/config:/app/config \ + -e SPRING_PROFILES_ACTIVE=$your_active_profiles + delta-force-guide-server +``` + +Pre-built images are published to **Self-hosted GitLab Container Registry** (`registry.onixbyte.cn/onixbyte/delta-force-guide-server`) on every release. + + +## CI/CD + +GitLab CI triggers on **tags**. The pipeline: + +1. Builds the boot JAR with the release tag as the version +2. Uploads the JAR as a release asset +3. Builds and pushes a multi-arch Docker image to GHCR tagged with both `:latest` and `:` + +No tests are run in CI by design — tests are expected to pass locally before a release is cut. + + +## Profiles + +| Profile | Purpose | +|---------|-----------------------------------------------------------------------| +| `dev` | Enables Swagger UI, connects to dev DB/Redis at `dfguide.onixbyte.cn` | +| default | Production mode, no Swagger, uses production datasource | + + +## Project Structure + +``` +src/main/java/com/onixbyte/deltaforceguide/ +├── client/ External service HTTP clients +├── config/ Spring bean definitions (Security, CORS, Cache, Jackson, MyBatis) +├── controller/ REST controllers +├── domain/ +│ ├── converter/ JPA attribute converters +│ ├── dto/ Request/response records +│ └── entity/ JPA entities +├── enumeration/ Enums (FirearmType) +├── exception/ Custom BizException with HTTP status mapping +├── filter/ TokenAuthenticationFilter (JWT through OncePerRequestFilter) +├── manager/ @Transactional wrappers around repositories +├── mapper/ MyBatis mapper interfaces (reserved for future use) +├── properties/ @ConfigurationProperties records +├── repository/ Spring Data JPA repositories +├── security/ +│ ├── authentication/ Custom UsernamePasswordAuthentication +│ └── provider/ UsernamePasswordAuthenticationProvider +├── service/ Business logic +├── shared/ Constants and utility classes +└── utils/ General-purpose helpers +``` + + +## Versioning + +The application exposes its version at `/versions` (GET). + + +## Licence + +This project is licensed under the [MIT Licence](LICENCE). Copyright © 2026 OnixByte. + diff --git a/config/application-prod.yaml.example b/config/application-prod.yaml.example new file mode 100644 index 0000000..ae00f78 --- /dev/null +++ b/config/application-prod.yaml.example @@ -0,0 +1,55 @@ +spring: + datasource: + url: jdbc:postgresql://localhost:5432/dfguide_dev + username: postgres + password: 123456 + driver-class-name: org.postgresql.Driver + data: + redis: + host: localhost + port: 6379 + database: 0 + # password: 6hLFVqfGPviTYukn # Uncomment if password is necessary + +logging: + pattern: + # dateformat: dd MMM yyyy HH:mm:ss.SSS # Modify this for custom date format. + +app: + common: + version: 1.3.0.8-dev # Application version, you can change to any version you like, used for communication with frontend. + cors: + allowed-origins: # Cross-origin allowed origins + - "http://localhost:5173" # Dev server for vite. + - "http://localhost:4173" # Preview server for vite. + allow-credentials: true # Must be set to `true` since we are using cookie. You can change it only when you have modified how this application executing authentication. + allow-private-network: false + allowed-headers: + - "Content-Type" + - "Authorization" + allowed-methods: + - GET + - POST + - PUT + - PATCH + - DELETE + exposed-headers: + - "Content-Type" + - "Authorization" + max-age: PT2H + jwt: + issuer: dfguide.local # Issuer host + secret: qwertyuiopasdfghjklzxcvbnm123456 # JWT singing secret, a 32-byte long or longer string is recommended + valid-time: PT2H # JWT valid duration + cookie: # Cookie settings. + http-only: true + secure: false + same-site: lax + path: '/' + max-age: PT2H + +springdoc: + api-docs: + enabled: true # Set to `false` if you do not need api docs (recommended in production mode). + swagger-ui: + enabled: true # Set to `false` if you do not need swagger ui (recommended in production mode). \ No newline at end of file diff --git a/gradle.properties b/gradle.properties deleted file mode 100644 index 1235085..0000000 --- a/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -artefactVersion = 1.2.0 \ No newline at end of file diff --git a/src/main/java/com/onixbyte/deltaforceguide/config/AppConfig.java b/src/main/java/com/onixbyte/deltaforceguide/config/AppConfig.java new file mode 100644 index 0000000..32ec283 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/config/AppConfig.java @@ -0,0 +1,10 @@ +package com.onixbyte.deltaforceguide.config; + +import com.onixbyte.deltaforceguide.properties.AppProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(AppProperties.class) +public class AppConfig { +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/controller/VersionController.java b/src/main/java/com/onixbyte/deltaforceguide/controller/VersionController.java new file mode 100644 index 0000000..a73b43c --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/controller/VersionController.java @@ -0,0 +1,24 @@ +package com.onixbyte.deltaforceguide.controller; + +import com.onixbyte.deltaforceguide.service.AppService; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/versions") +public class VersionController { + + private final AppService appService; + + public VersionController(AppService appService) { + this.appService = appService; + } + + @Operation(description = "获取当前应用版本号") + @GetMapping + public String getVersion() { + return appService.getVersion(); + } +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/manager/AppManager.java b/src/main/java/com/onixbyte/deltaforceguide/manager/AppManager.java new file mode 100644 index 0000000..64f8ddd --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/manager/AppManager.java @@ -0,0 +1,23 @@ +package com.onixbyte.deltaforceguide.manager; + +import com.onixbyte.deltaforceguide.properties.AppProperties; +import org.springframework.stereotype.Component; + +@Component +public class AppManager { + + private final AppProperties appProperties; + + public AppManager(AppProperties appProperties) { + this.appProperties = appProperties; + } + + /** + * Retrieves the application version. + * + * @return the version string of this application + */ + public String getVersion() { + return appProperties.version(); + } +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/properties/AppProperties.java b/src/main/java/com/onixbyte/deltaforceguide/properties/AppProperties.java new file mode 100644 index 0000000..79b056c --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/properties/AppProperties.java @@ -0,0 +1,9 @@ +package com.onixbyte.deltaforceguide.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "app.common") +public record AppProperties( + String version +) { +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/AppService.java b/src/main/java/com/onixbyte/deltaforceguide/service/AppService.java new file mode 100644 index 0000000..68f50bd --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/service/AppService.java @@ -0,0 +1,18 @@ +package com.onixbyte.deltaforceguide.service; + +import com.onixbyte.deltaforceguide.manager.AppManager; +import org.springframework.stereotype.Service; + +@Service +public class AppService { + + private final AppManager appManager; + + public AppService(AppManager appManager) { + this.appManager = appManager; + } + + public String getVersion() { + return appManager.getVersion(); + } +}