news 2026/8/7 9:26:24

小白勇闯《苍穹外卖》Day3

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
小白勇闯《苍穹外卖》Day3

碎碎念:开学马上大四了,但是前几年一直没有好好学习专业课,只是临近考试的时候突击复习一下,导致到现在还是小白T_T 7月放暑假在家陆陆续续看了黑马的Java基础和web相关课程,但是并没有完全看完,也没有深入理解...眼看时间已经来不及了,简历上还是一片空白,决定硬着头皮学项目了,干中学吧,加油!!!

叠个甲:只是发表一下学习记录,内容不一定正确,希望大家包容一下,欢迎大佬们多多指正!!!

菜品管理

前两天已经完成了环境搭建,员工管理,分类管理,今天开始菜品管理了

公共字段自动填充

技术点:枚举、注解、AOP、反射

——————————————这里恶补了一下知识点————————————————

枚举:适合做信息分类和标志。这里用来标识操作类型,insert/update

注解:自定义注解

public @i8nterface 注解名称{ public 属性类型 属性名() default 默认值 ; }

特殊属性value:在使用时如果只有一个value, value名称可省略不写

原理:本质上是一个接口继承了annotation类,里面定义的属性其实上是一个一个的抽象方法

使用举例:

@注解名(aaa="李四",bbb=true,ccc={"java","python"}) public void test(){ }

元注解:注解注解的注解

@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface Test{ }

@Retention约束存活范围

@Target约束标记范围

解析注释:使用解析注解的方法

AOP:面向切面编程。通用公共功能从业务代码中剥离。

@Aspect+@Component定义切面类;@Pointcut定义切点表达式;编写通知@Before前置,@AfterReturning返回,@AfterThrowing 异常通知,@After 最终通知,@Around 环绕通知

反射:加载类,并且以编程的方式解剖类中各种成分(成员变量,方法,构造器等)

①加载类,获取类的字节码文件:Calss对象(三种方法)

②获取类的构造器:Constructor对象

③获取类的成员变量:Field对象

set()复制,get()取值

④获取类的成员方法:Method对象

invoke()触发执行

反射作用:可以得到一个类的全部成分然后操作;可以破坏封装性;可以绕过泛型约束

需求

(1).在新增数据时, 将createTime、updateTime 设置为当前时间, createUser、updateUser设置为当前登录用户ID。

(2).在更新数据时, 将updateTime 设置为当前时间, updateUser设置为当前登录用户ID。

实现思路

代码开发

创建注解和切面的包和类

定义注解类

/** * 自动填充 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AutoFill { /** * 数据库操作类型 * @return */ OperationType value(); }

已定义好的枚举类

/** * 数据库操作类型 */ public enum OperationType { /** * 更新操作 */ UPDATE, /** * 插入操作 */ INSERT }

已定义好的AutoFillConstant常量类

/** * 公共字段自动填充相关常量 */ public class AutoFillConstant { /** * 实体类中的方法名称 */ public static final String SET_CREATE_TIME = "setCreateTime"; public static final String SET_UPDATE_TIME = "setUpdateTime"; public static final String SET_CREATE_USER = "setCreateUser"; public static final String SET_UPDATE_USER = "setUpdateUser"; }

切面类

(@Slf4j 是 lombok 提供注解,编译期自动生成 Logger 日志实例,简化日志对象创建,直接使用log.info/log.error输出日志。)

