更新+mock部分
This commit is contained in:
21
.agents/skills/k12-api-response-standard/SKILL.md
Normal file
21
.agents/skills/k12-api-response-standard/SKILL.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
name: k12-api-response-standard
|
||||||
|
description: 当任务涉及前后端接口联调、请求封装、异常处理时使用,统一 code/message/data/traceId 响应体契约
|
||||||
|
---
|
||||||
|
# K12 统一响应体技能
|
||||||
|
## 何时使用
|
||||||
|
- 新增或改造后端 API
|
||||||
|
- 改动前端/小程序请求层
|
||||||
|
- 修复“后端已报错但前端当成功处理”的联调问题
|
||||||
|
## 统一契约
|
||||||
|
- 响应体结构固定为:`code/message/data/traceId`
|
||||||
|
- 成功条件:`code === 0`
|
||||||
|
- 业务失败:`code !== 0`,前端应抛错并使用 `message` 透出
|
||||||
|
## 执行步骤
|
||||||
|
1. 后端控制器与异常处理统一返回 `ApiResponse`。
|
||||||
|
2. 前端与小程序请求封装统一校验 `code` 字段,不仅依赖 HTTP 状态码。
|
||||||
|
3. 新接口落地时同步检查类型声明与调用方数据解包方式,避免重复定义结构。
|
||||||
|
4. 同步更新 `docs/architecture/api-design.md` 中响应体约定,保持文档与代码一致。
|
||||||
|
## 约束
|
||||||
|
- 不引入破坏性字段重命名(如把 `message` 改为 `msg`)。
|
||||||
|
- 保留 `traceId` 以支持链路追踪。
|
||||||
17
.agents/skills/k12-comment-header-standard/SKILL.md
Normal file
17
.agents/skills/k12-comment-header-standard/SKILL.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: k12-comment-header-standard
|
||||||
|
description: 当任务涉及注释模板、global.code-snippets 或 .fileheader 配置时使用,确保 author 与当前仓库 git user.name 对齐
|
||||||
|
---
|
||||||
|
# K12 注释头规范技能
|
||||||
|
## 何时使用
|
||||||
|
- 用户要求统一文件头/函数注释规范
|
||||||
|
- 用户要求 `@author` 与 git 用户名一致
|
||||||
|
- 任务涉及 `global.code-snippets` 或 `.fileheader/fileheader.config.yaml`
|
||||||
|
## 执行步骤
|
||||||
|
1. 读取仓库 `git config user.name`,以此作为 `author` 基线。
|
||||||
|
2. 更新 `global.code-snippets` 中 `FileHeader` 与 `Method` 的 author 默认值。
|
||||||
|
3. 若项目使用 `turbo-file-header`,检查 `.fileheader/fileheader.config.yaml` 的 `@author` 字段,确保与团队约定一致。
|
||||||
|
4. 修改后验证 JSON/YAML 可解析,避免因注释模板格式错误导致编辑器失效。
|
||||||
|
## 约束
|
||||||
|
- 不修改业务逻辑代码,只处理注释与模板规范。
|
||||||
|
- 需要保留已有注释字段顺序(description/filename/author/copyright/since)。
|
||||||
22
.agents/skills/k12-restful-api-style/SKILL.md
Normal file
22
.agents/skills/k12-restful-api-style/SKILL.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: k12-restful-api-style
|
||||||
|
description: 当任务涉及接口命名、路径规划或网关转发时使用,统一 K12Study 的 RESTful 风格并强制旧接口不兼容
|
||||||
|
---
|
||||||
|
# K12 RESTful 接口风格技能
|
||||||
|
## 何时使用
|
||||||
|
- 新增 API 设计
|
||||||
|
- 旧接口路径改造(如 `current-user`、`* /tree`)
|
||||||
|
- 需要同步网关白名单与客户端调用路径
|
||||||
|
## 设计规则
|
||||||
|
- 路径优先使用资源名(名词)而不是动作名。
|
||||||
|
- 集合资源使用复数:`/users`、`/departments`。
|
||||||
|
- “当前用户”使用语义路径:`/users/current`。
|
||||||
|
- 树形结构优先通过资源路径表达:`/areas`、`/tenants`、`/departments`。
|
||||||
|
## 执行步骤
|
||||||
|
1. 在控制器中提供 RESTful 主路径。
|
||||||
|
2. 删除旧路径映射,不保留兼容别名。
|
||||||
|
3. 同步更新前端、小程序 API 调用路径。
|
||||||
|
4. 同步更新鉴权白名单与文档(`docs/architecture/api-design.md`、`docs/architecture/logical-view.md`)。
|
||||||
|
## 约束
|
||||||
|
- 保持网关 `/api/*` 统一入口不变。
|
||||||
|
- 旧接口不兼容,禁止新增或恢复旧路径别名。
|
||||||
19
.fileheader/fileheader.config.yaml
Normal file
19
.fileheader/fileheader.config.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
dateFormat: YYYY-MM-DD
|
||||||
|
autoInsertOnCreateFile: true
|
||||||
|
autoUpdateOnSave: false
|
||||||
|
useJSDocStyle: false
|
||||||
|
customVariables:
|
||||||
|
- name: description
|
||||||
|
value: ""
|
||||||
|
fileheader:
|
||||||
|
- label: " * @description"
|
||||||
|
value: "{{description}}"
|
||||||
|
usePrevious: true
|
||||||
|
- label: " * @filename"
|
||||||
|
value: "{{fileName}}"
|
||||||
|
- label: " * @author"
|
||||||
|
value: "{{userName}}"
|
||||||
|
- label: " * @copyright"
|
||||||
|
value: "xyzh"
|
||||||
|
- label: " * @since"
|
||||||
|
value: "{{birthtime}}"
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,7 +2,9 @@ urbanLifeline
|
|||||||
Tik
|
Tik
|
||||||
schoolNewsServ
|
schoolNewsServ
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/extensions.json
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.tmp-run/
|
.tmp-run/
|
||||||
|
|
||||||
|
|||||||
5
.vscode/extensions.json
vendored
Normal file
5
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"ygqygq2.turbo-file-header"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"java.compile.nullAnalysis.mode": "automatic"
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ const { request } = require("../utils/request");
|
|||||||
|
|
||||||
function login(data) {
|
function login(data) {
|
||||||
return request({
|
return request({
|
||||||
url: "/api/auth/login",
|
url: "/api/auth/tokens",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
data
|
data
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,38 @@ function getRouteMeta() {
|
|||||||
method: "GET"
|
method: "GET"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function getCurrentUser() {
|
||||||
|
return request({
|
||||||
|
url: "/api/upms/users/current",
|
||||||
|
method: "GET"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAreas() {
|
||||||
|
return request({
|
||||||
|
url: "/api/upms/areas",
|
||||||
|
method: "GET"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTenants() {
|
||||||
|
return request({
|
||||||
|
url: "/api/upms/tenants",
|
||||||
|
method: "GET"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDepartments() {
|
||||||
|
return request({
|
||||||
|
url: "/api/upms/departments",
|
||||||
|
method: "GET"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getRouteMeta
|
getRouteMeta,
|
||||||
|
getCurrentUser,
|
||||||
|
getAreas,
|
||||||
|
getTenants,
|
||||||
|
getDepartments
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
const BASE_URL = "http://localhost:8088";
|
const BASE_URL = "http://localhost:8088";
|
||||||
|
|
||||||
|
function isApiResponse(payload) {
|
||||||
|
return (
|
||||||
|
payload &&
|
||||||
|
typeof payload === "object" &&
|
||||||
|
Object.prototype.hasOwnProperty.call(payload, "code") &&
|
||||||
|
Object.prototype.hasOwnProperty.call(payload, "message") &&
|
||||||
|
Object.prototype.hasOwnProperty.call(payload, "data")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function request(options) {
|
function request(options) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
wx.request({
|
wx.request({
|
||||||
@@ -10,7 +20,28 @@ function request(options) {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(options.header || {})
|
...(options.header || {})
|
||||||
},
|
},
|
||||||
success: (response) => resolve(response.data),
|
success: (response) => {
|
||||||
|
const payload = response.data;
|
||||||
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||||
|
const message =
|
||||||
|
payload && typeof payload === "object" && payload.message
|
||||||
|
? String(payload.message)
|
||||||
|
: `Request failed with status ${response.statusCode}`;
|
||||||
|
reject(new Error(message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isApiResponse(payload)) {
|
||||||
|
if (payload.code !== 0) {
|
||||||
|
reject(new Error(payload.message || `Business request failed with code ${payload.code}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(new Error("响应体不符合统一规范:缺少 code/message/data 字段"));
|
||||||
|
},
|
||||||
fail: reject
|
fail: reject
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.k12study.api.auth.dto;
|
||||||
|
|
||||||
|
public record RefreshTokenRequest(String refreshToken) {
|
||||||
|
}
|
||||||
@@ -4,9 +4,9 @@ import java.util.List;
|
|||||||
|
|
||||||
public record AreaNodeDto(
|
public record AreaNodeDto(
|
||||||
String areaCode,
|
String areaCode,
|
||||||
|
String parentCode,
|
||||||
String areaName,
|
String areaName,
|
||||||
String areaLevel,
|
String areaLevel,
|
||||||
String provinceCode,
|
|
||||||
List<AreaNodeDto> children
|
List<AreaNodeDto> children
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ public record CurrentRouteUserDto(
|
|||||||
String userId,
|
String userId,
|
||||||
String username,
|
String username,
|
||||||
String displayName,
|
String displayName,
|
||||||
|
String adcode,
|
||||||
String tenantId,
|
String tenantId,
|
||||||
|
String tenantPath,
|
||||||
String deptId,
|
String deptId,
|
||||||
|
String deptPath,
|
||||||
List<String> permissionCodes
|
List<String> permissionCodes
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import java.util.List;
|
|||||||
|
|
||||||
public record DeptNodeDto(
|
public record DeptNodeDto(
|
||||||
String deptId,
|
String deptId,
|
||||||
|
String parentDeptId,
|
||||||
String deptName,
|
String deptName,
|
||||||
String deptType,
|
String deptType,
|
||||||
String tenantId,
|
String tenantId,
|
||||||
|
String adcode,
|
||||||
|
String tenantPath,
|
||||||
String deptPath,
|
String deptPath,
|
||||||
List<DeptNodeDto> children
|
List<DeptNodeDto> children
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import java.util.List;
|
|||||||
|
|
||||||
public record TenantNodeDto(
|
public record TenantNodeDto(
|
||||||
String tenantId,
|
String tenantId,
|
||||||
|
String parentTenantId,
|
||||||
String tenantName,
|
String tenantName,
|
||||||
String tenantType,
|
String tenantType,
|
||||||
String provinceCode,
|
String adcode,
|
||||||
String areaCode,
|
|
||||||
String tenantPath,
|
String tenantPath,
|
||||||
List<TenantNodeDto> children
|
List<TenantNodeDto> children
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.k12study.api.upms.remote;
|
||||||
|
|
||||||
|
public final class UpmsApiPaths {
|
||||||
|
private UpmsApiPaths() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String BASE = "/upms";
|
||||||
|
public static final String ROUTES = BASE + "/routes";
|
||||||
|
public static final String USERS_CURRENT = BASE + "/users/current";
|
||||||
|
public static final String AREAS = BASE + "/areas";
|
||||||
|
public static final String TENANTS = BASE + "/tenants";
|
||||||
|
public static final String DEPARTMENTS = BASE + "/departments";
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.k12study.api.upms.remote;
|
||||||
|
|
||||||
|
import com.k12study.api.upms.dto.AreaNodeDto;
|
||||||
|
import com.k12study.api.upms.dto.CurrentRouteUserDto;
|
||||||
|
import com.k12study.api.upms.dto.DeptNodeDto;
|
||||||
|
import com.k12study.api.upms.dto.RouteNodeDto;
|
||||||
|
import com.k12study.api.upms.dto.TenantNodeDto;
|
||||||
|
import com.k12study.common.api.response.ApiResponse;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface UpmsRemoteApi {
|
||||||
|
ApiResponse<List<RouteNodeDto>> routes();
|
||||||
|
|
||||||
|
ApiResponse<CurrentRouteUserDto> currentUser();
|
||||||
|
|
||||||
|
ApiResponse<List<AreaNodeDto>> areas();
|
||||||
|
|
||||||
|
ApiResponse<List<TenantNodeDto>> tenants();
|
||||||
|
|
||||||
|
ApiResponse<List<DeptNodeDto>> departments();
|
||||||
|
}
|
||||||
@@ -2,15 +2,16 @@ package com.k12study.auth.controller;
|
|||||||
|
|
||||||
import com.k12study.api.auth.dto.CurrentUserResponse;
|
import com.k12study.api.auth.dto.CurrentUserResponse;
|
||||||
import com.k12study.api.auth.dto.LoginRequest;
|
import com.k12study.api.auth.dto.LoginRequest;
|
||||||
|
import com.k12study.api.auth.dto.RefreshTokenRequest;
|
||||||
import com.k12study.api.auth.dto.TokenResponse;
|
import com.k12study.api.auth.dto.TokenResponse;
|
||||||
import com.k12study.auth.service.AuthService;
|
import com.k12study.auth.service.AuthService;
|
||||||
import com.k12study.common.api.response.ApiResponse;
|
import com.k12study.common.api.response.ApiResponse;
|
||||||
|
import com.k12study.common.web.exception.BizException;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestHeader;
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
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.bind.annotation.RestController;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -22,17 +23,21 @@ public class AuthController {
|
|||||||
this.authService = authService;
|
this.authService = authService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/tokens")
|
||||||
public ApiResponse<TokenResponse> login(@RequestBody LoginRequest request) {
|
public ApiResponse<TokenResponse> login(@RequestBody LoginRequest request) {
|
||||||
return ApiResponse.success("登录成功", authService.login(request));
|
return ApiResponse.success("登录成功", authService.login(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/refresh")
|
@PostMapping("/tokens/refresh")
|
||||||
public ApiResponse<TokenResponse> refresh(@RequestParam("refreshToken") String refreshToken) {
|
public ApiResponse<TokenResponse> refresh(@RequestBody RefreshTokenRequest request) {
|
||||||
|
String refreshToken = request == null ? null : request.refreshToken();
|
||||||
|
if (refreshToken == null || refreshToken.isBlank()) {
|
||||||
|
throw new BizException(400, "refreshToken 不能为空");
|
||||||
|
}
|
||||||
return ApiResponse.success("刷新成功", authService.refresh(refreshToken));
|
return ApiResponse.success("刷新成功", authService.refresh(refreshToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/current-user")
|
@GetMapping("/users/current")
|
||||||
public ApiResponse<CurrentUserResponse> currentUser(
|
public ApiResponse<CurrentUserResponse> currentUser(
|
||||||
@RequestHeader(value = "Authorization", required = false) String authorizationHeader) {
|
@RequestHeader(value = "Authorization", required = false) String authorizationHeader) {
|
||||||
return ApiResponse.success(authService.currentUser(authorizationHeader));
|
return ApiResponse.success(authService.currentUser(authorizationHeader));
|
||||||
|
|||||||
@@ -23,6 +23,6 @@ auth:
|
|||||||
enabled: true
|
enabled: true
|
||||||
gateway-mode: true
|
gateway-mode: true
|
||||||
whitelist:
|
whitelist:
|
||||||
- /auth/login
|
- /auth/tokens
|
||||||
- /auth/refresh
|
- /auth/tokens/refresh
|
||||||
- /actuator/**
|
- /actuator/**
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ auth:
|
|||||||
enabled: true
|
enabled: true
|
||||||
gateway-mode: false
|
gateway-mode: false
|
||||||
whitelist:
|
whitelist:
|
||||||
- /auth/login
|
- /auth/tokens
|
||||||
- /auth/refresh
|
- /auth/tokens/refresh
|
||||||
- /actuator/**
|
- /actuator/**
|
||||||
|
|
||||||
ai:
|
ai:
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ public class AuthProperties {
|
|||||||
private String secret = "k12study-dev-secret-k12study-dev-secret";
|
private String secret = "k12study-dev-secret-k12study-dev-secret";
|
||||||
private Duration accessTokenTtl = Duration.ofHours(12);
|
private Duration accessTokenTtl = Duration.ofHours(12);
|
||||||
private Duration refreshTokenTtl = Duration.ofDays(7);
|
private Duration refreshTokenTtl = Duration.ofDays(7);
|
||||||
private List<String> whitelist = new ArrayList<>(List.of("/actuator/**", "/auth/login", "/auth/refresh"));
|
private List<String> whitelist = new ArrayList<>(List.of(
|
||||||
|
"/actuator/**",
|
||||||
|
"/auth/tokens",
|
||||||
|
"/auth/tokens/refresh"
|
||||||
|
));
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
return enabled;
|
return enabled;
|
||||||
|
|||||||
@@ -29,6 +29,6 @@ management:
|
|||||||
auth:
|
auth:
|
||||||
enabled: true
|
enabled: true
|
||||||
whitelist:
|
whitelist:
|
||||||
- /api/auth/login
|
- /api/auth/tokens
|
||||||
- /api/auth/refresh
|
- /api/auth/tokens/refresh
|
||||||
- /actuator/**
|
- /actuator/**
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.k12study.api.upms.dto.CurrentRouteUserDto;
|
|||||||
import com.k12study.api.upms.dto.DeptNodeDto;
|
import com.k12study.api.upms.dto.DeptNodeDto;
|
||||||
import com.k12study.api.upms.dto.RouteNodeDto;
|
import com.k12study.api.upms.dto.RouteNodeDto;
|
||||||
import com.k12study.api.upms.dto.TenantNodeDto;
|
import com.k12study.api.upms.dto.TenantNodeDto;
|
||||||
|
import com.k12study.api.upms.remote.UpmsApiPaths;
|
||||||
import com.k12study.common.api.response.ApiResponse;
|
import com.k12study.common.api.response.ApiResponse;
|
||||||
import com.k12study.upms.service.UpmsQueryService;
|
import com.k12study.upms.service.UpmsQueryService;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -13,7 +14,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/upms")
|
@RequestMapping(UpmsApiPaths.BASE)
|
||||||
public class UpmsController {
|
public class UpmsController {
|
||||||
private final UpmsQueryService upmsQueryService;
|
private final UpmsQueryService upmsQueryService;
|
||||||
|
|
||||||
@@ -26,22 +27,22 @@ public class UpmsController {
|
|||||||
return ApiResponse.success(upmsQueryService.routes());
|
return ApiResponse.success(upmsQueryService.routes());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/areas/tree")
|
@GetMapping("/areas")
|
||||||
public ApiResponse<List<AreaNodeDto>> areas() {
|
public ApiResponse<List<AreaNodeDto>> areas() {
|
||||||
return ApiResponse.success(upmsQueryService.areas());
|
return ApiResponse.success(upmsQueryService.areas());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/tenants/tree")
|
@GetMapping("/tenants")
|
||||||
public ApiResponse<List<TenantNodeDto>> tenants() {
|
public ApiResponse<List<TenantNodeDto>> tenants() {
|
||||||
return ApiResponse.success(upmsQueryService.tenants());
|
return ApiResponse.success(upmsQueryService.tenants());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/depts/tree")
|
@GetMapping("/departments")
|
||||||
public ApiResponse<List<DeptNodeDto>> departments() {
|
public ApiResponse<List<DeptNodeDto>> departments() {
|
||||||
return ApiResponse.success(upmsQueryService.departments());
|
return ApiResponse.success(upmsQueryService.departments());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/current-user")
|
@GetMapping("/users/current")
|
||||||
public ApiResponse<CurrentRouteUserDto> currentUser() {
|
public ApiResponse<CurrentRouteUserDto> currentUser() {
|
||||||
return ApiResponse.success(upmsQueryService.currentUser());
|
return ApiResponse.success(upmsQueryService.currentUser());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package com.k12study.upms.domain;
|
package com.k12study.upms.domain;
|
||||||
|
|
||||||
public record SysArea(
|
public record SysArea(
|
||||||
String areaCode,
|
long id,
|
||||||
String parentCode,
|
long pid,
|
||||||
|
long adcode,
|
||||||
String areaName,
|
String areaName,
|
||||||
String areaLevel,
|
String areaType,
|
||||||
String provinceCode
|
String areaStatus,
|
||||||
|
String delFlag
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ public record SysDept(
|
|||||||
String tenantId,
|
String tenantId,
|
||||||
String deptName,
|
String deptName,
|
||||||
String deptType,
|
String deptType,
|
||||||
|
String adcode,
|
||||||
|
String tenantPath,
|
||||||
String deptPath
|
String deptPath
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ public record SysTenant(
|
|||||||
String parentTenantId,
|
String parentTenantId,
|
||||||
String tenantName,
|
String tenantName,
|
||||||
String tenantType,
|
String tenantType,
|
||||||
String provinceCode,
|
String adcode,
|
||||||
String areaCode,
|
String tenantPath,
|
||||||
String tenantPath
|
String status
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,109 +3,17 @@ package com.k12study.upms.service;
|
|||||||
import com.k12study.api.upms.dto.AreaNodeDto;
|
import com.k12study.api.upms.dto.AreaNodeDto;
|
||||||
import com.k12study.api.upms.dto.CurrentRouteUserDto;
|
import com.k12study.api.upms.dto.CurrentRouteUserDto;
|
||||||
import com.k12study.api.upms.dto.DeptNodeDto;
|
import com.k12study.api.upms.dto.DeptNodeDto;
|
||||||
import com.k12study.api.upms.dto.LayoutType;
|
|
||||||
import com.k12study.api.upms.dto.RouteMetaDto;
|
|
||||||
import com.k12study.api.upms.dto.RouteNodeDto;
|
import com.k12study.api.upms.dto.RouteNodeDto;
|
||||||
import com.k12study.api.upms.dto.TenantNodeDto;
|
import com.k12study.api.upms.dto.TenantNodeDto;
|
||||||
import com.k12study.common.security.context.RequestUserContextHolder;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.springframework.stereotype.Service;
|
public interface UpmsQueryService {
|
||||||
|
List<RouteNodeDto> routes();
|
||||||
|
|
||||||
@Service
|
List<AreaNodeDto> areas();
|
||||||
public class UpmsQueryService {
|
|
||||||
|
|
||||||
public List<RouteNodeDto> routes() {
|
List<TenantNodeDto> tenants();
|
||||||
return List.of(
|
|
||||||
new RouteNodeDto(
|
|
||||||
"dashboard",
|
|
||||||
"/",
|
|
||||||
"dashboard",
|
|
||||||
"dashboard",
|
|
||||||
LayoutType.SIDEBAR,
|
|
||||||
new RouteMetaDto("控制台", "layout-dashboard", List.of("dashboard:view"), false),
|
|
||||||
List.of()
|
|
||||||
),
|
|
||||||
new RouteNodeDto(
|
|
||||||
"tenant-management",
|
|
||||||
"/tenant",
|
|
||||||
"tenant-management",
|
|
||||||
"tenant",
|
|
||||||
LayoutType.SIDEBAR,
|
|
||||||
new RouteMetaDto("租户组织", "building-2", List.of("tenant:view"), false),
|
|
||||||
List.of()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<AreaNodeDto> areas() {
|
List<DeptNodeDto> departments();
|
||||||
return List.of(
|
|
||||||
new AreaNodeDto(
|
|
||||||
"330000",
|
|
||||||
"浙江省",
|
|
||||||
"province",
|
|
||||||
"330000",
|
|
||||||
List.of(
|
|
||||||
new AreaNodeDto("330100", "杭州市", "city", "330000", List.of())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<TenantNodeDto> tenants() {
|
CurrentRouteUserDto currentUser();
|
||||||
return List.of(
|
|
||||||
new TenantNodeDto(
|
|
||||||
"SCH-HQ",
|
|
||||||
"K12Study 总校",
|
|
||||||
"head_school",
|
|
||||||
"330000",
|
|
||||||
"330100",
|
|
||||||
"/SCH-HQ/",
|
|
||||||
List.of(
|
|
||||||
new TenantNodeDto(
|
|
||||||
"SCH-ZJ-HZ-01",
|
|
||||||
"杭州分校",
|
|
||||||
"city_school",
|
|
||||||
"330000",
|
|
||||||
"330100",
|
|
||||||
"/SCH-HQ/SCH-ZJ-HZ-01/",
|
|
||||||
List.of()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<DeptNodeDto> departments() {
|
|
||||||
return List.of(
|
|
||||||
new DeptNodeDto(
|
|
||||||
"DEPT-HQ",
|
|
||||||
"总校教学部",
|
|
||||||
"grade",
|
|
||||||
"SCH-HQ",
|
|
||||||
"/DEPT-HQ/",
|
|
||||||
List.of(
|
|
||||||
new DeptNodeDto(
|
|
||||||
"DEPT-HQ-MATH",
|
|
||||||
"数学学科组",
|
|
||||||
"subject",
|
|
||||||
"SCH-HQ",
|
|
||||||
"/DEPT-HQ/DEPT-HQ-MATH/",
|
|
||||||
List.of()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CurrentRouteUserDto currentUser() {
|
|
||||||
var context = RequestUserContextHolder.get();
|
|
||||||
return new CurrentRouteUserDto(
|
|
||||||
context == null ? "U10001" : context.userId(),
|
|
||||||
context == null ? "admin" : context.username(),
|
|
||||||
context == null ? "K12Study 管理员" : context.displayName(),
|
|
||||||
context == null ? "SCH-HQ" : context.tenantId(),
|
|
||||||
context == null ? "DEPT-HQ-ADMIN" : context.deptId(),
|
|
||||||
List.of("dashboard:view", "tenant:view", "dept:view")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package com.k12study.upms.service.impl;
|
||||||
|
|
||||||
|
import com.k12study.api.upms.dto.AreaNodeDto;
|
||||||
|
import com.k12study.api.upms.dto.CurrentRouteUserDto;
|
||||||
|
import com.k12study.api.upms.dto.DeptNodeDto;
|
||||||
|
import com.k12study.api.upms.dto.LayoutType;
|
||||||
|
import com.k12study.api.upms.dto.RouteMetaDto;
|
||||||
|
import com.k12study.api.upms.dto.RouteNodeDto;
|
||||||
|
import com.k12study.api.upms.dto.TenantNodeDto;
|
||||||
|
import com.k12study.common.security.context.RequestUserContextHolder;
|
||||||
|
import com.k12study.upms.service.UpmsQueryService;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class UpmsQueryServiceImpl implements UpmsQueryService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<RouteNodeDto> routes() {
|
||||||
|
return List.of(
|
||||||
|
new RouteNodeDto(
|
||||||
|
"dashboard",
|
||||||
|
"/",
|
||||||
|
"dashboard",
|
||||||
|
"dashboard",
|
||||||
|
LayoutType.SIDEBAR,
|
||||||
|
new RouteMetaDto("控制台", "layout-dashboard", List.of("dashboard:view"), false),
|
||||||
|
List.of()
|
||||||
|
),
|
||||||
|
new RouteNodeDto(
|
||||||
|
"tenant-management",
|
||||||
|
"/tenant",
|
||||||
|
"tenant-management",
|
||||||
|
"tenant",
|
||||||
|
LayoutType.SIDEBAR,
|
||||||
|
new RouteMetaDto("租户组织", "building-2", List.of("tenant:view"), false),
|
||||||
|
List.of()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AreaNodeDto> areas() {
|
||||||
|
return List.of(
|
||||||
|
new AreaNodeDto(
|
||||||
|
"330000",
|
||||||
|
"100000",
|
||||||
|
"浙江省",
|
||||||
|
"PROVINCE",
|
||||||
|
List.of(
|
||||||
|
new AreaNodeDto(
|
||||||
|
"330100",
|
||||||
|
"330000",
|
||||||
|
"杭州市",
|
||||||
|
"CITY",
|
||||||
|
List.of()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<TenantNodeDto> tenants() {
|
||||||
|
return List.of(
|
||||||
|
new TenantNodeDto(
|
||||||
|
"SCH-HQ",
|
||||||
|
null,
|
||||||
|
"K12Study 总校",
|
||||||
|
"HEAD_SCHOOL",
|
||||||
|
"330100",
|
||||||
|
"/SCH-HQ/",
|
||||||
|
List.of(
|
||||||
|
new TenantNodeDto(
|
||||||
|
"SCH-ZJ-HZ-01",
|
||||||
|
"SCH-HQ",
|
||||||
|
"杭州分校",
|
||||||
|
"CITY_SCHOOL",
|
||||||
|
"330100",
|
||||||
|
"/SCH-HQ/SCH-ZJ-HZ-01/",
|
||||||
|
List.of()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DeptNodeDto> departments() {
|
||||||
|
return List.of(
|
||||||
|
new DeptNodeDto(
|
||||||
|
"DEPT-HQ",
|
||||||
|
null,
|
||||||
|
"总校教学部",
|
||||||
|
"GRADE",
|
||||||
|
"SCH-HQ",
|
||||||
|
"330100",
|
||||||
|
"/SCH-HQ/",
|
||||||
|
"/DEPT-HQ/",
|
||||||
|
List.of(
|
||||||
|
new DeptNodeDto(
|
||||||
|
"DEPT-HQ-MATH",
|
||||||
|
"DEPT-HQ",
|
||||||
|
"数学学科组",
|
||||||
|
"SUBJECT",
|
||||||
|
"SCH-HQ",
|
||||||
|
"330100",
|
||||||
|
"/SCH-HQ/",
|
||||||
|
"/DEPT-HQ/DEPT-HQ-MATH/",
|
||||||
|
List.of()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CurrentRouteUserDto currentUser() {
|
||||||
|
var context = RequestUserContextHolder.get();
|
||||||
|
return new CurrentRouteUserDto(
|
||||||
|
context == null ? "U10001" : context.userId(),
|
||||||
|
context == null ? "admin" : context.username(),
|
||||||
|
context == null ? "K12Study 管理员" : context.displayName(),
|
||||||
|
"330100",
|
||||||
|
context == null ? "SCH-HQ" : context.tenantId(),
|
||||||
|
"/SCH-HQ/",
|
||||||
|
context == null ? "DEPT-HQ-ADMIN" : context.deptId(),
|
||||||
|
"/DEPT-HQ/DEPT-HQ-ADMIN/",
|
||||||
|
List.of("dashboard:view", "tenant:view", "dept:view")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
docs/MAXHUBShareSetup_6.7.8.27_108.exe
Normal file
BIN
docs/MAXHUBShareSetup_6.7.8.27_108.exe
Normal file
Binary file not shown.
269
docs/apijson/upms.api.json
Normal file
269
docs/apijson/upms.api.json
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"module": "upms",
|
||||||
|
"prefix": "/api/upms",
|
||||||
|
"responseEnvelope": {
|
||||||
|
"code": "int, 0=成功, 非0=业务失败",
|
||||||
|
"message": "string, 业务描述",
|
||||||
|
"data": "T, 业务数据",
|
||||||
|
"traceId": "string, 链路追踪ID"
|
||||||
|
},
|
||||||
|
"errorExample": {
|
||||||
|
"code": 500,
|
||||||
|
"message": "系统异常",
|
||||||
|
"data": null,
|
||||||
|
"traceId": "abc123"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"apis": [
|
||||||
|
{
|
||||||
|
"id": "upms-routes",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/upms/routes",
|
||||||
|
"summary": "获取当前用户的动态路由/菜单树",
|
||||||
|
"sqlSource": "upms.tb_sys_menu + upms.tb_sys_role_menu",
|
||||||
|
"request": {
|
||||||
|
"headers": { "Authorization": "Bearer {accessToken}" },
|
||||||
|
"params": null,
|
||||||
|
"body": null
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 0,
|
||||||
|
"message": "OK",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "dashboard",
|
||||||
|
"path": "/",
|
||||||
|
"name": "dashboard",
|
||||||
|
"component": "dashboard",
|
||||||
|
"layout": "SIDEBAR",
|
||||||
|
"meta": {
|
||||||
|
"title": "控制台",
|
||||||
|
"icon": "layout-dashboard",
|
||||||
|
"permissionCodes": ["dashboard:view"],
|
||||||
|
"hidden": false
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tenant-management",
|
||||||
|
"path": "/tenant",
|
||||||
|
"name": "tenant-management",
|
||||||
|
"component": "tenant",
|
||||||
|
"layout": "SIDEBAR",
|
||||||
|
"meta": {
|
||||||
|
"title": "租户组织",
|
||||||
|
"icon": "building-2",
|
||||||
|
"permissionCodes": ["tenant:view"],
|
||||||
|
"hidden": false
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"traceId": "trace-001"
|
||||||
|
},
|
||||||
|
"fieldMapping": {
|
||||||
|
"id": "tb_sys_menu.route_id",
|
||||||
|
"path": "tb_sys_menu.route_path",
|
||||||
|
"name": "tb_sys_menu.route_name",
|
||||||
|
"component": "tb_sys_menu.component_key",
|
||||||
|
"layout": "tb_sys_menu.layout_type",
|
||||||
|
"meta.title": "tb_sys_menu.title",
|
||||||
|
"meta.icon": "tb_sys_menu.icon",
|
||||||
|
"meta.permissionCodes": "tb_sys_menu.permission_code (拆分为数组)",
|
||||||
|
"meta.hidden": "tb_sys_menu.hidden",
|
||||||
|
"children": "通过 parent_route_id 递归组装"
|
||||||
|
},
|
||||||
|
"backendDto": "com.k12study.api.upms.dto.RouteNodeDto",
|
||||||
|
"frontendType": "UpmsRouteNode (types/upms.ts)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "upms-users-current",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/upms/users/current",
|
||||||
|
"summary": "获取当前登录用户信息(含权限编码)",
|
||||||
|
"sqlSource": "upms.tb_sys_user + upms.tb_sys_role + upms.tb_sys_role_menu",
|
||||||
|
"request": {
|
||||||
|
"headers": { "Authorization": "Bearer {accessToken}" },
|
||||||
|
"params": null,
|
||||||
|
"body": null
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 0,
|
||||||
|
"message": "OK",
|
||||||
|
"data": {
|
||||||
|
"userId": "U10001",
|
||||||
|
"username": "admin",
|
||||||
|
"displayName": "K12Study 管理员",
|
||||||
|
"adcode": "330100",
|
||||||
|
"tenantId": "SCH-HQ",
|
||||||
|
"tenantPath": "/SCH-HQ/",
|
||||||
|
"deptId": "DEPT-HQ-ADMIN",
|
||||||
|
"deptPath": "/DEPT-HQ/DEPT-HQ-ADMIN/",
|
||||||
|
"permissionCodes": ["dashboard:view", "tenant:view", "dept:view"]
|
||||||
|
},
|
||||||
|
"traceId": "trace-002"
|
||||||
|
},
|
||||||
|
"fieldMapping": {
|
||||||
|
"userId": "tb_sys_user.user_id",
|
||||||
|
"username": "tb_sys_user.username",
|
||||||
|
"displayName": "tb_sys_user.display_name",
|
||||||
|
"adcode": "tb_sys_user.adcode",
|
||||||
|
"tenantId": "tb_sys_user.tenant_id",
|
||||||
|
"tenantPath": "tb_sys_user.tenant_path",
|
||||||
|
"deptId": "tb_sys_user.dept_id",
|
||||||
|
"deptPath": "tb_sys_user.dept_path",
|
||||||
|
"permissionCodes": "通过 role->role_menu->menu.permission_code 聚合"
|
||||||
|
},
|
||||||
|
"backendDto": "com.k12study.api.upms.dto.CurrentRouteUserDto",
|
||||||
|
"frontendType": "UpmsCurrentUser (types/upms.ts)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "upms-areas",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/upms/areas",
|
||||||
|
"summary": "获取行政区划树",
|
||||||
|
"sqlSource": "upms.tb_sys_area",
|
||||||
|
"request": {
|
||||||
|
"headers": { "Authorization": "Bearer {accessToken}" },
|
||||||
|
"params": null,
|
||||||
|
"body": null
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 0,
|
||||||
|
"message": "OK",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"areaCode": "330000",
|
||||||
|
"parentCode": "100000",
|
||||||
|
"areaName": "浙江省",
|
||||||
|
"areaLevel": "PROVINCE",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"areaCode": "330100",
|
||||||
|
"parentCode": "330000",
|
||||||
|
"areaName": "杭州市",
|
||||||
|
"areaLevel": "CITY",
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"traceId": "trace-003"
|
||||||
|
},
|
||||||
|
"fieldMapping": {
|
||||||
|
"areaCode": "tb_sys_area.adcode (转为字符串)",
|
||||||
|
"parentCode": "tb_sys_area.pid (转为字符串)",
|
||||||
|
"areaName": "tb_sys_area.name",
|
||||||
|
"areaLevel": "tb_sys_area.area_type (映射: 0->COUNTRY, 1->PROVINCE, 2->CITY)",
|
||||||
|
"children": "通过 pid 递归组装"
|
||||||
|
},
|
||||||
|
"backendDto": "com.k12study.api.upms.dto.AreaNodeDto",
|
||||||
|
"frontendType": "UpmsAreaNode (types/upms.ts)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "upms-tenants",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/upms/tenants",
|
||||||
|
"summary": "获取租户树",
|
||||||
|
"sqlSource": "upms.tb_sys_tenant",
|
||||||
|
"request": {
|
||||||
|
"headers": { "Authorization": "Bearer {accessToken}" },
|
||||||
|
"params": null,
|
||||||
|
"body": null
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 0,
|
||||||
|
"message": "OK",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"tenantId": "SCH-HQ",
|
||||||
|
"parentTenantId": null,
|
||||||
|
"tenantName": "K12Study 总校",
|
||||||
|
"tenantType": "HEAD_SCHOOL",
|
||||||
|
"adcode": "330100",
|
||||||
|
"tenantPath": "/SCH-HQ/",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"tenantId": "SCH-ZJ-HZ-01",
|
||||||
|
"parentTenantId": "SCH-HQ",
|
||||||
|
"tenantName": "杭州分校",
|
||||||
|
"tenantType": "CITY_SCHOOL",
|
||||||
|
"adcode": "330100",
|
||||||
|
"tenantPath": "/SCH-HQ/SCH-ZJ-HZ-01/",
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"traceId": "trace-004"
|
||||||
|
},
|
||||||
|
"fieldMapping": {
|
||||||
|
"tenantId": "tb_sys_tenant.tenant_id",
|
||||||
|
"parentTenantId": "tb_sys_tenant.parent_tenant_id",
|
||||||
|
"tenantName": "tb_sys_tenant.tenant_name",
|
||||||
|
"tenantType": "tb_sys_tenant.tenant_type",
|
||||||
|
"adcode": "tb_sys_tenant.adcode",
|
||||||
|
"tenantPath": "tb_sys_tenant.tenant_path",
|
||||||
|
"children": "通过 parent_tenant_id 递归组装"
|
||||||
|
},
|
||||||
|
"backendDto": "com.k12study.api.upms.dto.TenantNodeDto",
|
||||||
|
"frontendType": "UpmsTenantNode (types/upms.ts)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "upms-departments",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/upms/departments",
|
||||||
|
"summary": "获取部门/组织树",
|
||||||
|
"sqlSource": "upms.tb_sys_dept",
|
||||||
|
"request": {
|
||||||
|
"headers": { "Authorization": "Bearer {accessToken}" },
|
||||||
|
"params": null,
|
||||||
|
"body": null
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 0,
|
||||||
|
"message": "OK",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"deptId": "DEPT-HQ",
|
||||||
|
"parentDeptId": null,
|
||||||
|
"deptName": "总校教学部",
|
||||||
|
"deptType": "GRADE",
|
||||||
|
"tenantId": "SCH-HQ",
|
||||||
|
"adcode": "330100",
|
||||||
|
"tenantPath": "/SCH-HQ/",
|
||||||
|
"deptPath": "/DEPT-HQ/",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"deptId": "DEPT-HQ-MATH",
|
||||||
|
"parentDeptId": "DEPT-HQ",
|
||||||
|
"deptName": "数学学科组",
|
||||||
|
"deptType": "SUBJECT",
|
||||||
|
"tenantId": "SCH-HQ",
|
||||||
|
"adcode": "330100",
|
||||||
|
"tenantPath": "/SCH-HQ/",
|
||||||
|
"deptPath": "/DEPT-HQ/DEPT-HQ-MATH/",
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"traceId": "trace-005"
|
||||||
|
},
|
||||||
|
"fieldMapping": {
|
||||||
|
"deptId": "tb_sys_dept.dept_id",
|
||||||
|
"parentDeptId": "tb_sys_dept.parent_dept_id",
|
||||||
|
"deptName": "tb_sys_dept.dept_name",
|
||||||
|
"deptType": "tb_sys_dept.dept_type",
|
||||||
|
"tenantId": "tb_sys_dept.tenant_id",
|
||||||
|
"adcode": "tb_sys_dept.adcode",
|
||||||
|
"tenantPath": "tb_sys_dept.tenant_path",
|
||||||
|
"deptPath": "tb_sys_dept.dept_path",
|
||||||
|
"children": "通过 parent_dept_id 递归组装"
|
||||||
|
},
|
||||||
|
"backendDto": "com.k12study.api.upms.dto.DeptNodeDto",
|
||||||
|
"frontendType": "UpmsDeptNode (types/upms.ts)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# API 设计(基础架构 + 业务功能)
|
# API 设计(基础架构 + 业务功能)
|
||||||
## 1. API 设计原则
|
## 1. API 设计原则
|
||||||
- 对外统一前缀:`/api/*`。
|
- 对外统一前缀:`/api/*`。
|
||||||
- 统一响应结构:`code/message/data/traceId`。
|
- 统一响应结构:`code/message/data/traceId`,其中 `code=0` 表示成功,非 0 表示业务失败。
|
||||||
- 认证策略:JWT + RBAC,网关做统一鉴权透传。
|
- 认证策略:JWT + RBAC,网关做统一鉴权透传。
|
||||||
- API 冻结点:
|
- API 冻结点:
|
||||||
- 基础架构 API 在 M3 冻结。
|
- 基础架构 API 在 M3 冻结。
|
||||||
@@ -9,16 +9,16 @@
|
|||||||
|
|
||||||
## 2. 基础架构 API(M3)
|
## 2. 基础架构 API(M3)
|
||||||
### 2.1 认证域(auth)
|
### 2.1 认证域(auth)
|
||||||
- `POST /api/auth/login`
|
- `POST /api/auth/tokens`(登录)
|
||||||
- `POST /api/auth/refresh`
|
- `POST /api/auth/tokens/refresh`(刷新)
|
||||||
- `GET /api/auth/current-user`
|
- `GET /api/auth/users/current`(当前用户)
|
||||||
|
|
||||||
### 2.2 权限与组织域(upms)
|
### 2.2 权限与组织域(upms)
|
||||||
- `GET /api/upms/routes`
|
- `GET /api/upms/routes`
|
||||||
- `GET /api/upms/current-user`
|
- `GET /api/upms/users/current`
|
||||||
- `GET /api/upms/areas/tree`
|
- `GET /api/upms/areas`
|
||||||
- `GET /api/upms/tenants/tree`
|
- `GET /api/upms/tenants`
|
||||||
- `GET /api/upms/depts/tree`
|
- `GET /api/upms/departments`
|
||||||
|
|
||||||
### 2.3 基础扩展 API(建议补充)
|
### 2.3 基础扩展 API(建议补充)
|
||||||
- 文件域
|
- 文件域
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ graph TD
|
|||||||
|
|
||||||
## 3. 主链路视角
|
## 3. 主链路视角
|
||||||
### 3.1 基础架构链路
|
### 3.1 基础架构链路
|
||||||
- 登录:`frontend/app -> /api/auth/login -> auth -> token`
|
- 登录:`frontend/app -> /api/auth/tokens -> auth -> token`
|
||||||
- 权限与路由:`frontend/app -> /api/upms/routes -> upms`
|
- 权限与路由:`frontend/app -> /api/upms/routes -> upms`
|
||||||
- 组织数据:`/api/upms/areas/tree|tenants/tree|depts/tree`
|
- 组织数据:`/api/upms/areas|tenants|departments`
|
||||||
|
|
||||||
### 3.2 教学业务链路
|
### 3.2 教学业务链路
|
||||||
- 教师发作业:题库/试卷/作业配置 -> 投放班级。
|
- 教师发作业:题库/试卷/作业配置 -> 投放班级。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<mxfile host="65bd71144e">
|
<mxfile host="65bd71144e">
|
||||||
<diagram id="full-business-flow-v3" name="完整业务流程图">
|
<diagram id="full-business-flow-v3" name="完整业务流程图">
|
||||||
<mxGraphModel dx="1312" dy="773" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="5600" pageHeight="2300" math="0" shadow="0">
|
<mxGraphModel dx="1658" dy="703" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="5600" pageHeight="2300" math="0" shadow="0">
|
||||||
<root>
|
<root>
|
||||||
<mxCell id="0"/>
|
<mxCell id="0"/>
|
||||||
<mxCell id="1" parent="0"/>
|
<mxCell id="1" parent="0"/>
|
||||||
@@ -35,13 +35,13 @@
|
|||||||
<mxGeometry x="30" y="110" width="5400" height="220" as="geometry"/>
|
<mxGeometry x="30" y="110" width="5400" height="220" as="geometry"/>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
<mxCell id="11" value="学生主线" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=13;fontStyle=1;align=left;spacingLeft=10;" parent="1" vertex="1">
|
<mxCell id="11" value="学生主线" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=13;fontStyle=1;align=left;spacingLeft=10;" parent="1" vertex="1">
|
||||||
<mxGeometry x="30" y="360" width="5400" height="260" as="geometry"/>
|
<mxGeometry x="20" y="370" width="5400" height="260" as="geometry"/>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
<mxCell id="12" value="AI/系统主线" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=13;fontStyle=1;align=left;spacingLeft=10;" parent="1" vertex="1">
|
<mxCell id="12" value="AI/系统主线" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=13;fontStyle=1;align=left;spacingLeft=10;" parent="1" vertex="1">
|
||||||
<mxGeometry x="30" y="660" width="5400" height="620" as="geometry"/>
|
<mxGeometry x="50" y="670" width="5400" height="620" as="geometry"/>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
<mxCell id="13" value="管理运营主线" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=13;fontStyle=1;align=left;spacingLeft=10;" parent="1" vertex="1">
|
<mxCell id="13" value="管理运营主线" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=13;fontStyle=1;align=left;spacingLeft=10;" parent="1" vertex="1">
|
||||||
<mxGeometry x="30" y="1320" width="5400" height="300" as="geometry"/>
|
<mxGeometry y="1330" width="5400" height="300" as="geometry"/>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
<mxCell id="20" value="开始" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" parent="1" vertex="1">
|
<mxCell id="20" value="开始" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" parent="1" vertex="1">
|
||||||
<mxGeometry x="70" y="470" width="110" height="64" as="geometry"/>
|
<mxGeometry x="70" y="470" width="110" height="64" as="geometry"/>
|
||||||
|
|||||||
@@ -10,5 +10,11 @@ interface LoginInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function login(input: LoginInput) {
|
export async function login(input: LoginInput) {
|
||||||
return http.post<ApiResponse<{ accessToken: string; refreshToken: string }>>("/auth/login", input);
|
return http.post<ApiResponse<{ accessToken: string; refreshToken: string }>>("/auth/tokens", input);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function refreshToken(refreshToken: string) {
|
||||||
|
return http.post<ApiResponse<{ accessToken: string; refreshToken: string }>>("/auth/tokens/refresh", {
|
||||||
|
refreshToken
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,55 @@
|
|||||||
import type { ApiResponse } from "../types/api";
|
import {
|
||||||
|
getUpmsAreasRemote,
|
||||||
|
getUpmsCurrentUserRemote,
|
||||||
|
getUpmsDepartmentsRemote,
|
||||||
|
getUpmsRoutesRemote,
|
||||||
|
getUpmsTenantsRemote
|
||||||
|
} from "../remote/upmsRemote";
|
||||||
import type { CurrentRouteUser, RouteNode } from "../types/route";
|
import type { CurrentRouteUser, RouteNode } from "../types/route";
|
||||||
import { http } from "../utils/http";
|
import type { UpmsAreaNode, UpmsDeptNode, UpmsTenantNode } from "../types/upms";
|
||||||
|
|
||||||
|
function normalizeAreaNodes(nodes: UpmsAreaNode[]): UpmsAreaNode[] {
|
||||||
|
return nodes.map((node) => ({
|
||||||
|
...node,
|
||||||
|
children: normalizeAreaNodes(node.children ?? [])
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTenantNodes(nodes: UpmsTenantNode[]): UpmsTenantNode[] {
|
||||||
|
return nodes.map((node) => ({
|
||||||
|
...node,
|
||||||
|
children: normalizeTenantNodes(node.children ?? [])
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDeptNodes(nodes: UpmsDeptNode[]): UpmsDeptNode[] {
|
||||||
|
return nodes.map((node) => ({
|
||||||
|
...node,
|
||||||
|
children: normalizeDeptNodes(node.children ?? [])
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchDynamicRoutes(): Promise<RouteNode[]> {
|
export async function fetchDynamicRoutes(): Promise<RouteNode[]> {
|
||||||
const response = await http.get<ApiResponse<RouteNode[]>>("/upms/routes");
|
const response = await getUpmsRoutesRemote();
|
||||||
return response.data as RouteNode[];
|
return response.data as RouteNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchCurrentUser(): Promise<CurrentRouteUser> {
|
export async function fetchCurrentUser(): Promise<CurrentRouteUser> {
|
||||||
const response = await http.get<ApiResponse<CurrentRouteUser>>("/upms/current-user");
|
const response = await getUpmsCurrentUserRemote();
|
||||||
return response.data as CurrentRouteUser;
|
return response.data as CurrentRouteUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchAreas(): Promise<UpmsAreaNode[]> {
|
||||||
|
const response = await getUpmsAreasRemote();
|
||||||
|
return normalizeAreaNodes(response.data as UpmsAreaNode[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchTenants(): Promise<UpmsTenantNode[]> {
|
||||||
|
const response = await getUpmsTenantsRemote();
|
||||||
|
return normalizeTenantNodes(response.data as UpmsTenantNode[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchDepartments(): Promise<UpmsDeptNode[]> {
|
||||||
|
const response = await getUpmsDepartmentsRemote();
|
||||||
|
return normalizeDeptNodes(response.data as UpmsDeptNode[]);
|
||||||
|
}
|
||||||
|
|||||||
23
frontend/src/remote/upmsRemote.ts
Normal file
23
frontend/src/remote/upmsRemote.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { ApiResponse } from "../types/api";
|
||||||
|
import type { UpmsAreaNode, UpmsCurrentUser, UpmsDeptNode, UpmsRouteNode, UpmsTenantNode } from "../types/upms";
|
||||||
|
import { http } from "../utils/http";
|
||||||
|
|
||||||
|
export function getUpmsRoutesRemote() {
|
||||||
|
return http.get<ApiResponse<UpmsRouteNode[]>>("/upms/routes");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUpmsCurrentUserRemote() {
|
||||||
|
return http.get<ApiResponse<UpmsCurrentUser>>("/upms/users/current");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUpmsAreasRemote() {
|
||||||
|
return http.get<ApiResponse<UpmsAreaNode[]>>("/upms/areas");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUpmsTenantsRemote() {
|
||||||
|
return http.get<ApiResponse<UpmsTenantNode[]>>("/upms/tenants");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUpmsDepartmentsRemote() {
|
||||||
|
return http.get<ApiResponse<UpmsDeptNode[]>>("/upms/departments");
|
||||||
|
}
|
||||||
@@ -1,27 +1,11 @@
|
|||||||
export type LayoutType = "DEFAULT" | "SIDEBAR";
|
import type {
|
||||||
|
UpmsCurrentUser,
|
||||||
|
UpmsLayoutType,
|
||||||
|
UpmsRouteMeta,
|
||||||
|
UpmsRouteNode
|
||||||
|
} from "./upms";
|
||||||
|
|
||||||
export interface RouteMeta {
|
export type LayoutType = UpmsLayoutType;
|
||||||
title: string;
|
export type RouteMeta = UpmsRouteMeta;
|
||||||
icon?: string;
|
export type RouteNode = UpmsRouteNode;
|
||||||
permissionCodes?: string[];
|
export type CurrentRouteUser = UpmsCurrentUser;
|
||||||
hidden?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RouteNode {
|
|
||||||
id: string;
|
|
||||||
path: string;
|
|
||||||
name: string;
|
|
||||||
component: string;
|
|
||||||
layout: LayoutType;
|
|
||||||
meta: RouteMeta;
|
|
||||||
children: RouteNode[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CurrentRouteUser {
|
|
||||||
userId: string;
|
|
||||||
username: string;
|
|
||||||
displayName: string;
|
|
||||||
tenantId: string;
|
|
||||||
deptId: string;
|
|
||||||
permissionCodes: string[];
|
|
||||||
}
|
|
||||||
|
|||||||
60
frontend/src/types/upms.ts
Normal file
60
frontend/src/types/upms.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
export type UpmsLayoutType = "DEFAULT" | "SIDEBAR";
|
||||||
|
|
||||||
|
export interface UpmsRouteMeta {
|
||||||
|
title: string;
|
||||||
|
icon?: string;
|
||||||
|
permissionCodes?: string[];
|
||||||
|
hidden?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpmsRouteNode {
|
||||||
|
id: string;
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
component: string;
|
||||||
|
layout: UpmsLayoutType;
|
||||||
|
meta: UpmsRouteMeta;
|
||||||
|
children: UpmsRouteNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpmsCurrentUser {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
adcode: string;
|
||||||
|
tenantId: string;
|
||||||
|
tenantPath: string;
|
||||||
|
deptId: string;
|
||||||
|
deptPath: string;
|
||||||
|
permissionCodes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpmsAreaNode {
|
||||||
|
areaCode: string;
|
||||||
|
parentCode: string;
|
||||||
|
areaName: string;
|
||||||
|
areaLevel: string;
|
||||||
|
children: UpmsAreaNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpmsTenantNode {
|
||||||
|
tenantId: string;
|
||||||
|
parentTenantId: string | null;
|
||||||
|
tenantName: string;
|
||||||
|
tenantType: string;
|
||||||
|
adcode: string;
|
||||||
|
tenantPath: string;
|
||||||
|
children: UpmsTenantNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpmsDeptNode {
|
||||||
|
deptId: string;
|
||||||
|
parentDeptId: string | null;
|
||||||
|
deptName: string;
|
||||||
|
deptType: string;
|
||||||
|
tenantId: string;
|
||||||
|
adcode: string;
|
||||||
|
tenantPath: string;
|
||||||
|
deptPath: string;
|
||||||
|
children: UpmsDeptNode[];
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ApiResponse } from "../types/api";
|
||||||
import { getAccessToken } from "./storage";
|
import { getAccessToken } from "./storage";
|
||||||
|
|
||||||
const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "/api";
|
const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "/api";
|
||||||
@@ -12,6 +13,14 @@ interface RequestOptions {
|
|||||||
timeout?: number;
|
timeout?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isApiResponse(payload: unknown): payload is ApiResponse<unknown> {
|
||||||
|
if (typeof payload !== "object" || payload === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "code" in payload && "message" in payload && "data" in payload;
|
||||||
|
}
|
||||||
|
|
||||||
function buildUrl(path: string) {
|
function buildUrl(path: string) {
|
||||||
if (/^https?:\/\//.test(path)) {
|
if (/^https?:\/\//.test(path)) {
|
||||||
return path;
|
return path;
|
||||||
@@ -57,6 +66,9 @@ async function request<T>(path: string, options: RequestOptions = {}): Promise<T
|
|||||||
: `Request failed with status ${response.status}`;
|
: `Request failed with status ${response.status}`;
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
|
if (isApiResponse(payload) && payload.code !== 0) {
|
||||||
|
throw new Error(payload.message || `Business request failed with code ${payload.code}`);
|
||||||
|
}
|
||||||
|
|
||||||
return payload as T;
|
return payload as T;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
65
global.code-snippets
Normal file
65
global.code-snippets
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
// VS Code 原生 snippet 不能直接读取 git config user.name。
|
||||||
|
// 团队统一文件头请使用 turbo-file-header 插件,配置见 .fileheader/fileheader.config.yaml。
|
||||||
|
// Place your global snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
||||||
|
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
||||||
|
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
||||||
|
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||||
|
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
|
||||||
|
// Placeholders with the same ids are connected.
|
||||||
|
// Example:
|
||||||
|
// "Print to console": {
|
||||||
|
// "scope": "javascript,typescript",
|
||||||
|
// "prefix": "log",
|
||||||
|
// "body": [
|
||||||
|
// "console.log('$1');",
|
||||||
|
// "$2"
|
||||||
|
// ],
|
||||||
|
// "description": "Log output to console"
|
||||||
|
// }
|
||||||
|
"FileHeader":{
|
||||||
|
"prefix": ".fileheader",
|
||||||
|
"body": [
|
||||||
|
"/**",
|
||||||
|
" * @description ${TM_FILENAME}文件描述",
|
||||||
|
" * @filename ${TM_FILENAME}",
|
||||||
|
" * @author ${1:wangys}",
|
||||||
|
" * @copyright xyzh",
|
||||||
|
" * @since $CURRENT_YEAR-$CURRENT_MONTH-$CURRENT_DATE",
|
||||||
|
" */"
|
||||||
|
],
|
||||||
|
"description": "文件头描述(静态备用,author 默认值同步 git user.name)"
|
||||||
|
},
|
||||||
|
|
||||||
|
"Method":{
|
||||||
|
"prefix": ".func",
|
||||||
|
"body": [
|
||||||
|
"/**",
|
||||||
|
" * @description 函数描述",
|
||||||
|
" * @param ",
|
||||||
|
" * @return 返回值描述",
|
||||||
|
" * @author ${1:wangys}",
|
||||||
|
" * @since $CURRENT_YEAR-$CURRENT_MONTH-$CURRENT_DATE",
|
||||||
|
" */"
|
||||||
|
],
|
||||||
|
"description": "函数描述(静态备用,author 默认值同步 git user.name)"
|
||||||
|
},
|
||||||
|
"Property":{
|
||||||
|
"prefix": ".prop",
|
||||||
|
"body": [
|
||||||
|
"/**",
|
||||||
|
" *",
|
||||||
|
" */"
|
||||||
|
],
|
||||||
|
"description": "属性描述"
|
||||||
|
},
|
||||||
|
"Variables":{
|
||||||
|
"prefix": ".var",
|
||||||
|
"body": [
|
||||||
|
"/**",
|
||||||
|
" *",
|
||||||
|
" */"
|
||||||
|
],
|
||||||
|
"description": "变量描述"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user