Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 106 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,106 @@
# server
theurl.io server project.
# Linkyou Server

Linkyou 的后端服务端仓库,采用 Maven 多模块 + Spring 生态,包含配置中心、身份服务、消息服务、聚合服务及公共模块。

## 项目结构

```text
server/
├── pom.xml # 根聚合 POM
├── docker-compose.yaml # 本地完整依赖编排
├── docker-compose.dev.yaml # 开发环境简化编排
├── bundle/ # bundle 服务
├── config/ # 配置中心服务
├── framework/ # 框架基础能力
├── identity/ # 身份认证服务
├── message/ # 消息服务
└── shared/ # 公共代码模块
```

## 技术栈

- Java 25
- Spring Boot 4.0.6
- Spring Cloud 2025.1.1
- Spring Cloud Alibaba 2025.1.0.0
- Maven 多模块构建
- PostgreSQL / Redis / MongoDB / RabbitMQ

> 版本来源:根 `pom.xml` 当前配置。

## 模块说明

- `config`:统一配置管理服务。
- `identity`:身份认证与授权相关业务。
- `message`:消息能力相关业务。
- `bundle`:聚合编排或对外统一服务入口。
- `framework`:基础框架和通用能力封装。
- `shared`:跨模块复用的公共代码。

## 本地开发前置条件

建议先准备以下环境:

- JDK 25
- Maven 3.9+
- Docker 与 Docker Compose(用于一键启动依赖)

## 快速开始

### 1) 构建全部模块

```bash
cd /Users/rong/Code/Github/Linkyou/server
mvn clean install
```

### 2) 仅运行测试

```bash
cd /Users/rong/Code/Github/Linkyou/server
mvn test
```

### 3) 启动本地依赖(Docker Compose)

```bash
cd /Users/rong/Code/Github/Linkyou/server
docker compose -f docker-compose.yaml up -d
```

### 4) 停止本地依赖

```bash
cd /Users/rong/Code/Github/Linkyou/server
docker compose -f docker-compose.yaml down
```

## 常用模块命令

在根目录执行(示例以 `identity` 模块为例):

```bash
cd /Users/rong/Code/Github/Linkyou/server
mvn -pl identity -am clean package
```
Comment on lines +52 to +85

## 端口与依赖(基于 compose 文件)

- 配置中心:`8900`
- 身份服务:`8901`
- 消息服务:`8902`
- 聚合服务:`8903`
- PostgreSQL:`5432`
- Redis:`6379`
- MongoDB:`27017`
- RabbitMQ:`5672`(管理端口见 `docker-compose.yaml`)

## 配置说明

- 示例环境变量可参考 `docker-compose.yaml` 与 `docker-compose.dev.yaml`。
- 生产环境请务必替换默认账号密码与第三方 OAuth 密钥。
- 建议通过外部配置中心或安全密钥管理系统注入敏感配置。

## 许可证

本项目使用仓库根目录 `LICENSE` 中声明的许可证。
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package io.theurl.bundle.application.command;

import com.neroyun.mediator.Command;
import lombok.Data;