/** * 自定义切面类,统一为公共字段赋值 */ @Aspect @Component @Slf4j public class AutoFillAspect { /** * 切入点 */ @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")//不仅要在mapper里还要满足注解 public void autoFillPointCut() {} /** * 通知 自动填充公共字段 * @param joinPoint */ @Before("autoFillPointCut()") public void autoFill(JoinPoint joinPoint) { log.info("公共字段自动填充..."); //获得方法签名对象 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); //获得方法上的注解 AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); //获得注解中的操作类型 OperationType operationType = autoFill.value(); //获取当前目标方法的参数 Object[] args = joinPoint.getArgs(); if (args == null || args.length == 0) { return; } //实体对象 Object entity = args[0]; //准备赋值的数据 LocalDateTime time = LocalDateTime.now(); Long empId = BaseContext.getCurrentId(); if (operationType == OperationType.INSERT) { //当前执行的是insert操作,为4个字段赋值 try { //获得set方法对象----Method Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class); Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class); Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); //通过反射调用目标对象的方法 setCreateTime.invoke(entity, time); setUpdateTime.invoke(entity, time); setCreateUser.invoke(entity, empId); setUpdateUser.invoke(entity, empId); } catch (Exception ex) { log.error("公共字段自动填充失败:{}", ex.getMessage()); } }else if(operationType == OperationType.UPDATE){ //当前执行的是update操作,为2个字段赋值 try { //获得set方法对象----Method Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); //通过反射调用目标对象的方法 setUpdateTime.invoke(entity, time); setUpdateUser.invoke(entity, empId); } catch (Exception ex) { log.error("公共字段自动填充失败:{}", ex.getMessage()); } } } }

这里我AOP掌握不熟练(下面是我问ai给我的解释)

public void autoFillPointCut () {}这个空方法 = 给切点表达式起别名,方便复用。

JoinPoint连接点,joinPoint相当于一个袋子装被拦截的 mapper 方法全部信息

这段代码的通俗理解

先从袋子里拿出来[被拦截的那个方法];再拿到写在 mapper 方法上面的注解;把注解里面的内容取出来,得到INSERT或者UPDATE;从袋子里,拿到调用 mapper 时传进去的参数;判断是否为空;若不为空,取第一个参数,这个就是我们要存数据库的实体;准备要填的数据,时间和id;若为INSERT,用反射在运行的时候,找到这 4 个 set 方法,拿到方法对象,.invoke(对象,值)调用这个 set 方法,把值设置进去;若为UPDATE,找到这 2 个 set 方法,拿到方法对象,.invoke(对象,值)调用这个 set 方法,把值设置进去;反射很容易报错,捕获异常,打印错误

在Mapper接口的方法上加入 AutoFill 注解

@AutoFill(OperationType.INSERT) @Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" + " VALUES" + " (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})") void insert(Category category); @AutoFill(OperationType.UPDATE) void update(Category category); @AutoFill(OperationType.UPDATE) void update(Employee employee);

注释掉原代码

测试

(测试的时候突然登录不上前端页面了,发现是没打开nginx,大家不要学我犯这么笨蛋的错误)

发现打了断点但是修改员工数据时断点不停,前端显示修改成功

排查了以下三点:

Employee 实体的@Data注解存在,字段updateTime、updateUser定义正确。

看控制台没有输出公共字段自动填充失败,定位反射哪里报错。

数据库里这条员工记录,update_timeupdate_user发生变化。

拦截的是 Mapper 接口,MyBatis 运行时会生成代理对象来执行方法,IDEA 调试器对动态代理生成的类,断点经常无法触发。后面我也尝试修改断点的 Suspend 模式,从 All 改成 Thread,断点换到set赋值那里,但是代理场景下依旧不稳定。

所以我直接手动输出日志了

功能测试成功!

所以实际开发中日志打印也是很重要的调试手段,不能完全依赖 IDEA 断点。


原来是因为没有debug运行。。。


新增菜品

需求分析和设计

接口设计:

根据类型查询分类(已完成)

文件上传

新增菜品

数据库设计

代码开发

文件上传

—————————————————这里去补了一下知识点————————————————

需要先注册阿里云--->充值--->开通OSS--->创建bucket--->获取并配置AccessKey

application-dev.yml

alioss: endpoint: 你自己的 access-key-id: 你自己的 access-key-secret: 你自己的 bucket-name: 你自己的

