更新+mock部分

This commit is contained in:
2026-04-16 18:12:09 +08:00
parent d5c06eca28
commit adadb3bf1d
40 changed files with 884 additions and 174 deletions

View 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` 以支持链路追踪。

View 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

View 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/*` 统一入口不变。
- 旧接口不兼容,禁止新增或恢复旧路径别名。

View 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
View File

@@ -2,7 +2,9 @@ urbanLifeline
Tik
schoolNewsServ
.idea/
.vscode/
.vscode/*
!.vscode/settings.json
!.vscode/extensions.json
.DS_Store
.tmp-run/

5
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"recommendations": [
"ygqygq2.turbo-file-header"
]
}

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"java.compile.nullAnalysis.mode": "automatic"
}

View File

@@ -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
});

View File

@@ -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
};

View File

@@ -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
});
});

View File

@@ -0,0 +1,4 @@
package com.k12study.api.auth.dto;
public record RefreshTokenRequest(String refreshToken) {
}

View File

@@ -4,9 +4,9 @@ import java.util.List;
public record AreaNodeDto(
String areaCode,
String parentCode,
String areaName,
String areaLevel,
String provinceCode,
List<AreaNodeDto> children
) {
}

View File

@@ -6,8 +6,11 @@ public record CurrentRouteUserDto(
String userId,
String username,
String displayName,
String adcode,
String tenantId,
String tenantPath,
String deptId,
String deptPath,
List<String> permissionCodes
) {
}

View File

@@ -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<DeptNodeDto> children
) {

View File

@@ -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<TenantNodeDto> children
) {

View File

@@ -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";
}

View File

@@ -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();
}

View File

@@ -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<TokenResponse> login(@RequestBody LoginRequest request) {
return ApiResponse.success("登录成功", authService.login(request));
}
@PostMapping("/refresh")
public ApiResponse<TokenResponse> refresh(@RequestParam("refreshToken") String refreshToken) {
@PostMapping("/tokens/refresh")
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));
}
@GetMapping("/current-user")
@GetMapping("/users/current")
public ApiResponse<CurrentUserResponse> currentUser(
@RequestHeader(value = "Authorization", required = false) String authorizationHeader) {
return ApiResponse.success(authService.currentUser(authorizationHeader));

View File

@@ -23,6 +23,6 @@ auth:
enabled: true
gateway-mode: true
whitelist:
- /auth/login
- /auth/refresh
- /auth/tokens
- /auth/tokens/refresh
- /actuator/**

View File

@@ -30,8 +30,8 @@ auth:
enabled: true
gateway-mode: false
whitelist:
- /auth/login
- /auth/refresh
- /auth/tokens
- /auth/tokens/refresh
- /actuator/**
ai:

View File

@@ -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<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() {
return enabled;

View File

@@ -29,6 +29,6 @@ management:
auth:
enabled: true
whitelist:
- /api/auth/login
- /api/auth/refresh
- /api/auth/tokens
- /api/auth/tokens/refresh
- /actuator/**

View File

@@ -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<List<AreaNodeDto>> areas() {
return ApiResponse.success(upmsQueryService.areas());
}
@GetMapping("/tenants/tree")
@GetMapping("/tenants")
public ApiResponse<List<TenantNodeDto>> tenants() {
return ApiResponse.success(upmsQueryService.tenants());
}
@GetMapping("/depts/tree")
@GetMapping("/departments")
public ApiResponse<List<DeptNodeDto>> departments() {
return ApiResponse.success(upmsQueryService.departments());
}
@GetMapping("/current-user")
@GetMapping("/users/current")
public ApiResponse<CurrentRouteUserDto> currentUser() {
return ApiResponse.success(upmsQueryService.currentUser());
}

View File

@@ -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
) {
}

View File

@@ -6,6 +6,8 @@ public record SysDept(
String tenantId,
String deptName,
String deptType,
String adcode,
String tenantPath,
String deptPath
) {
}

View File

@@ -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
) {
}

View File

@@ -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<RouteNodeDto> routes();
@Service
public class UpmsQueryService {
List<AreaNodeDto> areas();
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()
)
);
}
List<TenantNodeDto> tenants();
public List<AreaNodeDto> areas() {
return List.of(
new AreaNodeDto(
"330000",
"浙江省",
"province",
"330000",
List.of(
new AreaNodeDto("330100", "杭州市", "city", "330000", List.of())
)
)
);
}
List<DeptNodeDto> departments();
public List<TenantNodeDto> 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<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")
);
}
CurrentRouteUserDto currentUser();
}

View File

@@ -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")
);
}
}

Binary file not shown.

269
docs/apijson/upms.api.json Normal file
View 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)"
}
]
}

View File

@@ -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. 基础架构 APIM3
### 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建议补充
- 文件域

View File

@@ -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 教学业务链路
- 教师发作业:题库/试卷/作业配置 -> 投放班级。

View File

@@ -1,6 +1,6 @@
<mxfile host="65bd71144e">
<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>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
@@ -35,13 +35,13 @@
<mxGeometry x="30" y="110" width="5400" height="220" as="geometry"/>
</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">
<mxGeometry x="30" y="360" width="5400" height="260" as="geometry"/>
<mxGeometry x="20" y="370" width="5400" height="260" as="geometry"/>
</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">
<mxGeometry x="30" y="660" width="5400" height="620" as="geometry"/>
<mxGeometry x="50" y="670" width="5400" height="620" as="geometry"/>
</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">
<mxGeometry x="30" y="1320" width="5400" height="300" as="geometry"/>
<mxGeometry y="1330" width="5400" height="300" as="geometry"/>
</mxCell>
<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"/>

View File

@@ -10,5 +10,11 @@ interface 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
});
}

View File

@@ -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<RouteNode[]> {
const response = await http.get<ApiResponse<RouteNode[]>>("/upms/routes");
const response = await getUpmsRoutesRemote();
return response.data as RouteNode[];
}
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;
}
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[]);
}

View 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");
}

View File

@@ -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;

View 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[];
}

View File

@@ -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<unknown> {
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<T>(path: string, options: RequestOptions = {}): Promise<T
: `Request failed with status ${response.status}`;
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;
} finally {

65
global.code-snippets Normal file
View 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": "变量描述"
}
}