chore: initial commit
这个提交包含在:
当前提交
a719c08a5a
10
.gitignore
vendored
普通文件
10
.gitignore
vendored
普通文件
@ -0,0 +1,10 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.DS_Store
|
||||
*.class
|
||||
target/
|
||||
build/
|
||||
.gradle/
|
||||
*.iml
|
||||
.idea/
|
||||
*.log
|
||||
46
common/pom.xml
普通文件
46
common/pom.xml
普通文件
@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.xuqm</groupId>
|
||||
<artifactId>xuqmgroup-server-parent</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>common</artifactId>
|
||||
<name>common</name>
|
||||
<description>Shared utilities, models, and security for XuqmGroup services</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@ -0,0 +1,20 @@
|
||||
package com.xuqm.common.exception;
|
||||
|
||||
public class BusinessException extends RuntimeException {
|
||||
|
||||
private final int code;
|
||||
|
||||
public BusinessException(String message) {
|
||||
super(message);
|
||||
this.code = 400;
|
||||
}
|
||||
|
||||
public BusinessException(int code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package com.xuqm.common.model;
|
||||
|
||||
public record ApiResponse<T>(int code, String status, T data, String message) {
|
||||
|
||||
public static <T> ApiResponse<T> success(T data) {
|
||||
return new ApiResponse<>(200, "0", data, "success");
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> success(T data, String message) {
|
||||
return new ApiResponse<>(200, "0", data, message);
|
||||
}
|
||||
|
||||
public static ApiResponse<Void> ok() {
|
||||
return new ApiResponse<>(200, "0", null, "success");
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> error(int code, String message) {
|
||||
return new ApiResponse<>(code, "1", null, message);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> badRequest(String message) {
|
||||
return new ApiResponse<>(400, "1", null, message);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> unauthorized(String message) {
|
||||
return new ApiResponse<>(401, "1", null, message);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> forbidden(String message) {
|
||||
return new ApiResponse<>(403, "1", null, message);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package com.xuqm.common.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record PageResult<T>(List<T> items, long total, int page, int size) {
|
||||
|
||||
public static <T> PageResult<T> of(List<T> items, long total, int page, int size) {
|
||||
return new PageResult<>(items, total, page, size);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package com.xuqm.common.security;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class JwtAuthFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
public JwtAuthFilter(JwtUtil jwtUtil) {
|
||||
this.jwtUtil = jwtUtil;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
String header = request.getHeader("Authorization");
|
||||
if (header != null && header.startsWith("Bearer ")) {
|
||||
String token = header.substring(7);
|
||||
if (jwtUtil.isValid(token)) {
|
||||
Claims claims = jwtUtil.parse(token);
|
||||
String subject = claims.getSubject();
|
||||
String role = claims.get("role", String.class);
|
||||
List<SimpleGrantedAuthority> authorities = role != null
|
||||
? List.of(new SimpleGrantedAuthority("ROLE_" + role))
|
||||
: List.of();
|
||||
UsernamePasswordAuthenticationToken auth =
|
||||
new UsernamePasswordAuthenticationToken(subject, null, authorities);
|
||||
SecurityContextHolder.getContext().setAuthentication(auth);
|
||||
}
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
package com.xuqm.common.security;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class JwtUtil {
|
||||
|
||||
@Value("${jwt.secret}")
|
||||
private String secret;
|
||||
|
||||
@Value("${jwt.expiration:86400000}")
|
||||
private long expiration;
|
||||
|
||||
private SecretKey getSigningKey() {
|
||||
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
|
||||
return Keys.hmacShaKeyFor(keyBytes);
|
||||
}
|
||||
|
||||
public String generate(String subject, Map<String, Object> claims) {
|
||||
return Jwts.builder()
|
||||
.subject(subject)
|
||||
.claims(claims)
|
||||
.issuedAt(new Date())
|
||||
.expiration(new Date(System.currentTimeMillis() + expiration))
|
||||
.signWith(getSigningKey())
|
||||
.compact();
|
||||
}
|
||||
|
||||
public String generate(String subject) {
|
||||
return generate(subject, Map.of());
|
||||
}
|
||||
|
||||
public Claims parse(String token) {
|
||||
return Jwts.parser()
|
||||
.verifyWith(getSigningKey())
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
}
|
||||
|
||||
public String getSubject(String token) {
|
||||
return parse(token).getSubject();
|
||||
}
|
||||
|
||||
public boolean isValid(String token) {
|
||||
try {
|
||||
parse(token);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
89
im-service/pom.xml
普通文件
89
im-service/pom.xml
普通文件
@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.xuqm</groupId>
|
||||
<artifactId>xuqmgroup-server-parent</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>im-service</artifactId>
|
||||
<name>im-service</name>
|
||||
<description>Standalone IM service with WebSocket STOMP, groups, messages, webhooks</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.xuqm</groupId>
|
||||
<artifactId>common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@ -0,0 +1,15 @@
|
||||
package com.xuqm.im;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
@SpringBootApplication
|
||||
@ComponentScan(basePackages = {"com.xuqm.im", "com.xuqm.common"})
|
||||
@EnableAsync
|
||||
public class ImServiceApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ImServiceApplication.class, args);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
package com.xuqm.im.config;
|
||||
|
||||
import com.xuqm.common.security.JwtAuthFilter;
|
||||
import com.xuqm.common.security.JwtUtil;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
public SecurityConfig(JwtUtil jwtUtil) {
|
||||
this.jwtUtil = jwtUtil;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/api/im/auth/**", "/ws/**", "/actuator/**").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
package com.xuqm.im.config;
|
||||
|
||||
import com.xuqm.common.security.JwtUtil;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.simp.config.ChannelRegistration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.messaging.simp.stomp.StompCommand;
|
||||
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
|
||||
import org.springframework.messaging.support.ChannelInterceptor;
|
||||
import org.springframework.messaging.support.MessageHeaderAccessor;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
public WebSocketConfig(JwtUtil jwtUtil) {
|
||||
this.jwtUtil = jwtUtil;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
registry.enableSimpleBroker("/topic", "/queue");
|
||||
registry.setApplicationDestinationPrefixes("/app");
|
||||
registry.setUserDestinationPrefix("/user");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry.addEndpoint("/ws/im")
|
||||
.setAllowedOriginPatterns("*")
|
||||
.withSockJS();
|
||||
registry.addEndpoint("/ws/im")
|
||||
.setAllowedOriginPatterns("*");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureClientInboundChannel(ChannelRegistration registration) {
|
||||
registration.interceptors(new ChannelInterceptor() {
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(
|
||||
message, StompHeaderAccessor.class);
|
||||
if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
|
||||
String token = accessor.getFirstNativeHeader("Authorization");
|
||||
if (token != null && token.startsWith("Bearer ")) {
|
||||
token = token.substring(7);
|
||||
if (jwtUtil.isValid(token)) {
|
||||
String userId = jwtUtil.getSubject(token);
|
||||
UsernamePasswordAuthenticationToken auth =
|
||||
new UsernamePasswordAuthenticationToken(userId, null,
|
||||
List.of(new SimpleGrantedAuthority("ROLE_USER")));
|
||||
accessor.setUser(auth);
|
||||
}
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package com.xuqm.im.controller;
|
||||
|
||||
import com.xuqm.common.model.ApiResponse;
|
||||
import com.xuqm.im.service.ImAccountService;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/im/auth")
|
||||
public class AuthController {
|
||||
|
||||
private final ImAccountService accountService;
|
||||
|
||||
public AuthController(ImAccountService accountService) {
|
||||
this.accountService = accountService;
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<ApiResponse<Map<String, String>>> login(
|
||||
@RequestParam @NotBlank String appId,
|
||||
@RequestParam @NotBlank String userId,
|
||||
@RequestParam(required = false) String nickname,
|
||||
@RequestParam(required = false) String avatar) {
|
||||
String token = accountService.loginOrRegister(appId, userId, nickname, avatar);
|
||||
return ResponseEntity.ok(ApiResponse.success(Map.of("token", token)));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
package com.xuqm.im.controller;
|
||||
|
||||
import com.xuqm.common.model.ApiResponse;
|
||||
import com.xuqm.im.entity.ImMessageEntity;
|
||||
import com.xuqm.im.model.SendMessageRequest;
|
||||
import com.xuqm.im.service.MessageService;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/im/messages")
|
||||
public class MessageController {
|
||||
|
||||
private final MessageService messageService;
|
||||
|
||||
public MessageController(MessageService messageService) {
|
||||
this.messageService = messageService;
|
||||
}
|
||||
|
||||
@PostMapping("/send")
|
||||
public ResponseEntity<ApiResponse<ImMessageEntity>> send(
|
||||
@Valid @RequestBody SendMessageRequest req,
|
||||
@AuthenticationPrincipal String userId,
|
||||
@RequestParam String appId) {
|
||||
return ResponseEntity.ok(ApiResponse.success(messageService.send(appId, userId, req)));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/revoke")
|
||||
public ResponseEntity<ApiResponse<ImMessageEntity>> revoke(
|
||||
@PathVariable String id,
|
||||
@AuthenticationPrincipal String userId,
|
||||
@RequestParam String appId) {
|
||||
return ResponseEntity.ok(ApiResponse.success(messageService.revoke(appId, id, userId)));
|
||||
}
|
||||
|
||||
@GetMapping("/history/{toId}")
|
||||
public ResponseEntity<ApiResponse<?>> history(
|
||||
@PathVariable String toId,
|
||||
@RequestParam String appId,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
return ResponseEntity.ok(ApiResponse.success(messageService.history(appId, toId, page, size)));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
package com.xuqm.im.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "im_account",
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = {"appId", "userId"}))
|
||||
public class ImAccountEntity {
|
||||
|
||||
public enum Gender { UNKNOWN, MALE, FEMALE }
|
||||
public enum Status { ACTIVE, BANNED }
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String appId;
|
||||
|
||||
@Column(nullable = false, length = 128)
|
||||
private String userId;
|
||||
|
||||
@Column(length = 64)
|
||||
private String nickname;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(length = 16)
|
||||
private Gender gender;
|
||||
|
||||
@Column(length = 512)
|
||||
private String avatar;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 16)
|
||||
private Status status;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getAppId() { return appId; }
|
||||
public void setAppId(String appId) { this.appId = appId; }
|
||||
|
||||
public String getUserId() { return userId; }
|
||||
public void setUserId(String userId) { this.userId = userId; }
|
||||
|
||||
public String getNickname() { return nickname; }
|
||||
public void setNickname(String nickname) { this.nickname = nickname; }
|
||||
|
||||
public Gender getGender() { return gender; }
|
||||
public void setGender(Gender gender) { this.gender = gender; }
|
||||
|
||||
public String getAvatar() { return avatar; }
|
||||
public void setAvatar(String avatar) { this.avatar = avatar; }
|
||||
|
||||
public Status getStatus() { return status; }
|
||||
public void setStatus(Status status) { this.status = status; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
package com.xuqm.im.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "im_group")
|
||||
public class ImGroupEntity {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String appId;
|
||||
|
||||
@Column(nullable = false, length = 128)
|
||||
private String name;
|
||||
|
||||
@Column(nullable = false, length = 128)
|
||||
private String creatorId;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "TEXT")
|
||||
private String memberIds;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "TEXT")
|
||||
private String adminIds;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getAppId() { return appId; }
|
||||
public void setAppId(String appId) { this.appId = appId; }
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
|
||||
public String getCreatorId() { return creatorId; }
|
||||
public void setCreatorId(String creatorId) { this.creatorId = creatorId; }
|
||||
|
||||
public String getMemberIds() { return memberIds; }
|
||||
public void setMemberIds(String memberIds) { this.memberIds = memberIds; }
|
||||
|
||||
public String getAdminIds() { return adminIds; }
|
||||
public void setAdminIds(String adminIds) { this.adminIds = adminIds; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
package com.xuqm.im.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Index;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "im_message", indexes = {
|
||||
@Index(name = "idx_app_from", columnList = "appId,fromUserId"),
|
||||
@Index(name = "idx_app_to", columnList = "appId,toId")
|
||||
})
|
||||
public class ImMessageEntity {
|
||||
|
||||
public enum ChatType { SINGLE, GROUP }
|
||||
public enum MsgType {
|
||||
TEXT, IMAGE, VIDEO, AUDIO, FILE, CUSTOM, LOCATION, NOTIFY,
|
||||
RICH_TEXT, CALL_AUDIO, CALL_VIDEO, REVOKED, FORWARD
|
||||
}
|
||||
public enum MsgStatus { SENT, DELIVERED, READ, REVOKED }
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String appId;
|
||||
|
||||
@Column(nullable = false, length = 128)
|
||||
private String fromUserId;
|
||||
|
||||
@Column(nullable = false, length = 128)
|
||||
private String toId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 16)
|
||||
private ChatType chatType;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 16)
|
||||
private MsgType msgType;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 16)
|
||||
private MsgStatus status;
|
||||
|
||||
@Column(length = 128)
|
||||
private String mentionedUserIds;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getAppId() { return appId; }
|
||||
public void setAppId(String appId) { this.appId = appId; }
|
||||
|
||||
public String getFromUserId() { return fromUserId; }
|
||||
public void setFromUserId(String fromUserId) { this.fromUserId = fromUserId; }
|
||||
|
||||
public String getToId() { return toId; }
|
||||
public void setToId(String toId) { this.toId = toId; }
|
||||
|
||||
public ChatType getChatType() { return chatType; }
|
||||
public void setChatType(ChatType chatType) { this.chatType = chatType; }
|
||||
|
||||
public MsgType getMsgType() { return msgType; }
|
||||
public void setMsgType(MsgType msgType) { this.msgType = msgType; }
|
||||
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
|
||||
public MsgStatus getStatus() { return status; }
|
||||
public void setStatus(MsgStatus status) { this.status = status; }
|
||||
|
||||
public String getMentionedUserIds() { return mentionedUserIds; }
|
||||
public void setMentionedUserIds(String mentionedUserIds) { this.mentionedUserIds = mentionedUserIds; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package com.xuqm.im.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "im_keyword_filter")
|
||||
public class KeywordFilterEntity {
|
||||
|
||||
public enum Action { REPLACE, BLOCK }
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String appId;
|
||||
|
||||
@Column(nullable = false, length = 512)
|
||||
private String pattern;
|
||||
|
||||
@Column(length = 128)
|
||||
private String replacement;
|
||||
|
||||
@Column(nullable = false, length = 16)
|
||||
private String action;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean enabled;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getAppId() { return appId; }
|
||||
public void setAppId(String appId) { this.appId = appId; }
|
||||
|
||||
public String getPattern() { return pattern; }
|
||||
public void setPattern(String pattern) { this.pattern = pattern; }
|
||||
|
||||
public String getReplacement() { return replacement; }
|
||||
public void setReplacement(String replacement) { this.replacement = replacement; }
|
||||
|
||||
public String getAction() { return action; }
|
||||
public void setAction(String action) { this.action = action; }
|
||||
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package com.xuqm.im.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "im_webhook_config")
|
||||
public class WebhookConfigEntity {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String appId;
|
||||
|
||||
@Column(nullable = false, length = 512)
|
||||
private String url;
|
||||
|
||||
@Column(length = 256)
|
||||
private String secret;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean enabled;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getAppId() { return appId; }
|
||||
public void setAppId(String appId) { this.appId = appId; }
|
||||
|
||||
public String getUrl() { return url; }
|
||||
public void setUrl(String url) { this.url = url; }
|
||||
|
||||
public String getSecret() { return secret; }
|
||||
public void setSecret(String secret) { this.secret = secret; }
|
||||
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package com.xuqm.im.model;
|
||||
|
||||
import com.xuqm.im.entity.ImMessageEntity;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public record SendMessageRequest(
|
||||
@NotBlank String toId,
|
||||
@NotNull ImMessageEntity.ChatType chatType,
|
||||
@NotNull ImMessageEntity.MsgType msgType,
|
||||
@NotBlank String content,
|
||||
String mentionedUserIds
|
||||
) {}
|
||||
@ -0,0 +1,10 @@
|
||||
package com.xuqm.im.repository;
|
||||
|
||||
import com.xuqm.im.entity.ImAccountEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ImAccountRepository extends JpaRepository<ImAccountEntity, String> {
|
||||
Optional<ImAccountEntity> findByAppIdAndUserId(String appId, String userId);
|
||||
boolean existsByAppIdAndUserId(String appId, String userId);
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.xuqm.im.repository;
|
||||
|
||||
import com.xuqm.im.entity.ImGroupEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.List;
|
||||
|
||||
public interface ImGroupRepository extends JpaRepository<ImGroupEntity, String> {
|
||||
List<ImGroupEntity> findByAppId(String appId);
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package com.xuqm.im.repository;
|
||||
|
||||
import com.xuqm.im.entity.ImMessageEntity;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ImMessageRepository extends JpaRepository<ImMessageEntity, String> {
|
||||
Page<ImMessageEntity> findByAppIdAndToIdOrderByCreatedAtDesc(
|
||||
String appId, String toId, Pageable pageable);
|
||||
Page<ImMessageEntity> findByAppIdAndFromUserIdAndToIdOrderByCreatedAtDesc(
|
||||
String appId, String fromUserId, String toId, Pageable pageable);
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.xuqm.im.repository;
|
||||
|
||||
import com.xuqm.im.entity.KeywordFilterEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.List;
|
||||
|
||||
public interface KeywordFilterRepository extends JpaRepository<KeywordFilterEntity, String> {
|
||||
List<KeywordFilterEntity> findByAppIdAndEnabledTrue(String appId);
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.xuqm.im.repository;
|
||||
|
||||
import com.xuqm.im.entity.WebhookConfigEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.List;
|
||||
|
||||
public interface WebhookConfigRepository extends JpaRepository<WebhookConfigEntity, String> {
|
||||
List<WebhookConfigEntity> findByAppIdAndEnabledTrue(String appId);
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
package com.xuqm.im.service;
|
||||
|
||||
import com.xuqm.common.exception.BusinessException;
|
||||
import com.xuqm.common.security.JwtUtil;
|
||||
import com.xuqm.im.entity.ImAccountEntity;
|
||||
import com.xuqm.im.repository.ImAccountRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class ImAccountService {
|
||||
|
||||
private final ImAccountRepository accountRepository;
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
public ImAccountService(ImAccountRepository accountRepository, JwtUtil jwtUtil) {
|
||||
this.accountRepository = accountRepository;
|
||||
this.jwtUtil = jwtUtil;
|
||||
}
|
||||
|
||||
public String loginOrRegister(String appId, String userId, String nickname, String avatar) {
|
||||
ImAccountEntity account = accountRepository.findByAppIdAndUserId(appId, userId)
|
||||
.orElseGet(() -> {
|
||||
ImAccountEntity e = new ImAccountEntity();
|
||||
e.setId(UUID.randomUUID().toString());
|
||||
e.setAppId(appId);
|
||||
e.setUserId(userId);
|
||||
e.setNickname(nickname);
|
||||
e.setAvatar(avatar);
|
||||
e.setGender(ImAccountEntity.Gender.UNKNOWN);
|
||||
e.setStatus(ImAccountEntity.Status.ACTIVE);
|
||||
e.setCreatedAt(LocalDateTime.now());
|
||||
return accountRepository.save(e);
|
||||
});
|
||||
|
||||
if (account.getStatus() == ImAccountEntity.Status.BANNED) {
|
||||
throw new BusinessException(403, "账号已被封禁");
|
||||
}
|
||||
|
||||
return jwtUtil.generate(userId, Map.of("appId", appId, "role", "USER"));
|
||||
}
|
||||
|
||||
public ImAccountEntity getAccount(String appId, String userId) {
|
||||
return accountRepository.findByAppIdAndUserId(appId, userId)
|
||||
.orElseThrow(() -> new BusinessException(404, "账号不存在"));
|
||||
}
|
||||
|
||||
public ImAccountEntity updateAccount(String appId, String userId, String nickname,
|
||||
String avatar, ImAccountEntity.Gender gender) {
|
||||
ImAccountEntity account = getAccount(appId, userId);
|
||||
if (nickname != null) account.setNickname(nickname);
|
||||
if (avatar != null) account.setAvatar(avatar);
|
||||
if (gender != null) account.setGender(gender);
|
||||
return accountRepository.save(account);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
package com.xuqm.im.service;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.xuqm.common.exception.BusinessException;
|
||||
import com.xuqm.im.entity.ImGroupEntity;
|
||||
import com.xuqm.im.repository.ImGroupRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class ImGroupService {
|
||||
|
||||
private final ImGroupRepository groupRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ImGroupService(ImGroupRepository groupRepository, ObjectMapper objectMapper) {
|
||||
this.groupRepository = groupRepository;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public ImGroupEntity create(String appId, String name, String creatorId, List<String> memberIds) {
|
||||
List<String> members = new ArrayList<>(memberIds);
|
||||
if (!members.contains(creatorId)) members.add(creatorId);
|
||||
|
||||
ImGroupEntity group = new ImGroupEntity();
|
||||
group.setId(UUID.randomUUID().toString());
|
||||
group.setAppId(appId);
|
||||
group.setName(name);
|
||||
group.setCreatorId(creatorId);
|
||||
group.setMemberIds(toJson(members));
|
||||
group.setAdminIds(toJson(List.of(creatorId)));
|
||||
group.setCreatedAt(LocalDateTime.now());
|
||||
return groupRepository.save(group);
|
||||
}
|
||||
|
||||
public ImGroupEntity addMember(String groupId, String userId) {
|
||||
ImGroupEntity group = groupRepository.findById(groupId)
|
||||
.orElseThrow(() -> new BusinessException(404, "群组不存在"));
|
||||
List<String> members = fromJson(group.getMemberIds());
|
||||
if (!members.contains(userId)) {
|
||||
members.add(userId);
|
||||
group.setMemberIds(toJson(members));
|
||||
groupRepository.save(group);
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
public ImGroupEntity removeMember(String groupId, String userId, String operatorId) {
|
||||
ImGroupEntity group = groupRepository.findById(groupId)
|
||||
.orElseThrow(() -> new BusinessException(404, "群组不存在"));
|
||||
List<String> admins = fromJson(group.getAdminIds());
|
||||
if (!admins.contains(operatorId) && !group.getCreatorId().equals(operatorId)) {
|
||||
throw new BusinessException(403, "无权操作");
|
||||
}
|
||||
List<String> members = new ArrayList<>(fromJson(group.getMemberIds()));
|
||||
members.remove(userId);
|
||||
group.setMemberIds(toJson(members));
|
||||
return groupRepository.save(group);
|
||||
}
|
||||
|
||||
public List<ImGroupEntity> listByApp(String appId) {
|
||||
return groupRepository.findByAppId(appId);
|
||||
}
|
||||
|
||||
private String toJson(List<String> list) {
|
||||
try { return objectMapper.writeValueAsString(list); } catch (Exception e) { return "[]"; }
|
||||
}
|
||||
|
||||
private List<String> fromJson(String json) {
|
||||
try { return objectMapper.readValue(json, new TypeReference<>() {}); } catch (Exception e) { return new ArrayList<>(); }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package com.xuqm.im.service;
|
||||
|
||||
import com.xuqm.im.entity.KeywordFilterEntity;
|
||||
import com.xuqm.im.repository.KeywordFilterRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Service
|
||||
public class KeywordFilterService {
|
||||
|
||||
private final KeywordFilterRepository repository;
|
||||
|
||||
public KeywordFilterService(KeywordFilterRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public String filter(String appId, String content) {
|
||||
List<KeywordFilterEntity> filters = repository.findByAppIdAndEnabledTrue(appId);
|
||||
String result = content;
|
||||
for (KeywordFilterEntity f : filters) {
|
||||
try {
|
||||
Pattern p = Pattern.compile(f.getPattern());
|
||||
if ("BLOCK".equals(f.getAction())) {
|
||||
if (p.matcher(result).find()) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
String replacement = f.getReplacement() != null ? f.getReplacement() : "***";
|
||||
result = p.matcher(result).replaceAll(replacement);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public KeywordFilterEntity add(String appId, String pattern, String replacement, String action) {
|
||||
KeywordFilterEntity entity = new KeywordFilterEntity();
|
||||
entity.setId(UUID.randomUUID().toString());
|
||||
entity.setAppId(appId);
|
||||
entity.setPattern(pattern);
|
||||
entity.setReplacement(replacement);
|
||||
entity.setAction(action);
|
||||
entity.setEnabled(true);
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
return repository.save(entity);
|
||||
}
|
||||
|
||||
public List<KeywordFilterEntity> list(String appId) {
|
||||
return repository.findByAppIdAndEnabledTrue(appId);
|
||||
}
|
||||
|
||||
public void delete(String id) {
|
||||
repository.deleteById(id);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,122 @@
|
||||
package com.xuqm.im.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.xuqm.common.exception.BusinessException;
|
||||
import com.xuqm.im.entity.ImMessageEntity;
|
||||
import com.xuqm.im.entity.WebhookConfigEntity;
|
||||
import com.xuqm.im.model.SendMessageRequest;
|
||||
import com.xuqm.im.repository.ImMessageRepository;
|
||||
import com.xuqm.im.repository.WebhookConfigRepository;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class MessageService {
|
||||
|
||||
private final ImMessageRepository messageRepository;
|
||||
private final WebhookConfigRepository webhookRepository;
|
||||
private final KeywordFilterService keywordFilterService;
|
||||
private final SimpMessagingTemplate messagingTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Value("${im.webhook-timeout-ms:3000}")
|
||||
private int webhookTimeoutMs;
|
||||
|
||||
public MessageService(ImMessageRepository messageRepository,
|
||||
WebhookConfigRepository webhookRepository,
|
||||
KeywordFilterService keywordFilterService,
|
||||
SimpMessagingTemplate messagingTemplate,
|
||||
ObjectMapper objectMapper) {
|
||||
this.messageRepository = messageRepository;
|
||||
this.webhookRepository = webhookRepository;
|
||||
this.keywordFilterService = keywordFilterService;
|
||||
this.messagingTemplate = messagingTemplate;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public ImMessageEntity send(String appId, String fromUserId, SendMessageRequest req) {
|
||||
String content = req.content();
|
||||
if (req.msgType() == ImMessageEntity.MsgType.TEXT) {
|
||||
content = keywordFilterService.filter(appId, content);
|
||||
if (content == null) {
|
||||
throw new BusinessException("消息包含违禁内容");
|
||||
}
|
||||
}
|
||||
|
||||
ImMessageEntity message = new ImMessageEntity();
|
||||
message.setId(UUID.randomUUID().toString());
|
||||
message.setAppId(appId);
|
||||
message.setFromUserId(fromUserId);
|
||||
message.setToId(req.toId());
|
||||
message.setChatType(req.chatType());
|
||||
message.setMsgType(req.msgType());
|
||||
message.setContent(content);
|
||||
message.setStatus(ImMessageEntity.MsgStatus.SENT);
|
||||
message.setMentionedUserIds(req.mentionedUserIds());
|
||||
message.setCreatedAt(LocalDateTime.now());
|
||||
messageRepository.save(message);
|
||||
|
||||
String destination = req.chatType() == ImMessageEntity.ChatType.SINGLE
|
||||
? "/user/" + req.toId() + "/queue/messages"
|
||||
: "/topic/group/" + req.toId();
|
||||
messagingTemplate.convertAndSend(destination, message);
|
||||
|
||||
dispatchWebhooks(appId, message);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public ImMessageEntity revoke(String appId, String messageId, String requestUserId) {
|
||||
ImMessageEntity message = messageRepository.findById(messageId)
|
||||
.orElseThrow(() -> new BusinessException(404, "消息不存在"));
|
||||
if (!message.getAppId().equals(appId)) {
|
||||
throw new BusinessException(403, "无权操作");
|
||||
}
|
||||
if (!message.getFromUserId().equals(requestUserId)) {
|
||||
throw new BusinessException(403, "只能撤回自己发送的消息");
|
||||
}
|
||||
message.setStatus(ImMessageEntity.MsgStatus.REVOKED);
|
||||
message.setMsgType(ImMessageEntity.MsgType.REVOKED);
|
||||
return messageRepository.save(message);
|
||||
}
|
||||
|
||||
public Page<ImMessageEntity> history(String appId, String toId, int page, int size) {
|
||||
return messageRepository.findByAppIdAndToIdOrderByCreatedAtDesc(
|
||||
appId, toId, PageRequest.of(page, size));
|
||||
}
|
||||
|
||||
@Async
|
||||
protected void dispatchWebhooks(String appId, ImMessageEntity message) {
|
||||
List<WebhookConfigEntity> webhooks = webhookRepository.findByAppIdAndEnabledTrue(appId);
|
||||
if (webhooks.isEmpty()) return;
|
||||
|
||||
try {
|
||||
String body = objectMapper.writeValueAsString(message);
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
for (WebhookConfigEntity webhook : webhooks) {
|
||||
try {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(webhook.getUrl()))
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body))
|
||||
.build();
|
||||
client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package com.xuqm.im.ws;
|
||||
|
||||
import com.xuqm.im.entity.ImMessageEntity;
|
||||
import com.xuqm.im.model.SendMessageRequest;
|
||||
import com.xuqm.im.service.MessageService;
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
import org.springframework.messaging.handler.annotation.Payload;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
@Controller
|
||||
public class ChatController {
|
||||
|
||||
private final MessageService messageService;
|
||||
|
||||
public ChatController(MessageService messageService) {
|
||||
this.messageService = messageService;
|
||||
}
|
||||
|
||||
@MessageMapping("/chat.send")
|
||||
public void send(@Payload WsMessageRequest request, Principal principal) {
|
||||
if (principal == null) return;
|
||||
String userId = principal.getName();
|
||||
SendMessageRequest req = new SendMessageRequest(
|
||||
request.toId(), request.chatType(), request.msgType(),
|
||||
request.content(), request.mentionedUserIds()
|
||||
);
|
||||
messageService.send(request.appId(), userId, req);
|
||||
}
|
||||
|
||||
@MessageMapping("/chat.revoke")
|
||||
public void revoke(@Payload WsRevokeRequest request, Principal principal) {
|
||||
if (principal == null) return;
|
||||
messageService.revoke(request.appId(), request.messageId(), principal.getName());
|
||||
}
|
||||
|
||||
public record WsMessageRequest(
|
||||
String appId, String toId,
|
||||
ImMessageEntity.ChatType chatType,
|
||||
ImMessageEntity.MsgType msgType,
|
||||
String content, String mentionedUserIds
|
||||
) {}
|
||||
|
||||
public record WsRevokeRequest(String appId, String messageId) {}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
server:
|
||||
port: 8082
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: im-service
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/xuqm_im?useSSL=false&serverTimezone=UTC&createDatabaseIfNotExist=true&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: root
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: false
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.MySQLDialect
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
jackson:
|
||||
time-zone: UTC
|
||||
serialization:
|
||||
write-dates-as-timestamps: false
|
||||
|
||||
jwt:
|
||||
secret: xuqm-im-service-secret-key-must-be-at-least-256-bits-long-for-hmac-sha
|
||||
expiration: 86400000
|
||||
|
||||
im:
|
||||
multi-login: true
|
||||
message-history-days: 30
|
||||
webhook-timeout-ms: 3000
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.xuqm: DEBUG
|
||||
85
pom.xml
普通文件
85
pom.xml
普通文件
@ -0,0 +1,85 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.xuqm</groupId>
|
||||
<artifactId>xuqmgroup-server-parent</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>xuqmgroup-server-parent</name>
|
||||
<description>XuqmGroup Platform — Multi-tenant SaaS backend microservices</description>
|
||||
|
||||
<modules>
|
||||
<module>common</module>
|
||||
<module>tenant-service</module>
|
||||
<module>im-service</module>
|
||||
<module>push-service</module>
|
||||
<module>update-service</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<spring-boot.version>3.4.4</spring-boot.version>
|
||||
<jjwt.version>0.12.6</jjwt.version>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<maven.compiler.release>21</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xuqm</groupId>
|
||||
<artifactId>common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<release>${java.version}</release>
|
||||
<encoding>${project.build.sourceEncoding}</encoding>
|
||||
<parameters>true</parameters>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
</project>
|
||||
77
push-service/pom.xml
普通文件
77
push-service/pom.xml
普通文件
@ -0,0 +1,77 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.xuqm</groupId>
|
||||
<artifactId>xuqmgroup-server-parent</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>push-service</artifactId>
|
||||
<name>push-service</name>
|
||||
<description>Offline push notification service: Huawei, Xiaomi, OPPO, vivo, Honor, APNs</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.xuqm</groupId>
|
||||
<artifactId>common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@ -0,0 +1,15 @@
|
||||
package com.xuqm.push;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
@SpringBootApplication
|
||||
@ComponentScan(basePackages = {"com.xuqm.push", "com.xuqm.common"})
|
||||
@EnableAsync
|
||||
public class PushServiceApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(PushServiceApplication.class, args);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package com.xuqm.push.controller;
|
||||
|
||||
import com.xuqm.common.model.ApiResponse;
|
||||
import com.xuqm.push.entity.DeviceTokenEntity;
|
||||
import com.xuqm.push.service.PushDispatcher;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/push")
|
||||
public class PushController {
|
||||
|
||||
private final PushDispatcher pushDispatcher;
|
||||
|
||||
public PushController(PushDispatcher pushDispatcher) {
|
||||
this.pushDispatcher = pushDispatcher;
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
public ResponseEntity<ApiResponse<Void>> register(
|
||||
@RequestParam @NotBlank String appId,
|
||||
@RequestParam @NotBlank String userId,
|
||||
@RequestParam @NotNull DeviceTokenEntity.Vendor vendor,
|
||||
@RequestParam @NotBlank String token) {
|
||||
pushDispatcher.registerToken(appId, userId, vendor, token);
|
||||
return ResponseEntity.ok(ApiResponse.ok());
|
||||
}
|
||||
|
||||
@PostMapping("/send")
|
||||
public ResponseEntity<ApiResponse<Void>> send(
|
||||
@RequestParam @NotBlank String appId,
|
||||
@RequestParam @NotBlank String userId,
|
||||
@RequestParam @NotBlank String title,
|
||||
@RequestParam @NotBlank String body,
|
||||
@RequestParam(required = false) String payload) {
|
||||
pushDispatcher.pushToUser(appId, userId, title, body, payload);
|
||||
return ResponseEntity.ok(ApiResponse.ok());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
package com.xuqm.push.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "push_device_token",
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = {"appId", "userId", "vendor"}))
|
||||
public class DeviceTokenEntity {
|
||||
|
||||
public enum Vendor { HUAWEI, XIAOMI, OPPO, VIVO, HONOR, APNS, FCM }
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String appId;
|
||||
|
||||
@Column(nullable = false, length = 128)
|
||||
private String userId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 16)
|
||||
private Vendor vendor;
|
||||
|
||||
@Column(nullable = false, length = 512)
|
||||
private String token;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getAppId() { return appId; }
|
||||
public void setAppId(String appId) { this.appId = appId; }
|
||||
|
||||
public String getUserId() { return userId; }
|
||||
public void setUserId(String userId) { this.userId = userId; }
|
||||
|
||||
public Vendor getVendor() { return vendor; }
|
||||
public void setVendor(Vendor vendor) { this.vendor = vendor; }
|
||||
|
||||
public String getToken() { return token; }
|
||||
public void setToken(String token) { this.token = token; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package com.xuqm.push.repository;
|
||||
|
||||
import com.xuqm.push.entity.DeviceTokenEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface DeviceTokenRepository extends JpaRepository<DeviceTokenEntity, String> {
|
||||
List<DeviceTokenEntity> findByAppIdAndUserId(String appId, String userId);
|
||||
Optional<DeviceTokenEntity> findByAppIdAndUserIdAndVendor(
|
||||
String appId, String userId, DeviceTokenEntity.Vendor vendor);
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
package com.xuqm.push.service;
|
||||
|
||||
import com.xuqm.push.entity.DeviceTokenEntity;
|
||||
import com.xuqm.push.repository.DeviceTokenRepository;
|
||||
import com.xuqm.push.service.provider.PushProvider;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class PushDispatcher {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PushDispatcher.class);
|
||||
|
||||
private final DeviceTokenRepository tokenRepository;
|
||||
private final Map<String, PushProvider> providers;
|
||||
|
||||
public PushDispatcher(DeviceTokenRepository tokenRepository, List<PushProvider> providerList) {
|
||||
this.tokenRepository = tokenRepository;
|
||||
this.providers = providerList.stream()
|
||||
.collect(Collectors.toMap(PushProvider::vendorName, p -> p));
|
||||
}
|
||||
|
||||
@Async
|
||||
public void pushToUser(String appId, String userId, String title, String body, String payload) {
|
||||
List<DeviceTokenEntity> tokens = tokenRepository.findByAppIdAndUserId(appId, userId);
|
||||
for (DeviceTokenEntity t : tokens) {
|
||||
PushProvider provider = providers.get(t.getVendor().name());
|
||||
if (provider != null) {
|
||||
boolean ok = provider.send(t.getToken(), title, body, payload);
|
||||
log.info("Push to {}@{} via {}: {}", userId, appId, t.getVendor(), ok ? "OK" : "FAIL");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void registerToken(String appId, String userId, DeviceTokenEntity.Vendor vendor, String token) {
|
||||
Optional<DeviceTokenEntity> existing = tokenRepository.findByAppIdAndUserIdAndVendor(appId, userId, vendor);
|
||||
DeviceTokenEntity entity = existing.orElseGet(() -> {
|
||||
DeviceTokenEntity e = new DeviceTokenEntity();
|
||||
e.setId(UUID.randomUUID().toString());
|
||||
e.setAppId(appId);
|
||||
e.setUserId(userId);
|
||||
e.setVendor(vendor);
|
||||
e.setCreatedAt(LocalDateTime.now());
|
||||
return e;
|
||||
});
|
||||
entity.setToken(token);
|
||||
entity.setUpdatedAt(LocalDateTime.now());
|
||||
tokenRepository.save(entity);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
package com.xuqm.push.service.provider;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class HuaweiPushProvider implements PushProvider {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(HuaweiPushProvider.class);
|
||||
|
||||
@Value("${push.huawei.app-id:}")
|
||||
private String appId;
|
||||
|
||||
@Value("${push.huawei.app-secret:}")
|
||||
private String appSecret;
|
||||
|
||||
@Value("${push.huawei.token-url:https://oauth-login.cloud.huawei.com/oauth2/v3/token}")
|
||||
private String tokenUrl;
|
||||
|
||||
@Value("${push.huawei.push-url:https://push-api.cloud.huawei.com/v1/{appId}/messages:send}")
|
||||
private String pushUrl;
|
||||
|
||||
private final HttpClient httpClient = HttpClient.newHttpClient();
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public String vendorName() {
|
||||
return "HUAWEI";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean send(String token, String title, String body, String payload) {
|
||||
if (appId.isBlank() || appSecret.isBlank()) {
|
||||
log.warn("Huawei push not configured");
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
String accessToken = getAccessToken();
|
||||
String url = pushUrl.replace("{appId}", appId);
|
||||
Map<String, Object> message = Map.of(
|
||||
"message", Map.of(
|
||||
"token", new String[]{token},
|
||||
"notification", Map.of("title", title, "body", body),
|
||||
"data", payload != null ? payload : "{}"
|
||||
)
|
||||
);
|
||||
String requestBody = objectMapper.writeValueAsString(message);
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", "Bearer " + accessToken)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
|
||||
.build();
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
return response.statusCode() == 200;
|
||||
} catch (Exception e) {
|
||||
log.error("Huawei push failed: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String getAccessToken() throws Exception {
|
||||
String form = "grant_type=client_credentials&client_id=" + appId + "&client_secret=" + appSecret;
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(tokenUrl))
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(form))
|
||||
.build();
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
Map<?, ?> json = objectMapper.readValue(response.body(), Map.class);
|
||||
return (String) json.get("access_token");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package com.xuqm.push.service.provider;
|
||||
|
||||
public interface PushProvider {
|
||||
String vendorName();
|
||||
boolean send(String token, String title, String body, String payload);
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package com.xuqm.push.service.provider;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@Component
|
||||
public class XiaomiPushProvider implements PushProvider {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(XiaomiPushProvider.class);
|
||||
|
||||
@Value("${push.xiaomi.app-secret:}")
|
||||
private String appSecret;
|
||||
|
||||
@Value("${push.xiaomi.push-url:https://api.xmpush.xiaomi.com/v3/message/regid}")
|
||||
private String pushUrl;
|
||||
|
||||
private final HttpClient httpClient = HttpClient.newHttpClient();
|
||||
|
||||
@Override
|
||||
public String vendorName() { return "XIAOMI"; }
|
||||
|
||||
@Override
|
||||
public boolean send(String token, String title, String body, String payload) {
|
||||
if (appSecret.isBlank()) {
|
||||
log.warn("Xiaomi push not configured");
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
String form = "registration_id=" + URLEncoder.encode(token, StandardCharsets.UTF_8)
|
||||
+ "&title=" + URLEncoder.encode(title, StandardCharsets.UTF_8)
|
||||
+ "&description=" + URLEncoder.encode(body, StandardCharsets.UTF_8)
|
||||
+ "&restricted_package_name=com.example.app"
|
||||
+ "¬ify_type=1";
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(pushUrl))
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.header("Authorization", "key=" + appSecret)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(form))
|
||||
.build();
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
return response.statusCode() == 200;
|
||||
} catch (Exception e) {
|
||||
log.error("Xiaomi push failed: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
server:
|
||||
port: 8083
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: push-service
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/xuqm_push?useSSL=false&serverTimezone=UTC&createDatabaseIfNotExist=true&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: root
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: false
|
||||
|
||||
jwt:
|
||||
secret: xuqm-push-service-secret-key-must-be-at-least-256-bits-long-for-hmac-sha
|
||||
expiration: 86400000
|
||||
|
||||
push:
|
||||
huawei:
|
||||
app-id: ${HUAWEI_APP_ID:}
|
||||
app-secret: ${HUAWEI_APP_SECRET:}
|
||||
token-url: https://oauth-login.cloud.huawei.com/oauth2/v3/token
|
||||
push-url: https://push-api.cloud.huawei.com/v1/{appId}/messages:send
|
||||
xiaomi:
|
||||
app-secret: ${XIAOMI_APP_SECRET:}
|
||||
push-url: https://api.xmpush.xiaomi.com/v3/message/regid
|
||||
apns:
|
||||
key-id: ${APNS_KEY_ID:}
|
||||
team-id: ${APNS_TEAM_ID:}
|
||||
key-path: ${APNS_KEY_PATH:}
|
||||
bundle-id: ${APNS_BUNDLE_ID:}
|
||||
production: false
|
||||
89
tenant-service/pom.xml
普通文件
89
tenant-service/pom.xml
普通文件
@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.xuqm</groupId>
|
||||
<artifactId>xuqmgroup-server-parent</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>tenant-service</artifactId>
|
||||
<name>tenant-service</name>
|
||||
<description>Tenant management: auth, sub-accounts, apps, feature services</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.xuqm</groupId>
|
||||
<artifactId>common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@ -0,0 +1,15 @@
|
||||
package com.xuqm.tenant;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
@SpringBootApplication
|
||||
@ComponentScan(basePackages = {"com.xuqm.tenant", "com.xuqm.common"})
|
||||
@EnableAsync
|
||||
public class TenantServiceApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(TenantServiceApplication.class, args);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.xuqm.tenant.config;
|
||||
|
||||
import com.xuqm.tenant.service.OpsService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class OpsAdminInitializer implements ApplicationRunner {
|
||||
|
||||
private final OpsService opsService;
|
||||
|
||||
@Value("${ops.admin.username:admin}")
|
||||
private String adminUsername;
|
||||
|
||||
@Value("${ops.admin.password:Admin@123456}")
|
||||
private String adminPassword;
|
||||
|
||||
public OpsAdminInitializer(OpsService opsService) {
|
||||
this.opsService = opsService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
opsService.initDefaultAdmin(adminUsername, adminPassword);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
package com.xuqm.tenant.config;
|
||||
|
||||
import com.xuqm.common.security.JwtAuthFilter;
|
||||
import com.xuqm.common.security.JwtUtil;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
public SecurityConfig(JwtUtil jwtUtil) {
|
||||
this.jwtUtil = jwtUtil;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers(
|
||||
"/api/auth/**",
|
||||
"/actuator/health",
|
||||
"/actuator/info"
|
||||
).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.xuqm.tenant.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns("*")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
package com.xuqm.tenant.controller;
|
||||
|
||||
import com.xuqm.common.model.ApiResponse;
|
||||
import com.xuqm.tenant.dto.CreateAppRequest;
|
||||
import com.xuqm.tenant.entity.AppEntity;
|
||||
import com.xuqm.tenant.service.AppService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/apps")
|
||||
public class AppController {
|
||||
|
||||
private final AppService appService;
|
||||
|
||||
public AppController(AppService appService) {
|
||||
this.appService = appService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<ApiResponse<List<AppEntity>>> list(@AuthenticationPrincipal String tenantId) {
|
||||
return ResponseEntity.ok(ApiResponse.success(appService.listByTenant(tenantId)));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<AppEntity>> get(@PathVariable String id,
|
||||
@AuthenticationPrincipal String tenantId) {
|
||||
return ResponseEntity.ok(ApiResponse.success(appService.getById(id, tenantId)));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<ApiResponse<AppEntity>> create(@Valid @RequestBody CreateAppRequest req,
|
||||
@AuthenticationPrincipal String tenantId) {
|
||||
return ResponseEntity.ok(ApiResponse.success(appService.create(tenantId, req)));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<AppEntity>> update(@PathVariable String id,
|
||||
@Valid @RequestBody CreateAppRequest req,
|
||||
@AuthenticationPrincipal String tenantId) {
|
||||
return ResponseEntity.ok(ApiResponse.success(appService.update(id, tenantId, req)));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable String id,
|
||||
@AuthenticationPrincipal String tenantId) {
|
||||
appService.delete(id, tenantId);
|
||||
return ResponseEntity.ok(ApiResponse.ok());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,114 @@
|
||||
package com.xuqm.tenant.controller;
|
||||
|
||||
import com.xuqm.common.model.ApiResponse;
|
||||
import com.xuqm.tenant.dto.LoginRequest;
|
||||
import com.xuqm.tenant.dto.RegisterRequest;
|
||||
import com.xuqm.tenant.service.AuthService;
|
||||
import com.xuqm.tenant.service.EmailService;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
public class AuthController {
|
||||
|
||||
private final AuthService authService;
|
||||
private final EmailService emailService;
|
||||
private static final SecureRandom random = new SecureRandom();
|
||||
private static final String CHARS = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz";
|
||||
|
||||
public AuthController(AuthService authService, EmailService emailService) {
|
||||
this.authService = authService;
|
||||
this.emailService = emailService;
|
||||
}
|
||||
|
||||
@GetMapping("/captcha")
|
||||
public ResponseEntity<ApiResponse<Map<String, String>>> captcha() throws Exception {
|
||||
String key = UUID.randomUUID().toString();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
sb.append(CHARS.charAt(random.nextInt(CHARS.length())));
|
||||
}
|
||||
String code = sb.toString();
|
||||
authService.storeCaptcha(key, code);
|
||||
|
||||
BufferedImage img = new BufferedImage(120, 40, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g = img.createGraphics();
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g.setColor(new Color(245, 245, 245));
|
||||
g.fillRect(0, 0, 120, 40);
|
||||
g.setFont(new Font("Arial", Font.BOLD, 24));
|
||||
for (int i = 0; i < code.length(); i++) {
|
||||
g.setColor(new Color(random.nextInt(150), random.nextInt(150), random.nextInt(150)));
|
||||
g.drawString(String.valueOf(code.charAt(i)), 10 + i * 28, 30);
|
||||
}
|
||||
for (int i = 0; i < 5; i++) {
|
||||
g.setColor(new Color(random.nextInt(200), random.nextInt(200), random.nextInt(200)));
|
||||
g.drawLine(random.nextInt(120), random.nextInt(40), random.nextInt(120), random.nextInt(40));
|
||||
}
|
||||
g.dispose();
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(img, "png", baos);
|
||||
String base64 = Base64.getEncoder().encodeToString(baos.toByteArray());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(Map.of(
|
||||
"key", key,
|
||||
"image", "data:image/png;base64," + base64
|
||||
)));
|
||||
}
|
||||
|
||||
@PostMapping("/send-email-code")
|
||||
public ResponseEntity<ApiResponse<Void>> sendEmailCode(@RequestParam @NotBlank @Email String email,
|
||||
@RequestParam @NotBlank String purpose) {
|
||||
emailService.sendVerificationCode(email, purpose);
|
||||
return ResponseEntity.ok(ApiResponse.ok());
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
public ResponseEntity<ApiResponse<Void>> register(@Valid @RequestBody RegisterRequest req) {
|
||||
authService.register(req);
|
||||
return ResponseEntity.ok(ApiResponse.ok());
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<ApiResponse<Map<String, String>>> login(@Valid @RequestBody LoginRequest req) {
|
||||
String token = authService.login(req);
|
||||
return ResponseEntity.ok(ApiResponse.success(Map.of("token", token)));
|
||||
}
|
||||
|
||||
@PostMapping("/forgot-password")
|
||||
public ResponseEntity<ApiResponse<Void>> forgotPassword(@RequestParam @NotBlank @Email String email) {
|
||||
authService.forgotPassword(email);
|
||||
return ResponseEntity.ok(ApiResponse.ok());
|
||||
}
|
||||
|
||||
@PostMapping("/reset-password")
|
||||
public ResponseEntity<ApiResponse<Void>> resetPassword(
|
||||
@RequestParam @NotBlank @Email String email,
|
||||
@RequestParam @NotBlank String code,
|
||||
@RequestParam @NotBlank String newPassword) {
|
||||
authService.resetPassword(email, code, newPassword);
|
||||
return ResponseEntity.ok(ApiResponse.ok());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
package com.xuqm.tenant.controller;
|
||||
|
||||
import com.xuqm.common.model.ApiResponse;
|
||||
import com.xuqm.tenant.entity.FeatureServiceEntity;
|
||||
import com.xuqm.tenant.service.AppService;
|
||||
import com.xuqm.tenant.service.FeatureServiceManager;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/apps/{appId}/services")
|
||||
public class FeatureServiceController {
|
||||
|
||||
private final FeatureServiceManager featureServiceManager;
|
||||
private final AppService appService;
|
||||
|
||||
public FeatureServiceController(FeatureServiceManager featureServiceManager, AppService appService) {
|
||||
this.featureServiceManager = featureServiceManager;
|
||||
this.appService = appService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<ApiResponse<List<FeatureServiceEntity>>> list(
|
||||
@PathVariable String appId, @AuthenticationPrincipal String tenantId) {
|
||||
appService.getById(appId, tenantId);
|
||||
return ResponseEntity.ok(ApiResponse.success(featureServiceManager.listByApp(appId)));
|
||||
}
|
||||
|
||||
@PostMapping("/toggle")
|
||||
public ResponseEntity<ApiResponse<FeatureServiceEntity>> toggle(
|
||||
@PathVariable String appId,
|
||||
@RequestParam FeatureServiceEntity.Platform platform,
|
||||
@RequestParam FeatureServiceEntity.ServiceType serviceType,
|
||||
@RequestParam boolean enable,
|
||||
@AuthenticationPrincipal String tenantId) {
|
||||
appService.getById(appId, tenantId);
|
||||
return ResponseEntity.ok(ApiResponse.success(
|
||||
featureServiceManager.toggle(appId, platform, serviceType, enable)));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/regenerate-key")
|
||||
public ResponseEntity<ApiResponse<FeatureServiceEntity>> regenerateKey(
|
||||
@PathVariable String appId,
|
||||
@PathVariable String id,
|
||||
@AuthenticationPrincipal String tenantId) {
|
||||
appService.getById(appId, tenantId);
|
||||
return ResponseEntity.ok(ApiResponse.success(featureServiceManager.regenerateKey(id)));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
package com.xuqm.tenant.controller;
|
||||
|
||||
import com.xuqm.common.exception.BusinessException;
|
||||
import com.xuqm.common.model.ApiResponse;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handle(BusinessException ex) {
|
||||
return ResponseEntity.status(ex.getCode())
|
||||
.body(ApiResponse.error(ex.getCode(), ex.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handle(MethodArgumentNotValidException ex) {
|
||||
String message = ex.getBindingResult().getFieldErrors().stream()
|
||||
.map(FieldError::getDefaultMessage)
|
||||
.collect(Collectors.joining("; "));
|
||||
return ResponseEntity.badRequest().body(ApiResponse.badRequest(message));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handle(Exception ex) {
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(ApiResponse.error(500, "服务器内部错误: " + ex.getMessage()));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
package com.xuqm.tenant.controller;
|
||||
|
||||
import com.xuqm.common.model.ApiResponse;
|
||||
import com.xuqm.tenant.entity.TenantEntity;
|
||||
import com.xuqm.tenant.service.OpsService;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping
|
||||
public class OpsController {
|
||||
|
||||
private final OpsService opsService;
|
||||
|
||||
public OpsController(OpsService opsService) {
|
||||
this.opsService = opsService;
|
||||
}
|
||||
|
||||
@PostMapping("/api/auth/ops/login")
|
||||
public ResponseEntity<ApiResponse<Map<String, String>>> opsLogin(@RequestBody Map<String, String> body) {
|
||||
String token = opsService.login(body.get("username"), body.get("password"));
|
||||
return ResponseEntity.ok(ApiResponse.success(Map.of("token", token)));
|
||||
}
|
||||
|
||||
@GetMapping("/api/ops/tenants")
|
||||
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> listTenants(
|
||||
@RequestParam(defaultValue = "") String keyword,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
Page<TenantEntity> result = opsService.listTenants(keyword, page, size);
|
||||
return ResponseEntity.ok(ApiResponse.success(Map.of(
|
||||
"content", result.getContent(),
|
||||
"total", result.getTotalElements(),
|
||||
"totalPages", result.getTotalPages()
|
||||
)));
|
||||
}
|
||||
|
||||
@PostMapping("/api/ops/tenants/{id}/toggle-status")
|
||||
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
||||
public ResponseEntity<ApiResponse<Void>> toggleStatus(@PathVariable String id) {
|
||||
opsService.toggleStatus(id);
|
||||
return ResponseEntity.ok(ApiResponse.ok());
|
||||
}
|
||||
|
||||
@GetMapping("/api/ops/statistics")
|
||||
@PreAuthorize("hasAuthority('ROLE_OPS')")
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> statistics() {
|
||||
return ResponseEntity.ok(ApiResponse.success(opsService.statistics()));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
package com.xuqm.tenant.controller;
|
||||
|
||||
import com.xuqm.common.exception.BusinessException;
|
||||
import com.xuqm.common.model.ApiResponse;
|
||||
import com.xuqm.tenant.dto.CreateSubAccountRequest;
|
||||
import com.xuqm.tenant.entity.TenantEntity;
|
||||
import com.xuqm.tenant.service.EmailService;
|
||||
import com.xuqm.tenant.service.SubAccountService;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/sub-accounts")
|
||||
public class SubAccountController {
|
||||
|
||||
private final SubAccountService subAccountService;
|
||||
private final EmailService emailService;
|
||||
|
||||
public SubAccountController(SubAccountService subAccountService, EmailService emailService) {
|
||||
this.subAccountService = subAccountService;
|
||||
this.emailService = emailService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<ApiResponse<List<TenantEntity>>> list(@AuthenticationPrincipal String tenantId) {
|
||||
return ResponseEntity.ok(ApiResponse.success(subAccountService.listByParent(tenantId)));
|
||||
}
|
||||
|
||||
@PostMapping("/send-verify-code")
|
||||
public ResponseEntity<ApiResponse<Void>> sendVerifyCode(@RequestParam @NotBlank @Email String email,
|
||||
@AuthenticationPrincipal String tenantId) {
|
||||
emailService.sendVerificationCode(email, "SUB_ACCOUNT");
|
||||
return ResponseEntity.ok(ApiResponse.ok());
|
||||
}
|
||||
|
||||
@PostMapping("/verify-email")
|
||||
public ResponseEntity<ApiResponse<Void>> verifyEmail(@RequestParam @NotBlank @Email String email,
|
||||
@RequestParam @NotBlank String code,
|
||||
@AuthenticationPrincipal String tenantId) {
|
||||
subAccountService.verifyEmail(tenantId, email, code);
|
||||
return ResponseEntity.ok(ApiResponse.ok());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<ApiResponse<TenantEntity>> create(@Valid @RequestBody CreateSubAccountRequest req,
|
||||
@AuthenticationPrincipal String tenantId) {
|
||||
if (!subAccountService.isEmailVerifiedInSession(tenantId)) {
|
||||
throw new BusinessException(403, "请先完成邮箱验证");
|
||||
}
|
||||
return ResponseEntity.ok(ApiResponse.success(subAccountService.create(tenantId, req)));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<Void>> disable(@PathVariable String id,
|
||||
@AuthenticationPrincipal String tenantId) {
|
||||
subAccountService.disable(id, tenantId);
|
||||
return ResponseEntity.ok(ApiResponse.ok());
|
||||
}
|
||||
|
||||
@GetMapping("/generate-password")
|
||||
public ResponseEntity<ApiResponse<Map<String, String>>> generatePassword() {
|
||||
return ResponseEntity.ok(ApiResponse.success(Map.of("password", subAccountService.generatePassword())));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.xuqm.tenant.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public record CreateAppRequest(
|
||||
@NotBlank @Size(max = 128) String packageName,
|
||||
@NotBlank @Size(max = 128) String name,
|
||||
@Size(max = 512) String description,
|
||||
String iconUrl
|
||||
) {}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.xuqm.tenant.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public record CreateSubAccountRequest(
|
||||
@NotBlank @Size(min = 3, max = 32) String username,
|
||||
@NotBlank @Size(min = 6, max = 64) String password,
|
||||
String email,
|
||||
@NotBlank @Size(max = 32) String nickname,
|
||||
String phone
|
||||
) {}
|
||||
@ -0,0 +1,10 @@
|
||||
package com.xuqm.tenant.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record LoginRequest(
|
||||
@NotBlank String account,
|
||||
@NotBlank String password,
|
||||
@NotBlank String captchaKey,
|
||||
@NotBlank String captchaCode
|
||||
) {}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.xuqm.tenant.dto;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public record RegisterRequest(
|
||||
@NotBlank @Size(min = 3, max = 32) String username,
|
||||
@NotBlank @Size(min = 6, max = 64) String password,
|
||||
@NotBlank @Email String email,
|
||||
@NotBlank @Size(max = 32) String nickname,
|
||||
String phone,
|
||||
@NotBlank String emailCode
|
||||
) {}
|
||||
@ -0,0 +1,66 @@
|
||||
package com.xuqm.tenant.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "t_app")
|
||||
public class AppEntity {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String tenantId;
|
||||
|
||||
@Column(nullable = false, length = 128)
|
||||
private String packageName;
|
||||
|
||||
@Column(nullable = false, length = 128)
|
||||
private String name;
|
||||
|
||||
@Column(length = 512)
|
||||
private String description;
|
||||
|
||||
@Column(length = 512)
|
||||
private String iconUrl;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 64)
|
||||
private String appKey;
|
||||
|
||||
@Column(nullable = false, length = 128)
|
||||
private String appSecret;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getTenantId() { return tenantId; }
|
||||
public void setTenantId(String tenantId) { this.tenantId = tenantId; }
|
||||
|
||||
public String getPackageName() { return packageName; }
|
||||
public void setPackageName(String packageName) { this.packageName = packageName; }
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
|
||||
public String getIconUrl() { return iconUrl; }
|
||||
public void setIconUrl(String iconUrl) { this.iconUrl = iconUrl; }
|
||||
|
||||
public String getAppKey() { return appKey; }
|
||||
public void setAppKey(String appKey) { this.appKey = appKey; }
|
||||
|
||||
public String getAppSecret() { return appSecret; }
|
||||
public void setAppSecret(String appSecret) { this.appSecret = appSecret; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package com.xuqm.tenant.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "t_email_verification")
|
||||
public class EmailVerificationEntity {
|
||||
|
||||
public enum Purpose { REGISTER, RESET_PASSWORD, SUB_ACCOUNT }
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, length = 128)
|
||||
private String email;
|
||||
|
||||
@Column(nullable = false, length = 16)
|
||||
private String code;
|
||||
|
||||
@Column(nullable = false, length = 32)
|
||||
private String purpose;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean used;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime expiresAt;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
|
||||
public String getCode() { return code; }
|
||||
public void setCode(String code) { this.code = code; }
|
||||
|
||||
public String getPurpose() { return purpose; }
|
||||
public void setPurpose(String purpose) { this.purpose = purpose; }
|
||||
|
||||
public boolean isUsed() { return used; }
|
||||
public void setUsed(boolean used) { this.used = used; }
|
||||
|
||||
public LocalDateTime getExpiresAt() { return expiresAt; }
|
||||
public void setExpiresAt(LocalDateTime expiresAt) { this.expiresAt = expiresAt; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
package com.xuqm.tenant.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "t_feature_service")
|
||||
public class FeatureServiceEntity {
|
||||
|
||||
public enum Platform { ANDROID, IOS, HARMONY }
|
||||
public enum ServiceType { IM, PUSH, UPDATE }
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String appId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 16)
|
||||
private Platform platform;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 16)
|
||||
private ServiceType serviceType;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean enabled;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 128)
|
||||
private String secretKey;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String config;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getAppId() { return appId; }
|
||||
public void setAppId(String appId) { this.appId = appId; }
|
||||
|
||||
public Platform getPlatform() { return platform; }
|
||||
public void setPlatform(Platform platform) { this.platform = platform; }
|
||||
|
||||
public ServiceType getServiceType() { return serviceType; }
|
||||
public void setServiceType(ServiceType serviceType) { this.serviceType = serviceType; }
|
||||
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
|
||||
public String getSecretKey() { return secretKey; }
|
||||
public void setSecretKey(String secretKey) { this.secretKey = secretKey; }
|
||||
|
||||
public String getConfig() { return config; }
|
||||
public void setConfig(String config) { this.config = config; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.xuqm.tenant.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "t_ops_admin")
|
||||
public class OpsAdminEntity {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 64)
|
||||
private String username;
|
||||
|
||||
@Column(nullable = false, length = 128)
|
||||
private String password;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
package com.xuqm.tenant.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "t_tenant")
|
||||
public class TenantEntity {
|
||||
|
||||
public enum Type { MAIN, SUB }
|
||||
public enum Status { ACTIVE, DISABLED, PENDING_EMAIL }
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 64)
|
||||
private String username;
|
||||
|
||||
@Column(nullable = false, length = 128)
|
||||
private String password;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 128)
|
||||
private String email;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String nickname;
|
||||
|
||||
@Column(length = 32)
|
||||
private String phone;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 16)
|
||||
private Type type;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 20)
|
||||
private Status status;
|
||||
|
||||
@Column(length = 64)
|
||||
private String parentId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
|
||||
public String getNickname() { return nickname; }
|
||||
public void setNickname(String nickname) { this.nickname = nickname; }
|
||||
|
||||
public String getPhone() { return phone; }
|
||||
public void setPhone(String phone) { this.phone = phone; }
|
||||
|
||||
public Type getType() { return type; }
|
||||
public void setType(Type type) { this.type = type; }
|
||||
|
||||
public Status getStatus() { return status; }
|
||||
public void setStatus(Status status) { this.status = status; }
|
||||
|
||||
public String getParentId() { return parentId; }
|
||||
public void setParentId(String parentId) { this.parentId = parentId; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.xuqm.tenant.repository;
|
||||
|
||||
import com.xuqm.tenant.entity.AppEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface AppRepository extends JpaRepository<AppEntity, String> {
|
||||
List<AppEntity> findByTenantId(String tenantId);
|
||||
Optional<AppEntity> findByAppKey(String appKey);
|
||||
boolean existsByPackageNameAndTenantId(String packageName, String tenantId);
|
||||
long count();
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.xuqm.tenant.repository;
|
||||
|
||||
import com.xuqm.tenant.entity.EmailVerificationEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface EmailVerificationRepository extends JpaRepository<EmailVerificationEntity, String> {
|
||||
Optional<EmailVerificationEntity> findTopByEmailAndPurposeAndUsedFalseOrderByCreatedAtDesc(
|
||||
String email, String purpose);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("UPDATE EmailVerificationEntity e SET e.used = true WHERE e.email = :email AND e.purpose = :purpose")
|
||||
void markAllUsed(String email, String purpose);
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.xuqm.tenant.repository;
|
||||
|
||||
import com.xuqm.tenant.entity.FeatureServiceEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface FeatureServiceRepository extends JpaRepository<FeatureServiceEntity, String> {
|
||||
List<FeatureServiceEntity> findByAppId(String appId);
|
||||
Optional<FeatureServiceEntity> findByAppIdAndPlatformAndServiceType(
|
||||
String appId,
|
||||
FeatureServiceEntity.Platform platform,
|
||||
FeatureServiceEntity.ServiceType serviceType);
|
||||
Optional<FeatureServiceEntity> findBySecretKey(String secretKey);
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package com.xuqm.tenant.repository;
|
||||
|
||||
import com.xuqm.tenant.entity.OpsAdminEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface OpsAdminRepository extends JpaRepository<OpsAdminEntity, String> {
|
||||
Optional<OpsAdminEntity> findByUsername(String username);
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package com.xuqm.tenant.repository;
|
||||
|
||||
import com.xuqm.tenant.entity.TenantEntity;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
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.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface TenantRepository extends JpaRepository<TenantEntity, String> {
|
||||
Optional<TenantEntity> findByUsername(String username);
|
||||
Optional<TenantEntity> findByEmail(String email);
|
||||
boolean existsByUsername(String username);
|
||||
boolean existsByEmail(String email);
|
||||
List<TenantEntity> findByParentId(String parentId);
|
||||
|
||||
@Query("SELECT t FROM TenantEntity t WHERE " +
|
||||
"(:keyword IS NULL OR :keyword = '' OR t.username LIKE %:keyword% OR t.email LIKE %:keyword%)")
|
||||
Page<TenantEntity> searchTenants(@Param("keyword") String keyword, Pageable pageable);
|
||||
|
||||
long countByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
package com.xuqm.tenant.service;
|
||||
|
||||
import com.xuqm.common.exception.BusinessException;
|
||||
import com.xuqm.tenant.dto.CreateAppRequest;
|
||||
import com.xuqm.tenant.entity.AppEntity;
|
||||
import com.xuqm.tenant.repository.AppRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class AppService {
|
||||
|
||||
private final AppRepository appRepository;
|
||||
private static final SecureRandom random = new SecureRandom();
|
||||
|
||||
public AppService(AppRepository appRepository) {
|
||||
this.appRepository = appRepository;
|
||||
}
|
||||
|
||||
public List<AppEntity> listByTenant(String tenantId) {
|
||||
return appRepository.findByTenantId(tenantId);
|
||||
}
|
||||
|
||||
public AppEntity getById(String id, String tenantId) {
|
||||
AppEntity app = appRepository.findById(id)
|
||||
.orElseThrow(() -> new BusinessException(404, "应用不存在"));
|
||||
if (!app.getTenantId().equals(tenantId)) {
|
||||
throw new BusinessException(403, "无权访问该应用");
|
||||
}
|
||||
return app;
|
||||
}
|
||||
|
||||
public AppEntity create(String tenantId, CreateAppRequest req) {
|
||||
if (appRepository.existsByPackageNameAndTenantId(req.packageName(), tenantId)) {
|
||||
throw new BusinessException("该包名下已存在同名应用");
|
||||
}
|
||||
AppEntity app = new AppEntity();
|
||||
app.setId(UUID.randomUUID().toString());
|
||||
app.setTenantId(tenantId);
|
||||
app.setPackageName(req.packageName());
|
||||
app.setName(req.name());
|
||||
app.setDescription(req.description());
|
||||
app.setIconUrl(req.iconUrl());
|
||||
app.setAppKey(generateAppKey());
|
||||
app.setAppSecret(generateSecret());
|
||||
app.setCreatedAt(LocalDateTime.now());
|
||||
return appRepository.save(app);
|
||||
}
|
||||
|
||||
public AppEntity update(String id, String tenantId, CreateAppRequest req) {
|
||||
AppEntity app = getById(id, tenantId);
|
||||
app.setName(req.name());
|
||||
app.setDescription(req.description());
|
||||
app.setIconUrl(req.iconUrl());
|
||||
return appRepository.save(app);
|
||||
}
|
||||
|
||||
public void delete(String id, String tenantId) {
|
||||
AppEntity app = getById(id, tenantId);
|
||||
appRepository.delete(app);
|
||||
}
|
||||
|
||||
private String generateAppKey() {
|
||||
return "ak_" + UUID.randomUUID().toString().replace("-", "").substring(0, 24);
|
||||
}
|
||||
|
||||
private String generateSecret() {
|
||||
byte[] bytes = new byte[32];
|
||||
random.nextBytes(bytes);
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
package com.xuqm.tenant.service;
|
||||
|
||||
import com.xuqm.common.exception.BusinessException;
|
||||
import com.xuqm.common.security.JwtUtil;
|
||||
import com.xuqm.tenant.dto.LoginRequest;
|
||||
import com.xuqm.tenant.dto.RegisterRequest;
|
||||
import com.xuqm.tenant.entity.TenantEntity;
|
||||
import com.xuqm.tenant.repository.TenantRepository;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service
|
||||
public class AuthService {
|
||||
|
||||
private final TenantRepository tenantRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtUtil jwtUtil;
|
||||
private final EmailService emailService;
|
||||
private final StringRedisTemplate redis;
|
||||
|
||||
private static final String CAPTCHA_PREFIX = "captcha:";
|
||||
|
||||
public AuthService(TenantRepository tenantRepository, PasswordEncoder passwordEncoder,
|
||||
JwtUtil jwtUtil, EmailService emailService, StringRedisTemplate redis) {
|
||||
this.tenantRepository = tenantRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.jwtUtil = jwtUtil;
|
||||
this.emailService = emailService;
|
||||
this.redis = redis;
|
||||
}
|
||||
|
||||
public void register(RegisterRequest req) {
|
||||
emailService.verify(req.email(), req.emailCode(), "REGISTER");
|
||||
|
||||
if (tenantRepository.existsByUsername(req.username())) {
|
||||
throw new BusinessException("用户名已存在");
|
||||
}
|
||||
if (tenantRepository.existsByEmail(req.email())) {
|
||||
throw new BusinessException("邮箱已被注册");
|
||||
}
|
||||
|
||||
TenantEntity tenant = new TenantEntity();
|
||||
tenant.setId(UUID.randomUUID().toString());
|
||||
tenant.setUsername(req.username());
|
||||
tenant.setPassword(passwordEncoder.encode(req.password()));
|
||||
tenant.setEmail(req.email());
|
||||
tenant.setNickname(req.nickname());
|
||||
tenant.setPhone(req.phone());
|
||||
tenant.setType(TenantEntity.Type.MAIN);
|
||||
tenant.setStatus(TenantEntity.Status.ACTIVE);
|
||||
tenant.setCreatedAt(LocalDateTime.now());
|
||||
tenantRepository.save(tenant);
|
||||
}
|
||||
|
||||
public String login(LoginRequest req) {
|
||||
String cachedCaptcha = redis.opsForValue().get(CAPTCHA_PREFIX + req.captchaKey());
|
||||
if (cachedCaptcha == null || !cachedCaptcha.equalsIgnoreCase(req.captchaCode())) {
|
||||
throw new BusinessException("验证码错误或已过期");
|
||||
}
|
||||
redis.delete(CAPTCHA_PREFIX + req.captchaKey());
|
||||
|
||||
TenantEntity tenant = tenantRepository.findByUsername(req.account())
|
||||
.or(() -> tenantRepository.findByEmail(req.account()))
|
||||
.orElseThrow(() -> new BusinessException("账号不存在"));
|
||||
|
||||
if (tenant.getStatus() == TenantEntity.Status.DISABLED) {
|
||||
throw new BusinessException("账号已被禁用");
|
||||
}
|
||||
if (!passwordEncoder.matches(req.password(), tenant.getPassword())) {
|
||||
throw new BusinessException("密码错误");
|
||||
}
|
||||
|
||||
return jwtUtil.generate(tenant.getId(), Map.of(
|
||||
"username", tenant.getUsername(),
|
||||
"nickname", tenant.getNickname(),
|
||||
"type", tenant.getType().name()
|
||||
));
|
||||
}
|
||||
|
||||
public void forgotPassword(String email) {
|
||||
if (!tenantRepository.existsByEmail(email)) {
|
||||
throw new BusinessException("邮箱未注册");
|
||||
}
|
||||
emailService.sendVerificationCode(email, "RESET_PASSWORD");
|
||||
}
|
||||
|
||||
public void resetPassword(String email, String code, String newPassword) {
|
||||
emailService.verify(email, code, "RESET_PASSWORD");
|
||||
TenantEntity tenant = tenantRepository.findByEmail(email)
|
||||
.orElseThrow(() -> new BusinessException("账号不存在"));
|
||||
tenant.setPassword(passwordEncoder.encode(newPassword));
|
||||
tenantRepository.save(tenant);
|
||||
}
|
||||
|
||||
public void storeCaptcha(String key, String code) {
|
||||
redis.opsForValue().set(CAPTCHA_PREFIX + key, code, 300, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
package com.xuqm.tenant.service;
|
||||
|
||||
import com.xuqm.common.exception.BusinessException;
|
||||
import com.xuqm.tenant.entity.EmailVerificationEntity;
|
||||
import com.xuqm.tenant.repository.EmailVerificationRepository;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.mail.SimpleMailMessage;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class EmailService {
|
||||
|
||||
private final JavaMailSender mailSender;
|
||||
private final EmailVerificationRepository verificationRepository;
|
||||
|
||||
@Value("${spring.mail.username}")
|
||||
private String fromAddress;
|
||||
|
||||
@Value("${email-verify.expire-seconds:600}")
|
||||
private int expireSeconds;
|
||||
|
||||
private static final SecureRandom random = new SecureRandom();
|
||||
|
||||
public EmailService(JavaMailSender mailSender, EmailVerificationRepository verificationRepository) {
|
||||
this.mailSender = mailSender;
|
||||
this.verificationRepository = verificationRepository;
|
||||
}
|
||||
|
||||
@Async
|
||||
public void sendVerificationCode(String email, String purpose) {
|
||||
String code = String.format("%06d", random.nextInt(1_000_000));
|
||||
|
||||
EmailVerificationEntity entity = new EmailVerificationEntity();
|
||||
entity.setId(UUID.randomUUID().toString());
|
||||
entity.setEmail(email);
|
||||
entity.setCode(code);
|
||||
entity.setPurpose(purpose);
|
||||
entity.setUsed(false);
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
entity.setExpiresAt(LocalDateTime.now().plusSeconds(expireSeconds));
|
||||
verificationRepository.save(entity);
|
||||
|
||||
SimpleMailMessage message = new SimpleMailMessage();
|
||||
message.setFrom(fromAddress);
|
||||
message.setTo(email);
|
||||
message.setSubject("XuqmGroup - 邮箱验证码");
|
||||
message.setText(String.format("您的验证码是:%s,%d分钟内有效。", code, expireSeconds / 60));
|
||||
mailSender.send(message);
|
||||
}
|
||||
|
||||
public void verify(String email, String code, String purpose) {
|
||||
EmailVerificationEntity entity = verificationRepository
|
||||
.findTopByEmailAndPurposeAndUsedFalseOrderByCreatedAtDesc(email, purpose)
|
||||
.orElseThrow(() -> new BusinessException("验证码无效"));
|
||||
|
||||
if (entity.getExpiresAt().isBefore(LocalDateTime.now())) {
|
||||
throw new BusinessException("验证码已过期");
|
||||
}
|
||||
if (!entity.getCode().equals(code)) {
|
||||
throw new BusinessException("验证码错误");
|
||||
}
|
||||
|
||||
entity.setUsed(true);
|
||||
verificationRepository.save(entity);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package com.xuqm.tenant.service;
|
||||
|
||||
import com.xuqm.common.exception.BusinessException;
|
||||
import com.xuqm.tenant.entity.FeatureServiceEntity;
|
||||
import com.xuqm.tenant.repository.FeatureServiceRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class FeatureServiceManager {
|
||||
|
||||
private final FeatureServiceRepository repository;
|
||||
private static final SecureRandom random = new SecureRandom();
|
||||
|
||||
public FeatureServiceManager(FeatureServiceRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public List<FeatureServiceEntity> listByApp(String appId) {
|
||||
return repository.findByAppId(appId);
|
||||
}
|
||||
|
||||
public FeatureServiceEntity toggle(String appId, FeatureServiceEntity.Platform platform,
|
||||
FeatureServiceEntity.ServiceType serviceType, boolean enable) {
|
||||
FeatureServiceEntity entity = repository
|
||||
.findByAppIdAndPlatformAndServiceType(appId, platform, serviceType)
|
||||
.orElseGet(() -> {
|
||||
FeatureServiceEntity e = new FeatureServiceEntity();
|
||||
e.setId(UUID.randomUUID().toString());
|
||||
e.setAppId(appId);
|
||||
e.setPlatform(platform);
|
||||
e.setServiceType(serviceType);
|
||||
e.setSecretKey(generateSecretKey());
|
||||
e.setCreatedAt(LocalDateTime.now());
|
||||
return e;
|
||||
});
|
||||
entity.setEnabled(enable);
|
||||
return repository.save(entity);
|
||||
}
|
||||
|
||||
public FeatureServiceEntity getOrFail(String appId, FeatureServiceEntity.Platform platform,
|
||||
FeatureServiceEntity.ServiceType serviceType) {
|
||||
return repository.findByAppIdAndPlatformAndServiceType(appId, platform, serviceType)
|
||||
.orElseThrow(() -> new BusinessException(404, "服务未配置"));
|
||||
}
|
||||
|
||||
public FeatureServiceEntity regenerateKey(String id) {
|
||||
FeatureServiceEntity entity = repository.findById(id)
|
||||
.orElseThrow(() -> new BusinessException(404, "服务不存在"));
|
||||
entity.setSecretKey(generateSecretKey());
|
||||
return repository.save(entity);
|
||||
}
|
||||
|
||||
private String generateSecretKey() {
|
||||
byte[] bytes = new byte[32];
|
||||
random.nextBytes(bytes);
|
||||
return "sk_" + Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
package com.xuqm.tenant.service;
|
||||
|
||||
import com.xuqm.common.security.JwtUtil;
|
||||
import com.xuqm.tenant.entity.OpsAdminEntity;
|
||||
import com.xuqm.tenant.entity.TenantEntity;
|
||||
import com.xuqm.tenant.repository.AppRepository;
|
||||
import com.xuqm.tenant.repository.OpsAdminRepository;
|
||||
import com.xuqm.tenant.repository.TenantRepository;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class OpsService {
|
||||
|
||||
private final TenantRepository tenantRepository;
|
||||
private final AppRepository appRepository;
|
||||
private final OpsAdminRepository opsAdminRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
public OpsService(TenantRepository tenantRepository, AppRepository appRepository,
|
||||
OpsAdminRepository opsAdminRepository, PasswordEncoder passwordEncoder,
|
||||
JwtUtil jwtUtil) {
|
||||
this.tenantRepository = tenantRepository;
|
||||
this.appRepository = appRepository;
|
||||
this.opsAdminRepository = opsAdminRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.jwtUtil = jwtUtil;
|
||||
}
|
||||
|
||||
public String login(String username, String password) {
|
||||
OpsAdminEntity admin = opsAdminRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new IllegalArgumentException("用户名或密码错误"));
|
||||
if (!passwordEncoder.matches(password, admin.getPassword())) {
|
||||
throw new IllegalArgumentException("用户名或密码错误");
|
||||
}
|
||||
return jwtUtil.generate(admin.getId(), Map.of("username", username, "role", "OPS"));
|
||||
}
|
||||
|
||||
public Page<TenantEntity> listTenants(String keyword, int page, int size) {
|
||||
return tenantRepository.searchTenants(
|
||||
keyword,
|
||||
PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"))
|
||||
);
|
||||
}
|
||||
|
||||
public void toggleStatus(String tenantId) {
|
||||
TenantEntity tenant = tenantRepository.findById(tenantId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("租户不存在"));
|
||||
if (tenant.getStatus() == TenantEntity.Status.ACTIVE) {
|
||||
tenant.setStatus(TenantEntity.Status.DISABLED);
|
||||
} else {
|
||||
tenant.setStatus(TenantEntity.Status.ACTIVE);
|
||||
}
|
||||
tenantRepository.save(tenant);
|
||||
}
|
||||
|
||||
public Map<String, Object> statistics() {
|
||||
long totalTenants = tenantRepository.count();
|
||||
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
|
||||
LocalDateTime todayEnd = todayStart.plusDays(1);
|
||||
long todayNew = tenantRepository.countByCreatedAtBetween(todayStart, todayEnd);
|
||||
long activeApps = appRepository.count();
|
||||
return Map.of(
|
||||
"totalTenants", totalTenants,
|
||||
"todayNew", todayNew,
|
||||
"activeApps", activeApps,
|
||||
"onlineUsers", 0
|
||||
);
|
||||
}
|
||||
|
||||
public void initDefaultAdmin(String username, String rawPassword) {
|
||||
if (opsAdminRepository.findByUsername(username).isPresent()) return;
|
||||
OpsAdminEntity admin = new OpsAdminEntity();
|
||||
admin.setId(UUID.randomUUID().toString());
|
||||
admin.setUsername(username);
|
||||
admin.setPassword(passwordEncoder.encode(rawPassword));
|
||||
admin.setCreatedAt(LocalDateTime.now());
|
||||
opsAdminRepository.save(admin);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,84 @@
|
||||
package com.xuqm.tenant.service;
|
||||
|
||||
import com.xuqm.common.exception.BusinessException;
|
||||
import com.xuqm.tenant.dto.CreateSubAccountRequest;
|
||||
import com.xuqm.tenant.entity.TenantEntity;
|
||||
import com.xuqm.tenant.repository.TenantRepository;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service
|
||||
public class SubAccountService {
|
||||
|
||||
private final TenantRepository tenantRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final EmailService emailService;
|
||||
private final StringRedisTemplate redis;
|
||||
|
||||
private static final String SUB_VERIFY_PREFIX = "sub_verified:";
|
||||
private static final SecureRandom random = new SecureRandom();
|
||||
|
||||
public SubAccountService(TenantRepository tenantRepository, PasswordEncoder passwordEncoder,
|
||||
EmailService emailService, StringRedisTemplate redis) {
|
||||
this.tenantRepository = tenantRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.emailService = emailService;
|
||||
this.redis = redis;
|
||||
}
|
||||
|
||||
public boolean isEmailVerifiedInSession(String tenantId) {
|
||||
return Boolean.TRUE.toString().equals(redis.opsForValue().get(SUB_VERIFY_PREFIX + tenantId));
|
||||
}
|
||||
|
||||
public void verifyEmail(String tenantId, String email, String code) {
|
||||
emailService.verify(email, code, "SUB_ACCOUNT");
|
||||
redis.opsForValue().set(SUB_VERIFY_PREFIX + tenantId, Boolean.TRUE.toString(), 24, TimeUnit.HOURS);
|
||||
}
|
||||
|
||||
public TenantEntity create(String parentId, CreateSubAccountRequest req) {
|
||||
if (tenantRepository.existsByUsername(req.username())) {
|
||||
throw new BusinessException("用户名已存在");
|
||||
}
|
||||
|
||||
TenantEntity sub = new TenantEntity();
|
||||
sub.setId(UUID.randomUUID().toString());
|
||||
sub.setUsername(req.username());
|
||||
sub.setPassword(passwordEncoder.encode(req.password()));
|
||||
sub.setEmail(req.email() != null ? req.email() : "");
|
||||
sub.setNickname(req.nickname());
|
||||
sub.setPhone(req.phone());
|
||||
sub.setType(TenantEntity.Type.SUB);
|
||||
sub.setStatus(TenantEntity.Status.ACTIVE);
|
||||
sub.setParentId(parentId);
|
||||
sub.setCreatedAt(LocalDateTime.now());
|
||||
return tenantRepository.save(sub);
|
||||
}
|
||||
|
||||
public List<TenantEntity> listByParent(String parentId) {
|
||||
return tenantRepository.findByParentId(parentId);
|
||||
}
|
||||
|
||||
public void disable(String subId, String parentId) {
|
||||
TenantEntity sub = tenantRepository.findById(subId)
|
||||
.orElseThrow(() -> new BusinessException(404, "子账号不存在"));
|
||||
if (!parentId.equals(sub.getParentId())) {
|
||||
throw new BusinessException(403, "无权操作");
|
||||
}
|
||||
sub.setStatus(TenantEntity.Status.DISABLED);
|
||||
tenantRepository.save(sub);
|
||||
}
|
||||
|
||||
public String generatePassword() {
|
||||
byte[] bytes = new byte[9];
|
||||
random.nextBytes(bytes);
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
server:
|
||||
port: 8081
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: tenant-service
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/xuqm_tenant?useSSL=false&serverTimezone=UTC&createDatabaseIfNotExist=true&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: root
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: false
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.MySQLDialect
|
||||
format_sql: true
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
timeout: 5000ms
|
||||
mail:
|
||||
host: smtp.example.com
|
||||
port: 587
|
||||
username: noreply@xuqm.com
|
||||
password: changeme
|
||||
properties:
|
||||
mail:
|
||||
smtp:
|
||||
auth: true
|
||||
starttls:
|
||||
enable: true
|
||||
jackson:
|
||||
time-zone: UTC
|
||||
serialization:
|
||||
write-dates-as-timestamps: false
|
||||
|
||||
jwt:
|
||||
secret: xuqm-tenant-service-secret-key-must-be-at-least-256-bits-long-for-hmac
|
||||
expiration: 86400000
|
||||
|
||||
captcha:
|
||||
expire-seconds: 300
|
||||
|
||||
email-verify:
|
||||
expire-seconds: 600
|
||||
sub-account-token-hours: 24
|
||||
|
||||
ops:
|
||||
admin:
|
||||
username: admin
|
||||
password: Admin@123456
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.xuqm: DEBUG
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info
|
||||
82
update-service/pom.xml
普通文件
82
update-service/pom.xml
普通文件
@ -0,0 +1,82 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.xuqm</groupId>
|
||||
<artifactId>xuqmgroup-server-parent</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>update-service</artifactId>
|
||||
<name>update-service</name>
|
||||
<description>App version management: APK release, RN bundle hot update</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.xuqm</groupId>
|
||||
<artifactId>common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.dongliu</groupId>
|
||||
<artifactId>apk-parser</artifactId>
|
||||
<version>2.6.10</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@ -0,0 +1,13 @@
|
||||
package com.xuqm.update;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
|
||||
@SpringBootApplication
|
||||
@ComponentScan(basePackages = {"com.xuqm.update", "com.xuqm.common"})
|
||||
public class UpdateServiceApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(UpdateServiceApplication.class, args);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
package com.xuqm.update.controller;
|
||||
|
||||
import com.xuqm.common.model.ApiResponse;
|
||||
import com.xuqm.update.entity.AppVersionEntity;
|
||||
import com.xuqm.update.repository.AppVersionRepository;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/updates")
|
||||
public class AppVersionController {
|
||||
|
||||
private final AppVersionRepository versionRepository;
|
||||
|
||||
@Value("${update.upload-dir:/tmp/xuqm-update}")
|
||||
private String uploadDir;
|
||||
|
||||
@Value("${update.base-url:http://localhost:8084}")
|
||||
private String baseUrl;
|
||||
|
||||
public AppVersionController(AppVersionRepository versionRepository) {
|
||||
this.versionRepository = versionRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/app/check")
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> checkUpdate(
|
||||
@RequestParam String appId,
|
||||
@RequestParam AppVersionEntity.Platform platform,
|
||||
@RequestParam int currentVersionCode) {
|
||||
|
||||
Optional<AppVersionEntity> latest = versionRepository
|
||||
.findTopByAppIdAndPlatformAndPublishStatusOrderByVersionCodeDesc(
|
||||
appId, platform, AppVersionEntity.PublishStatus.PUBLISHED);
|
||||
|
||||
if (latest.isEmpty() || latest.get().getVersionCode() <= currentVersionCode) {
|
||||
return ResponseEntity.ok(ApiResponse.success(Map.of("needsUpdate", false)));
|
||||
}
|
||||
|
||||
AppVersionEntity v = latest.get();
|
||||
return ResponseEntity.ok(ApiResponse.success(Map.of(
|
||||
"needsUpdate", true,
|
||||
"versionName", v.getVersionName(),
|
||||
"versionCode", v.getVersionCode(),
|
||||
"downloadUrl", v.getDownloadUrl() != null ? v.getDownloadUrl() : "",
|
||||
"changeLog", v.getChangeLog() != null ? v.getChangeLog() : "",
|
||||
"forceUpdate", v.isForceUpdate(),
|
||||
"appStoreUrl", v.getAppStoreUrl() != null ? v.getAppStoreUrl() : "",
|
||||
"marketUrl", v.getMarketUrl() != null ? v.getMarketUrl() : ""
|
||||
)));
|
||||
}
|
||||
|
||||
@PostMapping("/app/upload")
|
||||
public ResponseEntity<ApiResponse<AppVersionEntity>> upload(
|
||||
@RequestParam String appId,
|
||||
@RequestParam AppVersionEntity.Platform platform,
|
||||
@RequestParam String versionName,
|
||||
@RequestParam int versionCode,
|
||||
@RequestParam(required = false) String changeLog,
|
||||
@RequestParam(defaultValue = "false") boolean forceUpdate,
|
||||
@RequestParam(required = false) MultipartFile apkFile) throws IOException {
|
||||
|
||||
String downloadUrl = null;
|
||||
if (apkFile != null && !apkFile.isEmpty()) {
|
||||
String filename = UUID.randomUUID() + "_" + apkFile.getOriginalFilename();
|
||||
Path dir = Paths.get(uploadDir, "apk");
|
||||
Files.createDirectories(dir);
|
||||
Path dest = dir.resolve(filename);
|
||||
apkFile.transferTo(dest.toFile());
|
||||
downloadUrl = baseUrl + "/files/apk/" + filename;
|
||||
}
|
||||
|
||||
AppVersionEntity entity = new AppVersionEntity();
|
||||
entity.setId(UUID.randomUUID().toString());
|
||||
entity.setAppId(appId);
|
||||
entity.setPlatform(platform);
|
||||
entity.setVersionName(versionName);
|
||||
entity.setVersionCode(versionCode);
|
||||
entity.setDownloadUrl(downloadUrl);
|
||||
entity.setChangeLog(changeLog);
|
||||
entity.setForceUpdate(forceUpdate);
|
||||
entity.setPublishStatus(AppVersionEntity.PublishStatus.DRAFT);
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
return ResponseEntity.ok(ApiResponse.success(versionRepository.save(entity)));
|
||||
}
|
||||
|
||||
@PostMapping("/app/{id}/publish")
|
||||
public ResponseEntity<ApiResponse<AppVersionEntity>> publish(@PathVariable String id) {
|
||||
AppVersionEntity entity = versionRepository.findById(id).orElseThrow();
|
||||
entity.setPublishStatus(AppVersionEntity.PublishStatus.PUBLISHED);
|
||||
return ResponseEntity.ok(ApiResponse.success(versionRepository.save(entity)));
|
||||
}
|
||||
|
||||
@GetMapping("/app/list")
|
||||
public ResponseEntity<ApiResponse<List<AppVersionEntity>>> list(
|
||||
@RequestParam String appId, @RequestParam AppVersionEntity.Platform platform) {
|
||||
return ResponseEntity.ok(ApiResponse.success(
|
||||
versionRepository.findByAppIdAndPlatformOrderByVersionCodeDesc(appId, platform)));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,120 @@
|
||||
package com.xuqm.update.controller;
|
||||
|
||||
import com.xuqm.common.model.ApiResponse;
|
||||
import com.xuqm.update.entity.RnBundleEntity;
|
||||
import com.xuqm.update.repository.RnBundleRepository;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.DigestInputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/rn")
|
||||
public class RnBundleController {
|
||||
|
||||
private final RnBundleRepository bundleRepository;
|
||||
|
||||
@Value("${update.upload-dir:/tmp/xuqm-update}")
|
||||
private String uploadDir;
|
||||
|
||||
@Value("${update.base-url:http://localhost:8084}")
|
||||
private String baseUrl;
|
||||
|
||||
public RnBundleController(RnBundleRepository bundleRepository) {
|
||||
this.bundleRepository = bundleRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/update/check")
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> checkUpdate(
|
||||
@RequestParam String appId,
|
||||
@RequestParam String moduleId,
|
||||
@RequestParam String platform,
|
||||
@RequestParam String currentVersion) {
|
||||
|
||||
RnBundleEntity.Platform p = RnBundleEntity.Platform.valueOf(platform.toUpperCase());
|
||||
Optional<RnBundleEntity> latest = bundleRepository
|
||||
.findTopByAppIdAndModuleIdAndPlatformAndPublishStatusOrderByCreatedAtDesc(
|
||||
appId, moduleId, p, RnBundleEntity.PublishStatus.PUBLISHED);
|
||||
|
||||
if (latest.isEmpty()) {
|
||||
return ResponseEntity.ok(ApiResponse.success(Map.of("needsUpdate", false)));
|
||||
}
|
||||
|
||||
RnBundleEntity b = latest.get();
|
||||
boolean needsUpdate = !b.getVersion().equals(currentVersion);
|
||||
return ResponseEntity.ok(ApiResponse.success(Map.of(
|
||||
"needsUpdate", needsUpdate,
|
||||
"latestVersion", b.getVersion(),
|
||||
"downloadUrl", baseUrl + "/api/v1/rn/files/" + appId + "/" + platform.toLowerCase() + "/" + moduleId,
|
||||
"md5", b.getMd5(),
|
||||
"minCommonVersion", b.getMinCommonVersion() != null ? b.getMinCommonVersion() : "0.0.0",
|
||||
"note", b.getNote() != null ? b.getNote() : ""
|
||||
)));
|
||||
}
|
||||
|
||||
@PostMapping("/upload")
|
||||
public ResponseEntity<ApiResponse<RnBundleEntity>> upload(
|
||||
@RequestParam String appId,
|
||||
@RequestParam String moduleId,
|
||||
@RequestParam RnBundleEntity.Platform platform,
|
||||
@RequestParam String version,
|
||||
@RequestParam(required = false) String minCommonVersion,
|
||||
@RequestParam(required = false) String note,
|
||||
@RequestParam MultipartFile bundle) throws Exception {
|
||||
|
||||
String filename = moduleId + "." + platform.name().toLowerCase() + ".bundle";
|
||||
Path dir = Paths.get(uploadDir, "rn", appId, platform.name().toLowerCase(), moduleId);
|
||||
Files.createDirectories(dir);
|
||||
Path dest = dir.resolve(filename);
|
||||
|
||||
String md5 = computeMd5(bundle);
|
||||
bundle.transferTo(dest.toFile());
|
||||
|
||||
RnBundleEntity entity = new RnBundleEntity();
|
||||
entity.setId(UUID.randomUUID().toString());
|
||||
entity.setAppId(appId);
|
||||
entity.setModuleId(moduleId);
|
||||
entity.setPlatform(platform);
|
||||
entity.setVersion(version);
|
||||
entity.setBundleUrl(dest.toAbsolutePath().toString());
|
||||
entity.setMd5(md5);
|
||||
entity.setMinCommonVersion(minCommonVersion);
|
||||
entity.setNote(note);
|
||||
entity.setPublishStatus(RnBundleEntity.PublishStatus.DRAFT);
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
return ResponseEntity.ok(ApiResponse.success(bundleRepository.save(entity)));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/publish")
|
||||
public ResponseEntity<ApiResponse<RnBundleEntity>> publish(@PathVariable String id) {
|
||||
RnBundleEntity entity = bundleRepository.findById(id).orElseThrow();
|
||||
entity.setPublishStatus(RnBundleEntity.PublishStatus.PUBLISHED);
|
||||
return ResponseEntity.ok(ApiResponse.success(bundleRepository.save(entity)));
|
||||
}
|
||||
|
||||
private String computeMd5(MultipartFile file) throws Exception {
|
||||
MessageDigest digest = MessageDigest.getInstance("MD5");
|
||||
try (DigestInputStream dis = new DigestInputStream(file.getInputStream(), digest)) {
|
||||
byte[] buf = new byte[8192];
|
||||
while (dis.read(buf) != -1) {}
|
||||
}
|
||||
return HexFormat.of().formatHex(digest.digest());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
package com.xuqm.update.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "update_app_version")
|
||||
public class AppVersionEntity {
|
||||
|
||||
public enum Platform { ANDROID, IOS }
|
||||
public enum PublishStatus { DRAFT, PUBLISHED, DEPRECATED }
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String appId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 16)
|
||||
private Platform platform;
|
||||
|
||||
@Column(nullable = false, length = 32)
|
||||
private String versionName;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int versionCode;
|
||||
|
||||
@Column(length = 512)
|
||||
private String downloadUrl;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String changeLog;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean forceUpdate;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 16)
|
||||
private PublishStatus publishStatus;
|
||||
|
||||
@Column(length = 256)
|
||||
private String appStoreUrl;
|
||||
|
||||
@Column(length = 256)
|
||||
private String marketUrl;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getAppId() { return appId; }
|
||||
public void setAppId(String appId) { this.appId = appId; }
|
||||
|
||||
public Platform getPlatform() { return platform; }
|
||||
public void setPlatform(Platform platform) { this.platform = platform; }
|
||||
|
||||
public String getVersionName() { return versionName; }
|
||||
public void setVersionName(String versionName) { this.versionName = versionName; }
|
||||
|
||||
public int getVersionCode() { return versionCode; }
|
||||
public void setVersionCode(int versionCode) { this.versionCode = versionCode; }
|
||||
|
||||
public String getDownloadUrl() { return downloadUrl; }
|
||||
public void setDownloadUrl(String downloadUrl) { this.downloadUrl = downloadUrl; }
|
||||
|
||||
public String getChangeLog() { return changeLog; }
|
||||
public void setChangeLog(String changeLog) { this.changeLog = changeLog; }
|
||||
|
||||
public boolean isForceUpdate() { return forceUpdate; }
|
||||
public void setForceUpdate(boolean forceUpdate) { this.forceUpdate = forceUpdate; }
|
||||
|
||||
public PublishStatus getPublishStatus() { return publishStatus; }
|
||||
public void setPublishStatus(PublishStatus publishStatus) { this.publishStatus = publishStatus; }
|
||||
|
||||
public String getAppStoreUrl() { return appStoreUrl; }
|
||||
public void setAppStoreUrl(String appStoreUrl) { this.appStoreUrl = appStoreUrl; }
|
||||
|
||||
public String getMarketUrl() { return marketUrl; }
|
||||
public void setMarketUrl(String marketUrl) { this.marketUrl = marketUrl; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
package com.xuqm.update.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "update_rn_bundle")
|
||||
public class RnBundleEntity {
|
||||
|
||||
public enum Platform { ANDROID, IOS }
|
||||
public enum PublishStatus { DRAFT, PUBLISHED, DEPRECATED }
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String appId;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String moduleId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 16)
|
||||
private Platform platform;
|
||||
|
||||
@Column(nullable = false, length = 32)
|
||||
private String version;
|
||||
|
||||
@Column(nullable = false, length = 512)
|
||||
private String bundleUrl;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String md5;
|
||||
|
||||
@Column(length = 32)
|
||||
private String minCommonVersion;
|
||||
|
||||
@Column(length = 512)
|
||||
private String note;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 16)
|
||||
private PublishStatus publishStatus;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getAppId() { return appId; }
|
||||
public void setAppId(String appId) { this.appId = appId; }
|
||||
|
||||
public String getModuleId() { return moduleId; }
|
||||
public void setModuleId(String moduleId) { this.moduleId = moduleId; }
|
||||
|
||||
public Platform getPlatform() { return platform; }
|
||||
public void setPlatform(Platform platform) { this.platform = platform; }
|
||||
|
||||
public String getVersion() { return version; }
|
||||
public void setVersion(String version) { this.version = version; }
|
||||
|
||||
public String getBundleUrl() { return bundleUrl; }
|
||||
public void setBundleUrl(String bundleUrl) { this.bundleUrl = bundleUrl; }
|
||||
|
||||
public String getMd5() { return md5; }
|
||||
public void setMd5(String md5) { this.md5 = md5; }
|
||||
|
||||
public String getMinCommonVersion() { return minCommonVersion; }
|
||||
public void setMinCommonVersion(String minCommonVersion) { this.minCommonVersion = minCommonVersion; }
|
||||
|
||||
public String getNote() { return note; }
|
||||
public void setNote(String note) { this.note = note; }
|
||||
|
||||
public PublishStatus getPublishStatus() { return publishStatus; }
|
||||
public void setPublishStatus(PublishStatus publishStatus) { this.publishStatus = publishStatus; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.xuqm.update.repository;
|
||||
|
||||
import com.xuqm.update.entity.AppVersionEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface AppVersionRepository extends JpaRepository<AppVersionEntity, String> {
|
||||
List<AppVersionEntity> findByAppIdAndPlatformOrderByVersionCodeDesc(
|
||||
String appId, AppVersionEntity.Platform platform);
|
||||
Optional<AppVersionEntity> findTopByAppIdAndPlatformAndPublishStatusOrderByVersionCodeDesc(
|
||||
String appId, AppVersionEntity.Platform platform, AppVersionEntity.PublishStatus status);
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.xuqm.update.repository;
|
||||
|
||||
import com.xuqm.update.entity.RnBundleEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface RnBundleRepository extends JpaRepository<RnBundleEntity, String> {
|
||||
List<RnBundleEntity> findByAppIdAndModuleIdAndPlatformOrderByCreatedAtDesc(
|
||||
String appId, String moduleId, RnBundleEntity.Platform platform);
|
||||
Optional<RnBundleEntity> findTopByAppIdAndModuleIdAndPlatformAndPublishStatusOrderByCreatedAtDesc(
|
||||
String appId, String moduleId, RnBundleEntity.Platform platform, RnBundleEntity.PublishStatus status);
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
server:
|
||||
port: 8084
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: update-service
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/xuqm_update?useSSL=false&serverTimezone=UTC&createDatabaseIfNotExist=true&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: root
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: false
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 200MB
|
||||
max-request-size: 200MB
|
||||
|
||||
jwt:
|
||||
secret: xuqm-update-service-secret-key-must-be-at-least-256-bits-long-for-hmac
|
||||
expiration: 86400000
|
||||
|
||||
update:
|
||||
upload-dir: ${UPDATE_UPLOAD_DIR:/tmp/xuqm-update}
|
||||
base-url: ${UPDATE_BASE_URL:http://localhost:8084}
|
||||
正在加载...
在新工单中引用
屏蔽一个用户