application.yml

alioss: endpoint: ${sky.alioss.endpoint} access-key-id: ${sky.alioss.access-key-id} access-key-secret: ${sky.alioss.access-key-secret} bucket-name: ${sky.alioss.bucket-name}

新建类OssConfiguration

@Configuration @Slf4j public class OssConfiguration { /** * 通过spring管理对象 * @param aliOssProperties * @return */ @Bean @ConditionalOnMissingBean public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) { log.info("开始创建阿里云OSS工具类..."); return new AliOssUtil(aliOssProperties.getEndpoint(), aliOssProperties.getAccessKeyId(), aliOssProperties.getAccessKeySecret(), aliOssProperties.getBucketName()); } }

新建类CommonController

@RestController @RequestMapping("/admin/common") @Slf4j @Api(tags = "通用接口") public class CommonController { @Autowired private AliOssUtil aliOssUtil; /** * 文件上传 * @param file * @return */ @PostMapping("/upload") @ApiOperation("文件上传") public Result<String> upload(MultipartFile file){ log.info(file.getName()); //原始文件名 String originalFilename = file.getOriginalFilename(); String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); //将文件上传的阿里云 String fileName = UUID.randomUUID().toString() + extension; try { String filePath = aliOssUtil.upload(file.getBytes(), fileName); return Result.success(filePath); } catch (IOException e) { log.error("文件上传失败:{}", e.getMessage()); } return Result.error(MessageConstant.UPLOAD_FAILED); } }

测试一下,可以正常上传

新增菜品

DishController

@RestController @RequestMapping("/admin/dish") @Api(tags = "菜品相关接口") @Slf4j public class DishController { @Autowired private DishService dishService; /** * 新增菜品 * @param dishDTO * @return */ @PostMapping @ApiOperation("新增菜品") public Result<String> save(@RequestBody DishDTO dishDTO){ log.info("新增菜品:{}", dishDTO); dishService.saveWithFlavor(dishDTO); return Result.success(); } }

DishService

public interface DishService { /** * 新增菜品 * @param dishDTO */ void saveWithFlavor(DishDTO dishDTO); }

DishServiceImpl

@Service public class DishServiceImpl implements DishService { @Autowired private DishMapper dishMapper; @Autowired private DishFlavorMapper dishFlavorMapper; /** * 新增菜品 * @param dishDTO */ @Transactional public void saveWithFlavor(DishDTO dishDTO) { Dish dish = new Dish(); BeanUtils.copyProperties(dishDTO, dish); //向菜品表dish插入1条数据 dishMapper.insert(dish); //获取菜品的主键值 Long dishId = dish.getId(); List<DishFlavor> flavors = dishDTO.getFlavors(); if(flavors != null && flavors.size() > 0){ //向口味表dish_flavor插入n条 flavors.forEach(dishFlavor -> { dishFlavor.setDishId(dishId); }); //批量插入 dishFlavorMapper.insertBatch(flavors); } } }

DishMapper

/** * 插入菜品数据 * @param dish */ @AutoFill(OperationType.INSERT) void insert(Dish dish);

DishMapper.xml

<!-- useGeneratedKeys:true 表示获取主键值 keyProperty="id" 表示将主键值赋给id属性--> <insert id="insert" useGeneratedKeys="true" keyProperty="id"> insert into dish (status, name, category_id, price, image, description, create_time, update_time, create_user,update_user) values (#{status}, #{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime},#{createUser}, #{updateUser}) </insert>

DishFlavorMapper

@Mapper public interface DishFlavorMapper { /** * 批量插入口味数据 * @param flavors */ void insertBatch(List<DishFlavor> flavors); }

DishFlavorMapper.xml

<insert id="insertBatch"> insert into dish_flavor(dish_id, name, value) values <foreach collection="flavors" item="dishFlavor" separator=","> (#{dishFlavor.dishId},#{dishFlavor.name},#{dishFlavor.value}) </foreach> </insert>

菜品分页查询

需求分析和设计

代码开发

DishController

/** * 菜品分页查询 * @param dishPageQueryDTO * @return */ @GetMapping("/page") @ApiOperation("菜品分页查询") public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){ log.info("菜品分页查询:{}", dishPageQueryDTO); PageResult pageResult = dishService.pageQuery(dishPageQueryDTO); return Result.success(pageResult); }

DishService

/** * 菜品分页查询 * @param dishPageQueryDTO * @return */ PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);

DishServiceImpl

/** * 菜品分页查询 * @param dishPageQueryDTO * @return */ public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) { PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize()); Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO); return new PageResult(page.getTotal(), page.getResult()); }

DishMapper

DTO:接收前端传给后端的数据(入参) VO:后端返回给前端的数据(出参)

/** * 菜品分页查询 * @param dishPageQueryDTO * @return */ Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);

