Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
9fe292963c
|
|||
|
d27f6455d8
|
|||
|
0671937ecd
|
|||
|
e2a40795c5
|
|||
|
a8ff1cabad
|
|||
|
e4dca61f98
|
@@ -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 ==='
|
||||
|
||||
+5
-28
@@ -3,12 +3,10 @@ variables:
|
||||
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
|
||||
@@ -17,41 +15,20 @@ 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
|
||||
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 "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG"
|
||||
.
|
||||
- docker tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG" "$CI_REGISTRY_IMAGE:latest"
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
|
||||
deploy:
|
||||
stage: deploy
|
||||
image: docker:27
|
||||
needs:
|
||||
- package
|
||||
script:
|
||||
- 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
|
||||
- if: $CI_COMMIT_TAG && $CI_COMMIT_BRANCH == "main"
|
||||
|
||||
@@ -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.
|
||||
@@ -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 `:<version>`
|
||||
|
||||
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.
|
||||
|
||||
@@ -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).
|
||||
@@ -1 +0,0 @@
|
||||
artefactVersion = 1.2.0
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user