移动端和后端的对接远不止"调接口"三个字——上传 token 怎么带、网络断了怎么提示、统一错误码映射、超时重试。这篇文章把 uni-app 对接 Spring Boot 的所有坑和最佳实践一次讲清。


一、统一的请求封装

// utils/request.js
import { useUserStore } from '@/stores/user'

const BASE_URL_MAP = {
  development: 'http://192.168.1.100:8080',  // 局域网调试
  production: 'https://api.your-domain.com'
}

const BASE_URL = BASE_URL_MAP[process.env.NODE_ENV]

const request = (options) => {
  return new Promise((resolve, reject) => {
    const userStore = useUserStore()

    uni.request({
      url: BASE_URL + options.url,
      method: options.method || 'GET',
      data: options.data || {},
      header: {
        'Content-Type': 'application/json',
        'Authorization': userStore.token ? `Bearer ${userStore.token}` : '',
        ...options.header
      },
      timeout: options.timeout || 15000,

      success: (res) => {
        const { code, data, msg } = res.data

        // 成功
        if (code === 200) {
          resolve(data)
          return
        }

        // Token 过期
        if (code === 401) {
          userStore.logout()
          uni.reLaunch({ url: '/pages/login/index' })
          reject(new Error('登录已过期'))
          return
        }

        // 业务异常
        uni.showToast({ title: msg || '操作失败', icon: 'none' })
        reject(new Error(msg))
      },

      fail: (err) => {
        // 网络层面的错误
        const errorMsg = getNetworkErrorMsg(err)
        uni.showToast({ title: errorMsg, icon: 'none' })
        reject(err)
      }
    })
  })
}

// 网络错误信息映射
function getNetworkErrorMsg(err) {
  if (err.errMsg?.includes('timeout')) return '请求超时,请重试'
  if (err.errMsg?.includes('fail')) return '网络异常,请检查网络'
  return '请求失败'
}

export default request

二、与 Spring Boot 后端的格式约定

2.1 统一响应格式

// AjaxResult.java
public class AjaxResult extends HashMap<String, Object> {

    public static final int SUCCESS = 200;
    public static final int UNAUTHORIZED = 401;
    public static final int FORBIDDEN = 403;
    public static final int ERROR = 500;

    public static AjaxResult success(Object data) {
        AjaxResult result = new AjaxResult();
        result.put("code", SUCCESS);
        result.put("data", data);
        result.put("msg", "操作成功");
        return result;
    }

    public static AjaxResult error(int code, String msg) {
        AjaxResult result = new AjaxResult();
        result.put("code", code);
        result.put("msg", msg);
        return result;
    }
}

