Java 项目实战: 交友项目 - 前后端分离的工程实践

概述

交友项目的前端是 Flutter 写的 App,后端是一堆 Java 服务,两边从第一天起就是两拨人在写代码。分离本身不是难点,难的是"约定"------接口返回长什么样、字段叫什么、跨域怎么办、上线怎么部署。这篇只讲工程落地这一层。

纲要

  • 接口契约怎么约定
    • 统一响应结构:ErrorResult / HTTP 状态码口径
    • URL 资源化命名与 HTTP 方法表达动作
    • 分页结构:PageResult 的固定字段
    • 全局异常处理器如何把异常翻译成统一响应
  • 接口文档与协作流程
    • 串行协作(先文档后开发)与并行协作(Mock 先行)
    • 文档与实现漂移的风险与三种应对
  • 跨域问题与解决
    • 跨域从哪来:端口不同即跨域
    • 前端代理 / 后端 CORS / 网关统一处理 三条路径对比
    • 开发期用代理、生产期用网关的常见组合
  • 联调与调试手段
    • Postman / Apifox 自测、浏览器 Network 面板
    • 联调问题的四步定位顺序、跨域与业务错误的区分
  • 部署方式的变化
    • 前端 npm run build 产出静态资源、Nginx 托管
    • Nginx 配置示例:静态目录 + /api/ 反向代理
  • 端到端数据流(Mermaid 时序图)
  • 实践坑清单
  • 登录页原型与接口对照
  • API 速览
  • 常见误解澄清

接口契约怎么约定

前后端分离的核心不是"你写页面我写接口",而是接口契约驱动。契约不定死,两边各写各的,联调时就是互相甩锅现场。契约里最容易被忽略、也最影响协作效率的是返回结构------前端要能用一套代码处理所有接口的成败。

统一响应结构

后端所有接口的响应体应当是同一层封装。交友项目在 mf-model 的 vo 包里放了一份错误响应体,errCode 给机器判断,errMessage 给人看:

java 复制代码
package com.mf.model.vo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 统一错误响应体
 * errCode  ------ 业务错误码,前端按码分支处理
 * errMessage ------ 面向用户的提示文案
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ErrorResult {

    private String errCode;
    private String errMessage;

    public static ErrorResult error() {
        return ErrorResult.builder().errCode("999999").errMessage("系统异常稍后再试").build();
    }

    public static ErrorResult fail() {
        return ErrorResult.builder().errCode("000001").errMessage("发送验证码失败").build();
    }

    public static ErrorResult loginError() {
        return ErrorResult.builder().errCode("000002").errMessage("验证码失效").build();
    }

    public static ErrorResult mobileError() {
        return ErrorResult.builder().errCode("000004").errMessage("手机号码已注册").build();
    }
}

错误码用静态工厂方法集中定义,好处是错误码和文案只有一处来源,改文案不用满项目搜字符串。

HTTP 状态码的口径要和响应体配套。交友项目的做法是:成功走 ResponseEntity.ok(...)(200),错误走 ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)(500),body 统一是 ErrorResult。第 01 章 LoginController 里就是这么写的:

java 复制代码
package com.mf.server.controller;

import com.mf.server.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/user")
public class LoginController {

    @Autowired
    private UserService userService;

    /**
     * 获取登录验证码
     * 请求参数:phone
     */
    @PostMapping("/login")
    public ResponseEntity login(@RequestBody Map map) {
        String phone = (String) map.get("phone");
        userService.sendMsg(phone);
        return ResponseEntity.ok(null); // 正常返回状态码 200
    }

    /**
     * 校验登录
     * 请求参数:phone、verificationCode
     * 响应:{ token, isNew }
     */
    @PostMapping("/loginVerification")
    public ResponseEntity loginVerification(@RequestBody Map map) {
        String phone = (String) map.get("phone");
        String code = (String) map.get("verificationCode");
        Map retMap = userService.loginVerification(phone, code);
        return ResponseEntity.ok(retMap);
    }
}

另一种常见口径是HTTP 状态码永远是 200,业务成败全靠 body 里的 code 区分。两种都行,但必须全项目统一------最怕的是 A 接口用状态码、B 接口用 code,前端拦截器只能疯狂写特判。

全局异常处理器

契约要有"异常出口"。业务代码只管抛异常,由 @ControllerAdvice 统一翻译成响应体,前端永远只面对一种错误格式:

java 复制代码
package com.mf.server.exception;

import com.mf.model.vo.ErrorResult;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class ExceptionAdvice {

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity handlerException(BusinessException be) {
        be.printStackTrace();
        ErrorResult errorResult = be.getErrorResult();
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResult);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity handlerException1(Exception be) {
        be.printStackTrace();
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ErrorResult.error());
    }
}

