11 Commits
main ... v0.0.1

Author SHA1 Message Date
39e7c72527 chore: update and move to the new server
All checks were successful
Docker Image CI / build (push) Successful in 9m1s
2026-03-06 10:34:38 +08:00
ccaddf7df7 chore: update and move to the new server 2026-03-06 10:15:06 +08:00
merlin
ff49b4f508 fix: ci 2026-02-04 18:05:20 +08:00
merlin
38db950731 feat: add upload img logic 2025-11-06 11:09:45 +08:00
merlin
c3787aab2e fix(Dockerfile): remove debug tools 2025-11-05 16:31:43 +08:00
merlin
b65174e065 fix(Dockerfile): add debug tools 2025-11-05 15:11:41 +08:00
merlin
ecdb6537c5 fix: Dockerfile build order 2025-11-05 09:38:45 +08:00
merlin
f1968590a9 feat: add dicker ignore; add search logic 2025-11-05 09:36:08 +08:00
merlin
6d6fe921fc fix(Dockerfile): fix command mkdir 2025-11-02 11:34:35 +08:00
merlin
1a7fb6136b fix(Dockerfile): fix command mkdir 2025-11-02 11:32:55 +08:00
merlin
093c2cfe1d fix(ci): fix builder image name 2025-11-02 01:15:02 +08:00
14 changed files with 190 additions and 158 deletions

1
.dockerignore Normal file
View File

@@ -0,0 +1 @@
src/main/resources/application.yml

View File

@@ -1,35 +1,35 @@
name: Docker Image CI name: Docker Image CI
on: on:
push: push:
branches: branches:
- dev - dev
jobs: jobs:
build: build:
runs-on: gitea-official-runner runs-on: gitea-runner-group-blog
container: container:
image: harbor.merlin.xin/release/merlin/builder:v0.0.1 image: ${{ vars.HARBOR_URL }}/candlelight/action_builder:v0.0.1
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: docker login - name: docker login
env: env:
HARBOR_USERNAME: ${{ secrets.HARBOR_ROBOT }} HARBOR_USERNAME: ${{ secrets.HARBOR_ROBOT }}
HARBOR_PASSWORD: ${{ secrets.HARBOR_ROBOT_SECRET }} HARBOR_PASSWORD: ${{ secrets.HARBOR_ROBOT_SECRET }}
HARBOR_URL: ${{ vars.HARBOR_URL }} HARBOR_URL: ${{ vars.HARBOR_URL }}
run: docker login ${HARBOR_URL} -u ${HARBOR_USERNAME} -p ${HARBOR_PASSWORD} run: docker login ${HARBOR_URL} -u ${HARBOR_USERNAME} -p ${HARBOR_PASSWORD}
- name: Build and push Docker images - name: Build and push Docker images
env: env:
HARBOR_URL: ${{ vars.HARBOR_URL }} HARBOR_URL: ${{ vars.HARBOR_URL }}
TAG: ${{ github.sha }} TAG: ${{ github.sha }}
REPOSITORY: ${{ github.repository }} REPOSITORY: ${{ github.repository }}
run: | run: |
ROOT_DIR=$(pwd) ROOT_DIR=$(pwd)
IMAGE_NAME="${HARBOR_URL}/testing/$REPOSITORY:${TAG}" IMAGE_NAME="${HARBOR_URL}/testing/$REPOSITORY:${TAG}"
echo "Building image: ${IMAGE_NAME}" echo "Building image: ${IMAGE_NAME}"
docker build -t ${IMAGE_NAME} . docker build -t ${IMAGE_NAME} .
echo "Pushing image: ${IMAGE_NAME}" echo "Pushing image: ${IMAGE_NAME}"
docker push ${IMAGE_NAME} docker push ${IMAGE_NAME}
echo "Successfully pushed: ${IMAGE_NAME}" echo "Successfully pushed: ${IMAGE_NAME}"
docker rmi ${IMAGE_NAME} docker rmi ${IMAGE_NAME}
echo "cleaned up local image" echo "cleaned up local image"