DishMapper.xml

涉及多表查询,左连接

<select id="pageQuery" resultType="com.sky.vo.DishVO"> select d.*,c.name categoryName from dish d left join category c on d.category_id = c.id <where> <if test="name != null"> and d.name like concat('%',#{name},'%') </if> <if test="categoryId != null"> and d.category_id = #{categoryId} </if> <if test="status != null"> and d.status = #{status} </if> </where> order by d.create_time desc </select>

删除菜品

需求分析和设计

接口

数据库

代码开发

注意这里有批量删除,一旦选中的菜品绑定套餐,全部的都无法删除

DishController

/** * 品批量删除 * @param ids * @return */ @DeleteMapping @ApiOperation("菜品批量删除") public Result delete(@RequestParam List<Long> ids){ log.info("菜品批量删除:{}", ids); dishService.deleteBatch(ids); return Result.success(); }

DishService

void deleteBatch(List<Long> ids);

DishServiceImpl

@Transactional public void deleteBatch(List<Long> ids) { ids.forEach(id->{ Dish dish = dishMapper.getById(id); //判断当前要删除的菜品状态是否为起售中 if(dish.getStatus() == StatusConstant.ENABLE){ //如果是起售中,抛出业务异常 throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE); } }); //判断当前要删除的菜品是否被套餐关联了 List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids); if(setmealIds != null && setmealIds.size() > 0){ //如果关联了,抛出业务异常 throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL); } //删除菜品表中的数据 ids.forEach(id -> { dishMapper.deleteById(id); //删除口味表中的数据 dishFlavorMapper.deleteByDishId(id); });

DishMapper

/** * 根据主键查询菜品数据 * @param id * @return */ @Select("select * from dish where id = #{id}") Dish getById(Long id); /** * 根据主键删除菜品数 * @param id */ @Delete("delete from dish where id = #{id}") void deleteById(Long id);

SetmealDishMapper

@Mapper public interface SetmealDishMapper { /**根据菜品id查询关联的套餐id * @param ids * @return */ List<Long> getSetmealIdsByDishIds(List<Long> ids); }

SetmealDishMapper.xml

<select id="getSetmealIdsByDishIds" resultType="java.lang.Long"> select setmeal_id from setmeal_dish where dish_id in <foreach collection="ids" separator="," open="(" close=")" item="dishId"> #{dishId} </foreach> </select>

DishFlavorMapper

/** * 根据菜品id删除口味数据 * @param dishId */ @Delete("delete from dish_flavor where dish_id = #{dishId}") void deleteByDishId(Long dishId);

修改菜品

需求分析和设计

根据id查询菜品

修改菜品

代码开发

根据id查询菜品

DishController

/** * 根据id查询菜品和关联的口味数据 * @param id * @return */ @GetMapping("/{id}") @ApiOperation("根据id查询菜品和关联的口味数据") public Result<DishVO> getById(@PathVariable Long id){ return Result.success(dishService.getByIdWithFlavor(id)); }

DishService

/** * 根据id查询菜品和关联的口味数据 * @param id * @return */ DishVO getByIdWithFlavor(Long id);

DishServiceImpl

/** * 根据id查询菜品和关联的口味数据 * * @param id * @return */ public DishVO getByIdWithFlavor(Long id) { //查询菜品表 Dish dish = dishMapper.getById(id); //查询关联的口味 List<DishFlavor> dishFlavorList = dishFlavorMapper.getByDishId(id); //封装成VO DishVO dishVO = new DishVO(); BeanUtils.copyProperties(dish, dishVO); dishVO.setFlavors(dishFlavorList); return dishVO; }

DishFlavorMapper

/** * 根据菜品id查询对应的口味 * @param dishId * @return */ @Select("select * from dish_flavor where dish_id = #{dishId}") List<DishFlavor> getByDishId(Long dishId);

修改菜品

DishController

/** * 修改菜品 * @param dishDTO * @return */ @PutMapping @ApiOperation("修改菜品") public Result update(@RequestBody DishDTO dishDTO){ log.info("修改菜品:{}", dishDTO); dishService.updateWithFlavor(dishDTO); return Result.success(); }

DishService

/** * 根据id修改菜品和关联的口味 * @param dishDTO */ void updateWithFlavor(DishDTO dishDTO);

DishServiceImpl

/** * 根据id修改菜品和关联的口味 * * @param dishDTO */ @Transactional public void updateWithFlavor(DishDTO dishDTO) { Dish dish = new Dish(); BeanUtils.copyProperties(dishDTO, dish); //修改菜品表dish,执行update操作 dishMapper.update(dish); //删除当前菜品关联的口味数据,操作dish_flavor,执行delete操作 dishFlavorMapper.deleteByDishId(dishDTO.getId()); //插入最新的口味数据,操作dish_flavor,执行insert操作 List<DishFlavor> flavors = dishDTO.getFlavors(); if (flavors != null && flavors.size() > 0) { flavors.forEach(dishFlavor -> { dishFlavor.setDishId(dishDTO.getId()); }); dishFlavorMapper.insertBatch(flavors); } }

DishMapper

/** * 根据主键修改菜品信息 * @param dish */ @AutoFill(OperationType.UPDATE) void update(Dish dish);

DishMapper.xml

<update id="update"> update dish <set> <if test="name != null"> name = #{name}, </if> <if test="categoryId != null"> category_id = #{categoryId}, </if> <if test="price != null"> price = #{price}, </if> <if test="image != null"> image = #{image}, </if> <if test="description != null"> description = #{description}, </if> <if test="status != null"> status = #{status}, </if> <if test="updateTime != null"> update_time = #{updateTime}, </if> <if test="updateUser != null"> update_user = #{updateUser}, </if> </set> where id = #{id} </update>

菜品起售停售

需求分析和设计

代码开发

DishController

/** * 菜品起售停售 * @param status * @param id * @return */ @PostMapping("/status/{status}") @ApiOperation("菜品起售停售") public Result<String> startOrStop(@PathVariable Integer status, Long id){ dishService.startOrStop(status,id); return Result.success(); }

DishService

/** * 菜品起售停售 * @param status * @param id */ void startOrStop(Integer status, Long id);

DishServiceImpl

/** * 菜品起售停售 * * @param status * @param id */ @Transactional public void startOrStop(Integer status, Long id) { Dish dish = Dish.builder() .id(id) .status(status) .build(); dishMapper.update(dish); if (status == StatusConstant.DISABLE) { // 如果是停售操作,还需要将包含当前菜品的套餐也停售 List<Long> dishIds = new ArrayList<>(); dishIds.add(id); // select setmeal_id from setmeal_dish where dish_id in (?,?,?) List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(dishIds); if (setmealIds != null && setmealIds.size() > 0) { for (Long setmealId : setmealIds) { Setmeal setmeal = Setmeal.builder() .id(setmealId) .status(StatusConstant.DISABLE) .build(); setmealMapper.update(setmeal); } } } }

SetmealMapper

/** * 根据id修改套餐 * * @param setmeal */ @AutoFill(OperationType.UPDATE) void update(Setmeal setmeal);

SetmealMapper.xml

<update id="update" parameterType="Setmeal"> update setmeal <set> <if test="name != null"> name = #{name}, </if> <if test="categoryId != null"> category_id = #{categoryId}, </if> <if test="price != null"> price = #{price}, </if> <if test="status != null"> status = #{status}, </if> <if test="description != null"> description = #{description}, </if> <if test="image != null"> image = #{image}, </if> <if test="updateTime != null"> update_time = #{updateTime}, </if> <if test="updateUser != null"> update_user = #{updateUser} </if> </set> where id = #{id} </update>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/7 9:25:35

AI测试相关知识了解--生命周期与python基础

AI系统生命周期数据收集与准备&#xff1a;采集原始数据&#xff0c;数据清洗&#xff0c;数据标注&#xff0c;数据增强&#xff0c;特征工程模型训练&#xff1a;选择算法&#xff0c;配置超参数&#xff0c;模型训练&#xff0c;交叉验证模型评估&#xff1a;离线评估&#…

作者头像 李华
网站建设 2026/8/7 9:24:26

PSO-MPPT算法在光伏系统遮阴条件下的优化应用

1. 项目背景与核心价值 光伏发电系统在实际运行中常常面临局部遮阴的挑战&#xff0c;这种不均匀光照条件会导致功率-电压(P-V)特性曲线出现多峰现象。传统MPPT算法如扰动观察法(P&O)和电导增量法(INC)容易陷入局部极值点&#xff0c;无法追踪全局最大功率点。我们团队开发…

作者头像 李华
网站建设 2026/8/7 9:24:02

基于FPGA的AM调制系统实现:从数字信号处理到硬件设计实战

1. 从零到一&#xff1a;为什么选择FPGA来实现AM调制&#xff1f;如果你对无线电通信或者数字信号处理有点兴趣&#xff0c;大概率听说过“调制”这个词。简单说&#xff0c;调制就是把我们要传递的信息&#xff08;比如一段音乐、一段语音&#xff09;&#xff0c;“加载”到一…

作者头像 李华
网站建设 2026/8/7 9:22:13

Unity Resources.Load深度解析:避坑指南与高性能实战策略

1. 项目概述&#xff1a;为什么我们还在讨论Resources.Load&#xff1f; 在Unity开发圈子里&#xff0c; Resources.Load 大概是每个开发者最早接触、也最常被“告诫”要慎用的API之一。从Unity 4.x时代一路走来&#xff0c;到如今Addressables和AssetBundle大行其道&#xf…

作者头像 李华
网站建设 2026/8/7 9:20:11

AI编程助手分层设计:从工具到智能同事的Agent进化实战

1. 项目概述&#xff1a;从“工具”到“同事”的Agent进化 最近在深度使用Cursor时&#xff0c;我一直在思考一个问题&#xff1a;为什么我们总感觉AI编程助手像个“聪明的工具”&#xff0c;而不是一个能并肩作战的“新同事”&#xff1f;工具的特点是“你指哪&#xff0c;它打…

作者头像 李华
网站建设 2026/8/7 9:12:02

AI编程助手ClaudeCode:从安装配置到高效工作流全解析

1. 项目概述&#xff1a;ClaudeCode是什么&#xff0c;以及为什么你需要它 如果你是一名开发者&#xff0c;最近肯定在各种技术社区和社群里频繁听到“Claudecode”这个词。它不是什么新的编程语言&#xff0c;也不是某个神秘的框架&#xff0c;而是一个正在迅速崛起的AI编程助…

作者头像 李华