Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4218bea4c | ||
|
|
040801ac3b | ||
|
|
68c10979ff | ||
|
|
27bba95250 |
No files matched your search
@@ -0,0 +1,89 @@
|
||||
name: Build and Deploy
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
env:
|
||||
APP_NAME: ahsarah-guide-server
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 21 (Corretto)
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 21
|
||||
distribution: corretto
|
||||
cache: gradle
|
||||
|
||||
- name: Build with Gradle
|
||||
run: >
|
||||
./gradlew bootJar -x test
|
||||
-PartefactVersion="${{ gitea.event.release.tag_name }}"
|
||||
-PbuildChannel=stable
|
||||
-Pvendor=${{ vars.VENDOR }}
|
||||
|
||||
- name: Resolve JAR file path
|
||||
id: jar
|
||||
run: |
|
||||
JAR_PATH=$(find build/libs -name '*.jar' | head -1)
|
||||
echo "file=$JAR_PATH" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload JAR to Gitea Release
|
||||
run: |
|
||||
TAG="${{ gitea.event.release.tag_name }}"
|
||||
FILE="${{ steps.jar.outputs.file }}"
|
||||
ASSET_NAME="${APP_NAME}-${TAG}.jar"
|
||||
curl -X POST \
|
||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-F "attachment=@${FILE};filename=${ASSET_NAME}" \
|
||||
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases/${{ gitea.event.release.id }}/assets?name=${ASSET_NAME}"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ vars.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
|
||||
- name: Generate image tags
|
||||
id: meta
|
||||
run: |
|
||||
DOCKERHUB_USER="${{ vars.DOCKER_HUB_USERNAME }}"
|
||||
echo "tag_version=${DOCKERHUB_USER}/${APP_NAME}:${{ gitea.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag_latest=${DOCKERHUB_USER}/${APP_NAME}:latest" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: server
|
||||
file: server/Dockerfile.ci
|
||||
build-args: |
|
||||
JAR_FILE=${{ steps.jar.outputs.file }}
|
||||
IMAGE_VERSION=${{ gitea.event.release.tag_name }}
|
||||
IMAGE_VENDOR=${{ vars.VENDOR }}
|
||||
BUILD_DATE=${{ gitea.event.release.published_at }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ steps.meta.outputs.tag_version }}
|
||||
${{ steps.meta.outputs.tag_latest }}
|
||||
|
||||
- name: Push README to Docker Hub
|
||||
uses: christian-korneck/update-container-description-action@v1
|
||||
env:
|
||||
DOCKER_USER: ${{ vars.DOCKER_HUB_USERNAME }}
|
||||
DOCKER_PASS: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
with:
|
||||
destination_container_repo: ${{ vars.DOCKER_HUB_USERNAME }}/${{ env.APP_NAME }}
|
||||
provider: dockerhub
|
||||
readme_file: server/README.md
|
||||
@@ -0,0 +1,115 @@
|
||||
name: Build and Deploy
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build release archive
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 11
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build release archive
|
||||
run: pnpm build:tar
|
||||
|
||||
- name: Clean up previous build artifacts on server
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ vars.DEPLOY_HOST }}
|
||||
username: ${{ vars.DEPLOY_USER }}
|
||||
port: ${{ vars.DEPLOY_PORT }}
|
||||
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
command: rm -rf /tmp/dist.tar.gz /tmp/dist.tar.gz/
|
||||
|
||||
- name: Upload artifact to server
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ vars.DEPLOY_HOST }}
|
||||
username: ${{ vars.DEPLOY_USER }}
|
||||
port: ${{ vars.DEPLOY_PORT }}
|
||||
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
source: "dist.tar.gz"
|
||||
target: "/tmp/"
|
||||
|
||||
upload-release-asset:
|
||||
name: Upload to Gitea Release
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Download artifact from server
|
||||
env:
|
||||
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
|
||||
DEPLOY_USER: ${{ vars.DEPLOY_USER }}
|
||||
DEPLOY_PORT: ${{ vars.DEPLOY_PORT }}
|
||||
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
run: |
|
||||
set -e
|
||||
SSH_DIR="${RUNNER_TEMP}/ssh"
|
||||
mkdir -p "$SSH_DIR"
|
||||
chmod 700 "$SSH_DIR"
|
||||
printf '%s\n' "$DEPLOY_SSH_KEY" > "$SSH_DIR/key"
|
||||
chmod 600 "$SSH_DIR/key"
|
||||
scp -i "$SSH_DIR/key" \
|
||||
-P "$DEPLOY_PORT" \
|
||||
-o StrictHostKeyChecking=accept-new \
|
||||
-o UserKnownHostsFile="$SSH_DIR/known_hosts" \
|
||||
"${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/dist.tar.gz" \
|
||||
./dist.tar.gz
|
||||
rm -rf "$SSH_DIR"
|
||||
|
||||
- name: Upload release asset via Gitea API
|
||||
run: |
|
||||
set -e
|
||||
RELEASE_ID=$(jq -r '.release.id' "$GITEA_EVENT_PATH")
|
||||
URL="${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/${RELEASE_ID}/assets?name=dist.tar.gz"
|
||||
curl -fsSL \
|
||||
-X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/gzip" \
|
||||
--data-binary "@dist.tar.gz" \
|
||||
"${URL}"
|
||||
|
||||
deploy-to-server:
|
||||
name: Deploy to onixbyte.cn
|
||||
needs: [build, upload-release-asset]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Extract archive and deploy
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ vars.DEPLOY_HOST }}
|
||||
username: ${{ vars.DEPLOY_USER }}
|
||||
port: ${{ vars.DEPLOY_PORT }}
|
||||
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
script_stop: true
|
||||
script: |
|
||||
set -e
|
||||
DEPLOY_PATH="${{ vars.DEPLOY_PATH }}"
|
||||
mkdir -p "$DEPLOY_PATH"
|
||||
rm -rf "$DEPLOY_PATH"/*
|
||||
tar -xzf /tmp/dist.tar.gz -C "$DEPLOY_PATH" --strip-components=1
|
||||
chown -R caddy:caddy "$DEPLOY_PATH"
|
||||
rm -f /tmp/dist.tar.gz
|
||||
@@ -29,47 +29,6 @@ spring:
|
||||
# Uncomment if the server requires a password.
|
||||
#
|
||||
# password: qwerty
|
||||
mail:
|
||||
# SMTP endpoint.
|
||||
host: smtp.example.com
|
||||
|
||||
# SMTP port.
|
||||
#
|
||||
# 465 uses implicit TLS (SMTPS). If outbound 465 is blocked on the network, switch to 587 and
|
||||
# replace the `ssl` property below with `starttls.enable` instead.
|
||||
port: 465
|
||||
|
||||
# Sending address, which is also used as the `From` address of every dispatched message.
|
||||
#
|
||||
# It MUST belong to a verified sending domain in the Tencent Cloud Email console.
|
||||
username: app@example.com
|
||||
|
||||
# SMTP password of the sending address above, as configured in the Tencent Cloud Email console.
|
||||
password: 123456
|
||||
|
||||
# Encoding applied to the message headers and body.
|
||||
default-encoding: UTF-8
|
||||
|
||||
properties:
|
||||
mail:
|
||||
smtp:
|
||||
# Whether the SMTP server requires authentication.
|
||||
auth: true
|
||||
|
||||
# Implicit TLS, required by port 465.
|
||||
#
|
||||
# When using port 587 + STARTTLS instead, drop this property, then enable STARTTLS in
|
||||
# its place:
|
||||
#
|
||||
# starttls:
|
||||
# enable: true
|
||||
ssl:
|
||||
enable: true
|
||||
|
||||
# Fail fast instead of hanging when the SMTP server is unreachable.
|
||||
connectiontimeout: 10000
|
||||
timeout: 10000
|
||||
writetimeout: 10000
|
||||
|
||||
logging:
|
||||
pattern:
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package com.onixbyte.ahsarahguide.config;
|
||||
|
||||
import com.onixbyte.ahsarahguide.shared.JacksonRedisSerialiser;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
||||
import org.springframework.data.redis.cache.RedisCacheManager;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
@@ -41,24 +39,21 @@ import java.time.Duration;
|
||||
public class CacheConfig {
|
||||
|
||||
/**
|
||||
* Creates the application's primary cache manager, backed by Redis.
|
||||
* Creates a custom Redis cache manager with JSON serialisation support.
|
||||
* <p>
|
||||
* This manager is marked {@link Primary}, so it is the one selected for any
|
||||
* {@code @Cacheable} or {@code @CacheEvict} annotation that does not name a manager
|
||||
* explicitly, as well as for any {@link CacheManager} that is injected by type.
|
||||
* <p>
|
||||
* Cache entries are keyed using string serialisation and values are stored as JSON via
|
||||
* {@link JacksonRedisSerialiser}. Every entry expires after two hours, making this manager
|
||||
* suitable for data that is expensive to compute but not required to survive for long.
|
||||
* This method configures a {@link RedisCacheManager} that uses string serialisation for cache
|
||||
* keys and {@link GenericJackson2JsonRedisSerializer} for cache values. This setup ensures that
|
||||
* complex objects can be stored and retrieved from Redis cache whilst maintaining readability
|
||||
* and compatibility with JSON-based systems.
|
||||
*
|
||||
* @param connectionFactory the Redis connection factory used to establish connections
|
||||
* @return a fully configured {@link CacheManager} with a two-hour default entry TTL
|
||||
* @see #longTermCacheManager(RedisConnectionFactory)
|
||||
* @return a configured {@link RedisCacheManager} with custom serialisation settings
|
||||
* @see RedisCacheManager
|
||||
* @see GenericJackson2JsonRedisSerializer
|
||||
* @see RedisSerializationContext
|
||||
*/
|
||||
@Primary
|
||||
@Bean
|
||||
public CacheManager defaultCacheManager(
|
||||
public RedisCacheManager cacheManager(
|
||||
RedisConnectionFactory connectionFactory
|
||||
) {
|
||||
var _keySerializer = RedisSerializer.string();
|
||||
@@ -68,40 +63,7 @@ public class CacheConfig {
|
||||
.fromSerializer(_keySerializer))
|
||||
.serializeValuesWith(RedisSerializationContext.SerializationPair
|
||||
.fromSerializer(JacksonRedisSerialiser.INSTANCE))
|
||||
.entryTtl(Duration.ofHours(2L));
|
||||
|
||||
return RedisCacheManager.RedisCacheManagerBuilder
|
||||
.fromConnectionFactory(connectionFactory)
|
||||
.cacheDefaults(cacheConfiguration)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a secondary cache manager for data that must be retained for longer.
|
||||
* <p>
|
||||
* This manager is not {@link Primary}; it must be requested by name wherever it is needed,
|
||||
* as in {@code @Cacheable(cacheManager = "longTermCacheManager")}.
|
||||
* <p>
|
||||
* Serialisation is configured identically to {@link #defaultCacheManager}, but entries
|
||||
* expire after one day rather than two hours.
|
||||
*
|
||||
* @param connectionFactory the Redis connection factory used to establish connections
|
||||
* @return a fully configured {@link CacheManager} with a one-day default entry TTL
|
||||
* @see #defaultCacheManager(RedisConnectionFactory)
|
||||
* @see RedisCacheManager
|
||||
*/
|
||||
@Bean
|
||||
public CacheManager longTermCacheManager(
|
||||
RedisConnectionFactory connectionFactory
|
||||
) {
|
||||
var _keySerializer = RedisSerializer.string();
|
||||
|
||||
var cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
|
||||
.serializeKeysWith(RedisSerializationContext.SerializationPair
|
||||
.fromSerializer(_keySerializer))
|
||||
.serializeValuesWith(RedisSerializationContext.SerializationPair
|
||||
.fromSerializer(JacksonRedisSerialiser.INSTANCE))
|
||||
.entryTtl(Duration.ofDays(1L));
|
||||
.entryTtl(Duration.ofMinutes(90L));
|
||||
|
||||
return RedisCacheManager.RedisCacheManagerBuilder
|
||||
.fromConnectionFactory(connectionFactory)
|
||||
|
||||
@@ -4,15 +4,16 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||
import com.onixbyte.ahsarahguide.domain.dto.DailyPasswordResponse;
|
||||
import com.onixbyte.ahsarahguide.exeption.InternalServerErrorException;
|
||||
import com.onixbyte.ahsarahguide.shared.JacksonModules;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
@@ -23,13 +24,15 @@ import java.util.Objects;
|
||||
@Component
|
||||
public class DailyPasswordManager {
|
||||
|
||||
private static final String CACHE_NAME = "daily-password";
|
||||
private static final String CACHE_KEY_PREFIX = "daily-password:";
|
||||
|
||||
private final RestClient restClient;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Autowired
|
||||
public DailyPasswordManager(
|
||||
RestClient.Builder restClientBuilder
|
||||
RestClient.Builder restClientBuilder,
|
||||
RedisTemplate<String, Object> redisTemplate
|
||||
) {
|
||||
var snakeCaseMapper = new ObjectMapper();
|
||||
snakeCaseMapper.setPropertyNamingStrategy(
|
||||
@@ -47,23 +50,22 @@ public class DailyPasswordManager {
|
||||
new MappingJackson2HttpMessageConverter(snakeCaseMapper));
|
||||
})
|
||||
.build();
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the daily password from cache or generates a new one.
|
||||
* @return the daily password response
|
||||
*/
|
||||
@Cacheable(
|
||||
cacheNames = CACHE_NAME,
|
||||
key = "T(java.time.LocalDate).now().toString()",
|
||||
sync = true,
|
||||
unless = "#result == null",
|
||||
cacheManager = "longTermCacheManager"
|
||||
)
|
||||
public DailyPasswordResponse getDailyPassword() {
|
||||
DailyPasswordResponse response;
|
||||
try {
|
||||
response = restClient.get()
|
||||
var key = CACHE_KEY_PREFIX + LocalDate.now();
|
||||
|
||||
var cached = redisTemplate.opsForValue().get(key);
|
||||
if (cached != null) {
|
||||
return (DailyPasswordResponse) cached;
|
||||
}
|
||||
|
||||
var response = restClient.get()
|
||||
.uri((uriBuilder) -> uriBuilder
|
||||
.path("/sjzmm")
|
||||
.queryParam("ckey", "")
|
||||
@@ -71,15 +73,12 @@ public class DailyPasswordManager {
|
||||
.build())
|
||||
.retrieve()
|
||||
.body(DailyPasswordResponse.class);
|
||||
} catch (Exception e) {
|
||||
// Catch network errors, timeouts, or 4xx/5xx responses from the upstream service
|
||||
throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "无法获取每日密码数据。", e);
|
||||
}
|
||||
|
||||
if (Objects.isNull(response)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "无法获取每日密码数据。");
|
||||
throw new InternalServerErrorException("暂无每日密码数据。");
|
||||
}
|
||||
|
||||
redisTemplate.opsForValue().set(key, response, Duration.ofDays(1L));
|
||||
return response;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user