View File

@@ -1,34 +1,34 @@
name: Docker Image CI name: Docker Image CI
on: on:
push: push:
tags: tags:
- '*' - '*'
jobs: jobs:
build: build:
runs-on: gitea-official-runner runs-on: gitea-runner-group-blog
container: container:
image: harbor.merlin.xin/release/merlin/builder:v0.0.1 image: ${{ vars.HARBOR_URL }}/candlelight/action_builder:v0.0.1
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: docker login - name: docker login
env: env:
HARBOR_USERNAME: ${{ secrets.HARBOR_ROBOT }} HARBOR_USERNAME: ${{ secrets.HARBOR_ROBOT }}
HARBOR_PASSWORD: ${{ secrets.HARBOR_ROBOT_SECRET }} HARBOR_PASSWORD: ${{ secrets.HARBOR_ROBOT_SECRET }}
HARBOR_URL: ${{ vars.HARBOR_URL }} HARBOR_URL: ${{ vars.HARBOR_URL }}
run: docker login ${HARBOR_URL} -u ${HARBOR_USERNAME} -p ${HARBOR_PASSWORD} run: docker login ${HARBOR_URL} -u ${HARBOR_USERNAME} -p ${HARBOR_PASSWORD}
- name: Build and push Docker images - name: Build and push Docker images
env: env:
HARBOR_URL: ${{ vars.HARBOR_URL }} HARBOR_URL: ${{ vars.HARBOR_URL }}
REPOSITORY: ${{ github.repository }} REPOSITORY: ${{ github.repository }}
run: | run: |
ROOT_DIR=$(pwd) ROOT_DIR=$(pwd)
IMAGE_NAME="${HARBOR_URL}/release/$REPOSITORY:$GITHUB_REF_NAME" IMAGE_NAME="${HARBOR_URL}/$REPOSITORY:$GITHUB_REF_NAME"
echo "Building image: ${IMAGE_NAME}" echo "Building image: ${IMAGE_NAME}"
docker build -t ${IMAGE_NAME} . docker build -t ${IMAGE_NAME} .
echo "Pushing image: ${IMAGE_NAME}" echo "Pushing image: ${IMAGE_NAME}"
docker push ${IMAGE_NAME} docker push ${IMAGE_NAME}
echo "Successfully pushed: ${IMAGE_NAME}" echo "Successfully pushed: ${IMAGE_NAME}"
docker rmi ${IMAGE_NAME} docker rmi ${IMAGE_NAME}
echo "cleaned up local image" echo "cleaned up local image"

View File