配套的 BusinessException 只需要携带一个 ErrorResult:

java 复制代码
package com.mf.server.exception;

import com.mf.model.vo.ErrorResult;
import lombok.Data;

@Data
public class BusinessException extends RuntimeException {

    private ErrorResult errorResult;

    public BusinessException(ErrorResult errorResult) {
        super(errorResult.getErrMessage());
        this.errorResult = errorResult;
    }
}

这样 Service 层写 throw new BusinessException(ErrorResult.loginError()),前端收到的永远是 {"errCode":"000002","errMessage":"验证码失效"}。

URL 命名规范

资源化路径,动作交给 HTTP 方法。交友项目 app-server 用 @RequestMapping("/user") 做模块前缀,方法级再细化:

约定项 示例 为什么重要
模块前缀 /user、/movements、/messages 前端按模块聚合请求,代理和网关可按前缀分流
资源用名词复数 /movements、/comments 避免 /getMovements、/doLike 这类动词路径
动作交给 HTTP 方法 POST /user/login、GET /user/{id} 语义清晰,缓存/重试策略可按方法区分
路径参数用 {} /user/{id} 与 Spring @PathVariable 天然对应
查询参数用 kebab/小驼峰统一 page、pagesize 分页参数名跨接口一致,前端不用记两套

版本前缀(/api/v1/...)在交友项目这种内部 App 场景里没有强上,因为 App 是强制升级的,不存在老客户端长时间存活的问题。如果是对外开放 API,版本前缀几乎是必选项。

分页结构约定

分页是最高频的返回形态,字段名必须一次定死。交友项目用 PageResult,五个字段包打天下:

java 复制代码
package com.mf.model.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.Collections;
import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {

    private Long counts = 0L;      // 总记录数
    private Integer pagesize = 10; // 页大小
    private Long pages = 0L;       // 总页数
    private Integer page = 1;      // 当前页码
    private List<?> items = Collections.emptyList(); // 列表数据

    public PageResult(Integer page, Integer pagesize, Long counts, List list) {
        this.page = page;
        this.pagesize = pagesize;
        this.items = list;
        this.counts = counts;
        this.pages = counts % pagesize == 0 ? counts / pagesize : counts / pagesize + 1;
    }
}

注意 pages 是构造器里算出来的,不是让前端自己除。前端只管 items 渲染列表、counts 显示"共 N 条",落一屏加载更多就 page++。字段名 counts、pagesize、items 这种非标准写法一旦定了就别改------改一次前端全量返工。

接口文档与协作流程

分离之后,接口文档就是契约本身。讲义里明确点出来:"两端之间通过接口文档定义规范,前端和后端基于文档进行数据请求和格式的响应。" 团队协作通常两种走法:

串行:后端先把文档写出来(接口路径、请求参数、响应示例),前端照着文档先把页面和请求逻辑写完,后端再实现。风险是前端干等文档。

并行:后端先出文档 + Mock 数据(YApi、Apifox 都能一键生成 Mock 服务),前端直接连 Mock 地址开发,后端同时写实现。切换真实环境时只改一个 baseURL。

交友项目的实际做法偏并行------App 端用原型图(03-资料/交友项目移动端原型V1.2)先定页面和交互,接口文档定请求/响应格式,后端在 mf-app-server 里实现。第 3.1.3 节把 Postman 列为标配工具,就是为了后端在交付前能自测接口。

文档与实现漂移是分离模式最大的协作风险。 文档写的是 username,代码里返回的是 user_name,联调时前端只会看到 undefined。三种应对,从轻到重:

手段 做法 成本
契约评审 接口定稿前前后端一起过一遍,双方签字 低,但靠人
自动化校验 用 OpenAPI/Swagger 注解生成文档,与代码同源 中,需改造
接口测试兜底 后端每个接口配 Postman/Swagger 用例,回归时跑一遍 中,能提前发现
前端契约测试 Mock 层按文档写死结构,后端不符合就联调失败 高,但最拦得住

一句话:能把文档从代码里自动生成出来,就别手写文档。

跨域问题与解决

前端跑在一个端口、后端跑在另一个端口,浏览器就认为这是"跨域"。交友项目后端 mf-app-server 监听 18080,如果前端本地开发服务跑在 8080,端口不同 → 协议 + 域名 + 端口任一不同即为跨域 → 浏览器会在发请求前先做同源检查,不通过就把响应拦下来。

