Java 项目实战: 外卖平台-启用禁用员工账号与Long精度丢失修复

启用禁用员工账号与 Long 型 ID 精度丢失修复

纲要

这篇记录员工账号启用 / 禁用功能的实现,顺手解决一个藏得比较深的线上问题:Long 型雪花 ID 传到前端后被抹平精度,导致更新不生效。

  • 功能本质 :修改 employee.status 字段,是一个 UPDATE 操作
  • 接口契约PUT /employee,入参 {"id": ..., "status": ...}
  • 通用 update 方法:启用禁用与编辑员工复用同一个方法
  • 权限可见性 :只有 admin 能看到启用禁用按钮,靠 v-if 判断
  • 缺陷现象 :功能「不报错但也不生效」,UPDATEWHERE id = ? 与库中 ID 不一致
  • 根因Long 型雪花 ID 19 位,超出 JavaScript 安全整数范围 2^53,精度丢失
  • 修复方案 :自定义 JacksonObjectMapperLong 序列化为字符串;注册到 MVC 消息转换器队首

需求与页面分析

按钮的可见性控制

进入员工管理页会发现,只有 admin 账号能看到「启用 / 禁用」按钮,其他员工看不到。这由前端 v-if 控制:

html 复制代码
<el-table-column label="操作" width="160" align="center">
  <template slot-scope="scope">
    <el-button type="text" size="small" @click="addMemberHandle(scope.row.id)">编辑</el-button>
    <!-- v-if:仅当登录用户为 admin 时才渲染启用/禁用按钮 -->
    <el-button
      v-if="user === 'admin'"
      type="text"
      size="small"
      @click="statusHandle(scope.row)"
    >
      {{ String(scope.row.status) === '0' ? '启用' : '禁用' }}
    </el-button>
  </template>
</el-table-column>

user 取自 localStorage 中保存的登录用户名:

javascript 复制代码
created() {
  this.user = JSON.parse(localStorage.getItem('userInfo')).username
}

注意这只是前端可见性控制 ,不是权限控制。真正的权限校验必须在后端做 ------ 前端判断可以被绕过(直接构造 HTTP 请求即可)。本项目未做后端权限校验,生产环境必须补上。

按钮状态的反转逻辑

按钮文字与当前状态是反的:账号正常时显示「禁用」,账号禁用时显示「启用」。

javascript 复制代码
{{ String(scope.row.status) === '0' ? '启用' : '禁用' }}

请求参数的三目运算

点击按钮后执行 statusHandle

javascript 复制代码
statusHandle(row) {
  this.$confirm('确认调整该账号的状态?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    // 关键:status 取当前值的反转
    enableOrDisableEmployee({
      id: row.id,
      status: !row.status ? 1 : 0
    }).then(res => {
      if (String(res.code) === '1') {
        this.$message.success('账号状态更改成功!')
        this.handleQuery()
      }
    })
  })
}

status: !row.status ? 1 : 0 的含义:

当前 status !row.status 提交的 status 语义
1(正常) false 0 禁用
0(禁用) true 1 启用

member.js 中的接口封装:

javascript 复制代码
function enableOrDisableEmployee(params) {
  return $axios({
    url: '/employee',
    method: 'put',
    data: { ...params }
  })
}

请求特征:

text 复制代码
PUT http://localhost:8080/employee
Request Payload: {"id":"1420038345634918401","status":0}

后台实现

为什么写通用 update 而不是 updateStatus

启用禁用本质上只是修改 status 一个字段。如果写成 updateStatus(Long id, Integer status),后续「编辑员工信息」功能还要再写一个 update(Employee) ------ 两者底层都是 UPDATE employee SET ... WHERE id = ?,完全重复。

统一成一个 update 方法,两个功能复用:
启用/禁用员工

{id, status}
PUT /employee

update(Employee)
编辑员工信息

{id, name, phone, ...}
employeeService.updateById(employee)
UPDATE employee SET ... WHERE id = ?

方法实现

java 复制代码
/**
 * 根据id修改员工信息
 * @param request
 * @param employee
 * @return
 */
@PutMapping
public R<String> update(HttpServletRequest request, @RequestBody Employee employee) {
    log.info(employee.toString());

    long id = Thread.currentThread().getId();
    log.info("线程id为:{}", id);

    employeeService.updateById(employee);

    return R.success("员工信息修改成功");
}

关键点

@PutMapping 不写路径 :类上已有 @RequestMapping("/employee"),前端 PUT /employee 正好匹配。

