@ -0,0 +1,32 @@
|
||||
name: Backend CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, TEST ]
|
||||
pull_request:
|
||||
branches: [ main, TEST ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
cache: 'maven'
|
||||
|
||||
- name: Build with Maven
|
||||
working-directory: backend
|
||||
run: mvn clean package
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: studyingspace-backend
|
||||
path: backend/target/*.jar
|
||||
@ -0,0 +1,34 @@
|
||||
name: Build and Push Backend to Docker Hub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ,TEST]
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ secrets.DOCKER_HUB_USERNAME }}/studyingspace-backend
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./backend
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@ -0,0 +1,53 @@
|
||||
name: Build and Deploy to Server
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build backend image
|
||||
run: |
|
||||
cd backend
|
||||
docker build -t hetianci/studyingspace-backend:TEST .
|
||||
|
||||
- name: Build frontend image
|
||||
run: |
|
||||
cd frontend
|
||||
docker build -t hetianci/studyingspace-frontend:TEST .
|
||||
|
||||
- name: Save backend image to tar
|
||||
run: |
|
||||
docker save -o studyingspace-backend.tar hetianci/studyingspace-backend:TEST
|
||||
|
||||
- name: Save frontend image to tar
|
||||
run: |
|
||||
docker save -o studyingspace-frontend.tar hetianci/studyingspace-frontend:TEST
|
||||
|
||||
- name: Copy images to server
|
||||
uses: appleboy/scp-action@v0.1.4
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USER }}
|
||||
key: ${{ secrets.SERVER_SSH_PRIVATE_KEY }}
|
||||
source: "studyingspace-backend.tar,studyingspace-frontend.tar"
|
||||
target: "my_project/StudyingSpace/"
|
||||
|
||||
- name: Deploy to server
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USER }}
|
||||
key: ${{ secrets.SERVER_SSH_PRIVATE_KEY }}
|
||||
script: |
|
||||
cd my_project/StudyingSpace
|
||||
git pull
|
||||
docker load -i studyingspace-backend.tar
|
||||
docker load -i studyingspace-frontend.tar
|
||||
docker compose up -d
|
||||
rm -f studyingspace-backend.tar studyingspace-frontend.tar
|
||||
@ -0,0 +1,27 @@
|
||||
name: Frontend CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ,TEST]
|
||||
pull_request:
|
||||
branches: [ main ,TEST]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./frontend
|
||||
run: npm install
|
||||
|
||||
- name: Build
|
||||
working-directory: ./frontend
|
||||
run: npm run build
|
||||
@ -0,0 +1,34 @@
|
||||
name: Build and Push Frontend to Docker Hub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ,TEST]
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ secrets.DOCKER_HUB_USERNAME }}/studyingspace-frontend
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./frontend
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@ -0,0 +1,19 @@
|
||||
FROM maven:3.9.6-eclipse-temurin-21-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY pom.xml .
|
||||
RUN mvn dependency:go-offline -B
|
||||
|
||||
COPY src ./src
|
||||
RUN mvn clean package -DskipTests
|
||||
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/target/*.jar app.jar
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
@ -0,0 +1,111 @@
|
||||
package com.studyingspace.controller;
|
||||
|
||||
import com.studyingspace.common.Result;
|
||||
import com.studyingspace.entity.CheckIn;
|
||||
import com.studyingspace.entity.User;
|
||||
import com.studyingspace.repository.CheckInRepository;
|
||||
import com.studyingspace.repository.UserRepository;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/checkin")
|
||||
@Tag(name = "每日打卡", description = "每日学习打卡功能")
|
||||
@RequiredArgsConstructor
|
||||
public class CheckInController {
|
||||
|
||||
private final CheckInRepository checkInRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "获取打卡记录", description = "获取当前用户的所有打卡记录,按时间倒序排列")
|
||||
public Result<List<CheckIn>> getCheckInList(Authentication authentication) {
|
||||
Long userId = getCurrentUserId(authentication);
|
||||
List<CheckIn> checkInList = checkInRepository.findByUserIdOrderByCheckinDateDesc(userId);
|
||||
return Result.success(checkInList);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "每日打卡", description = "今天打卡学习")
|
||||
public Result<String> dailyCheckIn(
|
||||
Authentication authentication,
|
||||
@RequestParam(required = false) String remark
|
||||
) {
|
||||
Long userId = getCurrentUserId(authentication);
|
||||
LocalDate today = LocalDate.now();
|
||||
|
||||
if (checkInRepository.findByUserIdAndCheckinDate(userId, today).isPresent()) {
|
||||
return Result.error("今天已经打过卡了");
|
||||
}
|
||||
|
||||
CheckIn checkIn = new CheckIn();
|
||||
checkIn.setUserId(userId);
|
||||
checkIn.setCheckinDate(today);
|
||||
checkIn.setRemark(remark);
|
||||
checkInRepository.save(checkIn);
|
||||
|
||||
return Result.success("打卡成功");
|
||||
}
|
||||
|
||||
@GetMapping("/calendar")
|
||||
@Operation(summary = "获取打卡日历数据", description = "获取当前用户所有打卡日期,用于日历展示")
|
||||
public Result<List<String>> getCheckinCalendar(Authentication authentication) {
|
||||
Long userId = getCurrentUserId(authentication);
|
||||
List<LocalDate> dates = checkInRepository.findAllCheckinDatesByUserId(userId);
|
||||
List<String> result = dates.stream()
|
||||
.map(LocalDate::toString)
|
||||
.toList();
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@GetMapping("/stats")
|
||||
@Operation(summary = "获取打卡统计", description = "获取连续打卡天数、总打卡天数等统计信息")
|
||||
public Result<Map<String, Object>> getStats(Authentication authentication) {
|
||||
Long userId = getCurrentUserId(authentication);
|
||||
long totalDays = checkInRepository.countByUserId(userId);
|
||||
List<CheckIn> checkIns = checkInRepository.findByUserIdOrderByCheckinDateDesc(userId);
|
||||
|
||||
int consecutiveDays = calculateConsecutiveDays(checkIns);
|
||||
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
stats.put("totalDays", totalDays);
|
||||
stats.put("consecutiveDays", consecutiveDays);
|
||||
|
||||
return Result.success(stats);
|
||||
}
|
||||
|
||||
private Long getCurrentUserId(Authentication authentication) {
|
||||
String username = authentication.getName();
|
||||
Optional<User> userOpt = userRepository.findByUsername(username);
|
||||
if (userOpt.isPresent()) {
|
||||
return userOpt.get().getId();
|
||||
}
|
||||
return 1L;
|
||||
}
|
||||
|
||||
private int calculateConsecutiveDays(List<CheckIn> checkIns) {
|
||||
if (checkIns.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int consecutive = 0;
|
||||
LocalDate current = LocalDate.now();
|
||||
|
||||
for (CheckIn checkIn : checkIns) {
|
||||
if (checkIn.getCheckinDate().equals(current)) {
|
||||
consecutive++;
|
||||
current = current.minusDays(1);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return consecutive;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,166 @@
|
||||
package com.studyingspace.controller;
|
||||
|
||||
import com.studyingspace.common.Result;
|
||||
import com.studyingspace.entity.Friend;
|
||||
import com.studyingspace.entity.FriendRequest;
|
||||
import com.studyingspace.entity.User;
|
||||
import com.studyingspace.repository.CheckInRepository;
|
||||
import com.studyingspace.repository.FriendRepository;
|
||||
import com.studyingspace.repository.FriendRequestRepository;
|
||||
import com.studyingspace.repository.UserRepository;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/friend")
|
||||
@Tag(name = "好友关系", description = "好友申请和好友列表功能")
|
||||
@RequiredArgsConstructor
|
||||
public class FriendController {
|
||||
|
||||
private final FriendRepository friendRepository;
|
||||
private final FriendRequestRepository friendRequestRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final CheckInRepository checkInRepository;
|
||||
|
||||
@PostMapping("/request")
|
||||
@Operation(summary = "发送好友申请", description = "向指定用户发送好友申请")
|
||||
public Result<String> sendFriendRequest(
|
||||
Authentication authentication,
|
||||
@RequestParam String username
|
||||
) {
|
||||
Long fromUserId = getCurrentUserId(authentication);
|
||||
|
||||
Optional<User> toUserOpt = userRepository.findByUsername(username);
|
||||
if (toUserOpt.isEmpty()) {
|
||||
return Result.error("用户不存在");
|
||||
}
|
||||
|
||||
User toUser = toUserOpt.get();
|
||||
Long toUserId = toUser.getId();
|
||||
if (toUserId.equals(fromUserId)) {
|
||||
return Result.error("不能添加自己为好友");
|
||||
}
|
||||
|
||||
if (friendRepository.existsByUserIdAndFriendId(fromUserId, toUserId)) {
|
||||
return Result.error("该用户已经是你的好友");
|
||||
}
|
||||
|
||||
if (friendRequestRepository.existsByFromUserIdAndToUserIdAndStatus(
|
||||
fromUserId, toUserId, FriendRequest.RequestStatus.PENDING)) {
|
||||
return Result.error("已经发送过申请,请等待对方处理");
|
||||
}
|
||||
|
||||
FriendRequest request = new FriendRequest();
|
||||
request.setFromUserId(fromUserId);
|
||||
request.setToUserId(toUserId);
|
||||
friendRequestRepository.save(request);
|
||||
|
||||
return Result.success("好友申请已发送");
|
||||
}
|
||||
|
||||
@PostMapping("/handle")
|
||||
@Operation(summary = "处理好友申请", description = "同意或拒绝好友申请")
|
||||
public Result<String> handleFriendRequest(
|
||||
Authentication authentication,
|
||||
@RequestParam Long requestId,
|
||||
@RequestParam boolean accept
|
||||
) {
|
||||
Long toUserId = getCurrentUserId(authentication);
|
||||
|
||||
Optional<FriendRequest> requestOpt = friendRequestRepository.findByIdAndToUserId(requestId, toUserId);
|
||||
if (requestOpt.isEmpty()) {
|
||||
return Result.error("申请不存在");
|
||||
}
|
||||
|
||||
FriendRequest request = requestOpt.get();
|
||||
if (request.getStatus() != FriendRequest.RequestStatus.PENDING) {
|
||||
return Result.error("该申请已经处理过了");
|
||||
}
|
||||
|
||||
request.setHandleTime(LocalDateTime.now());
|
||||
if (accept) {
|
||||
request.setStatus(FriendRequest.RequestStatus.ACCEPTED);
|
||||
friendRequestRepository.save(request);
|
||||
|
||||
Friend friend1 = new Friend();
|
||||
friend1.setUserId(toUserId);
|
||||
friend1.setFriendId(request.getFromUserId());
|
||||
friendRepository.save(friend1);
|
||||
|
||||
Friend friend2 = new Friend();
|
||||
friend2.setUserId(request.getFromUserId());
|
||||
friend2.setFriendId(toUserId);
|
||||
friendRepository.save(friend2);
|
||||
|
||||
return Result.success("已同意好友申请");
|
||||
} else {
|
||||
request.setStatus(FriendRequest.RequestStatus.REJECTED);
|
||||
friendRequestRepository.save(request);
|
||||
return Result.success("已拒绝好友申请");
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/requests/pending")
|
||||
@Operation(summary = "获取待处理的好友申请", description = "获取当前用户收到的待处理好友申请列表")
|
||||
public Result<List<Map<String, Object>>> getPendingRequests(Authentication authentication) {
|
||||
Long userId = getCurrentUserId(authentication);
|
||||
List<FriendRequest> requests = friendRequestRepository.findByToUserIdAndStatus(
|
||||
userId, FriendRequest.RequestStatus.PENDING);
|
||||
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (FriendRequest request : requests) {
|
||||
User fromUser = userRepository.findById(request.getFromUserId()).orElse(null);
|
||||
if (fromUser != null) {
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("id", request.getId());
|
||||
item.put("fromUserId", request.getFromUserId());
|
||||
item.put("fromUsername", fromUser.getUsername());
|
||||
item.put("createTime", request.getCreateTime().toString());
|
||||
result.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "获取好友列表", description = "获取当前用户的所有好友,包含好友打卡天数统计")
|
||||
public Result<List<Map<String, Object>>> getFriendList(Authentication authentication) {
|
||||
Long userId = getCurrentUserId(authentication);
|
||||
List<Friend> friends = friendRepository.findByUserId(userId);
|
||||
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Friend friend : friends) {
|
||||
User friendUser = userRepository.findById(friend.getFriendId()).orElse(null);
|
||||
if (friendUser != null) {
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("id", friendUser.getId());
|
||||
item.put("username", friendUser.getUsername());
|
||||
long totalDays = checkInRepository.countByUserId(friendUser.getId());
|
||||
item.put("totalCheckinDays", totalDays);
|
||||
result.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
private Long getCurrentUserId(Authentication authentication) {
|
||||
String username = authentication.getName();
|
||||
Optional<User> userOpt = userRepository.findByUsername(username);
|
||||
if (userOpt.isPresent()) {
|
||||
return userOpt.get().getId();
|
||||
}
|
||||
return 1L;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package com.studyingspace.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "checkin")
|
||||
public class CheckIn {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDate checkinDate;
|
||||
|
||||
private String remark;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createTime = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.studyingspace.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "friends")
|
||||
public class Friend {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long friendId;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createTime = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package com.studyingspace.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "friend_requests")
|
||||
public class FriendRequest {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long fromUserId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long toUserId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
private RequestStatus status;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
private LocalDateTime handleTime;
|
||||
|
||||
public enum RequestStatus {
|
||||
PENDING,
|
||||
ACCEPTED,
|
||||
REJECTED
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createTime = LocalDateTime.now();
|
||||
status = RequestStatus.PENDING;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package com.studyingspace.repository;
|
||||
|
||||
import com.studyingspace.entity.CheckIn;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface CheckInRepository extends JpaRepository<CheckIn, Long> {
|
||||
|
||||
Optional<CheckIn> findByUserIdAndCheckinDate(Long userId, LocalDate checkinDate);
|
||||
|
||||
List<CheckIn> findByUserIdOrderByCheckinDateDesc(Long userId);
|
||||
|
||||
@Query("SELECT c.checkinDate FROM CheckIn c WHERE c.userId = :userId")
|
||||
List<LocalDate> findAllCheckinDatesByUserId(@Param("userId") Long userId);
|
||||
|
||||
long countByUserId(Long userId);
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package com.studyingspace.repository;
|
||||
|
||||
import com.studyingspace.entity.Friend;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface FriendRepository extends JpaRepository<Friend, Long> {
|
||||
List<Friend> findByUserId(Long userId);
|
||||
|
||||
Optional<Friend> findByUserIdAndFriendId(Long userId, Long friendId);
|
||||
|
||||
@Query("SELECT f.friendId FROM Friend f WHERE f.userId = :userId")
|
||||
List<Long> findFriendIdsByUserId(@Param("userId") Long userId);
|
||||
|
||||
boolean existsByUserIdAndFriendId(Long userId, Long friendId);
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.studyingspace.repository;
|
||||
|
||||
import com.studyingspace.entity.FriendRequest;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface FriendRequestRepository extends JpaRepository<FriendRequest, Long> {
|
||||
List<FriendRequest> findByToUserIdAndStatus(Long toUserId, FriendRequest.RequestStatus status);
|
||||
|
||||
List<FriendRequest> findByFromUserIdAndStatus(Long fromUserId, FriendRequest.RequestStatus status);
|
||||
|
||||
boolean existsByFromUserIdAndToUserIdAndStatus(Long fromUserId, Long toUserId, FriendRequest.RequestStatus status);
|
||||
|
||||
Optional<FriendRequest> findByIdAndToUserId(Long id, Long toUserId);
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
package com.studyingspace.security;
|
||||
|
||||
import com.studyingspace.utils.JwtUtils;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetails;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@Component
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
|
||||
|
||||
@Autowired
|
||||
private JwtUtils jwtUtils;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
try {
|
||||
String jwt = getJwtFromRequest(request);
|
||||
|
||||
if (jwt != null && !jwt.isEmpty() && jwtUtils.validateToken(jwt)) {
|
||||
String username = jwtUtils.getUsernameFromToken(jwt);
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(username, null, null);
|
||||
authentication.setDetails(new WebAuthenticationDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Cannot set user authentication: {}", e.getMessage());
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private String getJwtFromRequest(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader("Authorization");
|
||||
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
|
||||
return bearerToken.substring(7);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: studyingspace-postgres
|
||||
environment:
|
||||
POSTGRES_DB: studyingspace
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: 123456
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
restart: unless-stopped
|
||||
|
||||
backend:
|
||||
image: hetianci/studyingspace-backend:TEST
|
||||
container_name: studyingspace-backend
|
||||
ports:
|
||||
- "8081:8080"
|
||||
depends_on:
|
||||
- postgres
|
||||
environment:
|
||||
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/studyingspace
|
||||
SPRING_DATASOURCE_USERNAME: postgres
|
||||
SPRING_DATASOURCE_PASSWORD: 123456
|
||||
restart: unless-stopped
|
||||
|
||||
frontend:
|
||||
image: hetianci/studyingspace-frontend:TEST
|
||||
container_name: studyingspace-frontend
|
||||
ports:
|
||||
- "8000:80"
|
||||
depends_on:
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
@ -0,0 +1,18 @@
|
||||
FROM node:21-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@ -0,0 +1,18 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://backend:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getCheckinList() {
|
||||
return request({
|
||||
url: '/checkin',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function dailyCheckin(remark) {
|
||||
return request({
|
||||
url: '/checkin',
|
||||
method: 'post',
|
||||
params: { remark }
|
||||
})
|
||||
}
|
||||
|
||||
export function getCheckinCalendar() {
|
||||
return request({
|
||||
url: '/checkin/calendar',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getCheckinStats() {
|
||||
return request({
|
||||
url: '/checkin/stats',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function sendFriendRequest(username) {
|
||||
return request({
|
||||
url: '/friend/request',
|
||||
method: 'post',
|
||||
params: { username }
|
||||
})
|
||||
}
|
||||
|
||||
export function handleFriendRequest(requestId, accept) {
|
||||
return request({
|
||||
url: '/friend/handle',
|
||||
method: 'post',
|
||||
params: { requestId, accept }
|
||||
})
|
||||
}
|
||||
|
||||
export function getPendingFriendRequests() {
|
||||
return request({
|
||||
url: '/friend/requests/pending',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getFriendList() {
|
||||
return request({
|
||||
url: '/friend/list',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
Loading…
Reference in new issue