需要先纠正一个认知:跨域是浏览器的安全策略,不是后端的 bug。 后端其实收到请求并处理了,只是响应里缺了允许跨域的响应头,浏览器拒绝把结果交给 JS。所以 Network 面板里经常看到请求有状态码、却报了 CORS------请求成功了,是浏览器不给你用。

三条解决路径:

路径 做法 适用场景 代价
前端代理 开发服务器(Vite/webpack devServer)把 /api 代理到后端 开发期首选 只在本机生效,上线无效
后端 CORS 后端加 @CrossOrigin 或全局 CORS 配置 前后端同域不同端口的小项目 开发配置可能被带上生产
网关统一处理 网关(如 Spring Cloud Gateway)统一加 CORS 响应头 生产环境推荐 需要网关层支持

实际项目里最省心的组合是:开发期用前端代理,生产期用网关。

开发期用代理的原因很直接------前端配置里改一行 target 就完事,不用动后端代码,也避免了把"允许 localhost:8080 跨域"这种调试配置带进生产环境。生产期的跨域处理应该收敛到网关一处,而不是散在每个业务服务里各配各的。

如果要在后端配 CORS,注意区分"前端代理能不能解决"和"必须后端解决"两种场景:代理走的是服务器到服务器,压根不经过浏览器的同源检查,所以开发期代理能覆盖绝大多数情况。CORS 的响应头细节(Access-Control-Allow-Origin 等)属于网关配置范畴,本系列另有《网关的 cors 跨域配置》一篇专门展开,这里不重复。

联调与调试手段

联调出问题时,别急着怀疑代码,按固定顺序排查能省一大半时间。

第一步:请求发出去了吗? 打开浏览器开发者工具的 Network 面板,前端点了按钮但列表里一条记录都没有,说明问题在前端------请求根本没构造出来,或者被某个 return 提前跳过了。这一步能直接砍掉一半的排查范围。

第二步:状态码是什么? 4xx 是请求方的问题(路径写错 404、参数缺失 400、鉴权失败 401/403),5xx 是服务端的问题(业务异常 500、服务没起来连不上)。看到 500 就去后端看堆栈,别在前端瞎改。

第三步:响应体内容对不对? 状态码 200 也不代表对。展开响应体,字段名和文档对得上吗?data 里是不是空的?这一层最常见的问题是字段名拼写或大小写不一致。

第四步:是跨域还是业务错误? 区分方法很明确:跨域时浏览器控制台会显式打印 CORS 或 "blocked by CORS policy",且 Network 里请求往往显示成功却被拦;业务错误则是一个正常返回的响应体,里面 errCode/errMessage 有值。前者去改代理或网关配置,后者去查业务代码。

工具层面,后端用 Postman 或 Apifox 单独跑接口,能快速回答"是前端请求的问题还是后端逻辑的问题"。交友项目讲义把 Postman 作为环境搭建的一部分,就是这个用途。

部署方式的变化

分离部署带来的是两边可以各自独立上线。

前端:Flutter 项目打包成 APK 交给客户端团队(交友项目是 App,不是浏览器页面),如果换成 Web 前端则是 npm run build 产出 dist 静态资源目录,扔给 Nginx 托管。静态资源不经过 Java 进程,天然可以上 CDN。

后端:只提供 API,mf-app-server 按接口压力独立扩容,和前端发布互不影响。

典型的 Nginx 配置长这样------静态资源和后端接口在同一个域名下,前端请求 /api/ 时反向代理到后端服务,同源问题自然消失:

