Java项目:Java脚手架项目的 C 端用户服务(十五)

文章目录

  • 前言
  • [一、实现 C 端用户服务](#一、实现 C 端用户服务)
    • [1.1 C 端用户代码拆分](#1.1 C 端用户代码拆分)
    • [1.2 定义 admin 中的 Feign 客户端](#1.2 定义 admin 中的 Feign 客户端)
      • [1.2.1 在 admin-api 在创建 appuser 包并创建domain、feign包](#1.2.1 在 admin-api 在创建 appuser 包并创建domain、feign包)
      • [1.2.2 在 domain 下创建 portal 模块需要的dto、vo](#1.2.2 在 domain 下创建 portal 模块需要的dto、vo)
      • [1.2.3 在 feign 下创建 FeignClient](#1.2.3 在 feign 下创建 FeignClient)
    • [1.3 创建 portal 模块并实现](#1.3 创建 portal 模块并实现)
      • [1.3.1 在 frameworkjava 下创建 portal 模块](#1.3.1 在 frameworkjava 下创建 portal 模块)
      • [1.3.2 创建 portal-api 和portal-service 包](#1.3.2 创建 portal-api 和portal-service 包)
      • [1.3.3 在 portal-service 下创建启动类、包路径和 user 包](#1.3.3 在 portal-service 下创建启动类、包路径和 user 包)
      • [1.3.4 添加 maven 依赖](#1.3.4 添加 maven 依赖)
      • [1.3.5 配置 bootstrap.yml](#1.3.5 配置 bootstrap.yml)
      • [1.3.6 在 user 下创建 controller、service、domain包](#1.3.6 在 user 下创建 controller、service、domain包)
      • [1.3.7 在 domain 下定义 dto、vo](#1.3.7 在 domain 下定义 dto、vo)
      • [1.3.8 在 service 下创建接口及其实现类](#1.3.8 在 service 下创建接口及其实现类)
      • [1.3.9 在controller 下创建控制器](#1.3.9 在controller 下创建控制器)
    • [1.4 在 admin-service 下提供 CRUD 等服务](#1.4 在 admin-service 下提供 CRUD 等服务)
      • [1.4.1 在 user 包的 domain 下添加 C端用户的dto、vo、entity](#1.4.1 在 user 包的 domain 下添加 C端用户的dto、vo、entity)
      • [1.4.2 在 mapper 下创建对应数据表的接口](#1.4.2 在 mapper 下创建对应数据表的接口)
      • [1.4.3 在 service 下创建接口及其实现类](#1.4.3 在 service 下创建接口及其实现类)
      • [1.4.4 在 controller 下实现远程调用的控制器](#1.4.4 在 controller 下实现远程调用的控制器)
  • [二、添加 nacos 配置](#二、添加 nacos 配置)
  • 三、测试
  • END

鸡汤:
● 能准时下班、买到热乎的包子、路上遇到摇尾巴的狗------这些'不值一提的幸运',加起来就是生活偷偷给你的糖。
● 月亮只有一个,但星星有亿万颗。星空之所以动人,是因为每颗星都在自己的位置 quietly shining。

前言

前面封装了 B 端用户的服务,现在来实现 C 端用户的服务。

一、实现 C 端用户服务

1.1 C 端用户代码拆分

● 因为 C 端用户行为多、流量大,并且 C 端用户行为大部分依赖于应用端(移动端、小程序等)操作,因此这部分我们将放在 portal 模块 中去。

● 根据一般C/B端项目可知,管理端后台可查看管理应用端用户信息,因此,应用端用户的管理(CRUD)应放在 admin模块 中。

● 可以得出,portal模块提供应用端需要的C端用户行为接口,具体能力的实现则依 admin模块 提供的服务(例如CRUD),不同模块之间的调用则可通过 Feign 接口完成。

1.2 定义 admin 中的 Feign 客户端

1.2.1 在 admin-api 在创建 appuser 包并创建domain、feign包

1.2.2 在 domain 下创建 portal 模块需要的dto、vo


dto:
AppUserListReqDTO:

java 复制代码
package com.my.adminapi.appuser.domain.dto;

import com.my.commondomain.domain.dto.BasePageReqDTO;
import lombok.Data;

import java.io.Serializable;

@Data
public class AppUserListReqDTO extends BasePageReqDTO implements Serializable {
    /**
     * 用户ID
     */
    private Long userId;

    /**
     * 手机号
     */
    private String phoneNumber;

    /**
     * 昵称
     */
    private String nickName;

    /**
     * 微信openId
     */
    private String openId;
}

UserEditReqDTO:

java 复制代码
package com.my.adminapi.appuser.domain.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;

import java.io.Serializable;

/**
 * C端用户编辑接口 DTO
 */
@Data
public class UserEditReqDTO implements Serializable {

    /**
     * 用户ID
     */
    @NotNull(message = "用户ID不能为空")
    private Long id;

    /**
     * 用户昵称
     */
    @NotNull(message = "用户昵称不能为空")
    private String nickName;

    /**
     * 用户头像
     */
    @NotNull(message = "用户头像不能为空")
    private String avatar;
}

vo:
AppUserVO:

java 复制代码
package com.my.adminapi.appuser.domain.vo;

import lombok.Data;

import java.io.Serializable;

/**
 *  C 端用户 VO
 */
@Data
public class AppUserVO implements Serializable {

    /**
     * 用户id
     */
    private Long id;
    /**
     * 用户昵称
     */
    private String nickName;

    /**
     * 手机号
     */
    private String phoneNumber;

    /**
     * 微信 唯一索引
     */
    private String openId;

    /**
     * 头像
     */
    private String avatar;
}

1.2.3 在 feign 下创建 FeignClient


AppUserFeignClient:

java 复制代码
package com.my.adminapi.appuser.feign;

import com.my.adminapi.appuser.domain.dto.UserEditReqDTO;
import com.my.adminapi.appuser.domain.vo.AppUserVO;
import com.my.commondomain.domain.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@FeignClient(contextId = "appUserFeignClient",value = "admin",path = "/app_user")
public interface AppUserFeignClient {

    /**
     * 根据openId查询用户信息
     * @param openId 用户微信ID
     * @return C端用户VO
     */
    @GetMapping("/open_id_find")
    R<AppUserVO> findByOpenId(@RequestParam String openId);

    /**
     * 根据微信注册用户
     * @param openId 用户微信ID
     * @return C端用户VO
     */
    @GetMapping("/register/openid")
    R<AppUserVO> registerByOpenId(@RequestParam String openId);

    /**
     * 根据手机号注册用户
     * @param phoneNumber 手机号
     * @return C端用户VO
     */
    @GetMapping("/register/phone")
    R<AppUserVO> registerByPhone(@RequestParam String phoneNumber);

    /**
     * 根据phoneNumber查询用户信息
     * @param phoneNumber 用户手机号
     * @return C端用户VO
     */
    @GetMapping("/phone_find")
    R<AppUserVO> findByPhone(@RequestParam String phoneNumber);

    /**
     * 编辑C端用户
     * @param userEditReqDTO C端用户DTO
     * @return void
     */
    @PostMapping("/edit")
    R<Void> edit(@RequestBody @Validated UserEditReqDTO userEditReqDTO);

    /**
     * 根据用户ID获取用户信息
     * @param userId 用户ID
     * @return C端用户VO
     */
    @GetMapping("/id_find")
    R<AppUserVO> findById(@RequestParam Long userId);

    /**
     * 根据用户ID列表获取用户列表信息
     * @param userIds 用户ID列表
     * @return C端用户VO列表
     */
    @PostMapping("/list")
    R<List<AppUserVO>> list(@RequestBody List<Long> userIds);
}

1.3 创建 portal 模块并实现

1.3.1 在 frameworkjava 下创建 portal 模块

1.3.2 创建 portal-api 和portal-service 包

1.3.3 在 portal-service 下创建启动类、包路径和 user 包


PortalServiceApplication:

java 复制代码
package com.my.portalservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;

@SpringBootApplication
@EnableFeignClients(basePackages = "com.my.**.feign")
public class PortalServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(PortalServiceApplication.class, args);
    }
}

1.3.4 添加 maven 依赖

pom.xml:

java 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.my</groupId>
        <artifactId>portal</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <groupId>com.my</groupId>
    <artifactId>portal-service</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!-- SpringCloud Alibaba Nacos Config -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>


        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

        <dependency>
            <groupId>com.my</groupId>
            <artifactId>common-cache</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>com.my</groupId>
            <artifactId>portal-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>com.my</groupId>
            <artifactId>common-core</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>com.my</groupId>
            <artifactId>common-redis</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>com.my</groupId>
            <artifactId>common-security</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>com.my</groupId>
            <artifactId>admin-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>com.my</groupId>
            <artifactId>common-message</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

1.3.5 配置 bootstrap.yml


bootstrap.yml:

java 复制代码
server:
  port: 18083

spring:
  application:
    name: portal
  profiles:
    active: ${RUN_ENV}
  cloud:
    nacos:
      discovery:
        username: nacos
        password: bite@123
        namespace: frameworkjava-${RUN_ENV}
        server-addr: ${NACOS_ADDR}
      config:
        username: nacos
        password: bite@123
        namespace: frameworkjava-${RUN_ENV}
        server-addr: ${NACOS_ADDR}
        file-extension: yaml
        # 共享配置
        shared-configs:
          - data-id: share-common-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
            refresh: true
          - data-id: share-redis-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
            refresh: true
          - data-id: share-mysql-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
            refresh: true
          - data-id: share-caffeine-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
            refresh: true
          - data-id: share-map-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
            refresh: true
          - data-id: share-rabbitmq-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
            refresh: true
          - data-id: share-sms-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
            refresh: true

logging:
  pattern:
    console: '[%thread] %-5level %logger{36} - %msg%n'

1.3.6 在 user 下创建 controller、service、domain包

因为 CRUD 操作都在 admin 模块下,所以没有 mapper 包

1.3.7 在 domain 下定义 dto、vo


dto:
登录基类LoginDTO:

java 复制代码
package com.my.portalservice.user.domain.dto;

public class LoginDTO{
}

CodeLoginDTO:

java 复制代码
package com.my.portalservice.user.domain.dto;


import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 *  手机验证码登录
 */
@Data
@EqualsAndHashCode(callSuper = true)
public class CodeLoginDTO extends LoginDTO{

    /**
     * 登录手机号
     */
    @NotBlank(message = "手机号不能为空")
    private String phoneNumber;

    /**
     * 验证码
     */
    @NotBlank(message = "验证码不能为空")
    private String code;
}

WechatLoginDTO:

java 复制代码
package com.my.portalservice.user.domain.dto;

import jakarta.validation.constraints.NotBlank;
import lombok.Data;

/**
 * 微信登录信息
 */
@Data
public class WechatLoginDTO extends LoginDTO{

    /**
     * 微信openId
     */
    @NotBlank(message = "微信openId不能为空")
    private String openId;
}

UserDTO:

java 复制代码
package com.my.portalservice.user.domain.dto;

import com.my.commoncore.utils.BeanCopyUtil;
import com.my.commonsecurity.domain.dto.LoginUserDTO;
import com.my.portalservice.user.domain.vo.UserVO;
import lombok.Data;
import lombok.EqualsAndHashCode;

@EqualsAndHashCode(callSuper = true)
@Data
public class UserDTO extends LoginUserDTO {

    /**
     * 用户头像
     */
    private String avatar;

    /**
     * 对象转换
     * @return
     */
    public UserVO convertToVO() {
        UserVO userVO = new UserVO();
        BeanCopyUtil.copyProperties(this, userVO);
        return userVO;
    }
}

vo:
UserVO:

java 复制代码
package com.my.portalservice.user.domain.vo;

import com.my.commonsecurity.domain.vo.LoginUserVO;
import lombok.Data;
import lombok.EqualsAndHashCode;


/**
 * C端用户VO
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class UserVO extends LoginUserVO {

    /**
     * 用户头像
     */
    private String avatar;

}

1.3.8 在 service 下创建接口及其实现类


IUserService:

java 复制代码
package com.my.portalservice.user.service;

import com.my.adminapi.appuser.domain.dto.UserEditReqDTO;
import com.my.commonsecurity.domain.dto.TokenDTO;
import com.my.portalservice.user.domain.dto.LoginDTO;
import com.my.portalservice.user.domain.dto.UserDTO;
import com.my.portalservice.user.domain.dto.WechatLoginDTO;

public interface IUserService {

    /**
     * 发送短信验证码
     * @param phone 手机号
     * @return 验证码
     */
    void sendCode(String phone);

    /**
     * 登录逻辑
     * @param loginDTO 用户登录DTO
     * @return TokenDTO 加令牌
     */
    TokenDTO login(LoginDTO loginDTO);

    /**
     * 修改用户信息
     * @param userEditReqDTO C端用户编辑DTO
     * @return void
     */
    void edit(UserEditReqDTO userEditReqDTO);

    /**
     * 获取用户登录信息
     * @return 用户信息VO
     */
    UserDTO getLoginUser();

    /**
     * 退出登录
     * @return void
     */
    void logout();
}

UserServiceImpl:

java 复制代码
package com.my.portalservice.user.service.Impl;

import com.my.adminapi.appuser.domain.dto.UserEditReqDTO;
import com.my.adminapi.appuser.domain.vo.AppUserVO;
import com.my.adminapi.appuser.feign.AppUserFeignClient;
import com.my.commoncore.utils.BeanCopyUtil;
import com.my.commoncore.utils.VerifyUtil;
import com.my.commondomain.domain.R;
import com.my.commondomain.domain.ResultCode;
import com.my.commondomain.exception.ServiceException;
import com.my.commonmessage.service.CaptchaService;
import com.my.commonsecurity.domain.dto.LoginUserDTO;
import com.my.commonsecurity.domain.dto.TokenDTO;
import com.my.commonsecurity.service.TokenService;
import com.my.commonsecurity.utils.JWTUtil;
import com.my.commonsecurity.utils.SecurityUtil;
import com.my.portalservice.user.domain.dto.CodeLoginDTO;
import com.my.portalservice.user.domain.dto.LoginDTO;
import com.my.portalservice.user.domain.dto.UserDTO;
import com.my.portalservice.user.domain.dto.WechatLoginDTO;
import com.my.portalservice.user.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class UserServiceImpl implements IUserService {


    @Autowired
    private CaptchaService captchaService;

    @Autowired
    private AppUserFeignClient appUserFeignClient;

    @Autowired
    private TokenService tokenService;


    /**
     * 发送短信验证码
     * @param phone 手机号
     * @return 验证码
     */
    @Override
    public void sendCode(String phone) {
        if(!VerifyUtil.checkPhone(phone)){
            throw new ServiceException("手机号格式错误", ResultCode.INVALID_PARA.getCode());
        }
        captchaService.sendCode(phone);
    }

    /**
     * 登录逻辑
     * @param loginDTO 用户登录DTO
     * @return TokenDTO 加令牌
     */
    @Override
    public TokenDTO login(LoginDTO loginDTO) {
        // 1. 创建用户生命周期
        LoginUserDTO loginUserDTO = new LoginUserDTO();

        // 2. 区分登录方式进行,登录或注册
        if(loginDTO instanceof WechatLoginDTO wechatLoginDTO) {
            // 3. 处理微信登录的逻辑
            // 将 loginUserDTO 传入
            loginByWechat(wechatLoginDTO,loginUserDTO);
        } else if(loginDTO instanceof CodeLoginDTO codeLoginDTO) {
            // 4. 处理验证码登录的逻辑
            loginByCode(codeLoginDTO,loginUserDTO);
        }
        // 5. 设置用户生命周期
        loginUserDTO.setUserFrom("app");
        // 6. 调用TokenService 设置生命周期
        TokenDTO token = tokenService.createToken(loginUserDTO);
        return token;
    }

    /**
     * 修改用户信息
     * @param userEditReqDTO C端用户编辑DTO
     * @return void
     */
    @Override
    public void edit(UserEditReqDTO userEditReqDTO) {
        R<Void> result = appUserFeignClient.edit(userEditReqDTO);
        if(result == null || result.getCode() != ResultCode.SUCCESS.getCode()){
            throw new ServiceException("修改用户失败");
        }
    }

    /**
     * 获取用户登录信息
     * @return 用户信息VO
     */
    @Override
    public UserDTO getLoginUser() {
        // 1. 获取当前登录的用户
        LoginUserDTO loginUser = tokenService.getLoginUser();
        if(loginUser == null){
            throw new ServiceException("用户令牌有误", ResultCode.INVALID_PARA.getCode());
        }
        // 2. 远程查询用户信息
        R<AppUserVO> userInfo = appUserFeignClient.findById(loginUser.getUserId());
        if(userInfo == null || userInfo.getCode() != ResultCode.SUCCESS.getCode() || userInfo.getData() == null){
            throw new ServiceException("查询用户失败", ResultCode.INVALID_PARA.getCode());
        }
        // 3 对象拼装,返回结果
        UserDTO userDTO = new UserDTO();
        BeanCopyUtil.copyProperties(userInfo.getData(),userDTO);
        BeanCopyUtil.copyProperties(loginUser,userDTO);
        return userDTO;
    }

    /**
     * 退出登录
     * @return void
     */
    @Override
    public void logout() {
        // 1. 获取当前登录的用户
//        LoginUserDTO loginUser = tokenService.getLoginUser();
        // 2. 删除用户生命周期 -- 删除用户缓存记录
//        tokenService.delLoginUser(loginUser.getToken());

        // 1. 解析令牌
        String token = SecurityUtil.getToken();
        if(StringUtils.isBlank(token)) {
            return;
        }
        String userId = JWTUtil.getUserId(token);
        String userName = JWTUtil.getUserName(token);
        log.info("{}退出了系统, 用户ID{}", userName, userId);
        // 2. 删除用户生命周期 -- 删除用户缓存记录
        tokenService.delLoginUser(token);
    }

    /**
     * 验证码登录逻辑
     * @param codeLoginDTO  验证码登录DTO
     * @param loginUserDTO  用户生命周期
     */
    private void loginByCode(CodeLoginDTO codeLoginDTO, LoginUserDTO loginUserDTO) {
        AppUserVO appUserVO = null;
        // 1. 查找是否存在数据
        R<AppUserVO> result = appUserFeignClient.findByPhone(codeLoginDTO.getPhoneNumber());
        // 2. 对结果进行校验
        if(result == null || result.getCode() != ResultCode.SUCCESS.getCode() || result.getData() == null){
            // 3. 用户不存在进行注册
            appUserVO = register(codeLoginDTO);
        } else {
            // 4. 用户存在逻辑
            appUserVO = result.getData();
        }
        // 5. 判断验证码是否正确
        String code = captchaService.getCode(codeLoginDTO.getPhoneNumber());
        if(code == null) {
            throw new ServiceException("验证码无效",ResultCode.INVALID_PARA.getCode());
        }
        if(!code.equals(codeLoginDTO.getCode())) {
            throw new ServiceException("验证码错误",ResultCode.INVALID_PARA.getCode());
        }
        // 6. 删除 验证码
        captchaService.delCode(codeLoginDTO.getPhoneNumber());
        // 7. 构建 用户生命周期
        loginUserDTO.setUserId(appUserVO.getId());
        loginUserDTO.setUserName(appUserVO.getNickName());
    }

    /**
     * 微信登录逻辑
     * @param wechatLoginDTO  微信登录DTO
     * @param loginUserDTO  用户生命周期
     */
    private void loginByWechat(WechatLoginDTO wechatLoginDTO, LoginUserDTO loginUserDTO) {
        AppUserVO appUserVO = null;
        // 1. 查找是否存在数据
        R<AppUserVO> result = appUserFeignClient.findByOpenId(wechatLoginDTO.getOpenId());
        // 2. 对结果进行校验
        if(result == null || result.getCode() != ResultCode.SUCCESS.getCode() || result.getData() == null) {
            // 3. 用户不存在进行注册
            appUserVO = register(wechatLoginDTO);
        } else{
            // 4. 用户存在逻辑
            appUserVO = result.getData();
        }
        // 5. 构建 用户生命周期
        loginUserDTO.setUserName(appUserVO.getNickName());
        loginUserDTO.setUserId(appUserVO.getId());
    }

    /**
     * 根据入参来注册
     * @param loginDTO 用户生命周期信息
     * @return 用户VO
     */
    private AppUserVO register(LoginDTO loginDTO) {
        R<AppUserVO> result = null;
        // 1. 判断注册类型
        if(loginDTO instanceof WechatLoginDTO wechatLoginDTO) {
            // 2. 微信注册的逻辑
            result = appUserFeignClient.registerByOpenId(wechatLoginDTO.getOpenId());
            if(result == null || result.getCode() != ResultCode.SUCCESS.getCode() || result.getData() == null) {
                log.error("用户注册失败! {}", wechatLoginDTO.getOpenId());
                throw new ServiceException("用户注册失败");
            }
        } else if(loginDTO instanceof CodeLoginDTO codeLoginDTO) {
            // 3. 验证码登录逻辑
            result = appUserFeignClient.registerByPhone(codeLoginDTO.getPhoneNumber());
            if(result == null || result.getCode() != ResultCode.SUCCESS.getCode() || result.getData() == null) {
                log.error("用户注册失败! {}", codeLoginDTO.getPhoneNumber());
                throw new ServiceException("用户注册失败");
            }
        }
        return result.getData();
    }
}

1.3.9 在controller 下创建控制器


UserController:

java 复制代码
package com.my.portalservice.user.controller;

import com.my.adminapi.appuser.domain.dto.UserEditReqDTO;
import com.my.commondomain.domain.R;
import com.my.commonsecurity.domain.vo.TokenVO;
import com.my.portalservice.user.domain.dto.CodeLoginDTO;
import com.my.portalservice.user.domain.dto.WechatLoginDTO;
import com.my.portalservice.user.domain.vo.UserVO;
import com.my.portalservice.user.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

/**
 * 门户程序用户入口
 */
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private IUserService userService;

    /**
     * 微信登录
     * @param wechatLoginDTO 微信登录DTO
     * @return token令牌
     */
    @PostMapping("/login/wechat")
    public R<TokenVO> login(@RequestBody @Validated WechatLoginDTO wechatLoginDTO){
        return R.success(userService.login(wechatLoginDTO).convertTokenVO());
    }

    /**
     * 验证码登录
     * @param codeLoginDTO 验证码登录DTO
     * @return token令牌
     */
    @PostMapping("/login/code")
    public R<TokenVO> login(@RequestBody @Validated CodeLoginDTO codeLoginDTO){
        return R.success(userService.login(codeLoginDTO).convertTokenVO());
    }

    /**
     * 发送短信验证码
     * @param phone 手机号
     * @return 验证码
     */
    @GetMapping("/send_code")
    public R<Void> sendCode(@RequestParam String phone) {
        userService.sendCode(phone);
        return R.success();
    }

    /**
     * 修改用户信息
     * @param userEditReqDTO C端用户编辑DTO
     * @return void
     */
    @PostMapping("/edit")
    public R<Void> edit(@RequestBody @Validated UserEditReqDTO userEditReqDTO) {
        userService.edit(userEditReqDTO);
        return R.success();
    }

    /**
     * 获取用户登录信息
     * @return 用户信息VO
     */
    @GetMapping("/login_info/get")
    public R<UserVO> getLoginUser() {
        return R.success(userService.getLoginUser().convertToVO());
    }

    /**
     * 退出登录
     * @return void
     */
    @DeleteMapping("/logout")
    public R<Void> logout() {
        userService.logout();
        return R.success();
    }
}

1.4 在 admin-service 下提供 CRUD 等服务

1.4.1 在 user 包的 domain 下添加 C端用户的dto、vo、entity


dto:
AppUserDTO:

java 复制代码
package com.my.adminservice.user.domain.dto;

import com.my.adminapi.appuser.domain.vo.AppUserVO;
import com.my.commoncore.utils.BeanCopyUtil;
import lombok.Data;

import java.io.Serializable;

@Data
public class AppUserDTO implements Serializable {
    /**
     * 用户id
     */
    private Long id;
    /**
     * 用户昵称
     */
    private String nickName;

    /**
     * 手机号
     */
    private String phoneNumber;

    /**
     * 微信 唯一索引
     */
    private String openId;

    /**
     * 头像
     */
    private String avatar;

    /**
     *  DTO 到 VO 转换
     * @return AppUserVO
     */
    public AppUserVO convertToAppUserVO() {
        AppUserVO appUserVO = new AppUserVO();
        BeanCopyUtil.copyProperties(this, appUserVO);
        return appUserVO;
    }
}

entity:
AppUser:

java 复制代码
package com.my.adminservice.user.domain.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import com.my.commondomain.domain.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 *  C端用户对象 app_user
 */
@Data
@EqualsAndHashCode(callSuper = true)
@TableName(value = "app_user")
public class AppUser extends BaseDO {

    /**
     * 用户昵称
     */
    private String nickName;

    /**
     * 手机号
     */
    private String phoneNumber;

    /**
     * 微信 唯一索引
     */
    private String openId;

    /**
     * 头像
     */
    private String avatar;
}

1.4.2 在 mapper 下创建对应数据表的接口


AppUserMapper:

java 复制代码
package com.my.adminservice.user.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.my.adminapi.appuser.domain.dto.AppUserListReqDTO;
import com.my.adminservice.user.domain.entity.AppUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface AppUserMapper extends BaseMapper<AppUser> {

    AppUser selectByOpenId(@Param("openId") String openId);

    AppUser selectByPhone(@Param("phoneNumber") String phoneNumber);

    Long selectCount(@Param("item") AppUserListReqDTO appUserListReqDTO);

    List<AppUser> selectPage (@Param("item") AppUserListReqDTO appUserListReqDTO);
}

实现的 xml 文件
AppUserMapper.xml:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.my.adminservice.user.mapper.AppUserMapper">


    <select id="selectByOpenId" resultType="com.my.adminservice.user.domain.entity.AppUser">
        select * from app_user where open_id = #{openId}
    </select>

    <select id="selectByPhone" resultType="com.my.adminservice.user.domain.entity.AppUser">
        select * from app_user where phone_number = #{phoneNumber}
    </select>

    <select id="selectCount" resultType="java.lang.Long">
        select count(1) from app_user
        <where>
            <if test="item.userId != null and item.userId != ''">
                and id = #{item.userId}
            </if>
            <if test="item.phoneNumber != null and item.phoneNumber != ''">
                and phone_number = #{item.phoneNumber}
            </if>
            <if test="item.nickName != null and item.nickName != ''">
                and nick_name like concat('%', #{item.nickName}, '%')
            </if>
            <if test="item.openId != null and item.openId != ''">
                and open_id = #{item.openId}
            </if>
        </where>
    </select>

    <select id="selectPage" resultType="com.my.adminservice.user.domain.entity.AppUser">
        select * from app_user
        <where>
            <if test="item.userId != null and item.userId != ''">
                and id = #{item.userId}
            </if>
            <if test="item.phoneNumber != null and item.phoneNumber != ''">
                and phone_number = #{item.phoneNumber}
            </if>
            <if test="item.nickName != null and item.nickName != ''">
                and nick_name like concat('%', #{item.nickName}, '%')
            </if>
            <if test="item.openId != null and item.openId != ''">
                and open_id = #{item.openId}
            </if>
        </where>
        order by id desc
        limit #{item.offset},#{item.pageSize}
    </select>
</mapper>

1.4.3 在 service 下创建接口及其实现类


IAppUserService:

java 复制代码
package com.my.adminservice.user.service;

import com.my.adminapi.appuser.domain.dto.AppUserListReqDTO;
import com.my.adminapi.appuser.domain.dto.UserEditReqDTO;
import com.my.adminservice.user.domain.dto.AppUserDTO;
import com.my.commondomain.domain.dto.BasePageDTO;

import java.util.List;

public interface IAppUserService {

    /**
     * 根据openId查询用户信息
     * @param openId 用户微信ID
     * @return C端用户VO
     */
    AppUserDTO findByOpenId(String openId);

    /**
     * 根据微信注册用户
     * @param openId 用户微信ID
     * @return C端用户VO
     */
    AppUserDTO registerByOpenId(String openId);

    /**
     * 根据手机号注册用户
     * @param phoneNumber 手机号
     * @return C端用户VO
     */
    AppUserDTO registerByPhone(String phoneNumber);

    /**
     * 根据phoneNumber查询用户信息
     * @param phoneNumber 用户手机号
     * @return C端用户VO
     */
    AppUserDTO findByPhone(String phoneNumber);

    /**
     * 编辑C端用户
     * @param userEditReqDTO C端用户DTO
     * @return void
     */
    void edit(UserEditReqDTO userEditReqDTO);

    /**
     * 根据用户ID获取用户信息
     * @param userId 用户ID
     * @return C端用户VO
     */
    AppUserDTO findById(Long userId);

    /**
     * 根据用户ID列表获取用户列表信息
     * @param userIds 用户ID列表
     * @return C端用户VO列表
     */
    List<AppUserDTO> getUserList(List<Long> userIds);

    /**
     * 查询C端用户
     * @param appUserListReqDTO 查询C端用户DTO
     * @return C端用户分页结果
     */
    BasePageDTO<AppUserDTO> getUserList(AppUserListReqDTO appUserListReqDTO);
}

AppUserServiceImpl:

java 复制代码
package com.my.adminservice.user.service.Impl;

import com.my.adminapi.appuser.domain.dto.AppUserListReqDTO;
import com.my.adminapi.appuser.domain.dto.UserEditReqDTO;
import com.my.adminservice.user.config.RabbitConfig;
import com.my.adminservice.user.domain.dto.AppUserDTO;
import com.my.adminservice.user.domain.entity.AppUser;
import com.my.adminservice.user.mapper.AppUserMapper;
import com.my.adminservice.user.service.IAppUserService;
import com.my.commoncore.utils.AESUtil;
import com.my.commoncore.utils.BeanCopyUtil;
import com.my.commoncore.utils.PageUtil;
import com.my.commoncore.utils.VerifyUtil;
import com.my.commondomain.domain.dto.BasePageDTO;
import com.my.commondomain.domain.ResultCode;
import com.my.commondomain.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
@RefreshScope
public class AppUserServiceImpl implements IAppUserService {

    @Autowired
    private AppUserMapper appUserMapper;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Value("${appuser.info.defaultAvatar}")
    private String defaultAvatar;


    /**
     * 根据openId查询用户信息
     * @param openId 用户微信ID
     * @return C端用户VO
     */
    @Override
    public AppUserDTO findByOpenId(String openId) {
        if(StringUtils.isBlank(openId)){
            return null;
        }
        // 1 查询appUser实体类
        AppUser appUser = appUserMapper.selectByOpenId(openId);
        // 2 对查出来的结果进行判断
        if(appUser == null){
            return null;
        }
        AppUserDTO appUserDTO = new AppUserDTO();
        BeanCopyUtil.copyProperties(appUser,appUserDTO);
        // 3 处理手机号
        appUserDTO.setPhoneNumber(AESUtil.decryptHex(appUser.getPhoneNumber()));
        return appUserDTO;
    }

    /**
     * 根据微信注册用户
     * @param openId 用户微信ID
     * @return C端用户VO
     */
    @Override
    public AppUserDTO registerByOpenId(String openId) {
        if(StringUtils.isBlank(openId)){
            throw new ServiceException("微信ID不能为空", ResultCode.INVALID_PARA.getCode());
        }
        // 1. 构建注册信息
        AppUser appUser = new AppUser();
        appUser.setOpenId(openId);
        appUser.setNickName("普通用户" + (int) (Math.random() * 9000) + 1000);
        appUser.setAvatar(defaultAvatar);
        appUserMapper.insert(appUser);
        // 2. 构建返回信息
        AppUserDTO appUserDTO = new AppUserDTO();
        BeanCopyUtil.copyProperties(appUser,appUserDTO);
        return appUserDTO;
    }

    /**
     * 根据手机号注册用户
     * @param phoneNumber 手机号
     * @return C端用户VO
     */
    @Override
    public AppUserDTO registerByPhone(String phoneNumber) {
        if(!VerifyUtil.checkPhone(phoneNumber)){
            throw new ServiceException("手机号不能为空", ResultCode.INVALID_PARA.getCode());
        }
        // 1. 构建注册信息
        AppUser appUser = new AppUser();
        appUser.setPhoneNumber(AESUtil.encryptHex(phoneNumber));
        appUser.setNickName("普通用户" + (int) (Math.random() * 9000) + 1000);
        appUser.setAvatar(defaultAvatar);
        appUserMapper.insert(appUser);
        // 2. 构建返回信息
        AppUserDTO appUserDTO = new AppUserDTO();
        BeanCopyUtil.copyProperties(appUser,appUserDTO);
        appUserDTO.setPhoneNumber(phoneNumber);
        return appUserDTO;
    }

    /**
     * 根据phoneNumber查询用户信息
     * @param phoneNumber 用户手机号
     * @return C端用户VO
     */
    @Override
    public AppUserDTO findByPhone(String phoneNumber) {
        if(!VerifyUtil.checkPhone(phoneNumber)){
            throw new ServiceException("手机号不能为空", ResultCode.INVALID_PARA.getCode());
        }
        // 1. 查找数据库
        AppUser appUser = appUserMapper.selectByPhone(AESUtil.encryptHex(phoneNumber));
        if(appUser == null){
            return null;
        }
        // 2. 构建返回信息
        AppUserDTO appUserDTO = new AppUserDTO();
        BeanCopyUtil.copyProperties(appUser,appUserDTO);
        appUserDTO.setPhoneNumber(phoneNumber);
        return appUserDTO;
    }

    /**
     * 编辑C端用户
     * @param userEditReqDTO C端用户DTO
     * @return void
     */
    @Override
    public void edit(UserEditReqDTO userEditReqDTO) {
        // 1. 查找数据库,看用户是否存在
        AppUser appUser  = appUserMapper.selectById(userEditReqDTO.getId());
        if(appUser == null){
            throw new ServiceException("用户不存在", ResultCode.INVALID_PARA.getCode());
        }
        // 2 查到用户,进行编辑操作
        appUser.setNickName(userEditReqDTO.getNickName());
        appUser.setAvatar(defaultAvatar);
        appUserMapper.updateById(appUser);
        // 3 发送广播消息
        AppUserDTO appUserDTO = new AppUserDTO();
        BeanCopyUtil.copyProperties(appUser,appUserDTO);
        try{
            rabbitTemplate.convertAndSend(RabbitConfig.EXCHANGE_NAME,"",appUserDTO);
        } catch (Exception exception) {
            log.error("编辑用户发送消息失败", exception);
        }
    }

    /**
     * 根据用户ID获取用户信息
     * @param userId 用户ID
     * @return C端用户VO
     */
    @Override
    public AppUserDTO findById(Long userId) {
        if(userId == null){
            return null;
        }
        // 1. 查询信息
        AppUser appUser = appUserMapper.selectById(userId);
        if(appUser == null){
            return null;
        }
        // 2. 构建返回信息
        AppUserDTO appUserDTO = new AppUserDTO();
        BeanCopyUtil.copyProperties(appUser,appUserDTO);
        appUserDTO.setPhoneNumber(AESUtil.decryptHex(appUser.getPhoneNumber()));
        return appUserDTO;
    }

    /**
     * 根据用户ID列表获取用户列表信息
     * @param userIds 用户ID列表
     * @return C端用户VO列表
     */
    @Override
    public List<AppUserDTO> getUserList(List<Long> userIds) {
        // 1 对入参进行判空
        if(CollectionUtils.isEmpty(userIds)){
            return new ArrayList<>();
        }
        // 2 查询appUser列表
        List<AppUser> appUserList = appUserMapper.selectBatchIds(userIds);
        // 3. 对象转换
        return appUserList.stream()
                .map(entity -> {
                    AppUserDTO appUserDTO = new AppUserDTO();
                    BeanCopyUtil.copyProperties(entity,appUserDTO);
                    appUserDTO.setPhoneNumber(AESUtil.decryptHex(entity.getPhoneNumber()));
                    return appUserDTO;
                }).toList();
    }

    /**
     * 查询C端用户
     * @param appUserListReqDTO 查询C端用户DTO
     * @return C端用户分页结果
     */
    @Override
    public BasePageDTO<AppUserDTO> getUserList(AppUserListReqDTO appUserListReqDTO) {
        // 1. 将手机号加密
        appUserListReqDTO.setPhoneNumber(AESUtil.encryptHex(appUserListReqDTO.getPhoneNumber()));
        // 2. 创建返回结果
        BasePageDTO<AppUserDTO> result = new BasePageDTO();

        // 3. 查询总数
        Long totals = appUserMapper.selectCount(appUserListReqDTO);
        if(totals == 0) {
            result.setTotals(0);
            result.setTotalPages(0);
            result.setList(new ArrayList<>());
            return result;
        }
        // 4. 填充总数
        result.setTotals(totals.intValue());
        // 5. 分页查询
        List<AppUser> entityList = appUserMapper.selectPage(appUserListReqDTO);
        // 6. 填充页数
        result.setTotalPages(
                PageUtil.getTotalPages(totals,appUserListReqDTO.getPageSize()));
        // 7. 超页
        if (CollectionUtils.isEmpty(entityList)) {
            result.setList(new ArrayList<>());
            return result;
        }
        // 8. 填充用户结果
        result.setList(
                entityList.stream()
                        .map(entity -> {
                            AppUserDTO dto = new AppUserDTO();
                            BeanCopyUtil.copyProperties(entity,dto);
                            dto.setPhoneNumber(AESUtil.decryptHex(entity.getPhoneNumber()));
                            return dto;
                        }).toList()
        );
        // 9. 返回结果
        return result;
    }
}

1.4.4 在 controller 下实现远程调用的控制器


AppUserController:

java 复制代码
package com.my.adminservice.user.controller;

import com.my.adminapi.appuser.domain.dto.AppUserListReqDTO;
import com.my.adminapi.appuser.domain.dto.UserEditReqDTO;
import com.my.adminapi.appuser.domain.vo.AppUserVO;
import com.my.adminapi.appuser.feign.AppUserFeignClient;
import com.my.adminservice.user.domain.dto.AppUserDTO;
import com.my.adminservice.user.service.IAppUserService;
import com.my.commoncore.utils.BeanCopyUtil;
import com.my.commondomain.domain.dto.BasePageDTO;
import com.my.commondomain.domain.vo.BasePageVO;
import com.my.commondomain.domain.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Objects;

/**
 * C端用户相关接口实现
 */
@RestController
@RequestMapping("/app_user")
public class AppUserController implements AppUserFeignClient {

    @Autowired
    private IAppUserService appUserService;

    /**
     * 根据openId查询用户信息
     * @param openId 用户微信ID
     * @return C端用户VO
     */
    @Override
    public R<AppUserVO> findByOpenId(String openId) {
        AppUserDTO result = appUserService.findByOpenId(openId);
        if(result == null){
            return R.success();
        }
        return R.success(result.convertToAppUserVO());
    }

    /**
     * 根据微信注册用户
     * @param openId 用户微信ID
     * @return C端用户VO
     */
    @Override
    public R<AppUserVO> registerByOpenId(String openId) {
        return R.success(appUserService.registerByOpenId(openId).convertToAppUserVO());
    }

    /**
     * 根据手机号注册用户
     * @param phoneNumber 手机号
     * @return C端用户VO
     */
    @Override
    public R<AppUserVO> registerByPhone(String phoneNumber) {
        return R.success(appUserService.registerByPhone(phoneNumber).convertToAppUserVO());
    }

    /**
     * 根据phoneNumber查询用户信息
     * @param phoneNumber 用户手机号
     * @return C端用户VO
     */
    @Override
    public R<AppUserVO> findByPhone(String phoneNumber) {
        AppUserDTO result = appUserService.findByPhone(phoneNumber);
        if(result == null){
            return R.success();
        }
        return R.success(result.convertToAppUserVO());
    }

    /**
     * 编辑C端用户
     * @param userEditReqDTO C端用户DTO
     * @return void
     */
    @Override
    public R<Void> edit(UserEditReqDTO userEditReqDTO) {
        appUserService.edit(userEditReqDTO);
        return R.success();
    }

    /**
     * 根据用户ID获取用户信息
     * @param userId 用户ID
     * @return C端用户VO
     */
    @Override
    public R<AppUserVO> findById(Long userId) {
        AppUserDTO result = appUserService.findById(userId);
        if(result == null){
            return R.success();
        }
        return R.success(result.convertToAppUserVO());
    }

    /**
     * 根据用户ID列表获取用户列表信息
     * @param userIds 用户ID列表
     * @return C端用户VO列表
     */
    @Override
    public R<List<AppUserVO>> list(List<Long> userIds) {
        List<AppUserDTO> result = appUserService.getUserList(userIds);
        return R.success(
                result.stream()
                        .filter(Objects::nonNull)
                        .map(AppUserDTO::convertToAppUserVO)
                        .toList()
        );
    }

    /**
     * 查询C端用户
     * @param appUserListReqDTO 查询C端用户DTO
     * @return C端用户分页结果
     */
    @PostMapping("/list/search")
    public R<BasePageVO<AppUserVO>> list(@RequestBody AppUserListReqDTO appUserListReqDTO) {
        BasePageDTO<AppUserDTO> pageDTO = appUserService.getUserList(appUserListReqDTO);
        BasePageVO<AppUserVO> result = new BasePageVO<>();
        result.setTotals(pageDTO.getTotals());
        result.setTotalPages(pageDTO.getTotalPages());
        result.setList(
                pageDTO.getList().stream()
                        .filter(Objects::nonNull)
                        .map(dto -> {
                            AppUserVO vo = new AppUserVO();
                            BeanCopyUtil.copyProperties(dto, vo);
                            return vo;
                        }).toList()
        );
        return R.success(result);
    }
}

二、添加 nacos 配置


admin-dev.yaml:

yaml 复制代码
# 用戶
appuser:
  info:
    defaultAvatar: 自己设置的默认头像链接



share-common-dev.yaml:

yaml 复制代码
# feign 配置
feign:
  okhttp:
    enabled: true
  httpclient:
    enabled: false
  client:
    config:
      default:
        connectTimeout: 10000
        readTimeout: 10000
  compression:
    request:
      enabled: true
    response:
      enabled: true

三、测试

发送验证码:


登录并注册:


获取用户登录信息:


END

因为字太多了,下一张补充短信服务

相关推荐
一个处女座的程序猿O(∩_∩)O2 小时前
Python异常处理完全指南:KeyError、TypeError、ValueError深度解析
开发语言·python
暮色妖娆丶2 小时前
Spring 源码分析 事务管理的实现原理(下)
数据库·spring boot·spring
暮色妖娆丶2 小时前
Spring 源码分析 事务管理的实现原理(上)
数据库·spring boot·spring
好学且牛逼的马2 小时前
从“Oak”到“虚拟线程”:JDK 1.0到25演进全记录与核心知识点详解a
java·开发语言·python
追随者永远是胜利者2 小时前
(LeetCode-Hot100)62. 不同路径
java·算法·leetcode·职场和发展·go
好学且牛逼的马2 小时前
从“XML汪洋”到“智能原生”:Spring Framework 1.x 到 7.x 演进全记录与核心知识点详解(超详细版)
java
追随者永远是胜利者2 小时前
(LeetCode-Hot100)56. 合并区间
java·算法·leetcode·职场和发展·go
Hello_Embed2 小时前
Modbus 传感器开发:STM32F030 libmodbus 移植
笔记·stm32·学习·freertos·modbus
追随者永远是胜利者2 小时前
(LeetCode-Hot100)55. 跳跃游戏
java·算法·leetcode·游戏·go