@RequestBody Employee :只提交了 idstatusEmployee 对象的其他属性都是 null。这恰恰是关键 ------ MyBatis-PlusupdateById 默认只更新非 null 字段 ,所以不会把 namephone 等误置空。

这是 MyBatis-Plus 的默认行为(FieldStrategy.NOT_NULL)。若想让某字段能被显式更新为 null,需在该字段上加 @TableField(updateStrategy = FieldStrategy.IGNORED)

Thread.currentThread().getId() 的用途 :这行日志是为了验证「一次请求从头到尾由同一个线程处理」------ 这是 BaseContextThreadLocal)能正确工作的前提。调试时观察同一个请求中过滤器、控制器、MetaObjectHandler 打印的线程 ID 是否一致。

生成的 SQL

text 复制代码
==>  Preparing: UPDATE employee SET status=?, update_time=?, update_user=? WHERE id=?
==> Parameters: 0(Integer), 2021-07-23 11:20:33(LocalDateTime), 1(Long), 1420038345634918401(Long)

缺陷:Long 精度丢失

现象

点击禁用,页面提示「账号状态更改成功」,但刷新后状态没变。控制台没有任何异常SQL 正常执行,Updates: 0

仔细观察 SQL 参数:

text 复制代码
数据库里的 id : 1420038345634918402
SQL 里的 id   : 1420038345634918400
                                ^^^ 末位被抹平

WHERE id = 1420038345634918400 匹配不到任何记录,所以 Updates: 0,功能静默失效。

根因

JavaScriptNumber 类型采用 IEEE 754 双精度浮点表示,能精确表示的整数范围是:

text 复制代码
[-2^53 + 1, 2^53 - 1]  即  [-9007199254740991, 9007199254740991]

而雪花算法生成的 ID 是 19 位,远超 9007199254740991(16 位)。超出的部分会被四舍五入到最接近的可表示值。

在浏览器控制台可以直接复现:

javascript 复制代码
console.log(1420038345634918402)
// 输出:1420038345634918400

console.log(1420038345634918402 === 1420038345634918400)
// 输出:true   ← 两个不同的数被判定为相等!

数据流转中的精度丢失点

JavaScript Jackson 默认序列化 Java (Long) MySQL JavaScript Jackson 默认序列化 Java (Long) MySQL 实际得到 1420038345634918400 id = 1420038345634918402 (bigint) Long 类型,值完整 {"id":1420038345634918402} ← 数字形态 JSON.parse → Number 精度丢失 PUT 请求带回失真的 ID UPDATE ... WHERE id = 1420038345634918400 影响 0 行

精度是在 JavaScript 解析 JSON 这一步丢的。所以最直接的修法,就是在 Jackson 序列化时把 Long 写成字符串,JavaScript 按字符串处理,就不会走数值解析这条路。

三类受影响的操作

操作 表现
启用 / 禁用 WHERE id 匹配不到,Updates: 0
编辑 同上,修改不生效
删除 同上,删除不生效

这个缺陷坑人的地方在于:页面看起来一切正常,不报错,只是功能不生效 。不知道 JavaScript 数字精度限制的话,排查起来会绕不少弯路,因为从后端日志到 SQL 都看不出明显问题。

修复方案

第一步:自定义对象映射器

src/main/java/com/itheima/reggie/common/JacksonObjectMapper.java

java 复制代码
package com.itheima.reggie.common;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;

import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
 */
public class JacksonObjectMapper extends ObjectMapper {

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    public JacksonObjectMapper() {
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

                // 核心:Long / BigInteger 序列化为字符串,避免前端精度丢失
                .addSerializer(BigInteger.class, ToStringSerializer.instance)
                .addSerializer(Long.class, ToStringSerializer.instance)
                // 时间类型按指定格式输出字符串
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}

第二步:注册到消息转换器

src/main/java/com/itheima/reggie/config/WebMvcConfig.java

java 复制代码
/**
 * 扩展mvc框架的消息转换器
 * @param converters
 */
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    log.info("扩展消息转换器...");
    //创建消息转换器对象
    MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
    //设置对象转换器,底层使用Jackson将Java对象转为json
    messageConverter.setObjectMapper(new JacksonObjectMapper());
    //将上面的消息转换器对象追加到mvc框架的转换器集合中
    converters.add(0, messageConverter);
}

为什么必须用 add(0, ...)

extendMessageConverters 传入的 convertersSpring MVC 已装配好的转换器列表 ,其中第一个通常是默认的 MappingJackson2HttpMessageConverter

Spring MVC 按列表顺序遍历,用第一个 canWrite 返回 true 的转换器。如果把自己的转换器 add 到末尾,默认转换器会先命中,自定义配置完全不生效。
是(自定义转换器)

