diff --git a/.agents/skills/k12-api-response-standard/SKILL.md b/.agents/skills/k12-api-response-standard/SKILL.md new file mode 100644 index 0000000..f0f0558 --- /dev/null +++ b/.agents/skills/k12-api-response-standard/SKILL.md @@ -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` 以支持链路追踪。 diff --git a/.agents/skills/k12-comment-header-standard/SKILL.md b/.agents/skills/k12-comment-header-standard/SKILL.md new file mode 100644 index 0000000..1c67d85 --- /dev/null +++ b/.agents/skills/k12-comment-header-standard/SKILL.md @@ -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)。 diff --git a/.agents/skills/k12-restful-api-style/SKILL.md b/.agents/skills/k12-restful-api-style/SKILL.md new file mode 100644 index 0000000..16b675d --- /dev/null +++ b/.agents/skills/k12-restful-api-style/SKILL.md @@ -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/*` 统一入口不变。 +- 旧接口不兼容,禁止新增或恢复旧路径别名。 diff --git a/.fileheader/fileheader.config.yaml b/.fileheader/fileheader.config.yaml new file mode 100644 index 0000000..4a2b20c --- /dev/null +++ b/.fileheader/fileheader.config.yaml @@ -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}}" diff --git a/.gitignore b/.gitignore index ac332d3..c03ff10 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,9 @@ urbanLifeline Tik schoolNewsServ .idea/ -.vscode/ +.vscode/* +!.vscode/settings.json +!.vscode/extensions.json .DS_Store .tmp-run/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..1039df6 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "ygqygq2.turbo-file-header" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7b016a8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.compile.nullAnalysis.mode": "automatic" +} \ No newline at end of file diff --git a/app/src/api/auth.js b/app/src/api/auth.js index 02e19d0..787f83b 100644 --- a/app/src/api/auth.js +++ b/app/src/api/auth.js @@ -2,7 +2,7 @@ const { request } = require("../utils/request"); function login(data) { return request({ - url: "/api/auth/login", + url: "/api/auth/tokens", method: "POST", data }); diff --git a/app/src/api/upms.js b/app/src/api/upms.js index a2acdbe..7a4a54d 100644 --- a/app/src/api/upms.js +++ b/app/src/api/upms.js @@ -6,7 +6,38 @@ function getRouteMeta() { 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 = { - getRouteMeta + getRouteMeta, + getCurrentUser, + getAreas, + getTenants, + getDepartments }; diff --git a/app/src/utils/request.js b/app/src/utils/request.js index 04694af..b864835 100644 --- a/app/src/utils/request.js +++ b/app/src/utils/request.js @@ -1,5 +1,15 @@ 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) { return new Promise((resolve, reject) => { wx.request({ @@ -10,7 +20,28 @@ function request(options) { "Content-Type": "application/json", ...(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 }); }); diff --git a/backend/apis/api-auth/src/main/java/com/k12study/api/auth/dto/RefreshTokenRequest.java b/backend/apis/api-auth/src/main/java/com/k12study/api/auth/dto/RefreshTokenRequest.java new file mode 100644 index 0000000..c6f6a25 --- /dev/null +++ b/backend/apis/api-auth/src/main/java/com/k12study/api/auth/dto/RefreshTokenRequest.java @@ -0,0 +1,4 @@ +package com.k12study.api.auth.dto; + +public record RefreshTokenRequest(String refreshToken) { +} diff --git a/backend/apis/api-upms/src/main/java/com/k12study/api/upms/dto/AreaNodeDto.java b/backend/apis/api-upms/src/main/java/com/k12study/api/upms/dto/AreaNodeDto.java index 61a0009..deec9c9 100644 --- a/backend/apis/api-upms/src/main/java/com/k12study/api/upms/dto/AreaNodeDto.java +++ b/backend/apis/api-upms/src/main/java/com/k12study/api/upms/dto/AreaNodeDto.java @@ -4,9 +4,9 @@ import java.util.List; public record AreaNodeDto( String areaCode, + String parentCode, String areaName, String areaLevel, - String provinceCode, List children ) { } diff --git a/backend/apis/api-upms/src/main/java/com/k12study/api/upms/dto/CurrentRouteUserDto.java b/backend/apis/api-upms/src/main/java/com/k12study/api/upms/dto/CurrentRouteUserDto.java index 07ffd37..572e6b8 100644 --- a/backend/apis/api-upms/src/main/java/com/k12study/api/upms/dto/CurrentRouteUserDto.java +++ b/backend/apis/api-upms/src/main/java/com/k12study/api/upms/dto/CurrentRouteUserDto.java @@ -6,8 +6,11 @@ public record CurrentRouteUserDto( String userId, String username, String displayName, + String adcode, String tenantId, + String tenantPath, String deptId, + String deptPath, List permissionCodes ) { } diff --git a/backend/apis/api-upms/src/main/java/com/k12study/api/upms/dto/DeptNodeDto.java b/backend/apis/api-upms/src/main/java/com/k12study/api/upms/dto/DeptNodeDto.java index 383b9d5..04fc7b3 100644 --- a/backend/apis/api-upms/src/main/java/com/k12study/api/upms/dto/DeptNodeDto.java +++ b/backend/apis/api-upms/src/main/java/com/k12study/api/upms/dto/DeptNodeDto.java @@ -4,9 +4,12 @@ import java.util.List; public record DeptNodeDto( String deptId, + String parentDeptId, String deptName, String deptType, String tenantId, + String adcode, + String tenantPath, String deptPath, List children ) { diff --git a/backend/apis/api-upms/src/main/java/com/k12study/api/upms/dto/TenantNodeDto.java b/backend/apis/api-upms/src/main/java/com/k12study/api/upms/dto/TenantNodeDto.java index c69f5ab..7029e0f 100644 --- a/backend/apis/api-upms/src/main/java/com/k12study/api/upms/dto/TenantNodeDto.java +++ b/backend/apis/api-upms/src/main/java/com/k12study/api/upms/dto/TenantNodeDto.java @@ -4,10 +4,10 @@ import java.util.List; public record TenantNodeDto( String tenantId, + String parentTenantId, String tenantName, String tenantType, - String provinceCode, - String areaCode, + String adcode, String tenantPath, List children ) { diff --git a/backend/apis/api-upms/src/main/java/com/k12study/api/upms/remote/UpmsApiPaths.java b/backend/apis/api-upms/src/main/java/com/k12study/api/upms/remote/UpmsApiPaths.java new file mode 100644 index 0000000..34709bd --- /dev/null +++ b/backend/apis/api-upms/src/main/java/com/k12study/api/upms/remote/UpmsApiPaths.java @@ -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"; +} diff --git a/backend/apis/api-upms/src/main/java/com/k12study/api/upms/remote/UpmsRemoteApi.java b/backend/apis/api-upms/src/main/java/com/k12study/api/upms/remote/UpmsRemoteApi.java new file mode 100644 index 0000000..756eb08 --- /dev/null +++ b/backend/apis/api-upms/src/main/java/com/k12study/api/upms/remote/UpmsRemoteApi.java @@ -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> routes(); + + ApiResponse currentUser(); + + ApiResponse> areas(); + + ApiResponse> tenants(); + + ApiResponse> departments(); +} diff --git a/backend/auth/src/main/java/com/k12study/auth/controller/AuthController.java b/backend/auth/src/main/java/com/k12study/auth/controller/AuthController.java index 3c40186..9e4fbce 100644 --- a/backend/auth/src/main/java/com/k12study/auth/controller/AuthController.java +++ b/backend/auth/src/main/java/com/k12study/auth/controller/AuthController.java @@ -2,15 +2,16 @@ package com.k12study.auth.controller; import com.k12study.api.auth.dto.CurrentUserResponse; import com.k12study.api.auth.dto.LoginRequest; +import com.k12study.api.auth.dto.RefreshTokenRequest; import com.k12study.api.auth.dto.TokenResponse; import com.k12study.auth.service.AuthService; 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.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -22,17 +23,21 @@ public class AuthController { this.authService = authService; } - @PostMapping("/login") + @PostMapping("/tokens") public ApiResponse login(@RequestBody LoginRequest request) { return ApiResponse.success("登录成功", authService.login(request)); } - @PostMapping("/refresh") - public ApiResponse refresh(@RequestParam("refreshToken") String refreshToken) { + @PostMapping("/tokens/refresh") + public ApiResponse 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)); } - @GetMapping("/current-user") + @GetMapping("/users/current") public ApiResponse currentUser( @RequestHeader(value = "Authorization", required = false) String authorizationHeader) { return ApiResponse.success(authService.currentUser(authorizationHeader)); diff --git a/backend/auth/src/main/resources/application.yml b/backend/auth/src/main/resources/application.yml index 5c86e53..7382309 100644 --- a/backend/auth/src/main/resources/application.yml +++ b/backend/auth/src/main/resources/application.yml @@ -23,6 +23,6 @@ auth: enabled: true gateway-mode: true whitelist: - - /auth/login - - /auth/refresh + - /auth/tokens + - /auth/tokens/refresh - /actuator/** diff --git a/backend/boot-dev/src/main/resources/application.yml b/backend/boot-dev/src/main/resources/application.yml index 334a2cc..bfc9c7a 100644 --- a/backend/boot-dev/src/main/resources/application.yml +++ b/backend/boot-dev/src/main/resources/application.yml @@ -30,8 +30,8 @@ auth: enabled: true gateway-mode: false whitelist: - - /auth/login - - /auth/refresh + - /auth/tokens + - /auth/tokens/refresh - /actuator/** ai: diff --git a/backend/common/common-security/src/main/java/com/k12study/common/security/config/AuthProperties.java b/backend/common/common-security/src/main/java/com/k12study/common/security/config/AuthProperties.java index 01ba9a1..3e9632f 100644 --- a/backend/common/common-security/src/main/java/com/k12study/common/security/config/AuthProperties.java +++ b/backend/common/common-security/src/main/java/com/k12study/common/security/config/AuthProperties.java @@ -14,7 +14,11 @@ public class AuthProperties { private String secret = "k12study-dev-secret-k12study-dev-secret"; private Duration accessTokenTtl = Duration.ofHours(12); private Duration refreshTokenTtl = Duration.ofDays(7); - private List whitelist = new ArrayList<>(List.of("/actuator/**", "/auth/login", "/auth/refresh")); + private List whitelist = new ArrayList<>(List.of( + "/actuator/**", + "/auth/tokens", + "/auth/tokens/refresh" + )); public boolean isEnabled() { return enabled; diff --git a/backend/gateway/src/main/resources/application.yml b/backend/gateway/src/main/resources/application.yml index 4bd2b45..5d601e3 100644 --- a/backend/gateway/src/main/resources/application.yml +++ b/backend/gateway/src/main/resources/application.yml @@ -29,6 +29,6 @@ management: auth: enabled: true whitelist: - - /api/auth/login - - /api/auth/refresh + - /api/auth/tokens + - /api/auth/tokens/refresh - /actuator/** diff --git a/backend/upms/src/main/java/com/k12study/upms/controller/UpmsController.java b/backend/upms/src/main/java/com/k12study/upms/controller/UpmsController.java index 9428d0a..54498d3 100644 --- a/backend/upms/src/main/java/com/k12study/upms/controller/UpmsController.java +++ b/backend/upms/src/main/java/com/k12study/upms/controller/UpmsController.java @@ -5,6 +5,7 @@ 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.api.upms.remote.UpmsApiPaths; import com.k12study.common.api.response.ApiResponse; import com.k12study.upms.service.UpmsQueryService; import java.util.List; @@ -13,7 +14,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/upms") +@RequestMapping(UpmsApiPaths.BASE) public class UpmsController { private final UpmsQueryService upmsQueryService; @@ -26,22 +27,22 @@ public class UpmsController { return ApiResponse.success(upmsQueryService.routes()); } - @GetMapping("/areas/tree") + @GetMapping("/areas") public ApiResponse> areas() { return ApiResponse.success(upmsQueryService.areas()); } - @GetMapping("/tenants/tree") + @GetMapping("/tenants") public ApiResponse> tenants() { return ApiResponse.success(upmsQueryService.tenants()); } - @GetMapping("/depts/tree") + @GetMapping("/departments") public ApiResponse> departments() { return ApiResponse.success(upmsQueryService.departments()); } - @GetMapping("/current-user") + @GetMapping("/users/current") public ApiResponse currentUser() { return ApiResponse.success(upmsQueryService.currentUser()); } diff --git a/backend/upms/src/main/java/com/k12study/upms/domain/SysArea.java b/backend/upms/src/main/java/com/k12study/upms/domain/SysArea.java index 1ab34e8..e873ba1 100644 --- a/backend/upms/src/main/java/com/k12study/upms/domain/SysArea.java +++ b/backend/upms/src/main/java/com/k12study/upms/domain/SysArea.java @@ -1,10 +1,12 @@ package com.k12study.upms.domain; public record SysArea( - String areaCode, - String parentCode, + long id, + long pid, + long adcode, String areaName, - String areaLevel, - String provinceCode + String areaType, + String areaStatus, + String delFlag ) { } diff --git a/backend/upms/src/main/java/com/k12study/upms/domain/SysDept.java b/backend/upms/src/main/java/com/k12study/upms/domain/SysDept.java index 57bd0ed..6a00860 100644 --- a/backend/upms/src/main/java/com/k12study/upms/domain/SysDept.java +++ b/backend/upms/src/main/java/com/k12study/upms/domain/SysDept.java @@ -6,6 +6,8 @@ public record SysDept( String tenantId, String deptName, String deptType, + String adcode, + String tenantPath, String deptPath ) { } diff --git a/backend/upms/src/main/java/com/k12study/upms/domain/SysTenant.java b/backend/upms/src/main/java/com/k12study/upms/domain/SysTenant.java index 9795bb7..626415e 100644 --- a/backend/upms/src/main/java/com/k12study/upms/domain/SysTenant.java +++ b/backend/upms/src/main/java/com/k12study/upms/domain/SysTenant.java @@ -5,8 +5,8 @@ public record SysTenant( String parentTenantId, String tenantName, String tenantType, - String provinceCode, - String areaCode, - String tenantPath + String adcode, + String tenantPath, + String status ) { } diff --git a/backend/upms/src/main/java/com/k12study/upms/service/UpmsQueryService.java b/backend/upms/src/main/java/com/k12study/upms/service/UpmsQueryService.java index 40f2b8c..438f9d5 100644 --- a/backend/upms/src/main/java/com/k12study/upms/service/UpmsQueryService.java +++ b/backend/upms/src/main/java/com/k12study/upms/service/UpmsQueryService.java @@ -3,109 +3,17 @@ package com.k12study.upms.service; 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 java.util.List; -import org.springframework.stereotype.Service; +public interface UpmsQueryService { + List routes(); -@Service -public class UpmsQueryService { + List areas(); - public List 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() - ) - ); - } + List tenants(); - public List areas() { - return List.of( - new AreaNodeDto( - "330000", - "浙江省", - "province", - "330000", - List.of( - new AreaNodeDto("330100", "杭州市", "city", "330000", List.of()) - ) - ) - ); - } + List departments(); - public List tenants() { - 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 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") - ); - } + CurrentRouteUserDto currentUser(); } diff --git a/backend/upms/src/main/java/com/k12study/upms/service/impl/UpmsQueryServiceImpl.java b/backend/upms/src/main/java/com/k12study/upms/service/impl/UpmsQueryServiceImpl.java new file mode 100644 index 0000000..c8ec429 --- /dev/null +++ b/backend/upms/src/main/java/com/k12study/upms/service/impl/UpmsQueryServiceImpl.java @@ -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 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 areas() { + return List.of( + new AreaNodeDto( + "330000", + "100000", + "浙江省", + "PROVINCE", + List.of( + new AreaNodeDto( + "330100", + "330000", + "杭州市", + "CITY", + List.of() + ) + ) + ) + ); + } + + @Override + public List 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 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") + ); + } +} diff --git a/docs/MAXHUBShareSetup_6.7.8.27_108.exe b/docs/MAXHUBShareSetup_6.7.8.27_108.exe new file mode 100644 index 0000000..a76a1e7 Binary files /dev/null and b/docs/MAXHUBShareSetup_6.7.8.27_108.exe differ diff --git a/docs/apijson/upms.api.json b/docs/apijson/upms.api.json new file mode 100644 index 0000000..5fef1cf --- /dev/null +++ b/docs/apijson/upms.api.json @@ -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)" + } + ] +} diff --git a/docs/architecture/api-design.md b/docs/architecture/api-design.md index 1a139fb..d2c7eb9 100644 --- a/docs/architecture/api-design.md +++ b/docs/architecture/api-design.md @@ -1,7 +1,7 @@ # API 设计(基础架构 + 业务功能) ## 1. API 设计原则 - 对外统一前缀:`/api/*`。 -- 统一响应结构:`code/message/data/traceId`。 +- 统一响应结构:`code/message/data/traceId`,其中 `code=0` 表示成功,非 0 表示业务失败。 - 认证策略:JWT + RBAC,网关做统一鉴权透传。 - API 冻结点: - 基础架构 API 在 M3 冻结。 @@ -9,16 +9,16 @@ ## 2. 基础架构 API(M3) ### 2.1 认证域(auth) -- `POST /api/auth/login` -- `POST /api/auth/refresh` -- `GET /api/auth/current-user` +- `POST /api/auth/tokens`(登录) +- `POST /api/auth/tokens/refresh`(刷新) +- `GET /api/auth/users/current`(当前用户) ### 2.2 权限与组织域(upms) - `GET /api/upms/routes` -- `GET /api/upms/current-user` -- `GET /api/upms/areas/tree` -- `GET /api/upms/tenants/tree` -- `GET /api/upms/depts/tree` +- `GET /api/upms/users/current` +- `GET /api/upms/areas` +- `GET /api/upms/tenants` +- `GET /api/upms/departments` ### 2.3 基础扩展 API(建议补充) - 文件域 diff --git a/docs/architecture/logical-view.md b/docs/architecture/logical-view.md index 6e2e0e3..63fd51d 100644 --- a/docs/architecture/logical-view.md +++ b/docs/architecture/logical-view.md @@ -55,9 +55,9 @@ graph TD ## 3. 主链路视角 ### 3.1 基础架构链路 -- 登录:`frontend/app -> /api/auth/login -> auth -> token` +- 登录:`frontend/app -> /api/auth/tokens -> auth -> token` - 权限与路由:`frontend/app -> /api/upms/routes -> upms` -- 组织数据:`/api/upms/areas/tree|tenants/tree|depts/tree` +- 组织数据:`/api/upms/areas|tenants|departments` ### 3.2 教学业务链路 - 教师发作业:题库/试卷/作业配置 -> 投放班级。 diff --git a/docs/architecture/完整业务流程图.drawio b/docs/architecture/完整业务流程图.drawio index 4bea98f..43c1e1c 100644 --- a/docs/architecture/完整业务流程图.drawio +++ b/docs/architecture/完整业务流程图.drawio @@ -1,6 +1,6 @@ - + @@ -35,13 +35,13 @@ - + - + - + diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts index 81a5669..fe48925 100644 --- a/frontend/src/api/auth.ts +++ b/frontend/src/api/auth.ts @@ -10,5 +10,11 @@ interface LoginInput { } export async function login(input: LoginInput) { - return http.post>("/auth/login", input); + return http.post>("/auth/tokens", input); +} + +export async function refreshToken(refreshToken: string) { + return http.post>("/auth/tokens/refresh", { + refreshToken + }); } diff --git a/frontend/src/api/upms.ts b/frontend/src/api/upms.ts index afbf7d8..05d4022 100644 --- a/frontend/src/api/upms.ts +++ b/frontend/src/api/upms.ts @@ -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 { 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 { - const response = await http.get>("/upms/routes"); + const response = await getUpmsRoutesRemote(); return response.data as RouteNode[]; } export async function fetchCurrentUser(): Promise { - const response = await http.get>("/upms/current-user"); + const response = await getUpmsCurrentUserRemote(); return response.data as CurrentRouteUser; } + +export async function fetchAreas(): Promise { + const response = await getUpmsAreasRemote(); + return normalizeAreaNodes(response.data as UpmsAreaNode[]); +} + +export async function fetchTenants(): Promise { + const response = await getUpmsTenantsRemote(); + return normalizeTenantNodes(response.data as UpmsTenantNode[]); +} + +export async function fetchDepartments(): Promise { + const response = await getUpmsDepartmentsRemote(); + return normalizeDeptNodes(response.data as UpmsDeptNode[]); +} diff --git a/frontend/src/remote/upmsRemote.ts b/frontend/src/remote/upmsRemote.ts new file mode 100644 index 0000000..183f5af --- /dev/null +++ b/frontend/src/remote/upmsRemote.ts @@ -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>("/upms/routes"); +} + +export function getUpmsCurrentUserRemote() { + return http.get>("/upms/users/current"); +} + +export function getUpmsAreasRemote() { + return http.get>("/upms/areas"); +} + +export function getUpmsTenantsRemote() { + return http.get>("/upms/tenants"); +} + +export function getUpmsDepartmentsRemote() { + return http.get>("/upms/departments"); +} diff --git a/frontend/src/types/route.ts b/frontend/src/types/route.ts index 24f61cf..5071e7d 100644 --- a/frontend/src/types/route.ts +++ b/frontend/src/types/route.ts @@ -1,27 +1,11 @@ -export type LayoutType = "DEFAULT" | "SIDEBAR"; +import type { + UpmsCurrentUser, + UpmsLayoutType, + UpmsRouteMeta, + UpmsRouteNode +} from "./upms"; -export interface RouteMeta { - title: string; - icon?: string; - permissionCodes?: string[]; - 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[]; -} +export type LayoutType = UpmsLayoutType; +export type RouteMeta = UpmsRouteMeta; +export type RouteNode = UpmsRouteNode; +export type CurrentRouteUser = UpmsCurrentUser; diff --git a/frontend/src/types/upms.ts b/frontend/src/types/upms.ts new file mode 100644 index 0000000..49d1527 --- /dev/null +++ b/frontend/src/types/upms.ts @@ -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[]; +} diff --git a/frontend/src/utils/http.ts b/frontend/src/utils/http.ts index 4c6bd94..953d56d 100644 --- a/frontend/src/utils/http.ts +++ b/frontend/src/utils/http.ts @@ -1,3 +1,4 @@ +import type { ApiResponse } from "../types/api"; import { getAccessToken } from "./storage"; const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "/api"; @@ -12,6 +13,14 @@ interface RequestOptions { timeout?: number; } +function isApiResponse(payload: unknown): payload is ApiResponse { + if (typeof payload !== "object" || payload === null) { + return false; + } + + return "code" in payload && "message" in payload && "data" in payload; +} + function buildUrl(path: string) { if (/^https?:\/\//.test(path)) { return path; @@ -57,6 +66,9 @@ async function request(path: string, options: RequestOptions = {}): Promise