@SuppressWarnings({"LombokGetterMayBeUsed", "LombokSetterMayBeUsed"})
@Data
public class BundleUpdateCommand implements Command {
private final String vanity;

Expand All @@ -13,32 +14,4 @@ public class BundleUpdateCommand implements Command {
public BundleUpdateCommand(String vanity) {
this.vanity = vanity;
}

public String getVanity() {
return vanity;
}

public String getName() {
return name;
}

public String getDescription() {
return description;
}

public String getImage() {
return image;
}

public void setName(String name) {
this.name = name;
}

public void setDescription(String description) {
this.description = description;
}

public void setImage(String image) {
this.image = image;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package io.theurl.bundle.application.contract;

import io.theurl.bundle.application.dto.BundleCreateDto;
import io.theurl.bundle.application.dto.BundleItemEditDto;
import io.theurl.bundle.application.dto.BundleUpdateDto;
import io.theurl.bundle.application.dto.*;
import io.theurl.framework.application.ApplicationService;

import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

/**
* Application service for managing bundles. Provides methods for creating, updating, and deleting bundles, as well as managing items within bundles.
*/
public interface BundleApplicationService extends ApplicationService {

/**
Expand Down Expand Up @@ -61,4 +64,12 @@ public interface BundleApplicationService extends ApplicationService {
* @return A CompletableFuture that will complete when the item is removed.
*/
CompletableFuture<Void> removeItemAsync(String vanity, long itemId);

CompletableFuture<List<BundleListDto>> searchAsync(Map<String, Object> criteria, int from, int size);

CompletableFuture<Integer> countAsync(Map<String, Object> criteria);

CompletableFuture<List<BundleItemListDto>> searchItemsAsync(String vanity, Map<String, Object> criteria, int from, int size);

CompletableFuture<Integer> countItemsAsync(String vanity, Map<String, Object> criteria);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.theurl.bundle.application.dto;

import lombok.Data;

import java.time.LocalDateTime;

@Data
public class BundleItemListDto {
private long id;
private String url;
private String title;
private String description;
private int order;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public BundleCreateCommandHandler(BundleRepository repository) {

@Override
public CompletableFuture<Void> handleAsync(BundleCreateCommand message, MessageContext context) {
var userId = Long.getLong(Objects.requireNonNull(getRequest()).getUserPrincipal().getName());
var userId = Long.parseLong(Objects.requireNonNull(getRequest()).getUserPrincipal().getName());
var aggregate = Bundle.create(message.getType(), message.getVanity(), message.getName());
if (message.getDescription() != null) {
aggregate.setDescription(message.getDescription());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import io.theurl.bundle.application.command.BundleDeleteCommand;
import io.theurl.bundle.application.command.BundleUpdateCommand;
import io.theurl.bundle.application.contract.BundleApplicationService;
import io.theurl.bundle.application.dto.BundleCreateDto;
import io.theurl.bundle.application.dto.BundleItemEditDto;
import io.theurl.bundle.application.dto.BundleUpdateDto;
import io.theurl.bundle.application.dto.*;
import io.theurl.bundle.persistence.query.BundleCountQuery;
import io.theurl.bundle.persistence.query.BundleItemCountQuery;
import io.theurl.bundle.persistence.query.BundleItemListQuery;
import io.theurl.bundle.persistence.query.BundleListQuery;
import io.theurl.framework.application.BaseApplicationService;
import io.theurl.framework.utility.ShortUniqueId;
import org.modelmapper.ModelMapper;
Expand All @@ -17,6 +19,7 @@

import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Flow;
Expand Down Expand Up @@ -135,4 +138,41 @@ public CompletableFuture<Void> updateItemAsync(String vanity, long itemId, Bundl
public CompletableFuture<Void> removeItemAsync(String vanity, long itemId) {
return null;
}

@Override
public CompletableFuture<List<BundleListDto>> searchAsync(Map<String, Object> criteria, int from, int size) {
if (criteria.getOrDefault("owned", false).equals(true)) {
criteria.put("ownerId", currentUserId());
}
var query = new BundleListQuery(criteria, from, size);
Comment on lines +143 to +147
return mediator.executeAsync(query)
.thenApply(models -> {
return models.stream()
.map(model -> mapper.map(model, BundleListDto.class))
.toList();
});
}

@Override
public CompletableFuture<Integer> countAsync(Map<String, Object> criteria) {
var query = new BundleCountQuery(criteria);
return mediator.executeAsync(query);
}

@Override
public CompletableFuture<List<BundleItemListDto>> searchItemsAsync(String vanity, Map<String, Object> criteria, int from, int size) {
var query = new BundleItemListQuery(vanity, criteria, from, size);
return mediator.executeAsync(query)
.thenApply(models -> {
return models.stream()
.map(model -> mapper.map(model, BundleItemListDto.class))
.toList();
});
}

@Override
public CompletableFuture<Integer> countItemsAsync(String vanity, Map<String, Object> criteria) {
var query = new BundleItemCountQuery(vanity, criteria);
return mediator.executeAsync(query);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package io.theurl.bundle.configure;

import io.theurl.framework.security.JwtAuthenticationFilter;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
Expand All @@ -22,22 +24,25 @@
public class SecurityConfiguration {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
io.theurl.framework.security.JwtAuthenticationFilter jwtAuthenticationFilter) {
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) {
http.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(ex -> ex.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized")))
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/error"
).permitAll()
.anyRequest().authenticated()
)
.authorizeHttpRequests(auth -> {

auth.requestMatchers(HttpMethod.GET, "/api/bundle/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/bookmark/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/bookmark/my").authenticated()
Comment on lines +35 to +37
.requestMatchers(
"/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/error"
).permitAll()
.anyRequest().authenticated();
})
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
Expand Down
Loading