默认转换器
Controller 返回 R 对象
遍历 converters 列表
索引 0 的转换器

可处理该类型?
使用 JacksonObjectMapper

Long → 字符串
继续下一个
Long 输出为数字

精度丢失

效果对比

修复前:

json 复制代码
{
  "code": 1,
  "data": {
    "records": [
      { "id": 1420038345634918402, "name": "张三", "createTime": [2021,7,23,11,20,33] }
    ],
    "total": 3
  }
}

修复后:

json 复制代码
{
  "code": 1,
  "data": {
    "records": [
      { "id": "1420038345634918402", "name": "张三", "createTime": "2021-07-23 11:20:33" }
    ],
    "total": 3
  }
}

两个变化同时发生:

  • id 从数字变为字符串 ------ 精度不再丢失
  • createTime 从数组变为 yyyy-MM-dd HH:mm:ss 字符串 ------ 前端可直接展示

为什么 Long 转字符串后后端仍能正常接收

前端回传 {"id":"1420038345634918402","status":0} 时,Jackson 反序列化会把字符串 "1420038345634918402" 自动转成 LongJackson 默认开启 ALLOW_STRING_TO_NUMBER 之类的宽松转换,所以序列化转字符串不会影响反序列化

完整可运行代码

目录结构

text 复制代码
src/main/java/com/itheima/reggie/
├── ReggieApplication.java
├── common/
│   ├── R.java
│   ├── BaseContext.java
│   ├── GlobalExceptionHandler.java
│   └── JacksonObjectMapper.java      # 本篇核心:Long → String
├── config/
│   ├── MybatisPlusConfig.java
│   └── WebMvcConfig.java             # 本篇核心:注册消息转换器
├── controller/
│   └── EmployeeController.java       # login / logout / save / page / update
├── entity/
│   └── Employee.java
├── filter/
│   └── LoginCheckFilter.java
├── mapper/
│   └── EmployeeMapper.java
└── service/
    ├── EmployeeService.java
    └── impl/
        └── EmployeeServiceImpl.java

EmployeeController.javaupdate 部分)

java 复制代码
package com.itheima.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.itheima.reggie.common.R;
import com.itheima.reggie.entity.Employee;
import com.itheima.reggie.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    /**
     * 根据id修改员工信息
     * 启用/禁用员工账号 与 编辑员工信息 复用此方法
     * @param request
     * @param employee
     * @return
     */
    @PutMapping
    public R<String> update(HttpServletRequest request, @RequestBody Employee employee) {
        log.info(employee.toString());

        long id = Thread.currentThread().getId();
        log.info("线程id为:{}", id);

        employeeService.updateById(employee);

        return R.success("员工信息修改成功");
    }
}

WebMvcConfig.java

java 复制代码
package com.itheima.reggie.config;

import com.itheima.reggie.common.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import java.util.List;

@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

    /**
     * 设置静态资源映射
     * @param registry
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("开始进行静态资源映射...");
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
    }

    /**
     * 扩展mvc框架的消息转换器
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器...");
        //创建消息转换器对象
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        //设置对象转换器,底层使用Jackson将Java对象转为json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        //将上面的消息转换器对象追加到mvc框架的转换器集合中,索引0表示最高优先级
        converters.add(0, messageConverter);
    }
}

JacksonObjectMapper.java

java 复制代码
package com.itheima.reggie.common;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;

import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 */
public class JacksonObjectMapper extends ObjectMapper {

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    public JacksonObjectMapper() {
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

                .addSerializer(BigInteger.class, ToStringSerializer.instance)
                .addSerializer(Long.class, ToStringSerializer.instance)
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}

前端 member.js

javascript 复制代码
// src/main/resources/backend/api/member.js

// 启用/禁用员工账号
function enableOrDisableEmployee(params) {
  return $axios({
    url: '/employee',
    method: 'put',
    data: { ...params }
  })
}

// 编辑员工信息
function editEmployee(params) {
  return $axios({
    url: '/employee',
    method: 'put',
    data: { ...params }
  })
}

前端 statusHandle 方法

javascript 复制代码
// src/main/resources/backend/page/member/list.html
statusHandle(row) {
  this.id = row.id
  this.status = row.status
  this.$confirm('确认调整该账号的状态?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    enableOrDisableEmployee({
      id: row.id,
      status: !row.status ? 1 : 0
    }).then(res => {
      if (String(res.code) === '1') {
        this.$message.success('账号状态更改成功!')
        this.handleQuery()
      } else {
        this.$message.error(res.msg || '操作失败')
      }
    }).catch(err => {
      this.$message.error('请求出错了:' + err)
    })
  })
}

