适用人群 :会 Vue,Java / Python 零基础
今日目标 :看懂项目结构,跟完「实施计划列表」从浏览器到数据库的完整流程
预计耗时 :8~10 小时(可分 3 段完成)
今日不写复杂代码,以「读代码 + 画链路 + 浏览器验证」为主
学习前提:你需要先知道的 3 件事
1. MES 项目 = 前端 + Java 后端(+ 巡检时才用 Python)
mes/
├── service-front/ ← 前端(Vue 3,你会的)
└── enmo_support/ ← Java 后端(今天要学的重点)
巡检分析还依赖独立的 Python 项目(BethuneAnalysis),Day 1 只了解,不深学。
2. 前端请求有两条「通道」(非常重要)
打开 vue.config.js,本地开发配了两个代理:
33:49:D:\mes\service-front\vue.config.js
proxy: {
'/api': {
target: 'http://localhost:8080/',
changeOrigin: true,
pathRewrite: {
'^/api': '/'
}
},
'/esapi': {
target: 'http://localhost:8084/',
changeOrigin: true,
pathRewrite: {
'^/esapi': '/'
}
}
}
| 通道 | 前端 baseURL | 本地转发到 | 用途 |
|---|---|---|---|
/api/ |
ajax({ proxy: '/api/' }) |
8080 端口 | 登录等公共接口 |
/esapi/ |
ajax() 默认 |
8084 端口 | MES 主业务(计划、故障、巡检...) |
打开 utils/ajax.ts 看默认值:
46:55:D:\mes\service-front\src\utils\ajax.ts
export function ajax(options: any): any {
const config = {
baseURL: options.proxy || '/esapi/',
url: options.url,
method: options.method || 'get',
params: options.params || {},
data: options.data || {},
headers: options.headers || {},
responseType: options.responseType || 'json'
}
结论:
- 登录走
/api/→ 8080 - 计划列表走
/esapi/→ 8084(enmo_support)
3. Java 后端固定四层结构(背下来)
Controller → 接 HTTP 请求(类似 apis/*.ts)
Service → 写业务逻辑(类似 composable / 页面里的 methods)
Dao → 访问数据库(类似直接写 SQL 的那一层)
Model → 数据结构(类似 TypeScript interface)
上午(9:00~12:00):建立项目地图
任务 1:浏览前端目录(60 分钟)
在 IDE 中打开 service-front/src/,按下面顺序看:
1.1 apis/ --- 所有接口定义(9 个文件)
apis/
├── manage.ts ← 最大(1300+ 行),计划/故障/登录/组织/统计
├── inspection.ts ← 巡检(11 个页面用)
├── serviceOrder.ts ← 电子服务单
├── weeklyReport.ts ← 工作报告
├── resource.ts ← 资源文章
├── doc.ts ← 文档
├── common.ts ← 公共
├── ai.ts ← AI 聊天
└── index.ts ← 统一导出
练习: 打开 apis/index.ts,看模块如何导出:
1:8:D:\mes\service-front\src\apis\index.ts
export * from './manage'
export * from './resource'
export * from './common'
export * from './inspection'
export * from './ai'
export * from './doc'
export * from './serviceOrder'
export * from './weeklyReport'
页面里 import { getPlanListApi } from '@/apis' 就是这样来的。
1.2 views/ --- 页面(135+ 个)
| 目录 | 业务 | 对应 apis |
|---|---|---|
sr/ |
服务台:计划、故障、咨询 | manage.ts |
manage/ |
管理:统计、工时、团队 | manage.ts |
organization/ |
组织:合同、成员 | manage.ts |
inspection/ |
巡检(11 页) | inspection.ts |
permission/ |
权限 | manage.ts |
练习: 数一数 inspection/ 有几个文件(11 个),对比 manage/(30+)。巡检只是项目一小部分。
1.3 interfaces/ --- TypeScript 类型
前端 Plan 接口定义在 interfaces/manage.ts:
218:234:D:\mes\service-front\src\interfaces\manage.ts
export interface Plan {
id: number
createdBy: number
createdByName: string
createdTime: string
daterange: string[]
startDate: string
endDate: string
endRealTime: string
status: any
cost: string
title: string
deliver: string
companyId: number
executorId: number | undefined
executorName: string
对照理解: 这相当于 Java 后端的 Plan.java,字段名基本一致,前后端才能对接。
1.4 统一返回格式
8:21:D:\mes\service-front\src\interfaces\index.d.ts
export interface PageListRes<T> {
total: number
list: Array<T>
operateCallBackObj: any
[key: any]: any
}
export interface OperateRes {
operateCallBackObj: any
operateCallBackUrl: string
operateCode: string | number
operateMessage: string
success: boolean
ResponseData?: any
}
- 列表接口:返回
{ total, list } - 操作接口(保存/删除):返回
{ success, operateMessage }
任务 2:浏览 Java 后端目录(60 分钟)
打开 enmo_support/src/main/java/com/enmo/enmo_support/:
enmo_support/
├── workbench/ ← 【最大】计划、故障、咨询、统计、服务单
├── user/ ← 用户、公司、合同、团队
├── content/ ← 知识库、文档、周报
├── perm/ ← 角色权限
├── security/ ← 登录、OAuth
├── bethune/ ← 巡检(Java 网关层)
├── common/ ← 公共:异常、注解、工具
├── aliyun/ ← OSS 文件
├── chat/ ← AI 聊天
└── search/ ← 搜索
每个包内部结构相同:
workbench/
├── controller/ ← 接 HTTP(对应 apis/*.ts)
├── service/ ← 业务逻辑
│ └── Impl/ ← 实现类(真正写逻辑的地方)
├── dao/ ← 数据库访问
├── model/ ← 实体类(对应 interfaces/*.ts)
└── ...
练习:
- 打开
workbench/controller/,数 Controller 个数(15+) - 打开
bethune/controller/,只有 2 个 - 建立直觉:先学 workbench,后学 bethune
Java 启动类
20:28:D:\mes\enmo_support\src\main\java\com\enmo\enmo_support\EnmoSupportApplication.java
@SpringBootApplication
@EnableScheduling
@EnableFeignClients
@EnableAspectJAutoProxy(exposeProxy = true)
@MapperScan(basePackages = {"com.enmo.enmo_support.*.dao", "com.enmo.enmo_support.*.mapper"})
public class EnmoSupportApplication {
public static void main(String[] args) {
SpringApplication.run(EnmoSupportApplication.class, args);
@SpringBootApplication:标记这是 Spring Boot 项目@MapperScan:扫描所有 Dao 接口@EnableFeignClients:支持远程调用 Python(巡检用)main方法:在 IDEA 里右键 Run 启动后端
任务 3:Java 零基础速读手册(60 分钟)
你不需要先学完 Java 再读项目,按下面「前端对照表」即可。
3.1 文件头部:package 和 import
java
package com.enmo.enmo_support.workbench.controller; // 包名 = 文件夹路径
import com.enmo.enmo_support.workbench.model.Plan; // 导入其他类
import org.springframework.web.bind.annotation.*;
≈ 前端的 import { Plan } from '@/interfaces'
3.2 类上的注解(重点)
以 PlanController.java 为例:
29:37:D:\mes\enmo_support\src\main\java\com\enmo\enmo_support\workbench\controller\PlanController.java
@Api(tags = "实施计划管理")
@Slf4j
@AllArgsConstructor
@EmcsController
@RequestMapping("/plan")
public class PlanController {
private PlanService planService;
private final PlanExecutorService planExecutorService;
| 注解 | 作用 | 前端类比 |
|---|---|---|
@EmcsController |
标记为 REST 控制器 | 类似一个 api 模块 |
@RequestMapping("/plan") |
基础路径 /plan |
url: 'plan/...' 前缀 |
@AllArgsConstructor |
构造器自动注入依赖 | 自动 import 并初始化 service |
@Slf4j |
日志 | console.log 的升级版 |
@EmcsController 本质是:
15:19:D:\mes\enmo_support\src\main\java\com\enmo\enmo_support\common\annotation\EmcsController.java
@RestController
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "transactionManager", propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
public @interface EmcsController {
= @RestController + 事务管理。
3.3 方法上的注解
39:49:D:\mes\enmo_support\src\main\java\com\enmo\enmo_support\workbench\controller\PlanController.java
@ApiOperation("实施计划列表")
@GetMapping("/list")
@PreAuthorize("hasAuthority('plan:query')")
@ApiImplicitParams({
@ApiImplicitParam(name = "Authorization", value = "token", required = true, dataType = "string", paramType = "header"),
...
})
public PageInfo<Plan> findPlanList(@RequestParam(required = false, defaultValue = "1") Integer pageNum,
@RequestParam(required = false, defaultValue = "10") Integer pageSize,
| 注解 | 含义 | 前端对应 |
|---|---|---|
@GetMapping("/list") |
GET /plan/list |
ajax({ url: '/plan/list' }) |
@PostMapping("/save") |
POST /plan/save |
method: 'post' |
@DeleteMapping("/delete/{id}") |
DELETE /plan/delete/123 |
url: `plan/delete/${id}`, method: 'delete' |
@RequestParam |
URL 查询参数 | params: { pageNum: 1 } |
@RequestBody |
POST 请求体 JSON | data: { title: '...' } |
@PathVariable |
路径里的变量 | ``url: `plan/detail/${id}``` |
@PreAuthorize |
权限校验 | 前端按钮 v-if + 后端二次校验 |
3.4 Java 类型对照
| Java | 含义 | 前端 |
|---|---|---|
Integer |
可空整数 | `number |
String |
字符串 | string |
List<Plan> |
Plan 数组 | Plan[] |
Boolean |
布尔 | boolean |
void |
无返回值 | void |
下午(14:00~18:00):跟完两条真实链路
任务 4:链路一 --- 登录(60 分钟)
Step 1:前端 Login.vue
用户点「登录」→ onSubmit → saveLogin:
125:137:D:\mes\service-front\src\views\Login.vue
const saveLogin = async () => {
let { data } = await submitLoginApi(state.loginForm)
cbSuccess(data, () => {
loginRecordApi()
if (route.query.markedid && route.query.redirect) {
let _redirect = route.query.redirect as string
router.push({ path: _redirect, query: { markedid: route.query.markedid } })
} else {
router.push((route.query.redirect as string) || (isMobile() ? 'work' : '/'))
}
})
}
表单数据:
102:107:D:\mes\service-front\src\views\Login.vue
loginForm: {
bizType: 'mes',
phoneNum: '',
password: '',
smsCode: ''
}
Step 2:apis/manage.ts
21:27:D:\mes\service-front\src\apis\manage.ts
export const submitLoginApi = (data = {}): Res<OperateRes> =>
ajax({
proxy: '/api/',
url: 'login',
method: 'post',
data: data
})
注意 proxy: '/api/' ,不是默认的 /esapi/。
实际请求:
POST http://localhost:8081/api/login
→ vue proxy 转发
→ POST http://localhost:8080/login
Step 3:ajax.ts 如何处理 token
请求发出时带 token:
62:64:D:\mes\service-front\src\utils\ajax.ts
if (store.getters.token && !isServiceOrderSharePage) {
config.headers.Authorization = store.getters.token
}
响应回来时保存 token:
22:27:D:\mes\service-front\src\utils\ajax.ts
axios.interceptors.response.use(
data => {
const _token = data.headers.authorization
if (_token) {
store.commit('SetToken', _token)
}
401/403 时跳转登录页:
66:82:D:\mes\service-front\src\utils\ajax.ts
if (err.response && [401, 403].includes(err.response.status)) {
...
store.commit('LOGOUT')
...
router.replace({
name: 'Login',
query: { redirect: _currentRoute.path }
})
Message(err.response.data.operateMessage || '请登录后操作')
登录链路图
Login.vue 点击登录
→ submitLoginApi({ phoneNum, password, bizType: 'mes' })
→ ajax({ proxy: '/api/', url: 'login', method: 'post' })
→ POST /api/login → 代理到 8080
→ 响应头 authorization → 存入 Vuex
→ 后续请求自动带 Authorization 头
浏览器验证:
- 打开 DevTools → Network
- 登录,找到
login请求 - 看 Request Headers 和 Response Headers 里的
authorization
任务 5:链路二 --- 实施计划列表(120 分钟,今日核心)
这是 MES 最典型的 Java 纯后端链路,后面 90% 接口都这个模式。
Step 1:前端页面 Plan.vue
页面加载时调用 fetchData():
169:187:D:\mes\service-front\src\views\sr\Plan.vue
const fetchData = async () => {
let {
data: { total, list }
} = await getPlanListApi(params)
state.total = total
if (isMobile()) {
state.plans.push(...list)
} else {
state.plans = list
}
}
...
fetchData()
const { formRef, params, setParams, search, onSubmit, operate } = useAdmq(fetchData)
params 来自 compositions/admq.ts(分页封装):
11:14:D:\mes\service-front\src\compositions\admq.ts
const params: Params = reactive({
pageNum: 1,
pageSize: 10
})
模板渲染:
17:24:D:\mes\service-front\src\views\sr\Plan.vue
v-for="item in plans"
:key="item.id"
...
<div class="f16 c2b">{{ item.title }}</div>
...
><span class="c6">{{ item.startDate }} 至 {{ item.endDate }}</span>
Step 2:apis/manage.ts
303:307:D:\mes\service-front\src\apis\manage.ts
export const getPlanListApi = (params = {}): Res<PageListRes<Plan>> =>
ajax({
url: '/plan/list',
params: params
})
没有写 proxy,走默认 /esapi/:
GET http://localhost:8081/esapi/plan/list?pageNum=1&pageSize=10
→ 代理到
→ GET http://localhost:8084/plan/list?pageNum=1&pageSize=10
Step 3:Java Controller
39:75:D:\mes\enmo_support\src\main\java\com\enmo\enmo_support\workbench\controller\PlanController.java
@ApiOperation("实施计划列表")
@GetMapping("/list")
@PreAuthorize("hasAuthority('plan:query')")
...
public PageInfo<Plan> findPlanList(@RequestParam(required = false, defaultValue = "1") Integer pageNum,
@RequestParam(required = false, defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Integer companyId,
...
@RequestParam(required = false) Boolean excludeEnded
) {
Page<Plan> page = new Page<>(pageNum, pageSize);
List<Plan> plans = planService.findList(page, companyId, companyName, startDate, endDate, title, status, mine, range, teamId, executorId, checkType, planType, contractId, accId,
hasAccId, hasImplementationId, missingAccId, missingImplementationId, missingPreSalesCase, excludeEnded);
return new PageInfo<>(plans);
}
逐行解读:
java
// 1. GET 请求,路径 = @RequestMapping("/plan") + @GetMapping("/list") = /plan/list
@GetMapping("/list")
// 2. 需要 plan:query 权限,没权限返回 403
@PreAuthorize("hasAuthority('plan:query')")
// 3. 从 URL 取参数,不传则默认值 pageNum=1, pageSize=10
@RequestParam(required = false, defaultValue = "1") Integer pageNum
// 4. 创建分页对象
Page<Plan> page = new Page<>(pageNum, pageSize);
// 5. 交给 Service 查数据
List<Plan> plans = planService.findList(page, ...);
// 6. 包装成 PageInfo 返回(含 total、list)
return new PageInfo<>(plans);
Step 4:Java Service(业务逻辑)
PlanServiceImpl.findList() 核心片段:
91:138:D:\mes\enmo_support\src\main\java\com\enmo\enmo_support\workbench\service\Impl\PlanServiceImpl.java
Integer createdBy = null;
// 权限控制:sys、pa和团队管理员可以查看更多数据,其他用户只能看到自己相关的计划
if (!UserUtils.isSys() && !userManagerService.isPA() && !userManagerService.isTA()) {
executorId = UserUtils.getCurrentUserId();
createdBy = UserUtils.getCurrentUserId();
} else {
...
}
...
startPage(page.getPageNum(), page.getPageSize());
List<Plan> planList = planDao.findList(companyName, startDate, endDate, title, status, executorId, companyId, null, rangeStart, rangeEnd, userIds, checkType, createdBy, planType, contractId, accId,
hasAccId, hasImplementationId,
missingAccId, missingImplementationId, missingPreSalesCase, excludeEnded);
planList.forEach(plan -> {
Service 做两件事:
- 权限:普通用户只能看自己的计划
- 分页 + 查库 :
startPage()然后planDao.findList()
Step 5:Java Dao + MyBatis SQL
Dao 接口:
15:21:D:\mes\enmo_support\src\main\java\com\enmo\enmo_support\workbench\dao\PlanDao.java
@Mapper
public interface PlanDao {
/**
* 查询计划列表
*/
List<Plan> findList(@Param("companyName") String companyName,
@Param("startDate") LocalDate startDate,
@Mapper:MyBatis 标记,Spring 自动实现- 只有方法声明,SQL 在 XML 里
SQL 文件 resources/mybatis/workbench/PlanMap.xml:
4:32:D:\mes\enmo_support\src\main\resources\mybatis\workbench\PlanMap.xml
<select id="findList" resultMap="PlanMap">
select
ep.id,
ep.created_by createdBy,
ep.title,
ce1.employee_name createdByName,
ep.created_time createdTime,
ep.company_id companyId,
cc.name companyName,
ep.start_date startDate,
ep.end_date endDate,
ep.status,
...
from es_plan ep
left join cs_employee ce1 on ep.created_by = ce1.created_by ...
left join cs_company cc on ep.company_id = cc.id
...
<where>
<if test="title != null and title != ''">and lower(ep.title) like concat('%',#{title},'%')</if>
<if test="status != null">and ep.status = #{status}</if>
from es_plan ep:主表left join:关联公司、员工<if test="...">:动态条件,有参数才拼进 SQL
Step 6:Model 实体类
17:37:D:\mes\enmo_support\src\main\java\com\enmo\enmo_support\workbench\model\Plan.java
@Data
public class Plan {
private Integer id;
private Integer createdBy;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", locale = "zh", timezone = "GMT+8")
private Date createdTime;
private Integer companyId;
@Length(max = 128, message = "{title}{lengthMax}")
private String title;
private Date startDate;
private Date endDate;
@Length(max = 300, message = "{deliver}{lengthMax}")
private String deliver;
private Integer status;
@Data:Lombok 自动生成 getter/setter- 字段名
createdBy对应 SQL 别名created_by createdBy - 对应前端
interface Plan
完整链路图(建议手画一遍)
【浏览器】打开 /plan 页面
↓
【Plan.vue】fetchData() 调用 getPlanListApi(params)
↓ params = { pageNum: 1, pageSize: 10, mine: false }
【apis/manage.ts】ajax({ url: '/plan/list', params })
↓ baseURL = '/esapi/'
【vue.config.js 代理】→ http://localhost:8084/plan/list?pageNum=1&pageSize=10
↓ Header: Authorization: xxx
【PlanController】@GetMapping("/list") findPlanList()
↓ @PreAuthorize 检查权限
【PlanServiceImpl】findList() 权限过滤 + startPage()
↓
【PlanDao】findList() 接口
↓
【PlanMap.xml】SELECT ... FROM es_plan WHERE ...
↓
【PostgreSQL】返回数据
↓
【PageInfo】包装 { total: 100, list: [...] }
↓ JSON
【Plan.vue】state.plans = list 渲染列表
浏览器验证(必做)
- 登录后打开
/plan - Network 筛选
plan/list - 填写:
| 项目 | 你的记录 |
|---|---|
| 完整 URL | |
| Request Method | GET |
| Query String | pageNum, pageSize, mine... |
| Request Headers 的 Authorization | 有/无 |
| Response 结构 | { total, list: [...] } |
| list0 字段 | id, title, startDate... |
- 对照
Plan.java和interface Plan看字段是否一致
任务 6:再跟一条「写操作」链路 --- 删除计划(30 分钟)
前端:
341:345:D:\mes\service-front\src\apis\manage.ts
export const delPlanApi = (id: number): Res<OperateRes> =>
ajax({
url: `plan/delete/${id}`,
method: 'delete'
})
Plan.vue 里:
75:78:D:\mes\service-front\src\views\sr\Plan.vue
<el-popconfirm v-if="isSys" title="删除后不可恢复,确定删除吗" @confirm="operate(delPlanApi, item.id)">
<template #reference>
<div class="c-primary cur-p ml10">删除</div>
Java:
124:130:D:\mes\enmo_support\src\main\java\com\enmo\enmo_support\workbench\controller\PlanController.java
@ApiOperation("删除实施计划")
@DeleteMapping("/delete/{id}")
@PreAuthorize("hasRole('sys')")
...
public OperationInfo<Object> delPlan(@PathVariable Integer id) {
return planService.deletePlan(id);
}
统一返回 OperationInfo:
13:19:D:\mes\enmo_support\src\main\java\com\enmo\enmo_support\common\result\OperationInfo.java
public class OperationInfo<M> {
private String operateCode;
private String operateMessage;
private String operateCallBackUrl;
private Boolean success;
private M operateCallBackObj;
≈ 前端 OperateRes 的 { success, operateMessage }。
读 / 写 对照:
| 操作 | HTTP | 前端 API | Java 注解 | 返回 |
|---|---|---|---|---|
| 查列表 | GET | getPlanListApi |
@GetMapping |
PageInfo<Plan> |
| 删除 | DELETE | delPlanApi |
@DeleteMapping |
OperationInfo |
| 保存 | POST | savePlanApi |
@PostMapping + @RequestBody |
OperationInfo |
任务 7:了解巡检模块(30 分钟,只建立概念)
巡检是唯一走 Python 的模块,Day 1 知道存在即可。
reportGroup.vue
→ apis/inspection.ts → getReportDetailMenuApi
→ Java BethuneAnalysisController
→ Feign BethuneAnalysisApi
→ Python group_preview.py
前端 inspection 接口示例:
typescript
// apis/inspection.ts(结构示意)
export const getReportDetailMenuApi = (data = {}) =>
ajax({
url: 'bethune/viewReportDetailMenu',
method: 'post',
data: data
})
仍走 /esapi/,但 Java 层会用 Feign 再调 Python。
Day 1 只需记住:
- 巡检前端:
views/inspection/+apis/inspection.ts - 巡检 Java:
bethune/controller/BethuneAnalysisController.java - 分析逻辑在 Python 项目,第 11 周再学
晚上(19:30~21:30):总结 + 笔记
任务 8:填写学习笔记
markdown
# Day 1 学习笔记
## 1. 项目结构
- 前端:service-front(Vue 3)
- 后端:enmo_support(Java Spring Boot,端口 8084)
- 登录:走 /api/ 代理到 8080
- 业务:走 /esapi/ 代理到 8084
## 2. 两条通道
| 用途 | proxy | 端口 |
|------|-------|------|
| 登录 | /api/ | 8080 |
| MES 业务 | /esapi/(默认) | 8084 |
## 3. Java 四层
Controller → Service → Dao → Model
对应:apis → composable → SQL → interface
## 4. 今日跟完的链路
### 登录
Login.vue → submitLoginApi → POST /api/login → token 存 Vuex
### 计划列表(重点)
Plan.vue → getPlanListApi → GET /esapi/plan/list
→ PlanController.findPlanList()
→ PlanServiceImpl.findList()
→ PlanDao.findList()
→ PlanMap.xml SQL
→ 返回 { total, list }
## 5. 注解对照
| 前端 | Java |
|------|------|
| ajax url | @GetMapping / @PostMapping |
| params | @RequestParam |
| data | @RequestBody |
| url 里的 ${id} | @PathVariable |
## 6. 疑问(Day 2 解决)
- [ ] PageInfo 和 PageListRes 怎么对应?
- [ ] @PreAuthorize 权限在哪里配置?
- [ ] MyBatis XML 怎么和 Dao 接口关联?
## 7. 明日计划
- 深入 MyBatis:读懂 PlanMap.xml 动态 SQL
- 跟一条 POST 保存链路:savePlanApi
- 开始本地跑 Java 后端