This commit is contained in:
2026-04-14 16:27:47 +08:00
commit 4b38a4c952
134 changed files with 7478 additions and 0 deletions

13
.devcontainer/README.md Normal file
View File

@@ -0,0 +1,13 @@
# VS Code Docker Dev
This repository provides two VS Code Dev Container modes:
- `external`: connect to externally managed PostgreSQL and Redis
- `internal`: create PostgreSQL and Redis inside the dev environment
Open the wanted config in VS Code:
- `.devcontainer/external/devcontainer.json`
- `.devcontainer/internal/devcontainer.json`
Both modes mount the repository into `/workspaces/K12study`.

8
.devcontainer/external/.env.example vendored Normal file
View File

@@ -0,0 +1,8 @@
K12STUDY_DB_HOST=host.docker.internal
K12STUDY_DB_PORT=5432
K12STUDY_DB_NAME=k12study
K12STUDY_DB_USER=k12study
K12STUDY_DB_PASSWORD=k12study
K12STUDY_REDIS_HOST=host.docker.internal
K12STUDY_REDIS_PORT=6379
K12STUDY_REDIS_PASSWORD=

View File

@@ -0,0 +1,31 @@
{
"name": "K12Study External DB Dev",
"dockerComposeFile": [
"docker-compose.yml"
],
"service": "workspace",
"workspaceFolder": "/workspaces/K12study",
"shutdownAction": "stopCompose",
"forwardPorts": [5173, 8080, 8081, 8082, 8088, 9000],
"customizations": {
"vscode": {
"settings": {
"terminal.integrated.defaultProfile.linux": "bash"
},
"extensions": [
"vscjava.vscode-java-pack",
"redhat.java",
"vmware.vscode-boot-dev-pack",
"ms-azuretools.vscode-docker",
"ms-python.python",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
},
"remoteEnv": {
"SPRING_PROFILES_ACTIVE": "dev",
"K12STUDY_DEV_MODE": "external"
},
"postCreateCommand": "cd /workspaces/K12study/frontend && pnpm install"
}

View File

@@ -0,0 +1,19 @@
services:
workspace:
image: mcr.microsoft.com/devcontainers/universal:2
volumes:
- ../../:/workspaces/K12study:cached
working_dir: /workspaces/K12study
command: sleep infinity
environment:
K12STUDY_DB_HOST: ${K12STUDY_DB_HOST:-host.docker.internal}
K12STUDY_DB_PORT: ${K12STUDY_DB_PORT:-5432}
K12STUDY_DB_NAME: ${K12STUDY_DB_NAME:-k12study}
K12STUDY_DB_USER: ${K12STUDY_DB_USER:-k12study}
K12STUDY_DB_PASSWORD: ${K12STUDY_DB_PASSWORD:-k12study}
K12STUDY_REDIS_HOST: ${K12STUDY_REDIS_HOST:-host.docker.internal}
K12STUDY_REDIS_PORT: ${K12STUDY_REDIS_PORT:-6379}
K12STUDY_REDIS_PASSWORD: ${K12STUDY_REDIS_PASSWORD:-}
PYTHONUNBUFFERED: "1"
extra_hosts:
- "host.docker.internal:host-gateway"

View File

@@ -0,0 +1,32 @@
{
"name": "K12Study Internal DB Dev",
"dockerComposeFile": [
"docker-compose.yml"
],
"service": "workspace",
"workspaceFolder": "/workspaces/K12study",
"runServices": ["workspace", "postgres", "redis"],
"shutdownAction": "stopCompose",
"forwardPorts": [5173, 8080, 8081, 8082, 8088, 9000, 5432, 6379],
"customizations": {
"vscode": {
"settings": {
"terminal.integrated.defaultProfile.linux": "bash"
},
"extensions": [
"vscjava.vscode-java-pack",
"redhat.java",
"vmware.vscode-boot-dev-pack",
"ms-azuretools.vscode-docker",
"ms-python.python",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
},
"remoteEnv": {
"SPRING_PROFILES_ACTIVE": "dev",
"K12STUDY_DEV_MODE": "internal"
},
"postCreateCommand": "cd /workspaces/K12study/frontend && pnpm install"
}

View File

@@ -0,0 +1,46 @@
services:
workspace:
image: mcr.microsoft.com/devcontainers/universal:2
volumes:
- ../../:/workspaces/K12study:cached
working_dir: /workspaces/K12study
command: sleep infinity
environment:
K12STUDY_DB_HOST: postgres
K12STUDY_DB_PORT: 5432
K12STUDY_DB_NAME: k12study
K12STUDY_DB_USER: k12study
K12STUDY_DB_PASSWORD: k12study
K12STUDY_REDIS_HOST: redis
K12STUDY_REDIS_PORT: 6379
K12STUDY_REDIS_PASSWORD: ""
PYTHONUNBUFFERED: "1"
depends_on:
- postgres
- redis
postgres:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_DB: k12study
POSTGRES_USER: k12study
POSTGRES_PASSWORD: k12study
ports:
- "5432:5432"
volumes:
- postgres-dev-data:/var/lib/postgresql/data
- ../../init/pg:/docker-entrypoint-initdb.d
redis:
image: redis:7
restart: unless-stopped
ports:
- "6379:6379"
command: redis-server --appendonly yes
volumes:
- redis-dev-data:/data
volumes:
postgres-dev-data:
redis-dev-data:

14
.editorconfig Normal file
View File

@@ -0,0 +1,14 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
[*.{js,ts,jsx,tsx,json,java,py,yml}]
indent_style = space
indent_size = 4
[*.{md,txt}]
indent_style = space
indent_size = 4

22
.gitignore vendored Normal file
View File

@@ -0,0 +1,22 @@
urbanLifeline
Tik
.idea/
.vscode/
.DS_Store
.tmp-run/
**/target/
**/node_modules/
**/dist/
**/.turbo/
**/.cache/
**/.pytest_cache/
**/__pycache__/
**/*.pyc
.env
.env.local
.env.*.local
frontend/pnpm-lock.yaml

71
README.md Normal file
View File

@@ -0,0 +1,71 @@
# K12Study
K12Study 是首版 K12 智能学习系统项目骨架,当前聚焦“可运行基础框架”,用于承接多区域、多租户、多角色的后续业务建设。
## 目录结构
- `backend`: Java 多模块后端、网关、本地聚合启动、Python AI 占位服务
- `frontend`: 单一 React 管理端项目,保留 `api / types / utils / components / dynamic route`
- `app`: 微信小程序项目骨架,独立于 Web 管理端
- `init/pg`: PostgreSQL 初始化脚本,按模块拆分维护
- `docs`: 产品、架构和实施计划文档
## 运行模式
- 分布式模式:独立启动 `gateway``auth``upms``python-ai`
- 本地模式:通过 `backend/boot-dev` 单进程暴露 `/api/auth/**``/api/upms/**`
## VS Code Docker Dev
提供两套 dev container
- `.devcontainer/external/devcontainer.json`
连接外部 PostgreSQL / Redis
- `.devcontainer/internal/devcontainer.json`
在开发环境内部创建 PostgreSQL / Redis
代码与本地目录同步方式是 Docker bind mount不是额外复制
- 宿主机项目目录直接挂载到容器工作目录
- 本地改代码,容器内实时可见
- 容器里安装的依赖和运行环境与本地源码共用同一份工程目录
## 快速开始
### Backend
```bash
cd backend
mvn -q -DskipTests package
```
### Frontend
```bash
cd frontend
pnpm install
pnpm dev
```
### App
```bash
cd app
npm run dev
```
然后使用微信开发者工具打开 `app/project.config.json`
### Python AI
```bash
cd backend/python-ai
pip install -r requirements.txt
uvicorn app.main:app --reload --port 9000
```
### Infrastructure
```bash
docker compose up -d postgres redis
```

17
app/README.md Normal file
View File

@@ -0,0 +1,17 @@
# K12Study App
`app` 目录用于微信小程序项目骨架,占位承接家长端、学生端或轻量移动端能力。
## 当前结构
- `src/app.*`: 小程序全局入口
- `src/pages/home`: 首页占位
- `src/pages/profile`: 我的页占位
- `src/api`: 调用后端 `/api/auth``/api/upms` 的接口封装预留
- `src/utils/request.js`: 小程序请求基础封装
## 使用方式
1. 使用微信开发者工具打开 [project.config.json](/f:/Project/K12study/app/project.config.json)
2. 确认 `miniprogramRoot` 指向 `src`
3. 按实际环境修改 `src/utils/request.js` 中的 `BASE_URL`

9
app/package.json Normal file
View File

@@ -0,0 +1,9 @@
{
"name": "k12study-app",
"private": true,
"version": "0.1.0",
"scripts": {
"dev": "echo Open this project with WeChat DevTools",
"lint": "echo Add miniprogram lint rules when business code grows"
}
}

16
app/project.config.json Normal file
View File

@@ -0,0 +1,16 @@
{
"description": "K12Study 微信小程序骨架",
"compileType": "miniprogram",
"miniprogramRoot": "src/",
"srcMiniprogramRoot": "src/",
"appid": "touristappid",
"setting": {
"urlCheck": false,
"es6": true,
"enhance": true,
"postcss": true,
"minified": false
},
"simulatorType": "wechat",
"libVersion": "trial"
}

13
app/src/api/auth.js Normal file
View File

@@ -0,0 +1,13 @@
const { request } = require("../utils/request");
function login(data) {
return request({
url: "/api/auth/login",
method: "POST",
data
});
}
module.exports = {
login
};

12
app/src/api/upms.js Normal file
View File

@@ -0,0 +1,12 @@
const { request } = require("../utils/request");
function getRouteMeta() {
return request({
url: "/api/upms/routes",
method: "GET"
});
}
module.exports = {
getRouteMeta
};

5
app/src/app.js Normal file
View File

@@ -0,0 +1,5 @@
App({
globalData: {
userInfo: null
}
});

14
app/src/app.json Normal file
View File

@@ -0,0 +1,14 @@
{
"pages": [
"pages/home/index",
"pages/profile/index"
],
"window": {
"navigationBarTitleText": "K12Study",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#f5f7fb"
},
"style": "v2",
"sitemapLocation": "sitemap.json"
}

17
app/src/app.wxss Normal file
View File

@@ -0,0 +1,17 @@
page {
background: #f5f7fb;
color: #1f2937;
font-size: 28rpx;
}
.page {
min-height: 100vh;
padding: 32rpx;
}
.card {
padding: 32rpx;
border-radius: 24rpx;
background: #ffffff;
box-shadow: 0 12rpx 40rpx rgba(15, 23, 42, 0.08);
}

View File

@@ -0,0 +1,6 @@
Page({
data: {
title: "K12Study 小程序骨架",
description: "这里先放首页占位,后续可扩展为家长端或学生端入口。"
}
});

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "首页"
}

View File

@@ -0,0 +1,6 @@
<view class="page">
<view class="card">
<view>{{title}}</view>
<view style="margin-top: 16rpx; color: #64748b;">{{description}}</view>
</view>
</view>

View File

@@ -0,0 +1,3 @@
view {
line-height: 1.6;
}

View File

@@ -0,0 +1,6 @@
Page({
data: {
title: "我的",
description: "这里预留账号中心、学校切换、消息入口等能力。"
}
});

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "我的"
}

View File

@@ -0,0 +1,6 @@
<view class="page">
<view class="card">
<view>{{title}}</view>
<view style="margin-top: 16rpx; color: #64748b;">{{description}}</view>
</view>
</view>

View File

@@ -0,0 +1,3 @@
view {
line-height: 1.6;
}

4
app/src/sitemap.json Normal file
View File

@@ -0,0 +1,4 @@
{
"desc": "K12Study sitemap",
"rules": []
}

21
app/src/utils/request.js Normal file
View File

@@ -0,0 +1,21 @@
const BASE_URL = "http://localhost:8088";
function request(options) {
return new Promise((resolve, reject) => {
wx.request({
url: `${BASE_URL}${options.url}`,
method: options.method || "GET",
data: options.data,
header: {
"Content-Type": "application/json",
...(options.header || {})
},
success: (response) => resolve(response.data),
fail: reject
});
});
}
module.exports = {
request
};

11
backend/.env.example Normal file
View File

@@ -0,0 +1,11 @@
K12STUDY_DB_HOST=localhost
K12STUDY_DB_PORT=5432
K12STUDY_DB_NAME=k12study
K12STUDY_DB_USER=k12study
K12STUDY_DB_PASSWORD=k12study
K12STUDY_REDIS_HOST=localhost
K12STUDY_REDIS_PORT=6379
K12STUDY_REDIS_PASSWORD=
AI_CLIENT_BASE_URL=http://localhost:9000

23
backend/ai-client/pom.xml Normal file
View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>ai-client</artifactId>
<dependencies>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>api-ai</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,25 @@
package com.k12study.aiclient.client;
import com.k12study.api.ai.dto.AiHealthDto;
import org.springframework.web.client.RestClient;
public class HttpPythonAiClient implements PythonAiClient {
private final RestClient restClient;
public HttpPythonAiClient(RestClient restClient) {
this.restClient = restClient;
}
@Override
public AiHealthDto health() {
try {
AiHealthDto response = restClient.get()
.uri("/health")
.retrieve()
.body(AiHealthDto.class);
return response == null ? new AiHealthDto("python-ai", "UNKNOWN", "0.1.0") : response;
} catch (Exception exception) {
return new AiHealthDto("python-ai", "UNREACHABLE", "0.1.0");
}
}
}

View File

@@ -0,0 +1,7 @@
package com.k12study.aiclient.client;
import com.k12study.api.ai.dto.AiHealthDto;
public interface PythonAiClient {
AiHealthDto health();
}

View File

@@ -0,0 +1,23 @@
package com.k12study.aiclient.config;
import com.k12study.aiclient.client.HttpPythonAiClient;
import com.k12study.aiclient.client.PythonAiClient;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
@Configuration
@EnableConfigurationProperties(AiClientProperties.class)
public class AiClientAutoConfiguration {
@Bean
public RestClient aiRestClient(AiClientProperties properties) {
return RestClient.builder().baseUrl(properties.getBaseUrl()).build();
}
@Bean
public PythonAiClient pythonAiClient(RestClient aiRestClient) {
return new HttpPythonAiClient(aiRestClient);
}
}

View File

@@ -0,0 +1,16 @@
package com.k12study.aiclient.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "ai.client")
public class AiClientProperties {
private String baseUrl = "http://localhost:9000";
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>api-ai</artifactId>
<dependencies>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,8 @@
package com.k12study.api.ai.dto;
public record AiHealthDto(
String name,
String status,
String version
) {
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>api-auth</artifactId>
<dependencies>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,15 @@
package com.k12study.api.auth.dto;
import java.util.List;
public record CurrentUserResponse(
String userId,
String username,
String displayName,
String provinceCode,
String areaCode,
String tenantId,
String deptId,
List<String> roles
) {
}

View File

@@ -0,0 +1,10 @@
package com.k12study.api.auth.dto;
public record LoginRequest(
String username,
String password,
String provinceCode,
String areaCode,
String tenantId
) {
}

View File

@@ -0,0 +1,9 @@
package com.k12study.api.auth.dto;
public record TokenResponse(
String accessToken,
String refreshToken,
String tokenType,
long expiresIn
) {
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>api-upms</artifactId>
<dependencies>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,12 @@
package com.k12study.api.upms.dto;
import java.util.List;
public record AreaNodeDto(
String areaCode,
String areaName,
String areaLevel,
String provinceCode,
List<AreaNodeDto> children
) {
}

View File

@@ -0,0 +1,13 @@
package com.k12study.api.upms.dto;
import java.util.List;
public record CurrentRouteUserDto(
String userId,
String username,
String displayName,
String tenantId,
String deptId,
List<String> permissionCodes
) {
}

View File

@@ -0,0 +1,13 @@
package com.k12study.api.upms.dto;
import java.util.List;
public record DeptNodeDto(
String deptId,
String deptName,
String deptType,
String tenantId,
String deptPath,
List<DeptNodeDto> children
) {
}

View File

@@ -0,0 +1,6 @@
package com.k12study.api.upms.dto;
public enum LayoutType {
DEFAULT,
SIDEBAR
}

View File

@@ -0,0 +1,11 @@
package com.k12study.api.upms.dto;
import java.util.List;
public record RouteMetaDto(
String title,
String icon,
List<String> permissionCodes,
boolean hidden
) {
}

View File

@@ -0,0 +1,14 @@
package com.k12study.api.upms.dto;
import java.util.List;
public record RouteNodeDto(
String id,
String path,
String name,
String component,
LayoutType layout,
RouteMetaDto meta,
List<RouteNodeDto> children
) {
}

View File

@@ -0,0 +1,14 @@
package com.k12study.api.upms.dto;
import java.util.List;
public record TenantNodeDto(
String tenantId,
String tenantName,
String tenantType,
String provinceCode,
String areaCode,
String tenantPath,
List<TenantNodeDto> children
) {
}

21
backend/apis/pom.xml Normal file
View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>apis-parent</artifactId>
<packaging>pom</packaging>
<modules>
<module>api-auth</module>
<module>api-upms</module>
<module>api-ai</module>
</modules>
</project>

50
backend/auth/pom.xml Normal file
View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>auth</artifactId>
<dependencies>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-web</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-security</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-redis</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>api-auth</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,11 @@
package com.k12study.auth;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = {"com.k12study.auth", "com.k12study.common"})
public class AuthApplication {
public static void main(String[] args) {
SpringApplication.run(AuthApplication.class, args);
}
}

View File

@@ -0,0 +1,14 @@
package com.k12study.auth.config;
import com.k12study.auth.AuthApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
@Configuration
@ComponentScan(
basePackages = "com.k12study.auth",
excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = AuthApplication.class)
)
public class AuthModuleConfiguration {
}

View File

@@ -0,0 +1,40 @@
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.TokenResponse;
import com.k12study.auth.service.AuthService;
import com.k12study.common.api.response.ApiResponse;
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
@RequestMapping("/auth")
public class AuthController {
private final AuthService authService;
public AuthController(AuthService authService) {
this.authService = authService;
}
@PostMapping("/login")
public ApiResponse<TokenResponse> login(@RequestBody LoginRequest request) {
return ApiResponse.success("登录成功", authService.login(request));
}
@PostMapping("/refresh")
public ApiResponse<TokenResponse> refresh(@RequestParam("refreshToken") String refreshToken) {
return ApiResponse.success("刷新成功", authService.refresh(refreshToken));
}
@GetMapping("/current-user")
public ApiResponse<CurrentUserResponse> currentUser(
@RequestHeader(value = "Authorization", required = false) String authorizationHeader) {
return ApiResponse.success(authService.currentUser(authorizationHeader));
}
}

View File

@@ -0,0 +1,66 @@
package com.k12study.auth.service;
import com.k12study.api.auth.dto.CurrentUserResponse;
import com.k12study.api.auth.dto.LoginRequest;
import com.k12study.api.auth.dto.TokenResponse;
import com.k12study.common.security.context.RequestUserContextHolder;
import com.k12study.common.security.jwt.JwtTokenProvider;
import com.k12study.common.security.jwt.JwtUserPrincipal;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class AuthService {
private final JwtTokenProvider jwtTokenProvider;
public AuthService(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
public TokenResponse login(LoginRequest request) {
String username = request.username() == null || request.username().isBlank() ? "admin" : request.username();
JwtUserPrincipal principal = new JwtUserPrincipal(
"U10001",
username,
"K12Study 管理员",
request.tenantId() == null || request.tenantId().isBlank() ? "SCH-HQ" : request.tenantId(),
"DEPT-HQ-ADMIN"
);
String accessToken = jwtTokenProvider.createAccessToken(principal);
String refreshToken = jwtTokenProvider.createAccessToken(principal);
return new TokenResponse(accessToken, refreshToken, "Bearer", 12 * 60 * 60);
}
public TokenResponse refresh(String refreshToken) {
JwtUserPrincipal principal = jwtTokenProvider.parse(refreshToken);
String accessToken = jwtTokenProvider.createAccessToken(principal);
return new TokenResponse(accessToken, refreshToken, "Bearer", 12 * 60 * 60);
}
public CurrentUserResponse currentUser(String authorizationHeader) {
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
JwtUserPrincipal principal = jwtTokenProvider.parse(authorizationHeader.substring("Bearer ".length()));
return new CurrentUserResponse(
principal.userId(),
principal.username(),
principal.displayName(),
"330000",
"330100",
principal.tenantId(),
principal.deptId(),
List.of("SUPER_ADMIN", "ORG_ADMIN")
);
}
var context = RequestUserContextHolder.get();
return new CurrentUserResponse(
context == null ? "U10001" : context.userId(),
context == null ? "admin" : context.username(),
context == null ? "K12Study 管理员" : context.displayName(),
"330000",
"330100",
context == null ? "SCH-HQ" : context.tenantId(),
context == null ? "DEPT-HQ-ADMIN" : context.deptId(),
List.of("SUPER_ADMIN", "ORG_ADMIN")
);
}
}

View File

@@ -0,0 +1,28 @@
server:
port: 8081
spring:
application:
name: k12study-auth
data:
redis:
host: ${K12STUDY_REDIS_HOST:localhost}
port: ${K12STUDY_REDIS_PORT:6379}
password: ${K12STUDY_REDIS_PASSWORD:}
management:
health:
redis:
enabled: false
endpoints:
web:
exposure:
include: health,info
auth:
enabled: true
gateway-mode: true
whitelist:
- /auth/login
- /auth/refresh
- /actuator/**

41
backend/boot-dev/pom.xml Normal file
View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>boot-dev</artifactId>
<dependencies>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>auth</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>upms</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>ai-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,16 @@
package com.k12study.bootdev;
import com.k12study.aiclient.config.AiClientAutoConfiguration;
import com.k12study.auth.config.AuthModuleConfiguration;
import com.k12study.upms.config.UpmsModuleConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;
@SpringBootApplication(scanBasePackages = {"com.k12study.bootdev", "com.k12study.common"})
@Import({AuthModuleConfiguration.class, UpmsModuleConfiguration.class, AiClientAutoConfiguration.class})
public class BootDevApplication {
public static void main(String[] args) {
SpringApplication.run(BootDevApplication.class, args);
}
}

View File

@@ -0,0 +1,39 @@
server:
port: 8088
servlet:
context-path: /api
spring:
application:
name: k12study-boot-dev
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
- org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
- com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration
data:
redis:
host: ${K12STUDY_REDIS_HOST:localhost}
port: ${K12STUDY_REDIS_PORT:6379}
password: ${K12STUDY_REDIS_PASSWORD:}
management:
health:
redis:
enabled: false
endpoints:
web:
exposure:
include: health,info
auth:
enabled: true
gateway-mode: false
whitelist:
- /auth/login
- /auth/refresh
- /actuator/**
ai:
client:
base-url: http://localhost:9000

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>common-api</artifactId>
<dependencies>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,31 @@
package com.k12study.common.api.response;
import com.k12study.common.core.utils.TraceIdHolder;
import lombok.Getter;
@Getter
public class ApiResponse<T> {
private final int code;
private final String message;
private final T data;
private final String traceId;
private ApiResponse(int code, String message, T data, String traceId) {
this.code = code;
this.message = message;
this.data = data;
this.traceId = traceId;
}
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(0, "OK", data, TraceIdHolder.getOrCreate());
}
public static <T> ApiResponse<T> success(String message, T data) {
return new ApiResponse<>(0, message, data, TraceIdHolder.getOrCreate());
}
public static <T> ApiResponse<T> failure(int code, String message) {
return new ApiResponse<>(code, message, null, TraceIdHolder.getOrCreate());
}
}

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>common-core</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,13 @@
package com.k12study.common.core.constants;
public final class SecurityConstants {
public static final String TRACE_ID = "X-Trace-Id";
public static final String HEADER_USER_ID = "X-User-Id";
public static final String HEADER_USERNAME = "X-Username";
public static final String HEADER_DISPLAY_NAME = "X-Display-Name";
public static final String HEADER_TENANT_ID = "X-Tenant-Id";
public static final String HEADER_DEPT_ID = "X-Dept-Id";
private SecurityConstants() {
}
}

View File

@@ -0,0 +1,11 @@
package com.k12study.common.core.domain;
public record RouteKey(
String provinceCode,
String areaCode,
String tenantId,
String tenantPath,
String deptId,
String deptPath
) {
}

View File

@@ -0,0 +1,27 @@
package com.k12study.common.core.utils;
import java.util.UUID;
public final class TraceIdHolder {
private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>();
private TraceIdHolder() {
}
public static String getOrCreate() {
String value = TRACE_ID.get();
if (value == null || value.isBlank()) {
value = UUID.randomUUID().toString().replace("-", "");
TRACE_ID.set(value);
}
return value;
}
public static void set(String traceId) {
TRACE_ID.set(traceId);
}
public static void clear() {
TRACE_ID.remove();
}
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>common-mybatis</artifactId>
<dependencies>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,7 @@
package com.k12study.common.mybatis.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfiguration {
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>common-redis</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,7 @@
package com.k12study.common.redis.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedisConfiguration {
}

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>common-security</artifactId>
<dependencies>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,82 @@
package com.k12study.common.security.config;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "auth")
public class AuthProperties {
private boolean enabled = true;
private boolean gatewayMode = false;
private String tokenHeader = "Authorization";
private String tokenPrefix = "Bearer ";
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"));
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isGatewayMode() {
return gatewayMode;
}
public void setGatewayMode(boolean gatewayMode) {
this.gatewayMode = gatewayMode;
}
public String getTokenHeader() {
return tokenHeader;
}
public void setTokenHeader(String tokenHeader) {
this.tokenHeader = tokenHeader;
}
public String getTokenPrefix() {
return tokenPrefix;
}
public void setTokenPrefix(String tokenPrefix) {
this.tokenPrefix = tokenPrefix;
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public Duration getAccessTokenTtl() {
return accessTokenTtl;
}
public void setAccessTokenTtl(Duration accessTokenTtl) {
this.accessTokenTtl = accessTokenTtl;
}
public Duration getRefreshTokenTtl() {
return refreshTokenTtl;
}
public void setRefreshTokenTtl(Duration refreshTokenTtl) {
this.refreshTokenTtl = refreshTokenTtl;
}
public List<String> getWhitelist() {
return whitelist;
}
public void setWhitelist(List<String> whitelist) {
this.whitelist = whitelist;
}
}

View File

@@ -0,0 +1,16 @@
package com.k12study.common.security.config;
import com.k12study.common.security.jwt.JwtTokenProvider;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(AuthProperties.class)
public class SecurityAutoConfiguration {
@Bean
public JwtTokenProvider jwtTokenProvider(AuthProperties authProperties) {
return new JwtTokenProvider(authProperties);
}
}

View File

@@ -0,0 +1,10 @@
package com.k12study.common.security.context;
public record RequestUserContext(
String userId,
String username,
String displayName,
String tenantId,
String deptId
) {
}

View File

@@ -0,0 +1,20 @@
package com.k12study.common.security.context;
public final class RequestUserContextHolder {
private static final ThreadLocal<RequestUserContext> CONTEXT = new ThreadLocal<>();
private RequestUserContextHolder() {
}
public static void set(RequestUserContext context) {
CONTEXT.set(context);
}
public static RequestUserContext get() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}

View File

@@ -0,0 +1,49 @@
package com.k12study.common.security.jwt;
import com.k12study.common.security.config.AuthProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Date;
import javax.crypto.SecretKey;
public class JwtTokenProvider {
private final AuthProperties authProperties;
private final SecretKey secretKey;
public JwtTokenProvider(AuthProperties authProperties) {
this.authProperties = authProperties;
this.secretKey = Keys.hmacShaKeyFor(authProperties.getSecret().getBytes(StandardCharsets.UTF_8));
}
public String createAccessToken(JwtUserPrincipal principal) {
Instant now = Instant.now();
return Jwts.builder()
.subject(principal.userId())
.claim("username", principal.username())
.claim("displayName", principal.displayName())
.claim("tenantId", principal.tenantId())
.claim("deptId", principal.deptId())
.issuedAt(Date.from(now))
.expiration(Date.from(now.plus(authProperties.getAccessTokenTtl())))
.signWith(secretKey)
.compact();
}
public JwtUserPrincipal parse(String token) {
Claims claims = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
return new JwtUserPrincipal(
claims.getSubject(),
claims.get("username", String.class),
claims.get("displayName", String.class),
claims.get("tenantId", String.class),
claims.get("deptId", String.class)
);
}
}

View File

@@ -0,0 +1,10 @@
package com.k12study.common.security.jwt;
public record JwtUserPrincipal(
String userId,
String username,
String displayName,
String tenantId,
String deptId
) {
}

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>common-web</artifactId>
<dependencies>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-security</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,40 @@
package com.k12study.common.web.config;
import com.k12study.common.core.constants.SecurityConstants;
import com.k12study.common.core.utils.TraceIdHolder;
import com.k12study.common.security.context.RequestUserContext;
import com.k12study.common.security.context.RequestUserContextHolder;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.OncePerRequestFilter;
@Configuration
public class CommonWebMvcConfiguration extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String traceId = request.getHeader(SecurityConstants.TRACE_ID);
TraceIdHolder.set(traceId == null || traceId.isBlank() ? TraceIdHolder.getOrCreate() : traceId);
RequestUserContextHolder.set(new RequestUserContext(
request.getHeader(SecurityConstants.HEADER_USER_ID),
request.getHeader(SecurityConstants.HEADER_USERNAME),
request.getHeader(SecurityConstants.HEADER_DISPLAY_NAME),
request.getHeader(SecurityConstants.HEADER_TENANT_ID),
request.getHeader(SecurityConstants.HEADER_DEPT_ID)
));
response.setHeader(SecurityConstants.TRACE_ID, TraceIdHolder.getOrCreate());
filterChain.doFilter(request, response);
} finally {
TraceIdHolder.clear();
RequestUserContextHolder.clear();
}
}
}

View File

@@ -0,0 +1,14 @@
package com.k12study.common.web.exception;
public class BizException extends RuntimeException {
private final int code;
public BizException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
}

View File

@@ -0,0 +1,19 @@
package com.k12study.common.web.exception;
import com.k12study.common.api.response.ApiResponse;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BizException.class)
public ApiResponse<Void> handleBizException(BizException exception) {
return ApiResponse.failure(exception.getCode(), exception.getMessage());
}
@ExceptionHandler(Exception.class)
public ApiResponse<Void> handleException(Exception exception) {
return ApiResponse.failure(500, exception.getMessage());
}
}

24
backend/common/pom.xml Normal file
View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>common-parent</artifactId>
<packaging>pom</packaging>
<modules>
<module>common-api</module>
<module>common-core</module>
<module>common-web</module>
<module>common-security</module>
<module>common-mybatis</module>
<module>common-redis</module>
</modules>
</project>

35
backend/gateway/pom.xml Normal file
View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>gateway</artifactId>
<dependencies>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-security</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,11 @@
package com.k12study.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = {"com.k12study.gateway", "com.k12study.common"})
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}

View File

@@ -0,0 +1,75 @@
package com.k12study.gateway.filter;
import com.k12study.common.core.constants.SecurityConstants;
import com.k12study.common.security.config.AuthProperties;
import com.k12study.common.security.jwt.JwtTokenProvider;
import com.k12study.common.security.jwt.JwtUserPrincipal;
import java.util.List;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class JwtRelayFilter implements GlobalFilter, Ordered {
private final AuthProperties authProperties;
private final JwtTokenProvider jwtTokenProvider;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
public JwtRelayFilter(AuthProperties authProperties, JwtTokenProvider jwtTokenProvider) {
this.authProperties = authProperties;
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
if (!authProperties.isEnabled() || matches(path, authProperties.getWhitelist())) {
return chain.filter(exchange);
}
String authorization = exchange.getRequest().getHeaders().getFirst(authProperties.getTokenHeader());
if (authorization == null || !authorization.startsWith(authProperties.getTokenPrefix())) {
return unauthorized(exchange, "Missing token");
}
try {
String token = authorization.substring(authProperties.getTokenPrefix().length());
JwtUserPrincipal principal = jwtTokenProvider.parse(token);
var mutatedRequest = exchange.getRequest().mutate()
.header(SecurityConstants.HEADER_USER_ID, principal.userId())
.header(SecurityConstants.HEADER_USERNAME, principal.username())
.header(SecurityConstants.HEADER_DISPLAY_NAME, principal.displayName())
.header(SecurityConstants.HEADER_TENANT_ID, principal.tenantId())
.header(SecurityConstants.HEADER_DEPT_ID, principal.deptId())
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
} catch (Exception exception) {
return unauthorized(exchange, "Invalid token");
}
}
@Override
public int getOrder() {
return -100;
}
private boolean matches(String path, List<String> patterns) {
return patterns.stream().anyMatch(pattern -> antPathMatcher.match(pattern, path));
}
private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
exchange.getResponse().getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
byte[] body = ("{\"code\":401,\"message\":\"" + message + "\",\"data\":null}").getBytes();
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse()
.bufferFactory()
.wrap(body)));
}
}

View File

@@ -0,0 +1,34 @@
server:
port: 8080
spring:
application:
name: k12study-gateway
cloud:
gateway:
routes:
- id: auth
uri: http://localhost:8081
predicates:
- Path=/api/auth/**
filters:
- StripPrefix=1
- id: upms
uri: http://localhost:8082
predicates:
- Path=/api/upms/**
filters:
- StripPrefix=1
management:
endpoints:
web:
exposure:
include: health,info
auth:
enabled: true
whitelist:
- /api/auth/login
- /api/auth/refresh
- /actuator/**

115
backend/pom.xml Normal file
View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>k12study-backend</name>
<modules>
<module>common</module>
<module>apis</module>
<module>ai-client</module>
<module>auth</module>
<module>upms</module>
<module>gateway</module>
<module>boot-dev</module>
</modules>
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>${java.version}</maven.compiler.release>
<spring.boot.version>3.3.5</spring.boot.version>
<spring.cloud.version>2023.0.3</spring.cloud.version>
<mybatis.plus.version>3.5.7</mybatis.plus.version>
<postgresql.version>42.7.4</postgresql.version>
<jjwt.version>0.12.6</jjwt.version>
<springdoc.version>2.6.0</springdoc.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis.plus.version}</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgresql.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>${maven.compiler.release}</release>
<encoding>${project.build.sourceEncoding}</encoding>
<parameters>true</parameters>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
</plugin>
</plugins>
</pluginManagement>
</build>
<repositories>
<repository>
<id>aliyun-public</id>
<name>aliyun-public</name>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
</repositories>
</project>

View File

@@ -0,0 +1,14 @@
# Python AI Placeholder
This service is the placeholder for OCR, grading and speech evaluation capabilities.
## Run
```bash
pip install -r requirements.txt
uvicorn app.main:app --reload --port 9000
```
## Endpoints
- `GET /health`

View File

@@ -0,0 +1,13 @@
from fastapi import FastAPI
app = FastAPI(title="K12Study Python AI", version="0.1.0")
@app.get("/health")
def health():
return {
"name": "python-ai",
"status": "UP",
"version": "0.1.0",
}

View File

@@ -0,0 +1,2 @@
fastapi==0.115.12
uvicorn==0.34.0

50
backend/upms/pom.xml Normal file
View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.k12study</groupId>
<artifactId>k12study-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>upms</artifactId>
<dependencies>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-web</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-security</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>common-mybatis</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.k12study</groupId>
<artifactId>api-upms</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,11 @@
package com.k12study.upms;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = {"com.k12study.upms", "com.k12study.common"})
public class UpmsApplication {
public static void main(String[] args) {
SpringApplication.run(UpmsApplication.class, args);
}
}

View File

@@ -0,0 +1,14 @@
package com.k12study.upms.config;
import com.k12study.upms.UpmsApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
@Configuration
@ComponentScan(
basePackages = "com.k12study.upms",
excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = UpmsApplication.class)
)
public class UpmsModuleConfiguration {
}

View File

@@ -0,0 +1,48 @@
package com.k12study.upms.controller;
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 com.k12study.upms.service.UpmsQueryService;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/upms")
public class UpmsController {
private final UpmsQueryService upmsQueryService;
public UpmsController(UpmsQueryService upmsQueryService) {
this.upmsQueryService = upmsQueryService;
}
@GetMapping("/routes")
public ApiResponse<List<RouteNodeDto>> routes() {
return ApiResponse.success(upmsQueryService.routes());
}
@GetMapping("/areas/tree")
public ApiResponse<List<AreaNodeDto>> areas() {
return ApiResponse.success(upmsQueryService.areas());
}
@GetMapping("/tenants/tree")
public ApiResponse<List<TenantNodeDto>> tenants() {
return ApiResponse.success(upmsQueryService.tenants());
}
@GetMapping("/depts/tree")
public ApiResponse<List<DeptNodeDto>> departments() {
return ApiResponse.success(upmsQueryService.departments());
}
@GetMapping("/current-user")
public ApiResponse<CurrentRouteUserDto> currentUser() {
return ApiResponse.success(upmsQueryService.currentUser());
}
}

View File

@@ -0,0 +1,10 @@
package com.k12study.upms.domain;
public record SysArea(
String areaCode,
String parentCode,
String areaName,
String areaLevel,
String provinceCode
) {
}

View File

@@ -0,0 +1,11 @@
package com.k12study.upms.domain;
public record SysDept(
String deptId,
String parentDeptId,
String tenantId,
String deptName,
String deptType,
String deptPath
) {
}

View File

@@ -0,0 +1,12 @@
package com.k12study.upms.domain;
public record SysTenant(
String tenantId,
String parentTenantId,
String tenantName,
String tenantType,
String provinceCode,
String areaCode,
String tenantPath
) {
}

View File

@@ -0,0 +1,111 @@
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;
@Service
public class UpmsQueryService {
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()
)
);
}
public List<AreaNodeDto> areas() {
return List.of(
new AreaNodeDto(
"330000",
"浙江省",
"province",
"330000",
List.of(
new AreaNodeDto("330100", "杭州市", "city", "330000", List.of())
)
)
);
}
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")
);
}
}

View File

@@ -0,0 +1,23 @@
server:
port: 8082
spring:
application:
name: k12study-upms
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
- org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
- com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration
management:
endpoints:
web:
exposure:
include: health,info
auth:
enabled: true
gateway-mode: true
whitelist:
- /actuator/**

28
docker-compose.yml Normal file
View File

@@ -0,0 +1,28 @@
services:
postgres:
image: postgres:16
container_name: k12study-postgres
restart: unless-stopped
environment:
POSTGRES_DB: k12study
POSTGRES_USER: k12study
POSTGRES_PASSWORD: k12study
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
- ./init/pg:/docker-entrypoint-initdb.d
redis:
image: redis:7
container_name: k12study-redis
restart: unless-stopped
ports:
- "6379:6379"
command: redis-server --appendonly yes
volumes:
- redis-data:/data
volumes:
postgres-data:
redis-data:

Binary file not shown.

View File

@@ -0,0 +1,158 @@
<?xml version='1.0' encoding='utf-8'?>
<mxfile host="app.diagrams.net" modified="2026-04-14T09:05:00.000Z" agent="Codex GPT-5" version="24.7.17">
<diagram id="multi-role-flow" name="数据流图(多角色)">
<mxGraphModel dx="1800" dy="1200" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1800" pageHeight="1200" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="2" value="多角色数据流图" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=24;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="30" y="20" width="220" height="30" as="geometry" />
</mxCell>
<mxCell id="3" value="入口:微信小程序 / React 后台处理链路Java 分布式服务 -&gt; Python AI 服务 -&gt; Redis / PostgreSQL" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=13;fontColor=#666666;" vertex="1" parent="1">
<mxGeometry x="30" y="52" width="860" height="22" as="geometry" />
</mxCell>
<mxCell id="10" value="学生" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=15;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="70" y="140" width="160" height="70" as="geometry" />
</mxCell>
<mxCell id="11" value="教师" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=15;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="70" y="350" width="160" height="70" as="geometry" />
</mxCell>
<mxCell id="12" value="机构管理员" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=15;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="70" y="560" width="160" height="70" as="geometry" />
</mxCell>
<mxCell id="20" value="P1 接入与身份认证&lt;br&gt;微信小程序 / React -&gt; Java Gateway" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="340" y="80" width="290" height="80" as="geometry" />
</mxCell>
<mxCell id="21" value="P2 作业 / 资料管理&lt;br&gt;Java 作业服务" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="340" y="205" width="290" height="80" as="geometry" />
</mxCell>
<mxCell id="22" value="P3 AI批改与错因分析&lt;br&gt;Java 编排服务" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="340" y="330" width="290" height="80" as="geometry" />
</mxCell>
<mxCell id="23" value="P4 错题本 / 复习计划&lt;br&gt;Java 复习服务" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="340" y="455" width="290" height="80" as="geometry" />
</mxCell>
<mxCell id="24" value="P5 推荐与消息推送&lt;br&gt;Java 推荐服务" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="340" y="580" width="290" height="80" as="geometry" />
</mxCell>
<mxCell id="25" value="P6 Python AI处理服务&lt;br&gt;OCR / 批改 / 语音评测" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="760" y="250" width="300" height="80" as="geometry" />
</mxCell>
<mxCell id="26" value="P7 讲解评估 / 教学干预 / 运营监控&lt;br&gt;Java 教学与运营服务" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="760" y="470" width="330" height="90" as="geometry" />
</mxCell>
<mxCell id="30" value="D1 PostgreSQL 用户 / 班级库&lt;br&gt;支持分库分表 / 分区" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="1180" y="70" width="260" height="75" as="geometry" />
</mxCell>
<mxCell id="31" value="D2 PostgreSQL 作业 / 批改库&lt;br&gt;支持分库分表 / 分区" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="1180" y="190" width="260" height="75" as="geometry" />
</mxCell>
<mxCell id="32" value="D3 PostgreSQL 错题 / 复习库&lt;br&gt;支持分区归档" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="1180" y="310" width="260" height="75" as="geometry" />
</mxCell>
<mxCell id="33" value="D4 知识库 / 题库 / 对象存储" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="1180" y="430" width="260" height="75" as="geometry" />
</mxCell>
<mxCell id="34" value="D5 Redis Cluster&lt;br&gt;会话、缓存、热数据、推荐结果" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="1180" y="550" width="260" height="75" as="geometry" />
</mxCell>
<mxCell id="35" value="D6 日志 / 审计 / BI" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="1180" y="670" width="260" height="75" as="geometry" />
</mxCell>
<mxCell id="40" value="登录 / 绑定" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#82b366;" edge="1" parent="1" source="10" target="20">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="41" value="登录 / 班级关系" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="11" target="20">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="42" value="高权限登录" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#b85450;" edge="1" parent="1" source="12" target="20">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="43" value="用户 / 班级 / 权限" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="20" target="30">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="44" value="会话 / Token" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="20" target="34">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="45" value="发布作业 / 资料" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="11" target="21">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="46" value="上传作业 / 答题" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#82b366;" edge="1" parent="1" source="10" target="21">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="47" value="作业 / 资料 / 提交" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="21" target="31">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="48" value="文件 / 课件" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="21" target="33">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="49" value="批改请求" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="21" target="22">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="50" value="调用 Python AI" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="22" target="25">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="51" value="解析结果 / 错因 / 评分" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="25" target="22">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="52" value="知识点 / 题库匹配" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="25" target="33">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="53" value="批改结果" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="22" target="31">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="54" value="错题 / 薄弱项" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="22" target="23">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="55" value="批改结果 / 错因" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#82b366;" edge="1" parent="1" source="22" target="10">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="56" value="复核任务 / 学情" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="22" target="11">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="57" value="错题 / 复习节点" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="23" target="32">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="58" value="画像 / 复习信号" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="23" target="24">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="59" value="错题本 / 复习计划" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#82b366;" edge="1" parent="1" source="23" target="10">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="60" value="微课 / 变式题召回" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="24" target="33">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="61" value="缓存推荐结果" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="24" target="34">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="62" value="推荐内容 / 提醒" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#82b366;" edge="1" parent="1" source="24" target="10">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="63" value="语音讲解提交" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#82b366;" edge="1" parent="1" source="10" target="26">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="64" value="点评 / 评分 / 学情查询" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="11" target="26">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="65" value="机构监控 / 审核" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#b85450;" edge="1" parent="1" source="12" target="26">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="66" value="讲解评估调用" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="26" target="25">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="67" value="讲解记录 / 教学数据" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="26" target="31">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="68" value="报表 / 审计" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="26" target="35">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="69" value="班级学情 / 教学建议" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="26" target="11">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="70" value="机构看板 / 质量报表" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#b85450;" edge="1" parent="1" source="26" target="12">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -0,0 +1,450 @@
<mxfile host="65bd71144e">
<diagram id="sys-arch" name="系统架构">
<mxGraphModel dx="1267" dy="1104" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1800" pageHeight="1200" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="AI智能学习系统系统架构" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=24;fontStyle=1;" parent="1" vertex="1">
<mxGeometry x="30" y="20" width="420" height="30" as="geometry"/>
</mxCell>
<mxCell id="3" value="技术栈:微信小程序 + React + Java + Python(AI处理供 Java 调用) + Redis + PostgreSQL" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=13;fontColor=#666666;" parent="1" vertex="1">
<mxGeometry x="30" y="52" width="760" height="22" as="geometry"/>
</mxCell>
<mxCell id="10" value="角色与前端接入层" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=16;fontStyle=1;align=left;spacingLeft=12;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="40" y="100" width="240" height="600" as="geometry"/>
</mxCell>
<mxCell id="11" value="学生端&lt;br&gt;微信小程序&lt;br&gt;学习执行 / 作业上传 / 推荐接收" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="70" y="155" width="180" height="90" as="geometry"/>
</mxCell>
<mxCell id="12" value="教师端&lt;br&gt;React 管理后台&lt;br&gt;班级管理 / 作业发布 / 教学干预" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="70" y="345" width="180" height="90" as="geometry"/>
</mxCell>
<mxCell id="13" value="机构端&lt;br&gt;React 管理后台&lt;br&gt;运营管控 / 质控 / 知识库审核" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="70" y="535" width="180" height="90" as="geometry"/>
</mxCell>
<mxCell id="20" value="接入层&lt;br&gt;SLB / Nginx / Java Gateway(BFF) 集群&lt;br&gt;统一鉴权 / Token / RBAC / 限流 / 聚合接口" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="340" y="315" width="230" height="120" as="geometry"/>
</mxCell>
<mxCell id="30" value="Java 分布式业务服务集群" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=16;fontStyle=1;align=left;spacingLeft=12;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="640" y="80" width="760" height="420" as="geometry"/>
</mxCell>
<mxCell id="31" value="认证与用户中心&lt;br&gt;登录、绑定、机构 / 班级 / 角色权限" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="690" y="140" width="220" height="80" as="geometry"/>
</mxCell>
<mxCell id="32" value="作业与内容服务&lt;br&gt;作业发布、上传、文件元数据、提交记录" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="940" y="140" width="220" height="80" as="geometry"/>
</mxCell>
<mxCell id="33" value="批改编排与学情服务&lt;br&gt;任务编排、错因分析、讲解评估、学情画像" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="1190" y="140" width="220" height="80" as="geometry"/>
</mxCell>
<mxCell id="34" value="错题 / 复习 / 推荐服务&lt;br&gt;错题本、艾宾浩斯计划、推荐触达" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="690" y="280" width="220" height="80" as="geometry"/>
</mxCell>
<mxCell id="35" value="知识库与机构运营服务&lt;br&gt;知识库审核、运营看板、质量监控、报表导出" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="940" y="280" width="220" height="80" as="geometry"/>
</mxCell>
<mxCell id="36" value="分布式治理能力&lt;br&gt;服务注册发现 / 配置中心 / 链路追踪 / 灰度发布" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="1190" y="280" width="220" height="80" as="geometry"/>
</mxCell>
<mxCell id="40" value="Python AI处理层供 Java 调用)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=16;fontStyle=1;align=left;spacingLeft=12;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="1460" y="80" width="300" height="260" as="geometry"/>
</mxCell>
<mxCell id="41" value="Python OCR 服务&lt;br&gt;文档解析 / 图片增强 / 结构化提取" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="1490" y="135" width="240" height="60" as="geometry"/>
</mxCell>
<mxCell id="42" value="Python 批改与生成服务&lt;br&gt;LLM批改 / 错因标签 / 变式题生成" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="1490" y="215" width="240" height="60" as="geometry"/>
</mxCell>
<mxCell id="43" value="Python 语音评测服务&lt;br&gt;ASR / 讲解评分 / 教学建议" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="1490" y="295" width="240" height="60" as="geometry"/>
</mxCell>
<mxCell id="50" value="公共支撑层" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=16;fontStyle=1;align=left;spacingLeft=12;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="1460" y="390" width="300" height="210" as="geometry"/>
</mxCell>
<mxCell id="51" value="知识库 / 题库 / 课程资源&lt;br&gt;学科 / 年级 / 知识点内容组织" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="1490" y="445" width="240" height="60" as="geometry"/>
</mxCell>
<mxCell id="52" value="画像与推荐策略&lt;br&gt;召回、排序、效果回流" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="1490" y="525" width="240" height="60" as="geometry"/>
</mxCell>
<mxCell id="60" value="数据层与基础设施" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=16;fontStyle=1;align=left;spacingLeft=12;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="550" y="625" width="1200" height="250" as="geometry"/>
</mxCell>
<mxCell id="61" value="PostgreSQL 集群&lt;br&gt;业务主库 + 分库分表 + 分区表" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" parent="1" vertex="1">
<mxGeometry x="620" y="650" width="200" height="85" as="geometry"/>
</mxCell>
<mxCell id="62" value="Redis Cluster&lt;br&gt;缓存、会话、排行榜、热数据" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" parent="1" vertex="1">
<mxGeometry x="860" y="650" width="200" height="85" as="geometry"/>
</mxCell>
<mxCell id="63" value="对象存储&lt;br&gt;图片 / PDF / 音频 / 课件 / 讲解录音" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" parent="1" vertex="1">
<mxGeometry x="1100" y="650" width="200" height="85" as="geometry"/>
</mxCell>
<mxCell id="64" value="MQ / 任务调度&lt;br&gt;异步批改、推荐推送、报表任务" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" parent="1" vertex="1">
<mxGeometry x="1340" y="650" width="200" height="85" as="geometry"/>
</mxCell>
<mxCell id="65" value="日志 / 审计 / BI&lt;br&gt;操作日志、链路日志、运营分析" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" parent="1" vertex="1">
<mxGeometry x="1580" y="650" width="160" height="85" as="geometry"/>
</mxCell>
<mxCell id="70" value="外部能力" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=16;fontStyle=1;align=left;spacingLeft=12;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="40" y="740" width="500" height="150" as="geometry"/>
</mxCell>
<mxCell id="71" value="微信登录 / 订阅消息 / 短信" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="80" y="795" width="180" height="60" as="geometry"/>
</mxCell>
<mxCell id="72" value="第三方 OCR / LLM / ASR 模型接口" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="300" y="795" width="200" height="60" as="geometry"/>
</mxCell>
<mxCell id="80" value="学习接口" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#82b366;" parent="1" source="11" target="20" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="81" value="教学接口" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" parent="1" source="12" target="20" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="82" value="运营接口" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#b85450;" parent="1" source="13" target="20" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="83" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#9673a6;" parent="1" source="20" target="31" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="84" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#82b366;" parent="1" source="20" target="32" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="85" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#82b366;" parent="1" source="20" target="33" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="86" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" parent="1" source="20" target="34" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="87" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#b85450;" parent="1" source="20" target="35" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="88" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#9673a6;" parent="1" source="20" target="36" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="89" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="31" target="61" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="90" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="31" target="62" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="91" value="登录 / 通知" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#82b366;" parent="1" source="31" target="71" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="92" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="32" target="61" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="93" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="32" target="63" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="94" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="32" target="64" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="95" value="Java RPC / HTTP" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="33" target="41" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="96" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="33" target="42" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="97" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="33" target="43" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="98" value="知识点 / 课程匹配" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" parent="1" source="33" target="51" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="99" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="33" target="61" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="100" value="画像 / 策略" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" parent="1" source="34" target="52" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="101" value="复习内容" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" parent="1" source="34" target="51" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="102" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="34" target="61" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="103" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="34" target="62" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="104" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" parent="1" source="35" target="51" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="105" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="35" target="61" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="106" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="35" target="65" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="107" value="监控 / 链路" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="36" target="65" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="108" value="模型调用" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#b85450;" parent="1" source="41" target="72" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="109" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#b85450;" parent="1" source="42" target="72" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="110" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#b85450;" parent="1" source="43" target="72" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="111" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="51" target="61" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="112" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="51" target="63" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="113" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="52" target="61" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="114" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="52" target="62" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="code-arch" name="代码架构">
<mxGraphModel dx="887" dy="773" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1800" pageHeight="1200" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="AI智能学习系统代码架构" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=24;fontStyle=1;" parent="1" vertex="1">
<mxGeometry x="30" y="20" width="420" height="30" as="geometry"/>
</mxCell>
<mxCell id="3" value="前端:微信小程序 + React后端Java 分布式服务AIPython 服务数据Redis + PostgreSQL" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=13;fontColor=#666666;" parent="1" vertex="1">
<mxGeometry x="30" y="52" width="780" height="22" as="geometry"/>
</mxCell>
<mxCell id="10" value="前端应用层" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=16;fontStyle=1;align=left;spacingLeft=12;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="40" y="90" width="1700" height="150" as="geometry"/>
</mxCell>
<mxCell id="11" value="apps/weapp-student&lt;br&gt;Taro / UniApp 风格小程序&lt;br&gt;作业上传、错题本、推荐页" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="90" y="145" width="270" height="70" as="geometry"/>
</mxCell>
<mxCell id="12" value="apps/react-teacher-admin&lt;br&gt;React 教师后台&lt;br&gt;班级管理、作业发布、教学点评" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="420" y="145" width="270" height="70" as="geometry"/>
</mxCell>
<mxCell id="13" value="apps/react-org-admin&lt;br&gt;React 机构后台&lt;br&gt;权限配置、运营看板、质量监控" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="750" y="145" width="270" height="70" as="geometry"/>
</mxCell>
<mxCell id="20" value="Java 接入层" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=16;fontStyle=1;align=left;spacingLeft=12;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="40" y="270" width="1700" height="150" as="geometry"/>
</mxCell>
<mxCell id="21" value="gateway/java-bff-cluster&lt;br&gt;REST API、聚合接口、限流、灰度、鉴权" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="90" y="325" width="290" height="70" as="geometry"/>
</mxCell>
<mxCell id="22" value="security/auth-center&lt;br&gt;JWT、RBAC、登录态、审计中间件" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="430" y="325" width="290" height="70" as="geometry"/>
</mxCell>
<mxCell id="23" value="integration/openapi-adapter&lt;br&gt;微信、短信、对象存储、消息回调" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="770" y="325" width="290" height="70" as="geometry"/>
</mxCell>
<mxCell id="24" value="governance/distributed-kit&lt;br&gt;注册发现、配置中心、链路追踪、熔断重试" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="1110" y="325" width="320" height="70" as="geometry"/>
</mxCell>
<mxCell id="30" value="Java 领域服务层" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=16;fontStyle=1;align=left;spacingLeft=12;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="40" y="450" width="1700" height="250" as="geometry"/>
</mxCell>
<mxCell id="31" value="service/user-class-center&lt;br&gt;用户、班级、机构、角色权限" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="70" y="530" width="240" height="80" as="geometry"/>
</mxCell>
<mxCell id="32" value="service/homework-content&lt;br&gt;作业、资料、上传、文件元数据" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="340" y="530" width="240" height="80" as="geometry"/>
</mxCell>
<mxCell id="33" value="service/grading-orchestrator&lt;br&gt;批改编排、错因分析、学情画像" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="610" y="530" width="240" height="80" as="geometry"/>
</mxCell>
<mxCell id="34" value="service/wrongbook-review&lt;br&gt;错题本、复习计划、掌握状态" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="880" y="530" width="240" height="80" as="geometry"/>
</mxCell>
<mxCell id="35" value="service/recommendation&lt;br&gt;召回排序、内容推荐、消息触达" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="1150" y="530" width="240" height="80" as="geometry"/>
</mxCell>
<mxCell id="36" value="service/ops-knowledge&lt;br&gt;机构运营、质控、知识库审核、报表" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="1420" y="530" width="260" height="80" as="geometry"/>
</mxCell>
<mxCell id="40" value="Python AI服务层独立部署供 Java 调用)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=16;fontStyle=1;align=left;spacingLeft=12;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="40" y="730" width="980" height="180" as="geometry"/>
</mxCell>
<mxCell id="41" value="python/ocr-service&lt;br&gt;文档解析、图片增强、结构化输出" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="80" y="795" width="210" height="60" as="geometry"/>
</mxCell>
<mxCell id="42" value="python/grading-llm-service&lt;br&gt;主观题批改、错因标签、变式题生成" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="320" y="795" width="240" height="60" as="geometry"/>
</mxCell>
<mxCell id="43" value="python/speech-eval-service&lt;br&gt;ASR、讲解评分、反馈建议" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="590" y="795" width="210" height="60" as="geometry"/>
</mxCell>
<mxCell id="44" value="python/feature-worker&lt;br&gt;画像特征、推荐特征、离线计算" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="830" y="795" width="160" height="60" as="geometry"/>
</mxCell>
<mxCell id="50" value="共享库 / 契约层" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=16;fontStyle=1;align=left;spacingLeft=12;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="1070" y="730" width="670" height="180" as="geometry"/>
</mxCell>
<mxCell id="51" value="contracts/api-schema&lt;br&gt;DTO、API Schema、OpenAPI" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="1100" y="795" width="140" height="60" as="geometry"/>
</mxCell>
<mxCell id="52" value="contracts/domain-model&lt;br&gt;实体、值对象、枚举、状态机" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="1260" y="795" width="140" height="60" as="geometry"/>
</mxCell>
<mxCell id="53" value="common/toolkit&lt;br&gt;日志、异常、配置、工具组件" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="1420" y="795" width="140" height="60" as="geometry"/>
</mxCell>
<mxCell id="54" value="client/ai-bridge&lt;br&gt;Java 调 Python AI RPC / HTTP SDK" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="1580" y="795" width="130" height="60" as="geometry"/>
</mxCell>
<mxCell id="60" value="数据与基础设施层" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=16;fontStyle=1;align=left;spacingLeft=12;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="40" y="940" width="1700" height="190" as="geometry"/>
</mxCell>
<mxCell id="61" value="infra/postgresql-cluster&lt;br&gt;业务主库、分库分表、分区表、读写分离" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" parent="1" vertex="1">
<mxGeometry x="70" y="1010" width="250" height="75" as="geometry"/>
</mxCell>
<mxCell id="62" value="infra/redis-cluster&lt;br&gt;缓存、会话、排行榜、热点数据" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" parent="1" vertex="1">
<mxGeometry x="360" y="1010" width="250" height="75" as="geometry"/>
</mxCell>
<mxCell id="63" value="infra/object-storage&lt;br&gt;图片、PDF、音频、视频、课件" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" parent="1" vertex="1">
<mxGeometry x="650" y="1010" width="250" height="75" as="geometry"/>
</mxCell>
<mxCell id="64" value="infra/mq-scheduler&lt;br&gt;异步批改、推荐任务、定时报表" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" parent="1" vertex="1">
<mxGeometry x="940" y="1010" width="250" height="75" as="geometry"/>
</mxCell>
<mxCell id="65" value="infra/search-bi&lt;br&gt;检索索引、统计聚合、运营分析" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" parent="1" vertex="1">
<mxGeometry x="1230" y="1010" width="250" height="75" as="geometry"/>
</mxCell>
<mxCell id="66" value="infra/open-platform&lt;br&gt;微信、短信、OCR、LLM、ASR" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" parent="1" vertex="1">
<mxGeometry x="1520" y="1010" width="190" height="75" as="geometry"/>
</mxCell>
<mxCell id="80" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#82b366;" parent="1" source="11" target="21" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="81" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" parent="1" source="12" target="21" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="82" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#b85450;" parent="1" source="13" target="21" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="83" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="21" target="22" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="84" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="21" target="23" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="85" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#9673a6;" parent="1" source="21" target="24" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="86" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#9673a6;" parent="1" source="22" target="31" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="87" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#82b366;" parent="1" source="21" target="32" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="88" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#82b366;" parent="1" source="21" target="33" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="89" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" parent="1" source="21" target="34" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="90" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" parent="1" source="21" target="35" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="91" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#b85450;" parent="1" source="21" target="36" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="92" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="33" target="41" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="93" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="33" target="42" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="94" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="33" target="43" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="95" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="35" target="44" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="96" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#9673a6;" parent="1" source="31" target="51" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="97" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#9673a6;" parent="1" source="32" target="52" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="98" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#9673a6;" parent="1" source="33" target="54" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="99" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#9673a6;" parent="1" source="34" target="52" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="100" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#9673a6;" parent="1" source="35" target="51" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="101" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#9673a6;" parent="1" source="36" target="53" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="102" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="31" target="61" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="103" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="31" target="62" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="104" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="32" target="61" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="105" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="32" target="63" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="106" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="32" target="64" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="107" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="33" target="61" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="108" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="33" target="64" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="109" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="34" target="61" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="110" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="34" target="62" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="111" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="35" target="61" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="112" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="35" target="62" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="113" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="35" target="64" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="114" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="36" target="61" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="115" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="36" target="65" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="116" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="23" target="63" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="117" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="23" target="66" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="118" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="24" target="65" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="119" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#b85450;" parent="1" source="41" target="66" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="120" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#b85450;" parent="1" source="42" target="66" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="121" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#b85450;" parent="1" source="43" target="66" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="122" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="44" target="61" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="123" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="44" target="62" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

Some files were not shown because too many files have changed in this diff Show More