功能测试

验证精度问题已修复

在浏览器控制台执行:

javascript 复制代码
// 从接口响应中复制一个 id
JSON.parse('{"id":"1420038345634918402"}').id
// 输出:"1420038345634918402"   ← 字符串,精度完整

测试用例

场景 操作 预期
禁用账号 admin 登录,点击某员工的「禁用」 提示成功,status0,按钮变「启用」
启用账号 再点「启用」 status1,按钮变「禁用」
被禁账号登录 用被禁员工的账号密码登录 提示「账号已禁用」
admin 登录 用普通员工账号登录 看不到启用禁用按钮
查看响应 JSON 打开分页接口的 Response id 是字符串,createTime 是格式化时间

SQL 验证

text 复制代码
==>  Preparing: UPDATE employee SET status=?, update_time=?, update_user=? WHERE id=?
==> Parameters: 0(Integer), 2021-07-23 11:20:33(LocalDateTime), 1(Long), 1420038345634918402(Long)
<==    Updates: 1          ← 关键:影响行数必须是 1

修复前 Updates: 0,修复后 Updates: 1

知识点延伸:JSON 数值精度的其他场景

场景 风险 应对
主键 ID 雪花 ID 19 位,必然丢失 序列化为字符串(本篇方案)
金额 0.1 + 0.2 !== 0.3 BigDecimal,序列化为字符串
时间戳 13 位毫秒时间戳在安全范围内 一般安全,但仍建议用格式化字符串
前端生成 ID 前端不应生成主键 由后端雪花算法统一生成

一句话记住:超过 16 位的整数,以及涉及金额的小数,跨系统传输都建议用字符串,别让数值解析环节有机会丢精度。

API 速览

API / 注解 / 类 所属 作用
@PutMapping Spring MVC 映射 PUT 请求
@RequestBody Spring MVC JSON 请求体反序列化
IService.updateById(T) MyBatis-Plus 按主键更新,默认只更新非 null 字段
@TableField(updateStrategy = FieldStrategy.IGNORED) MyBatis-Plus 允许将字段显式更新为 null
extendMessageConverters Spring MVC 扩展消息转换器集合
MappingJackson2HttpMessageConverter Spring MVC JSON 消息转换器
ObjectMapper Jackson 对象与 JSON 互转核心类
SimpleModule Jackson 注册自定义(反)序列化器
ToStringSerializer Jackson 序列化为 toString() 结果(字符串)
LocalDateTimeSerializer Jackson jsr310 按指定格式序列化 LocalDateTime
LocalDateTimeDeserializer Jackson jsr310 按指定格式反序列化
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES Jackson 未知字段是否报错
Thread.currentThread().getId() Java 获取当前线程 ID,用于验证 ThreadLocal 作用域
$confirm ElementUI 确认对话框

官方文档

总结

整个问题的根因不在后端 SQL,也不在 MyBatis-Plus,而在前端 JavaScript 解析 JSON 数字时发生了精度丢失。处理方式是在 Jackson 序列化阶段就把 Long 转成字符串,并通过 add(0, ...) 保证自定义转换器优先生效。

排查这类问题时,如果看到「接口 200、SQL 正常、Updates: 0」,就值得回头检查请求参数里的雪花 ID 是否已经被前端改写。

相关推荐
乌暮1 小时前
Java 抽象类与接口详解:半成品图纸 vs 能力合同
java·开发语言·后端·学习
一条泥憨鱼1 小时前
苍穹外卖【day12| 工作台,通过Apache POI导入/导出运营数据】
java·后端·苍穹外卖
心平气和量大福大1 小时前
android-权限
android·java
006_1 小时前
ruoyi-vue-plus项目 minio外网访问配置
java
渡我白衣2 小时前
HttpRequest与HttpResponse的实现
服务器·数据结构·c++·人工智能·tcp/ip·机器学习·caffe
刃神太酷啦2 小时前
前端入门第一课:HTML 基础语法 + 常用标签 + 实战全解
服务器·c语言·前端·javascript·css·c++·html
程序猿在线码字2 小时前
数据结构前置知识(java版)
java
甜到心里的蛋糕2 小时前
如何用企业微信实现让微信收到通知实时通知
服务器·python·微信小程序·fastapi
牢姐与蒯2 小时前
Linux进程间通信(三).基于匿名管道的进程池的实现
linux·运维·服务器·ubuntu