feat: finalize backend deployment and api docs
这个提交包含在:
父节点
05c9639523
当前提交
8fe4ae99cc
22
Dockerfile
普通文件
22
Dockerfile
普通文件
@ -0,0 +1,22 @@
|
|||||||
|
ARG SERVICE_MODULE=tenant-service
|
||||||
|
|
||||||
|
FROM maven:3.9.9-eclipse-temurin-21 AS build
|
||||||
|
ARG SERVICE_MODULE
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
COPY pom.xml ./pom.xml
|
||||||
|
COPY common ./common
|
||||||
|
COPY tenant-service ./tenant-service
|
||||||
|
COPY im-service ./im-service
|
||||||
|
COPY push-service ./push-service
|
||||||
|
COPY update-service ./update-service
|
||||||
|
|
||||||
|
RUN mvn -pl ${SERVICE_MODULE} -am -DskipTests package
|
||||||
|
|
||||||
|
FROM eclipse-temurin:21-jre-jammy
|
||||||
|
WORKDIR /app
|
||||||
|
ARG SERVICE_MODULE
|
||||||
|
|
||||||
|
COPY --from=build /workspace/${SERVICE_MODULE}/target/${SERVICE_MODULE}-0.1.0-SNAPSHOT.jar /app/app.jar
|
||||||
|
|
||||||
|
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
|
||||||
@ -1,5 +1,9 @@
|
|||||||
# XuqmGroup-Server 后端文档
|
# XuqmGroup-Server 后端文档
|
||||||
|
|
||||||
|
> 当前推荐阅读入口:[`/docs/server/README.md`](../docs/server/README.md)
|
||||||
|
> 仓库内联调文档:[`docs/API_ACCESS.md`](./docs/API_ACCESS.md)
|
||||||
|
> 该文档保留为仓库内说明,线上地址、初始化账号与最新联调接口请以最新文档为准。
|
||||||
|
|
||||||
> Spring Boot 3.4.4 · Java 21 · Maven 多模块
|
> Spring Boot 3.4.4 · Java 21 · Maven 多模块
|
||||||
|
|
||||||
## 模块结构
|
## 模块结构
|
||||||
|
|||||||
143
docs/API_ACCESS.md
普通文件
143
docs/API_ACCESS.md
普通文件
@ -0,0 +1,143 @@
|
|||||||
|
# XuqmGroup Server 联调接口文档
|
||||||
|
|
||||||
|
> 最后更新:2026-04-24
|
||||||
|
|
||||||
|
## 线上入口
|
||||||
|
|
||||||
|
| 服务 | 地址 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 租户服务 | `https://sentry.xuqinmin.com/api/` | 认证、应用、子账号、运营平台 |
|
||||||
|
| IM HTTP | `https://sentry.xuqinmin.com/api/im/` | IM 登录、消息发送、撤回、历史消息 |
|
||||||
|
| IM WebSocket | `wss://sentry.xuqinmin.com/ws/im` | 实时消息 |
|
||||||
|
| App 更新 | `https://sentry.xuqinmin.com/api/v1/updates/` | 原生版本管理 |
|
||||||
|
| RN 热更新 | `https://sentry.xuqinmin.com/api/v1/rn/` | Bundle 热更新 |
|
||||||
|
|
||||||
|
## 初始化管理员账号
|
||||||
|
|
||||||
|
| 字段 | 值 |
|
||||||
|
|------|----|
|
||||||
|
| 用户名 | `admin` |
|
||||||
|
| 初始密码 | `Admin@123456` |
|
||||||
|
| 登录接口 | `POST /api/auth/ops/login` |
|
||||||
|
|
||||||
|
## 统一响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"status": "0",
|
||||||
|
"data": {},
|
||||||
|
"message": "success"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见错误码
|
||||||
|
|
||||||
|
| `code` | `status` | 说明 |
|
||||||
|
|--------|----------|------|
|
||||||
|
| `200` | `"0"` | 成功 |
|
||||||
|
| `400` | `"1"` | 参数校验失败或请求不合法 |
|
||||||
|
| `401` | `"1"` | 未登录、Token 无效或已过期 |
|
||||||
|
| `403` | `"1"` | 无权限访问 |
|
||||||
|
| `500` | `"1"` | 服务端内部错误 |
|
||||||
|
|
||||||
|
## 鉴权规则
|
||||||
|
|
||||||
|
| 场景 | 鉴权方式 |
|
||||||
|
|------|----------|
|
||||||
|
| 租户平台接口 | `Authorization: Bearer <tenant_jwt>` |
|
||||||
|
| 运营平台接口 | `Authorization: Bearer <ops_jwt>` |
|
||||||
|
| IM HTTP 接口 | `Authorization: Bearer <im_jwt>` |
|
||||||
|
| IM WebSocket | `?token=<im_jwt>` |
|
||||||
|
| App 更新检查 | 无需登录 |
|
||||||
|
| RN 更新检查 | 无需登录 |
|
||||||
|
| Bundle 下载 | 无需登录 |
|
||||||
|
|
||||||
|
## 核心接口清单
|
||||||
|
|
||||||
|
### tenant-service
|
||||||
|
|
||||||
|
| 方法 | 路径 | 鉴权 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| GET | `/api/auth/captcha` | 否 | 获取图形验证码 |
|
||||||
|
| POST | `/api/auth/send-email-code` | 否 | 发送邮箱验证码 |
|
||||||
|
| POST | `/api/auth/register` | 否 | 注册主账号 |
|
||||||
|
| POST | `/api/auth/login` | 否 | 租户登录 |
|
||||||
|
| POST | `/api/auth/forgot-password` | 否 | 发送找回密码邮件 |
|
||||||
|
| POST | `/api/auth/reset-password` | 否 | 重置密码 |
|
||||||
|
| GET | `/api/apps` | 是 | 应用列表 |
|
||||||
|
| GET | `/api/apps/{id}` | 是 | 应用详情 |
|
||||||
|
| POST | `/api/apps` | 是 | 创建应用 |
|
||||||
|
| PUT | `/api/apps/{id}` | 是 | 更新应用 |
|
||||||
|
| DELETE | `/api/apps/{id}` | 是 | 删除应用 |
|
||||||
|
| GET | `/api/apps/{appId}/services` | 是 | 服务列表 |
|
||||||
|
| POST | `/api/apps/{appId}/services/toggle` | 是 | 开关服务 |
|
||||||
|
| POST | `/api/apps/{appId}/services/{id}/regenerate-key` | 是 | 重新生成服务密钥 |
|
||||||
|
| GET | `/api/sub-accounts` | 是 | 子账号列表 |
|
||||||
|
| POST | `/api/sub-accounts/send-verify-code` | 是 | 子账号邮箱验证码 |
|
||||||
|
| POST | `/api/sub-accounts/verify-email` | 是 | 校验子账号邮箱 |
|
||||||
|
| POST | `/api/sub-accounts` | 是 | 创建子账号 |
|
||||||
|
| DELETE | `/api/sub-accounts/{id}` | 是 | 禁用子账号 |
|
||||||
|
| POST | `/api/auth/ops/login` | 否 | 运营管理员登录 |
|
||||||
|
| GET | `/api/ops/tenants` | 是 | 运营租户列表 |
|
||||||
|
| POST | `/api/ops/tenants/{id}/toggle-status` | 是 | 租户启停 |
|
||||||
|
| GET | `/api/ops/statistics` | 是 | 统计面板 |
|
||||||
|
|
||||||
|
### im-service
|
||||||
|
|
||||||
|
| 方法 | 路径 | 鉴权 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| POST | `/api/im/auth/login` | 否 | 获取 IM Token |
|
||||||
|
| POST | `/api/im/messages/send` | 是 | 发送消息 |
|
||||||
|
| POST | `/api/im/messages/{id}/revoke` | 是 | 撤回消息 |
|
||||||
|
| GET | `/api/im/messages/history/{toId}` | 是 | 查询历史消息 |
|
||||||
|
| WS | `/ws/im` | IM Token | 建立实时连接 |
|
||||||
|
|
||||||
|
### push-service
|
||||||
|
|
||||||
|
| 方法 | 路径 | 鉴权 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| POST | `/api/push/register` | 是 | 注册设备 token |
|
||||||
|
| POST | `/api/push/send` | 是 | 发送推送通知 |
|
||||||
|
|
||||||
|
### update-service
|
||||||
|
|
||||||
|
| 方法 | 路径 | 鉴权 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| GET | `/api/v1/updates/app/check` | 否 | 检查 App 更新 |
|
||||||
|
| POST | `/api/v1/updates/app/upload` | 是 | 上传 App 版本 |
|
||||||
|
| POST | `/api/v1/updates/app/{id}/publish` | 是 | 发布 App 版本 |
|
||||||
|
| GET | `/api/v1/updates/app/list` | 是 | App 版本列表 |
|
||||||
|
| GET | `/api/v1/updates/files/apk/{filename}` | 否 | 下载 APK |
|
||||||
|
| GET | `/api/v1/rn/update/check` | 否 | 检查 RN 热更新 |
|
||||||
|
| POST | `/api/v1/rn/upload` | 是 | 上传 Bundle |
|
||||||
|
| POST | `/api/v1/rn/{id}/publish` | 是 | 发布 Bundle |
|
||||||
|
| GET | `/api/v1/rn/files/{appId}/{platform}/{moduleId}` | 否 | 下载 Bundle |
|
||||||
|
|
||||||
|
## curl 示例
|
||||||
|
|
||||||
|
### 运营平台登录
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'https://sentry.xuqinmin.com/api/auth/ops/login' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"username":"admin","password":"Admin@123456"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### App 更新检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl 'https://sentry.xuqinmin.com/api/v1/updates/app/check?appId=ak_demo_chat&platform=ANDROID¤tVersionCode=1'
|
||||||
|
```
|
||||||
|
|
||||||
|
### RN 热更新检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl 'https://sentry.xuqinmin.com/api/v1/rn/update/check?appId=ak_demo_chat&platform=ANDROID&moduleId=chat-home¤tVersion=1.0.0'
|
||||||
|
```
|
||||||
|
|
||||||
|
### IM 登录
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'https://sentry.xuqinmin.com/api/im/auth/login?appId=ak_demo_chat&userId=demo_alice'
|
||||||
|
```
|
||||||
@ -45,9 +45,10 @@ public class MessageController {
|
|||||||
@GetMapping("/history/{toId}")
|
@GetMapping("/history/{toId}")
|
||||||
public ResponseEntity<ApiResponse<?>> history(
|
public ResponseEntity<ApiResponse<?>> history(
|
||||||
@PathVariable String toId,
|
@PathVariable String toId,
|
||||||
|
@AuthenticationPrincipal String userId,
|
||||||
@RequestParam String appId,
|
@RequestParam String appId,
|
||||||
@RequestParam(defaultValue = "0") int page,
|
@RequestParam(defaultValue = "0") int page,
|
||||||
@RequestParam(defaultValue = "20") int size) {
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
return ResponseEntity.ok(ApiResponse.success(messageService.history(appId, toId, page, size)));
|
return ResponseEntity.ok(ApiResponse.success(messageService.history(appId, userId, toId, page, size)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,26 @@ import com.xuqm.im.entity.ImMessageEntity;
|
|||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
public interface ImMessageRepository extends JpaRepository<ImMessageEntity, String> {
|
public interface ImMessageRepository extends JpaRepository<ImMessageEntity, String> {
|
||||||
Page<ImMessageEntity> findByAppIdAndToIdOrderByCreatedAtDesc(
|
Page<ImMessageEntity> findByAppIdAndToIdOrderByCreatedAtDesc(
|
||||||
String appId, String toId, Pageable pageable);
|
String appId, String toId, Pageable pageable);
|
||||||
Page<ImMessageEntity> findByAppIdAndFromUserIdAndToIdOrderByCreatedAtDesc(
|
Page<ImMessageEntity> findByAppIdAndFromUserIdAndToIdOrderByCreatedAtDesc(
|
||||||
String appId, String fromUserId, String toId, Pageable pageable);
|
String appId, String fromUserId, String toId, Pageable pageable);
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
select m from ImMessageEntity m
|
||||||
|
where m.appId = :appId
|
||||||
|
and m.chatType = com.xuqm.im.entity.ImMessageEntity$ChatType.SINGLE
|
||||||
|
and ((m.fromUserId = :userId and m.toId = :peerId)
|
||||||
|
or (m.fromUserId = :peerId and m.toId = :userId))
|
||||||
|
order by m.createdAt desc
|
||||||
|
""")
|
||||||
|
Page<ImMessageEntity> findSingleConversation(
|
||||||
|
@Param("appId") String appId,
|
||||||
|
@Param("userId") String userId,
|
||||||
|
@Param("peerId") String peerId,
|
||||||
|
Pageable pageable);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,6 +72,9 @@ public class MessageService {
|
|||||||
? "/user/" + req.toId() + "/queue/messages"
|
? "/user/" + req.toId() + "/queue/messages"
|
||||||
: "/topic/group/" + req.toId();
|
: "/topic/group/" + req.toId();
|
||||||
messagingTemplate.convertAndSend(destination, message);
|
messagingTemplate.convertAndSend(destination, message);
|
||||||
|
if (req.chatType() == ImMessageEntity.ChatType.SINGLE && !fromUserId.equals(req.toId())) {
|
||||||
|
messagingTemplate.convertAndSend("/user/" + fromUserId + "/queue/messages", message);
|
||||||
|
}
|
||||||
|
|
||||||
dispatchWebhooks(appId, message);
|
dispatchWebhooks(appId, message);
|
||||||
|
|
||||||
@ -89,12 +92,21 @@ public class MessageService {
|
|||||||
}
|
}
|
||||||
message.setStatus(ImMessageEntity.MsgStatus.REVOKED);
|
message.setStatus(ImMessageEntity.MsgStatus.REVOKED);
|
||||||
message.setMsgType(ImMessageEntity.MsgType.REVOKED);
|
message.setMsgType(ImMessageEntity.MsgType.REVOKED);
|
||||||
return messageRepository.save(message);
|
ImMessageEntity saved = messageRepository.save(message);
|
||||||
|
if (saved.getChatType() == ImMessageEntity.ChatType.SINGLE) {
|
||||||
|
messagingTemplate.convertAndSend("/user/" + saved.getToId() + "/queue/messages", saved);
|
||||||
|
if (!saved.getFromUserId().equals(saved.getToId())) {
|
||||||
|
messagingTemplate.convertAndSend("/user/" + saved.getFromUserId() + "/queue/messages", saved);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
messagingTemplate.convertAndSend("/topic/group/" + saved.getToId(), saved);
|
||||||
|
}
|
||||||
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Page<ImMessageEntity> history(String appId, String toId, int page, int size) {
|
public Page<ImMessageEntity> history(String appId, String userId, String toId, int page, int size) {
|
||||||
return messageRepository.findByAppIdAndToIdOrderByCreatedAtDesc(
|
return messageRepository.findSingleConversation(
|
||||||
appId, toId, PageRequest.of(page, size));
|
appId, userId, toId, PageRequest.of(page, size));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Async
|
@Async
|
||||||
|
|||||||
7
pom.xml
7
pom.xml
@ -78,6 +78,13 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
<version>${spring-boot.version}</version>
|
<version>${spring-boot.version}</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>repackage</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</pluginManagement>
|
</pluginManagement>
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import com.xuqm.tenant.repository.EmailVerificationRepository;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.mail.SimpleMailMessage;
|
import org.springframework.mail.SimpleMailMessage;
|
||||||
import org.springframework.mail.javamail.JavaMailSender;
|
import org.springframework.mail.javamail.JavaMailSender;
|
||||||
import org.springframework.scheduling.annotation.Async;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@ -32,7 +32,7 @@ public class EmailService {
|
|||||||
this.verificationRepository = verificationRepository;
|
this.verificationRepository = verificationRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Async
|
@Transactional
|
||||||
public void sendVerificationCode(String email, String purpose) {
|
public void sendVerificationCode(String email, String purpose) {
|
||||||
String code = String.format("%06d", random.nextInt(1_000_000));
|
String code = String.format("%06d", random.nextInt(1_000_000));
|
||||||
|
|
||||||
@ -44,13 +44,13 @@ public class EmailService {
|
|||||||
entity.setUsed(false);
|
entity.setUsed(false);
|
||||||
entity.setCreatedAt(LocalDateTime.now());
|
entity.setCreatedAt(LocalDateTime.now());
|
||||||
entity.setExpiresAt(LocalDateTime.now().plusSeconds(expireSeconds));
|
entity.setExpiresAt(LocalDateTime.now().plusSeconds(expireSeconds));
|
||||||
verificationRepository.save(entity);
|
|
||||||
|
|
||||||
SimpleMailMessage message = new SimpleMailMessage();
|
SimpleMailMessage message = new SimpleMailMessage();
|
||||||
message.setFrom(fromAddress);
|
message.setFrom(fromAddress);
|
||||||
message.setTo(email);
|
message.setTo(email);
|
||||||
message.setSubject("XuqmGroup - 邮箱验证码");
|
message.setSubject("XuqmGroup - 邮箱验证码");
|
||||||
message.setText(String.format("您的验证码是:%s,%d分钟内有效。", code, expireSeconds / 60));
|
message.setText(String.format("您的验证码是:%s,%d分钟内有效。", code, expireSeconds / 60));
|
||||||
|
|
||||||
|
verificationRepository.save(entity);
|
||||||
mailSender.send(message);
|
mailSender.send(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,34 @@
|
|||||||
|
package com.xuqm.update.config;
|
||||||
|
|
||||||
|
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.web.SecurityFilterChain;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
|
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
.requestMatchers(
|
||||||
|
"/actuator/**",
|
||||||
|
"/api/v1/updates/app/check",
|
||||||
|
"/api/v1/rn/update/check",
|
||||||
|
"/api/v1/rn/files/**",
|
||||||
|
"/files/apk/**"
|
||||||
|
).permitAll()
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.httpBasic(AbstractHttpConfigurer::disable)
|
||||||
|
.formLogin(AbstractHttpConfigurer::disable);
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -62,7 +62,7 @@ public class RnBundleController {
|
|||||||
return ResponseEntity.ok(ApiResponse.success(Map.of(
|
return ResponseEntity.ok(ApiResponse.success(Map.of(
|
||||||
"needsUpdate", needsUpdate,
|
"needsUpdate", needsUpdate,
|
||||||
"latestVersion", b.getVersion(),
|
"latestVersion", b.getVersion(),
|
||||||
"downloadUrl", baseUrl + "/api/v1/rn/files/" + appId + "/" + platform.toLowerCase() + "/" + moduleId,
|
"downloadUrl", resolvePublicBaseUrl() + "/api/v1/rn/files/" + appId + "/" + platform.toLowerCase() + "/" + moduleId,
|
||||||
"md5", b.getMd5(),
|
"md5", b.getMd5(),
|
||||||
"minCommonVersion", b.getMinCommonVersion() != null ? b.getMinCommonVersion() : "0.0.0",
|
"minCommonVersion", b.getMinCommonVersion() != null ? b.getMinCommonVersion() : "0.0.0",
|
||||||
"note", b.getNote() != null ? b.getNote() : ""
|
"note", b.getNote() != null ? b.getNote() : ""
|
||||||
@ -117,4 +117,13 @@ public class RnBundleController {
|
|||||||
}
|
}
|
||||||
return HexFormat.of().formatHex(digest.digest());
|
return HexFormat.of().formatHex(digest.digest());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String resolvePublicBaseUrl() {
|
||||||
|
String normalized = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
|
||||||
|
String suffix = "/api/v1/updates";
|
||||||
|
if (normalized.endsWith(suffix)) {
|
||||||
|
return normalized.substring(0, normalized.length() - suffix.length());
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,54 @@
|
|||||||
|
package com.xuqm.update.controller;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.core.io.FileSystemResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.ContentDisposition;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
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.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping
|
||||||
|
public class UpdateFileController {
|
||||||
|
|
||||||
|
@Value("${update.upload-dir:/tmp/xuqm-update}")
|
||||||
|
private String uploadDir;
|
||||||
|
|
||||||
|
@GetMapping({"/files/apk/{filename:.+}", "/api/v1/updates/files/apk/{filename:.+}"})
|
||||||
|
public ResponseEntity<Resource> downloadApk(@PathVariable String filename) throws Exception {
|
||||||
|
Path file = Paths.get(uploadDir, "apk", filename).normalize();
|
||||||
|
return serveFile(file, MediaType.parseMediaType("application/vnd.android.package-archive"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/api/v1/rn/files/{appId}/{platform}/{moduleId}")
|
||||||
|
public ResponseEntity<Resource> downloadRnBundle(
|
||||||
|
@PathVariable String appId,
|
||||||
|
@PathVariable String platform,
|
||||||
|
@PathVariable String moduleId) throws Exception {
|
||||||
|
Path file = Paths.get(uploadDir, "rn", appId, platform, moduleId,
|
||||||
|
moduleId + "." + platform + ".bundle").normalize();
|
||||||
|
return serveFile(file, MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<Resource> serveFile(Path file, MediaType mediaType) throws Exception {
|
||||||
|
if (!Files.exists(file) || !Files.isRegularFile(file)) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
FileSystemResource resource = new FileSystemResource(file);
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(mediaType)
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||||
|
ContentDisposition.attachment().filename(file.getFileName().toString()).build().toString())
|
||||||
|
.body(resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户