2.2 后端全局异常处理

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public AjaxResult handleBusinessException(BusinessException e) {
        return AjaxResult.error(e.getCode(), e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public AjaxResult handleException(Exception e) {
        log.error("系统异常", e);
        return AjaxResult.error(500, "系统异常,请联系管理员");
    }
}

2.3 分页结果格式约定

// PageResult.java
public class PageResult {
    private List<?> rows;
    private long total;

    public static AjaxResult page(List<?> rows, long total) {
        Map<String, Object> map = new HashMap<>();
        map.put("rows", rows);
        map.put("total", total);
        return AjaxResult.success(map);
    }
}
// Controller
@GetMapping("/list")
public AjaxResult list(@RequestParam int pageNum, @RequestParam int pageSize) {
    PageHelper.startPage(pageNum, pageSize);
    List<Customer> list = customerService.selectList();
    return PageResult.page(list, new PageInfo<>(list).getTotal());
}

三、文件上传对接

3.1 前端上传

// api/upload.js
export async function uploadFile(filePath, type = 'image') {
  return new Promise((resolve, reject) => {
    uni.uploadFile({
      url: BASE_URL + '/common/upload',
      filePath: filePath,
      name: 'file',
      formData: { type },
      header: {
        Authorization: 'Bearer ' + useUserStore().token
      },
      success: (res) => {
        const { code, data, msg } = JSON.parse(res.data)
        if (code === 200) resolve(data.url)
        else reject(new Error(msg))
      },
      fail: reject
    })
  })
}

3.2 后端接收

@PostMapping("/common/upload")
public AjaxResult upload(@RequestParam("file") MultipartFile file) {
    String url = fileService.upload(file);
    return AjaxResult.success(Map.of("url", url));
}

四、WebSocket 实时推送(uni-app 版)

车间电子看板需要实时展示生产进度,用 WebSocket:

// utils/websocket.js
class WebSocketClient {
  constructor() {
    this.socketTask = null
    this.reconnectTimer = null
    this.reconnectCount = 0
  }

  connect() {
    const userStore = useUserStore()

    this.socketTask = uni.connectSocket({
      url: `wss://api.your-domain.com/ws/mes?token=${userStore.token}`,
      success: () => console.log('WebSocket 连接中...')
    })

    this.socketTask.onOpen(() => {
      console.log('WebSocket 已连接')
      this.reconnectCount = 0
    })

    this.socketTask.onMessage((res) => {
      const data = JSON.parse(res.data)
      // 分发给对应页面
      uni.$emit('ws-message', data)
    })

    this.socketTask.onClose(() => {
      this.reconnect()
    })

    this.socketTask.onError(() => {
      this.reconnect()
    })
  }

  reconnect() {
    if (this.reconnectCount >= 5) return
    this.reconnectCount++

    this.reconnectTimer = setTimeout(() => {
      this.connect()
    }, 3000 * this.reconnectCount)  // 递增等待
  }

  send(data) {
    if (this.socketTask) {
      this.socketTask.send({ data: JSON.stringify(data) })
    }
  }

  close() {
    if (this.socketTask) {
      this.socketTask.close()
    }
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer)
    }
  }
}

export const wsClient = new WebSocketClient()

五、错误处理体系

// utils/errorHandler.js
export const ERROR_MAP = {
  400: '请求参数有误',
  401: '登录已过期',
  403: '没有操作权限',
  404: '请求的资源不存在',
  500: '服务器异常',
  502: '网关异常',
  503: '服务暂时不可用'
}

export function handleHttpError(code, defaultMsg) {
  const msg = ERROR_MAP[code] || defaultMsg || '未知错误'

  if (code === 401) {
    // 避免多次弹窗
    uni.showModal({
      title: '登录过期',
      content: '您的登录已过期,请重新登录',
      showCancel: false,
      success: () => {
        const userStore = useUserStore()
        userStore.logout()
      }
    })
  } else if (code === 403) {
    uni.showToast({ title: msg, icon: 'none' })
  } else {
    uni.showToast({ title: msg, icon: 'none' })
  }
}

六、开发调试技巧

6.1 真机调试本地后端

// 使用局域网 IP
const BASE_URL = 'http://192.168.1.100:8080'

// 如果后端加了跨域
// 小程序不需要关心跨域,App 端需要后端配置 CORS
// 后端 CORS 配置
@Configuration
public class CorsConfig {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                    .allowedOriginPatterns("*")
                    .allowedMethods("*")
                    .allowedHeaders("*")
                    .allowCredentials(true);
            }
        };
    }
}

6.2 请求日志

// 开发环境下打印请求日志
if (process.env.NODE_ENV === 'development') {
  console.log(`[Request] ${options.method} ${options.url}`)
  console.log('[Request Data]', options.data)
}

七、总结

uni-app 对接 Spring Boot 的核心要点:

要点做法
统一响应格式后端 AjaxResult,前端根据 code 分流
Token 传递请求拦截器自动注入 Header
文件上传uni.uploadFile + MultipartFile
实时推送uni.connectSocket + Spring WebSocket
错误码映射前后端一致维护 ERROR_MAP
真机调试局域网 IP + CORS 配置

如果你也在独立开发产品,或者对制造业数字化感兴趣,欢迎关注这个公众号。我会持续分享从代码到产品的全过程——包括成功的经验,也包括踩过的坑。

一个人的产品之路,不孤单。👇

原创作者 MqCode(全栈开发者,印刷包装行业 MES+CRM 系统独立开发),欢迎自由转发。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