@@ -1,5 +1,5 @@
# ===== build stage ===== # ===== build stage =====
FROM harbor.merlin.xin/mirrors/docker.io/library/maven:3.9.6-eclipse-temurin-17 AS builder FROM registry.merlin.xin/library/maven:4.0.0-rc-5-eclipse-temurin-17 AS builder
WORKDIR /app WORKDIR /app
COPY pom.xml . COPY pom.xml .
@@ -10,15 +10,25 @@ RUN --mount=type=cache,target=/root/.m2 mvn -B -q package -DskipTests
# ===== runtime stage ===== # ===== runtime stage =====
FROM harbor.merlin.xin/mirrors/docker.io/library/eclipse-temurin:17-jre-alpine FROM registry.merlin.xin/library/eclipse-temurin:17-jre-alpine
# >>> Install debug tools <<<
#RUN apk update && apk add --no-cache \
# curl \
# bind-tools \
# busybox-extras \
# iproute2 \
# tcpdump \
# net-tools
WORKDIR /app WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar COPY --from=builder /app/target/*.jar app.jar
RUN mkdir uploads/photo
# 非 root 用户运行
RUN addgroup -S spring && adduser -S spring -G spring RUN addgroup -S spring && adduser -S spring -G spring
RUN mkdir -p /app/uploads/photo \
&& chown -R spring:spring /app/uploads
USER spring USER spring
ENTRYPOINT ["java","-jar","/app/app.jar"] ENTRYPOINT ["java","-jar","/app/app.jar","--spring.config.location=file:/app/application.yml"]

View File

@@ -87,6 +87,14 @@
</dependencies> </dependencies>
<build> <build>
<resources>
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>application.yml</exclude>
</excludes>
</resource>
</resources>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>

View File

@@ -30,7 +30,7 @@ public class SecurityConfig {
.requestMatchers( .requestMatchers(
"/login", "/login",
"/register", "/register",
"/test", "/health",
"/code/**", "/code/**",
"/blog/**" "/blog/**"
).permitAll() ).permitAll()

View File

@@ -3,7 +3,9 @@ package xin.merlin.myblog_server.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import xin.merlin.myblog_server.entity.Article; import xin.merlin.myblog_server.entity.Article;
import xin.merlin.myblog_server.entity.Comment; import xin.merlin.myblog_server.entity.Comment;
import xin.merlin.myblog_server.entity.News; import xin.merlin.myblog_server.entity.News;
@@ -15,6 +17,14 @@ import xin.merlin.myblog_server.utils.JwtUtil;
import xin.merlin.myblog_server.utils.RequestBack; import xin.merlin.myblog_server.utils.RequestBack;
import xin.merlin.myblog_server.utils.enums.ResultCode; import xin.merlin.myblog_server.utils.enums.ResultCode;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.UUID;
@RestController @RestController
@RequestMapping("/admin") @RequestMapping("/admin")
public class AdminController { public class AdminController {
@@ -32,6 +42,9 @@ public class AdminController {
@Autowired @Autowired
private CommentServiceImpl commentService; private CommentServiceImpl commentService;
@Value("${upload.dir}")
private String uploadDir;
//编辑,新增,删除新闻 //编辑,新增,删除新闻
@PostMapping("/update/news") @PostMapping("/update/news")
RequestBack editNews(@RequestBody News news, @RequestHeader("Authorization")String token) { RequestBack editNews(@RequestBody News news, @RequestHeader("Authorization")String token) {
@@ -100,4 +113,30 @@ public class AdminController {
commentService.removeById(user.getId()); commentService.removeById(user.getId());
return RequestBack.success(ResultCode.SUCCESS); return RequestBack.success(ResultCode.SUCCESS);
} }
//上传照片
@PostMapping("/upload/img")
RequestBack uploadImg(@RequestHeader("Authorization")String token, @RequestParam("image") MultipartFile file) throws IOException {
if(!jwtUtil.getUAccount(token.substring(7)).equals("admin")) return RequestBack.fail(ResultCode.USER_NOT_FOUND);
if (file == null || file.isEmpty()) {
return RequestBack.fail(ResultCode.NOT_FOUND);
}
String original = file.getOriginalFilename();
String ext = "";
if (original != null && original.contains(".")) {
ext = original.substring(original.lastIndexOf('.'));
}
String filename = UUID.randomUUID().toString() + ext;
Path dirPath = Paths.get(uploadDir);
Files.createDirectories(dirPath);
Path target = dirPath.resolve(filename);
try (InputStream in = file.getInputStream()) {
Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING);
}
return RequestBack.success(ResultCode.SUCCESS, "https://blog.merlin.xin/app/uploads/"+filename);
}
} }

View File

@@ -1,6 +1,9 @@
package xin.merlin.myblog_server.controller; package xin.merlin.myblog_server.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -13,6 +16,8 @@ import xin.merlin.myblog_server.utils.JwtUtil;
import xin.merlin.myblog_server.utils.RequestBack; import xin.merlin.myblog_server.utils.RequestBack;
import xin.merlin.myblog_server.utils.enums.ResultCode; import xin.merlin.myblog_server.utils.enums.ResultCode;
import java.util.List;
@RestController @RestController
@RequestMapping("/blog") @RequestMapping("/blog")
public class BasicController { public class BasicController {
@@ -43,8 +48,24 @@ public class BasicController {
@GetMapping("/get/news") @GetMapping("/get/news")
public RequestBack getNews(@RequestParam Integer current,@RequestParam Integer size) { public RequestBack getNews(@RequestParam Integer current,@RequestParam Integer size) {
Page<News> page = new Page<>(current,size); Page<News> page = new Page<>(current,size);
page.setOrders(List.of(OrderItem.desc("published")));
return RequestBack.success(ResultCode.SUCCESS,newsService.page(page)); return RequestBack.success(ResultCode.SUCCESS,newsService.page(page));
} }
// 搜索新闻
@PostMapping("/search/news")
public RequestBack searchNews(@RequestParam Integer current,
@RequestParam Integer size,
@RequestBody News news) {
Page<News> page = new Page<>(current, size);
QueryWrapper<News> qw = new QueryWrapper<>();
qw.like(news.getTitle() != null, "title", news.getTitle())
.orderByDesc("published");
IPage<News> result = newsService.page(page, qw);
return RequestBack.success(ResultCode.SUCCESS, result);
}
// 获取文章 // 获取文章
@GetMapping("/get/article/{a_id}") @GetMapping("/get/article/{a_id}")
public RequestBack getArticle(@PathVariable Integer a_id) { public RequestBack getArticle(@PathVariable Integer a_id) {
@@ -54,6 +75,7 @@ public class BasicController {
@GetMapping("/get/articles") @GetMapping("/get/articles")
public RequestBack getArticles(@RequestParam Integer current,@RequestParam Integer size) { public RequestBack getArticles(@RequestParam Integer current,@RequestParam Integer size) {
Page<Article> page = new Page<>(current,size); Page<Article> page = new Page<>(current,size);
page.setOrders(List.of(OrderItem.desc("published")));
return RequestBack.success(ResultCode.SUCCESS,articleService.page(page)); return RequestBack.success(ResultCode.SUCCESS,articleService.page(page));
} }
// 发表评论 // 发表评论
@@ -62,5 +84,20 @@ public class BasicController {
commentService.save(comment); commentService.save(comment);
return RequestBack.success(ResultCode.SUCCESS); return RequestBack.success(ResultCode.SUCCESS);
} }
// 搜索新闻
@PostMapping("/search/articles")
public RequestBack searchArticles(@RequestParam Integer current,
@RequestParam Integer size,
@RequestBody Article article) {
Page<Article> page = new Page<>(current, size);
QueryWrapper<Article> qw = new QueryWrapper<>();
qw.like(article.getTitle() != null, "title", article.getTitle())
.orderByDesc("published");
IPage<Article> result = articleService.page(page, qw);
return RequestBack.success(ResultCode.SUCCESS, result);
}
// 联系管理员 // 联系管理员
} }

View File

@@ -1,7 +1,5 @@
package xin.merlin.myblog_server.controller; package xin.merlin.myblog_server.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.github.benmanes.caffeine.cache.Cache;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -17,7 +15,6 @@ import xin.merlin.myblog_server.service.LoginService;
import xin.merlin.myblog_server.service.impl.UserServiceImpl; import xin.merlin.myblog_server.service.impl.UserServiceImpl;
import xin.merlin.myblog_server.utils.JwtUtil; import xin.merlin.myblog_server.utils.JwtUtil;
import xin.merlin.myblog_server.utils.RequestBack; import xin.merlin.myblog_server.utils.RequestBack;
import xin.merlin.myblog_server.utils.SHA256Util;
import xin.merlin.myblog_server.utils.enums.ResultCode; import xin.merlin.myblog_server.utils.enums.ResultCode;
import java.util.Map; import java.util.Map;
@@ -31,8 +28,6 @@ public class LoginController {
@Autowired @Autowired
private UserServiceImpl userServiceImpl; private UserServiceImpl userServiceImpl;
@Autowired
private SHA256Util sha256Util;
@Autowired @Autowired
private JwtUtil jwtUtil; private JwtUtil jwtUtil;

View File

@@ -1,19 +1,12 @@
package xin.merlin.myblog_server.controller; package xin.merlin.myblog_server.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import xin.merlin.myblog_server.utils.JwtUtil;
@RestController @RestController
public class TestController { public class TestController {
@GetMapping("/health")
@Autowired
private JwtUtil jwtUtil;
@GetMapping("/test")
public String test() { public String test() {
String token = jwtUtil.generateToken("1223",12); return "ok";
return token;
} }
} }

View File

@@ -3,7 +3,9 @@ package xin.merlin.myblog_server.utils;
import io.jsonwebtoken.*; import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import xin.merlin.myblog_server.config.JwtProperties; import xin.merlin.myblog_server.config.JwtProperties;
@@ -12,6 +14,8 @@ import java.nio.charset.StandardCharsets;
import java.util.Date; import java.util.Date;
import java.util.UUID; import java.util.UUID;
@Slf4j
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class JwtUtil { public class JwtUtil {
@@ -34,6 +38,16 @@ public class JwtUtil {
} }
} }
@PreDestroy
public void destroy() {
log.info("JWT 组件正在关闭...");
// 如有线程池、定时任务、IO 连接,在这里 shutdown/close
// 本例仅把引用置空,帮助 GC可选
this.jwtParser = null;
this.key = null;
log.info("JWT 组件已关闭");
}
/** /**
* 生成 JWT Token * 生成 JWT Token
*/ */