nginx 复制代码
server {
    listen       80;
    server_name  localhost;

    # 前端静态资源(Vue/React 打包产物)
    location / {
        root      /usr/share/nginx/html;
        index     index.html;
        # 单页应用刷新 404 兜底
        try_files $uri $uri/ /index.html;
    }

    # 后端接口反向代理
    location /api/ {
        proxy_pass       http://127.0.0.1:18080/;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

注意 proxy_pass 结尾有没有 / 会导致路径拼接结果不同:http://127.0.0.1:18080/ 会把 /api/ 剥掉,http://127.0.0.1:18080 则会把 /api/ 原样带上。这一条坑过很多人。

工程目录(本章快照)大致是这样组织的:

tree 复制代码
mf
├── mf-autoconfig              # 自动装配组件(短信等)
├── mf-model                   # 实体类 + VO(含 ErrorResult、PageResult)
├── mf-commons                 # 工具类(JwtUtils 等)
├── mf-app-server              # 与手机端交互的入口模块(18080)
│   └── src/main/java/com/mf/server
│       ├── controller             # 对外 HTTP 接口
│       ├── service                # 业务编排
│       └── exception              # 全局异常处理
└── mf-dubbo                   # Dubbo 子模块
    ├── mf-dubbo-interface     # 接口定义
    ├── mf-dubbo-db            # 数据库服务(18081)
    └── mf-dubbo-mongo         # MongoDB 服务

端到端数据流

把一次业务请求从浏览器到后端再回来的完整链路画清楚,分离模式的数据流就一目了然了:
Dubbo 服务/DB 后端应用(mf-app-server) Nginx 浏览器/App Dubbo 服务/DB 后端应用(mf-app-server) Nginx 浏览器/App #mermaid-svg-JWTAIbzi4BQf9mhL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JWTAIbzi4BQf9mhL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JWTAIbzi4BQf9mhL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JWTAIbzi4BQf9mhL .error-icon{fill:#552222;}#mermaid-svg-JWTAIbzi4BQf9mhL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JWTAIbzi4BQf9mhL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JWTAIbzi4BQf9mhL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JWTAIbzi4BQf9mhL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JWTAIbzi4BQf9mhL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JWTAIbzi4BQf9mhL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JWTAIbzi4BQf9mhL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JWTAIbzi4BQf9mhL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JWTAIbzi4BQf9mhL .marker.cross{stroke:#333333;}#mermaid-svg-JWTAIbzi4BQf9mhL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JWTAIbzi4BQf9mhL p{margin:0;}#mermaid-svg-JWTAIbzi4BQf9mhL .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JWTAIbzi4BQf9mhL text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-JWTAIbzi4BQf9mhL .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-JWTAIbzi4BQf9mhL .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-JWTAIbzi4BQf9mhL .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-JWTAIbzi4BQf9mhL .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-JWTAIbzi4BQf9mhL #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-JWTAIbzi4BQf9mhL .sequenceNumber{fill:white;}#mermaid-svg-JWTAIbzi4BQf9mhL #sequencenumber{fill:#333;}#mermaid-svg-JWTAIbzi4BQf9mhL #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-JWTAIbzi4BQf9mhL .messageText{fill:#333;stroke:none;}#mermaid-svg-JWTAIbzi4BQf9mhL .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JWTAIbzi4BQf9mhL .labelText,#mermaid-svg-JWTAIbzi4BQf9mhL .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-JWTAIbzi4BQf9mhL .loopText,#mermaid-svg-JWTAIbzi4BQf9mhL .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-JWTAIbzi4BQf9mhL .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-JWTAIbzi4BQf9mhL .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-JWTAIbzi4BQf9mhL .noteText,#mermaid-svg-JWTAIbzi4BQf9mhL .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-JWTAIbzi4BQf9mhL .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JWTAIbzi4BQf9mhL .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JWTAIbzi4BQf9mhL .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JWTAIbzi4BQf9mhL .actorPopupMenu{position:absolute;}#mermaid-svg-JWTAIbzi4BQf9mhL .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-JWTAIbzi4BQf9mhL .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JWTAIbzi4BQf9mhL .actor-man circle,#mermaid-svg-JWTAIbzi4BQf9mhL line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-JWTAIbzi4BQf9mhL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GET /api/user/1 (静态页已加载) 反向代理 /user/1 (剥掉 /api) 查询用户数据 领域对象 JSON 响应体 { code, data } 原样转发 JSON 前端渲染页面

前端拿到 JSON 后只管渲染,后端只管返回数据,中间靠 Nginx(或网关)把两边拼在一个域名下。

实践坑清单

坑 表现 处理方案
返回结构不统一 有的接口 {code,data},有的直接返回数组 全项目统一封装,异常也走同一结构
字段命名风格不一致 后端 user_name,前端读 userName 拿到 undefined 服务端序列化统一用驼峰,或前端拦截器做转换
日期格式不统一 一会儿时间戳、一会儿 yyyy-MM-dd 字符串 全局配置 Jackson 日期格式,契约里写死
分页参数名不一致 这个接口 page、那个接口 pageNum 分页字段进契约,统一用 page/pagesize
跨域配置开发/生产不一致 本地好使,一上线全报 CORS 开发用代理、生产用网关,配置分离
接口文档过期 照着文档写,联调时字段全对不上 文档从代码生成,或契约评审 + 接口测试
错误码含义不明 前端只知道 500,不知道具体原因 errCode 分段管理,前端按码分支
proxy_pass 尾斜杠 代理后路径多了一层 /api 明确剥前缀:proxy_pass http://host:port/;

登录页原型与接口对照

03-资料/交友项目移动端原型V1.2/登录页.html 是 App 端登录页的原型,正好可以拿来对照接口契约怎么落地:

doc 复制代码
┌──────────────────────────────┐
│                              │
│         项目 · 交友           │   ← 品牌区
│                              │
│  ┌────────────────────────┐  │
│  │  请输入手机号           │  │   ← 手机号输入框
│  └────────────────────────┘  │
│  ┌──────────────┬─────────┐  │
│  │ 请输入验证码  │ 获取验证码│  │   ← 验证码输入 + 获取按钮
│  └──────────────┴─────────┘  │
│                              │
│  ┌────────────────────────┐  │
│  │        登   录          │  │   ← 主按钮
│  └────────────────────────┘  │
│                              │
│  ☐ 已阅读并同意《用户协议》    │   ← 协议勾选
└──────────────────────────────┘

要点:

  • 手机号输入框:11 位数字校验,未填或格式错时"获取验证码"按钮应置灰。
  • "获取验证码"按钮:点击调 POST /user/login,参数 { "phone": "138xxxx" },成功后按钮进入 60 秒倒计时。
  • 登录按钮:点击调 POST /user/loginVerification,参数 { "phone": "...", "verificationCode": "123456" }。
  • 登录响应:{ "token": "xxx", "isNew": false }。token 是后续所有请求的鉴权凭证,isNew 为 true 时前端跳"首次登录信息完善"页。
  • 协议勾选:前端校验,未勾选不发请求------这类校验能放到前端的就别占后端资源。

API 速览

接口 方法 路径 请求参数 响应
获取登录验证码 POST /user/login { phone } 空 body,200
校验登录 POST /user/loginVerification { phone, verificationCode } { token, isNew }
错误响应(统一) --- 任意 --- { errCode, errMessage }
分页响应(统一) --- 列表类接口 page、pagesize { counts, pagesize, pages, page, items }

官方文档

常见误解澄清

"前后端分离就是前端写页面、后端写接口。" 这只是分工描述。真正决定成败的是接口契约驱动 和独立部署------没有统一契约,分工再清楚也是联调灾难。

"跨域是后端的 bug。" 跨域是浏览器的同源策略在起作用,后端请求其实处理了。排查时先看是不是 CORS 提示,别上来就改后端业务代码。

"前端不需要了解后端。" 不了解契约前端根本没法联调。至少要知道接口返回结构、错误码含义、分页字段名。

"分离之后就不用管 SEO 了。" 对 Web 端的 C 端页面,分离(尤其是纯前后端分离 + JS 渲染)确实会削弱 SEO,需要 SSR/预渲染补救。但交友项目是 App,走的是原生客户端,不涉及这个问题------这也是它敢全量分离的原因之一。

总结

  • 分离模式的工程重心在约定 :统一响应结构(ErrorResult)、分页结构(PageResult)、URL 命名规范,一次定死、全项目一致。
  • 错误处理要收敛到全局异常处理器,业务代码只抛异常,前端只面对一种错误格式。
  • 接口文档就是契约,能自动生成就别手写;文档漂移是分离模式头号协作风险。
  • 跨域开发期用前端代理、生产期用网关,别把调试用的 CORS 配置带上生产。
  • 部署上前端交给 Nginx 托管静态资源并反向代理 /api/,后端只管出 API、独立扩容。
  • 联调按"请求发了吗 → 状态码 → 响应体 → 跨域还是业务错"四步走,比盲目改代码快得多。

下一篇按讲义顺序进入「开发工具与环境搭建」,讲 Linux 虚拟机、Android 模拟器、Postman 的安装,以及 Maven 聚合工程从零搭起来的过程。

相关推荐
代码方舟1 小时前
零信任架构实战:基于天远普通维保查询构建自动化二手车融资租赁资产入库审计网关
人工智能·架构·自动化
2603_954708311 小时前
如何进一步优化微能网协调控制箱的架构优势
大数据·数据库·人工智能·分布式·架构
溪语流沙1 小时前
面试加分项!Manacher:O(n)求最长回文子串的对称之美
java·python·leetcode·面试·职场和发展
知守观1 小时前
Maven SNAPSHOT 更新排查:-U 无效、clean 无用,问题在 _remote.repositories(附完整链路)
java·后端·maven
牛战士coding1 小时前
从一句\“取消订单\“到退款通知:AI 写操作完整业务闭环
java·人工智能
啵啵啵鱼1 小时前
第三章 栈、队列和数组相关结构
java·数据结构·栈·队列
瑶山1 小时前
stable-diffusion.cpp 参数全集整理(sd-cli.exe / sd-server.exe)
java·数据库·stable diffusion·参数全集
CallFay云起未来1 小时前
2026年AI客服能不能减少人工回复?从知识检索到人机协同的技术实现
java·大数据·前端·人工智能·架构·文心一言