View File

@@ -1,53 +0,0 @@
package xin.merlin.myblog_server.utils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@Component
public class SHA256Util {
/**
* 对输入字符串进行SHA-256加密
* @param input 输入字符串
* @return 加密后的十六进制字符串
*/
public String encryptSHA256(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] encodedhash = digest.digest(input.getBytes());
return bytesToHex(encodedhash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 algorithm not found", e);
}
}
/**
* 将字节数组转换为十六进制字符串
* @param hash 字节数组
* @return 十六进制字符串
*/
private String bytesToHex(byte[] hash) {
StringBuilder hexString = new StringBuilder(2 * hash.length);
for (int i = 0; i < hash.length; i++) {
String hex = Integer.toHexString(0xff & hash[i]);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
/**
* 使用用户ID生成盐值并对密码进行加密
* @param password 用户输入的密码
* @return 加密后的密码哈希值
*/
public String encryptPassword(String password) {
// 将盐值与密码拼接后进行SHA-256加密
return encryptSHA256( password);
}
}

View File

@@ -1,11 +1,5 @@
server: server:
port: 8080 port: 8081
# port: 8443
# ssl:
# key-store: classpath:merlin.xin.pfx
# key-store-password: 7p7vcfmu
# key-store-type: PKCS12
# address: 0.0.0.0
jwt: jwt:
secret: CkmEXxVBNBsMUo4VNhDcH0YBhA1O4zSkQgSM243YzDY= secret: CkmEXxVBNBsMUo4VNhDcH0YBhA1O4zSkQgSM243YzDY=
@@ -13,6 +7,8 @@ jwt:
subject: Interesting subject: Interesting
expire: 604800 expire: 604800
upload:
dir: c:/uploads
spring: spring:
servlet: servlet:
@@ -27,14 +23,6 @@ spring:
jackson: jackson:
time-zone: Asia/Shanghai time-zone: Asia/Shanghai
date-format: yyyy-MM-dd HH:mm:ss date-format: yyyy-MM-dd HH:mm:ss
# url: jdbc:mysql://8.138.214.149:3306/blog
# username: root
# password: 3604162
# username: root
# password: server2025_xyf_Merlin
# driver-class-name: com.mysql.cj.jdbc.Driver
mail: mail:
protocol: smtps protocol: smtps
port: 465 port: 465

Binary file not shown.