【云岚到家】-day02-2-客户管理-认证授权

news2024/12/24 2:39:36

【云岚到家】-day02-2-客户管理-认证授权

  • 第二章 客户管理
  • 1 认证模块
    • 1.1 需求分析
    • 1.2 小程序认证
      • 1.2.1 测试小程序认证
        • 1.2.1.1 参考官方流程
        • 1.2.1.2 申请小程序账号
        • 1.2.1.3 创建jzo2o-customer
        • 1.2.1.4 部署前端
        • 1.2.1.5 编译运行
        • 1.2.1.6 真机调试
  • 2 阅读代码
    • 2.1 小程序认证流程
      • 2.1.1 阅读代码
        • 2.1.1.1 customer提供的小程序认证接口
        • 2.1.1.2 网关对token统一校验
    • 2.2 手机验证码认证
      • 2.2.1 测试手机验证码认证
        • 2.2.1.1 部署前端
        • 2.2.1.2 认证测试
      • 2.2.2 阅读代码
        • 2.2.2.1 手机验证码认证流程
        • 2.2.2.2 找到具体的接口
        • 2.2.2.3 具体校验验证码逻辑
        • 2.2.2.4 自动注册
  • 3 实战功能
    • 3.1 机构端账号密码认证测试
    • 3.2 完成机构注册功能开发
      • 3.2.1 设计须知
      • 3.2.2 mapper
      • 3.2.3 service
      • 3.2.4 controller
      • 3.2.5 测试
    • 3.3 完成忘记密码功能开发
      • 3.3.1 mapper
      • 3.3.2 service
      • 3.3.3 controller
      • 3.3.4 测试


第二章 客户管理

1 认证模块

1.1 需求分析

1)基础概念

一般情况有用户交互的项目都有认证授权功能,首先我们要搞清楚两个概念:认证和授权。

认证: 就是校验用户的身份是否合法,常见的认证方式有账号密码登录、手机验证码登录等。

授权:则是该用户登录系统成功后当用户去点击菜单或操作数据时系统判断该用户是否有权限,有权限则允许继续操作,没有权限则拒绝访问。

2) 小程序认证

了解了认证和授权的概念,本节对小程序认证功能进行需求分析。

本项目包括四个端:用户端(小程序)、服务端(app)、机构端(PC)、运营管理端(PC).

分别对应四类用户角色:家政需求方即c端用户,家政服务人员、家政服务公司(机构)、平台运营人员。

用户端通过小程序使用平台,初次使用小程序会进行认证,如下图:

在这里插入图片描述

点击“快速登录”弹出服务条款窗口:

在这里插入图片描述

点击“同意”进行认证,系统与微信进行交互获取用户在小程序中的唯一标识openid。

注意:点击“同意”弹出获取位置信息,此信息表示要进行定位,定位功能稍后介绍,这里选择允许或拒绝都可以。

在这里插入图片描述

初次认证通过会自动注册用户信息到本平台。

下边是小程序的认证流程:

在这里插入图片描述

3) 手机验证码认证

服务人员通过app登录采用手机验证码认证方式,输入手机号、发送验证码,验证码校验通过则认证通过,初次认证通过将自动注册服务人员信息。

如下图:

在这里插入图片描述

手机验证码认证流程如下:

在这里插入图片描述

4) 账号密码认证

机构端认证方式是账号密码认证方式,通过pc浏览器进入登录界面输入账号和密码登录系统,如下图:

在这里插入图片描述

机构端提供单独的注册页面,输入手机号,接收验证码进行注册,如下图:

在这里插入图片描述

管理端的认证方式也是账号密码方式,界面如下图:

在这里插入图片描述

管理端的账号由管理员在后台录入,不提供注册页面。

1.2 小程序认证

1.2.1 测试小程序认证

1.2.1.1 参考官方流程

下边测试用户端小程序的认证流程,我们先参考微信官方提供的小程序登录流程先大概知道小程序认证流程需要几部分,如下图:

(文档地址:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html)

从图上可以看出小程序认证流程需要三部分:

小程序:即前端程序

开发者服务器:后端微服务程序。

微信接口服务:即微信服务器。

在这里插入图片描述

1.前端调用wx.login()获取登录凭证code

2.前端请求后端进行认证,发送code

3.后端请求微信获取openid,发送appid、app密钥、code参数,微信返回openid

4.后端生成认证成功凭证返回给前端。

5.前端存储用户认证成功凭证

1.2.1.2 申请小程序账号

开发小程序首先要申请小程序账号,参考官方文档:https://developers.weixin.qq.com/miniprogram/dev/framework/quickstart/getstart.html#%E7%94%B3%E8%AF%B7%E8%B4%A6%E5%8F%B7

点击注册小程序(https://mp.weixin.qq.com/wxopen/waregister?action=step1)填写信息完成注册,获得appid和appsecret

在这里插入图片描述

1.2.1.3 创建jzo2o-customer

小程序账号申请成功,下边部署配置后端程序。

客户管理工程jzo2o-customer提供了小程序认证接口支持。

jzo2o-customer通过jzo2o-publics请求微信获取openid。(jzo2o-publics在第一章环境配置中已完成创建)

下边创建jzo2o-customer工程,创建过程参考jzo2o-foundations工程。

创建gitee的jzo2o-customer仓库

在这里插入图片描述

在idea中新建

在这里插入图片描述

jzo2o-customer工程的初始代码 在:jzo2o-customer-01-0.zip解压后导入,检查jdk,maven版本仓库等

在这里插入图片描述

转为maven后提交到gitee的master分支后,创建一个新的分支dev_01并且推送

在这里插入图片描述

工程创建完成修改bootstrap-dev.yml配置文件:

在这里插入图片描述

接下来进入nacos修改jzo2o-publics.yaml中小程序的appid和密钥,如下图:

在这里插入图片描述

微服务工程配置好下边需要创建jzo2o-customer工程的数据库,从课程资料下的sql脚本目录找到jzo2o-customer-init.sql,执行脚本创建jzo2o-customer数据库。

在这里插入图片描述

启动jzo2o-customer工程,如下图:

在这里插入图片描述

小程序认证需要启动的微服务包括:网关jzo2o-gateway、客户管理jzo2o-customer、公共服务jzo2o-publics,将其它服务也正常启动。

在这里插入图片描述

启动这三个微服务成功,下边开始部署前端。

1.2.1.4 部署前端

本部分内容可参考微信开发文档:https://developers.weixin.qq.com/ebook?action=get_post_info&docid=000e8842960070ab0086d162c5b80a

首先下载微信小程序开发工具,也可从课程资料中“小程序开发工具”获取安装程序。

用户端是基于微信小程序开发的,首先需要下载并安装微信开发者工具。

配置小程序开发环境

首先拷贝到课程资料下源码目录中的project-xzb-xcx-uniapp-java.zip到你的代码目录并解压到project-xzb-xcx-uniapp-java目录下。

打开小程序软件

在这里插入图片描述

进入添加小程序项目界面,如下图:

目录:选择小程序前端工程的 project-xzb-xcx-uniapp-java\unpackage\dist\dev\mp-weixin目录。

AppID:填写申请小程序号获取的AppID。

选择不使用云服务。

在这里插入图片描述

点击确定进入下边的界面:

在这里插入图片描述

修改project-xzb-xcx-uniapp-java\unpackage\dist\dev\mp-weixin\utils\env.js 配置文件,指定后端网关的地址

在这里插入图片描述

设置代理

在这里插入图片描述

1.2.1.5 编译运行

小程序认证需要启动的微服务包括:网关jzo2o-gateway、客户管理jzo2o-customer、公共服务jzo2o-publics,保证这三个服务全部启动。

注意:保证jzo2o-publics服务配置高德地图key(参考:高德地图web服务配置文档)、微信的appid和app密钥。配置完成将jzo2o-publics服务重新启动。

小程序开发环境配置完成进行编译运行。

首先清除缓存,然后编译运行:

在这里插入图片描述

点击“快速登录”按照前边讲的小程序认证流程进行操作,请求认证接口进行认证,进入调试器–>Network观察请求记录,如下图:

在这里插入图片描述

认证接口的地址是:/customer/open/login/common/user

此接口最终从微信拿到用户的openid(微信给用户分配的唯一标识),并将openid存储到数据库,认证通过生成token令牌返回给前端。

认证通过进入下边的界面:

在这里插入图片描述

1.2.1.6 真机调试

在开发环境还可以通过手机打开小程序进行测试,下边介绍具体的配置方法,注意此部分内容作为了解,正常开发使用上边介绍的通过微信开发工具进行测试,方便跟踪接口交互数据。

首先保证手机和PC在同一个网络,因为在手机上打开小程序需要访问PC上的微服务接口。

可以让手机和PC连接同一个热点,连接热点后查询无线网卡的IP,如下图:

在这里插入图片描述

192.168.137.1是我的测试环境,同时要保证手机的IP地址和192.168.137.1在同一个网段。

接下来配置网关地址

在这里插入图片描述

设置代理

在这里插入图片描述

在这里插入图片描述

然后点击预览

在这里插入图片描述

生成二维码后打开手机微信扫码将在手机上预览。

2 阅读代码

下边通过阅读代码理解小程序认证的流程。

2.1 小程序认证流程

我们去开发整个小程序认证流程还先参考官方流程,如下图:

(文档地址:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html)

整个过程包括三部分:

小程序:即前端程序

开发者服务器:后端微服务程序。

微信接口服务:即微信服务器。

具体的流程如下:

1.前端调用wx.login()获取登录凭证code

2.前端请求后端进行认证,发送code

3.后端请求微信获取openid

4.后端生成认证成功凭证返回给前端。

在这里插入图片描述

根据官方的认证流程我们定义本项目小程序认证的交互流程:

customer工程提供认证接口,publics工程作为一个公共服务提供与微信通信的接口。

前端与cutomer交互不与publics交互。

在这里插入图片描述

2.1.1 阅读代码

下边根据认证流程阅读代码,我们以断点调试的方式跟踪接口交互过程。

2.1.1.1 customer提供的小程序认证接口

uniapp前端请求

前端点击快速登录,授权获取手机号,请求jzo2o-customer的普通用户登录接口,普通用户登录接口如下

在这里插入图片描述

customer请求publics申请获取openid

publics服务获取openid接口如下:publics服务中的com.jzo2o.publics.controller.inner.InnerWechatController实现api模块的feign远程接口WechatApi

WechatApi:

@FeignClient(
    contextId = "jzo2o-publics",
    value = "jzo2o-publics",
    path = "/publics/inner/wechat"
)
public interface WechatApi {
    @GetMapping({"/getOpenId"})
    OpenIdResDTO getOpenId(@RequestParam("code") String code);

    @GetMapping({"/getPhone"})
    PhoneResDTO getPhone(@RequestParam("code") String code);
}

InnerWechatController:

@RestController
@RequestMapping("/inner/wechat")
@Api(tags = "内部接口 - 微信服务相关接口")
public class InnerWechatController implements WechatApi {

    @Resource
    private WechatService wechatService;

    @Override
    @GetMapping("/getOpenId")
    @ApiOperation("获取openId")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "code", value = "登录凭证", required = true, dataTypeClass = String.class)
    })
    public OpenIdResDTO getOpenId(@RequestParam("code") String code) {
        String openId = wechatService.getOpenid(code);
        return new OpenIdResDTO(openId);
    }

    @Override
    @GetMapping("/getPhone")
    @ApiOperation("获取手机号")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "code", value = "手机号凭证", required = true, dataTypeClass = String.class)
    })
    public PhoneResDTO getPhone(@RequestParam("code") String code) {
        String phone = wechatService.getPhone(code);
        return new PhoneResDTO(phone);
    }
}

publics请求weixin 获取openid

customer收到openid查询数据库获取用户信息并生成token.

customer调用 oginService.loginForCommonUser(loginForCustomerReqDTO);

@Override
public LoginResDTO loginForCommonUser(LoginForCustomerReqDTO loginForCustomerReqDTO) {
    // code换openId
    OpenIdResDTO openIdResDTO = wechatApi.getOpenId(loginForCustomerReqDTO.getCode());
    if(ObjectUtil.isEmpty(openIdResDTO) || ObjectUtil.isEmpty(openIdResDTO.getOpenId())){
        // openid申请失败
        throw new CommonException(ErrorInfo.Code.LOGIN_TIMEOUT, ErrorInfo.Msg.REQUEST_FAILD);
    }
    CommonUser commonUser = commonUserService.findByOpenId(openIdResDTO.getOpenId());
    //如果未从数据库查到,需要新增数据
    if (ObjectUtil.isEmpty(commonUser)) {
        commonUser = BeanUtil.toBean(loginForCustomerReqDTO, CommonUser.class);
        long snowflakeNextId = IdUtil.getSnowflakeNextId();
        commonUser.setId(snowflakeNextId);
        commonUser.setOpenId(openIdResDTO.getOpenId());
        commonUser.setNickname("普通用户"+ RandomUtil.randomInt(10000,99999));
        commonUserService.save(commonUser);
    }else if(CommonStatusConstants.USER_STATUS_FREEZE == commonUser.getStatus()) {
        // 被冻结
        throw new CommonException(ErrorInfo.Code.ACCOUNT_FREEZED, commonUser.getAccountLockReason());
    }

    //构建token
    String token = jwtTool.createToken(commonUser.getId(), commonUser.getNickname(), commonUser.getAvatar(), UserType.C_USER);
    return new LoginResDTO(token);
}

openid是微信用户在家政o2o平台的唯一标识,首先根据openid查询jzo2o-customer的common_user表,是否存在用户,如果不存在则自动注册用户信息,用户信息存储到jzo2o-customer数据库的common_user表中

common_user表的结构如下:

CREATE TABLE `common_user` (
  `id` bigint NOT NULL COMMENT '用户id',
  `status` int NOT NULL DEFAULT '0' COMMENT '状态,0:正常,1:冻结',
  `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '昵称',
  `phone` varchar(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '电话',
  `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '头像',
  `open_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
  `account_lock_reason` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '账号冻结原因',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `is_deleted` int NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC

认证通过生成用户token返回给前端。

在这里插入图片描述

token令牌的格式我们使用的是JWT格式,JWT是一种常用的令牌格式,它可以防篡改,关于JWT不明白的同学可以通过视频自学(https://www.bilibili.com/video/BV1j8411N7Bm?p=110&vd_source=81d4489ba9312103debc8ee843169f23)

在JWT令牌中存储了当前登录用户的信息(json),包括如下属性:

用户id: id,对应common_user表的主键。
用户名称:String name,对应common_user表的nickname字段。
用户头像:String avatar,对应common_user表的avatar字段。
用户类型:Integer userType,c端用户的用户类型代码为1,具体定义在com.jzo2o.common.constants.UserType中。

2.1.1.2 网关对token统一校验

在网关对token进行解析校验,token不合法直接返回失败信息,token合法解析出用户信息放在http的head中继续请求微服务。

在这里插入图片描述

在微服务中解析http头信息中的用户信息,写入ThreadLocal方便应用程序使用。在com.jzo2o.mvc.interceptor.UserContextInteceptor中

@Slf4j
public class UserContextInteceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.尝试获取头信息中的用户信息
        String userInfo = request.getHeader(HeaderConstants.USER_INFO);
        // 2.判断是否为空
        if (userInfo == null) {
            return true;
        }
        try {
            // 3.base64解码用户信息
            String decodeUserInfo = Base64Utils.decodeStr(userInfo);
            CurrentUserInfo currentUserInfo = JsonUtils.toBean(decodeUserInfo, CurrentUserInfo.class);

            // 4.转为用户id并保存
            UserContext.set(currentUserInfo);
            return true;
        } catch (NumberFormatException e) {
            log.error("用户身份信息格式不正确,{}, 原因:{}", userInfo, e.getMessage());
            return true;
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 清理用户信息
        UserContext.clear();
    }
}

2.2 手机验证码认证

2.2.1 测试手机验证码认证

服务人员使用APP登录平台使用的是手机验证码认证方式,整个认证流程也需要部署前端、后端。

客户管理工程jzo2o-customer与公共服务jzo2o-publics提供手机验证码的接口,这两个服务在小程序认证时已经部署这里不再部署,我们只需要部署前端工程即可。

2.2.1.1 部署前端

服务端的前端工程需要使用 HBuilder 3.8.7 X 软件编译运行,从课程资料下的软件工具目录获取安装包HBuilderX.3.8.7.20230703.zip,也可以自行下载(https://www.dcloud.io/hbuilderx.html)。

启动HBuilderX

在这里插入图片描述

下边从课程资料拷贝project-xzb-app-uniapp-java.zip到代码目录并解压,cmd进入project-xzb-app-uniapp-java目录运行

npm install || yarn  或 cnpm install || yarn  

安装依赖包,如下图:

在这里插入图片描述

下边用HBuilderX打开project-xzb-app-uniapp-java目录

配置网关地址

在这里插入图片描述

配置完成,使用HBuilderX运行到浏览器

在这里插入图片描述

运行成功进入登录页面:

下边进入调试模式

在这里插入图片描述

选择布局方式:打开Network调试窗口:

在这里插入图片描述

2.2.1.2 认证测试

下边测试手机验证码认证流程。

首先输入手机号,服务人员的信息存储在jzo2o-customer数据库的serve_provider表中,从表中找一个手机号录入

点击发送验证码,此时前端请求后端发送验证码,在开发环境我们从控制台获取验证码,稍后后带大家分析发送验证码的程序。

在这里插入图片描述

注意此时因为请求后端发送验证码我们观察在浏览器的Network窗口有一条记录,如下图,该请求必须响应状态为200方可正常发送验证。

在这里插入图片描述

从控制台获取刚才发送的验证码

在这里插入图片描述

在这里插入图片描述

点击登录进行认证,认证过程会先校验验证码是否正确,如果验证码正确再根据手机号查询serve_provider表是否存在相应记录且用户未被冻结,全部成功则认证通过。

认证通过进入首页。

在这里插入图片描述

2.2.2 阅读代码

2.2.2.1 手机验证码认证流程

customer工程提供认证接口,publics工程作为一个公共服务提供与发送验证码接口。

在这里插入图片描述

2.2.2.2 找到具体的接口

前端请求publics服务发送验证码接口:publics/sms-code/send

代码如下:

在这里插入图片描述

具体发送验证码逻辑:

    @Override
    public void smsCodeSend(SmsCodeSendReqDTO smsCodeSendReqDTO) {
        if(StringUtils.isEmpty(smsCodeSendReqDTO.getPhone()) || StringUtils.isEmpty(smsCodeSendReqDTO.getBussinessType())) {
            log.debug("不能发送短信验证码,phone:{},bussinessType:{}", smsCodeSendReqDTO.getPhone(), smsCodeSendReqDTO.getBussinessType());
            return;
        }
        String redisKey = String.format(CommonRedisConstants.RedisKey.VERIFY_CODE, smsCodeSendReqDTO.getPhone(), smsCodeSendReqDTO.getBussinessType());
        // 取6位随机数
//        String verifyCode = (int)(Math.random() * 1000000) + "";
        String verifyCode = "123456";//为方便测试固定为123456
        log.info("向手机号{}发送验证码{}",smsCodeSendReqDTO.getPhone(),verifyCode);
        //todo调用短信平台接口向指定手机发验证码...
        // 短信验证码有效期5分钟
        redisTemplate.opsForValue().set(redisKey, verifyCode, 300, TimeUnit.SECONDS);
    }

前端请求customer服务的认证接口:/customer/open/login/worker

代码如下:

在这里插入图片描述

机构和和服务人员认证接口是同一个,根据类型判断是机构还是服务人员。

@PostMapping("/worker")
@ApiOperation("服务人员/机构人员登录接口")
public LoginResDTO loginForWorker(@RequestBody LoginForWorkReqDTO loginForWorkReqDTO) {

    //服务人员登录
    if(UserType.INSTITUTION == loginForWorkReqDTO.getUserType()){
        return loginService.loginForPassword(loginForWorkReqDTO);
    }else{
        //机构人员登录
        return loginService.loginForVerify(loginForWorkReqDTO);
    }
}

customer服务请求publics服务校验验证码 loginService.loginForVerify(loginForWorkReqDTO)

@Override
public LoginResDTO loginForVerify(LoginForWorkReqDTO loginForWorkReqDTO) {

    // 数据校验
    if(StringUtils.isEmpty(loginForWorkReqDTO.getVeriryCode())){
        throw new BadRequestException("验证码错误,请重新获取");
    }
    //远程调用publics服务校验验证码是否正确
    boolean verifyResult = smsCodeApi.verify(loginForWorkReqDTO.getPhone(), SmsBussinessTypeEnum.SERVE_STAFF_LOGIN, loginForWorkReqDTO.getVeriryCode()).getIsSuccess();
    if(!verifyResult) {
        throw new BadRequestException("验证码错误,请重新获取");
    }
    // 登录校验
    // 根据手机号和用户类型获取服务人员或机构信息
    ServeProvider serveProvider = serveProviderService.findByPhoneAndType(loginForWorkReqDTO.getPhone(), loginForWorkReqDTO.getUserType());
    // 账号禁用校验
    if(serveProvider != null && CommonStatusConstants.USER_STATUS_FREEZE == serveProvider.getStatus()) {
        throw new CommonException(ErrorInfo.Code.ACCOUNT_FREEZED, serveProvider.getAccountLockReason());
    }
    // 自动注册
    if(serveProvider == null) {
        serveProvider = serveProviderService.add(loginForWorkReqDTO.getPhone(), UserType.WORKER, null);
    }

    // 生成登录token
    String token = jwtTool.createToken(serveProvider.getId(), serveProvider.getName(), serveProvider.getAvatar(), loginForWorkReqDTO.getUserType());
    return new LoginResDTO(token);
}

smsCodeApi.verify(loginForWorkReqDTO.getPhone(), SmsBussinessTypeEnum.SERVE_STAFF_LOGIN, loginForWorkReqDTO.getVeriryCode()).getIsSuccess()是个远程feign接口

@FeignClient(
    contextId = "jzo2o-publics",
    value = "jzo2o-publics",
    path = "/publics/inner/sms-code"
)
public interface SmsCodeApi {
    @GetMapping({"/verify"})
    BooleanResDTO verify(@RequestParam("phone") String phone, @RequestParam("bussinessType") SmsBussinessTypeEnum bussinessType, @RequestParam("verifyCode") String verifyCode);
}

publics服务实现该远程feign接口,提供校验验证码接口。

@RestController
@RequestMapping("/inner/sms-code")
@Api(tags = "内部接口 - 验证码相关接口")
public class InnerSmsCodeController implements SmsCodeApi {
    @Resource
    private ISmsCodeService smsCodeService;

    @Override
    @GetMapping("/verify")
    @ApiOperation("校验短信验证码")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "phone", value = "验证手机号", required = true, dataTypeClass = String.class),
            @ApiImplicitParam(name = "bussinessType", value = "业务类型", required = true, dataTypeClass = SmsBussinessTypeEnum.class),
            @ApiImplicitParam(name = "verifyCode", value = "验证码", required = true, dataTypeClass = String.class)
    })
    public BooleanResDTO verify(@RequestParam("phone") String phone,
                                @RequestParam("bussinessType") SmsBussinessTypeEnum bussinessType,
                                @RequestParam("verifyCode") String verifyCode) {
        return new BooleanResDTO(smsCodeService.verify(phone, bussinessType, verifyCode));
    }
}
2.2.2.3 具体校验验证码逻辑

具体的验证码校验逻辑是先查询redis中的正确的验证码,再和用户输入的进行对比,如果不一致则说明输入错误,输入正确删除验证码。如下代码 smsCodeService.verify(phone, bussinessType, verifyCode)

@Override
public boolean verify(String phone, SmsBussinessTypeEnum bussinessType, String verifyCode) {
    // 1.验证前准备
    String redisKey = String.format(CommonRedisConstants.RedisKey.VERIFY_CODE, phone, bussinessType.getType());
    String verifyCodeInRedis = redisTemplate.opsForValue().get(redisKey);

    // 2.短验验证,验证通过后删除code,code只能使用一次
    boolean verifyResult = StringUtils.isNotEmpty(verifyCode) && verifyCode.equals(verifyCodeInRedis);
    if(verifyResult) {
        redisTemplate.delete(redisKey);
    }
    return verifyResult;
}

在使用redisTemplate时需要在工程中引入下边的依赖:

<dependency>
    <groupId>com.jzo2o</groupId>
    <artifactId>jzo2o-redis</artifactId>
</dependency>

在jzo2o-redis中定义了redisTemplate的定义,如下图:

在com.jzo2o.redis.config.RedisConfiguration中

@Configuration
@Slf4j
@EnableConfigurationProperties(RedisProperties.class)
@Import({CacheHelper.class, LockHelper.class})
public class RedisConfiguration {
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    static{
        JavaTimeModule timeModule = new JavaTimeModule();
        timeModule.addDeserializer(LocalDate.class,
                new LocalDateDeserializer(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_DATE_FORMAT)));
        timeModule.addDeserializer(LocalDateTime.class,
                new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_DATE_TIME_FORMAT)));
        timeModule.addSerializer(LocalDate.class,
                new LocalDateSerializer(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_DATE_FORMAT)));
        timeModule.addSerializer(LocalDateTime.class,
                new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_DATE_TIME_FORMAT)));
        OBJECT_MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        OBJECT_MAPPER.registerModule(timeModule);
    }


    @Bean("redisTemplate")
    @Primary
    public RedisTemplate<String, Object> restTemplate(RedisConnectionFactory redisConnnectionFactory) {

        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        log.info("redisTemplate hashCode : {}", redisTemplate.hashCode());
        redisTemplate.setConnectionFactory(redisConnnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setHashKeySerializer(new GenericToStringSerializer(String.class));
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer(OBJECT_MAPPER));
        return redisTemplate;
    }

    @Bean
    public HashCacheClearAspect hashCacheClearAspect(CacheHelper cacheHelper) {
        return new HashCacheClearAspect(cacheHelper);
    }
}

先添加序列化器,再添加redisTemplate并添加对应的string序列化器,在使用时注入上图中定义的redisTemplate即可。 @Resource

在测试验证码发送时可以打开redis进行跟踪,下图显示了存入redis中的验证码,注意观察key和value:

    @Override
    public void smsCodeSend(SmsCodeSendReqDTO smsCodeSendReqDTO) {
        if(StringUtils.isEmpty(smsCodeSendReqDTO.getPhone()) || StringUtils.isEmpty(smsCodeSendReqDTO.getBussinessType())) {
            log.debug("不能发送短信验证码,phone:{},bussinessType:{}", smsCodeSendReqDTO.getPhone(), smsCodeSendReqDTO.getBussinessType());
            return;
        }
        String redisKey = String.format(CommonRedisConstants.RedisKey.VERIFY_CODE, smsCodeSendReqDTO.getPhone(), smsCodeSendReqDTO.getBussinessType());
        // 取6位随机数
//        String verifyCode = (int)(Math.random() * 1000000) + "";
        String verifyCode = "123456";//为方便测试固定为123456
        log.info("向手机号{}发送验证码{}",smsCodeSendReqDTO.getPhone(),verifyCode);
        //todo调用短信平台接口向指定手机发验证码...
        // 短信验证码有效期5分钟
        redisTemplate.opsForValue().set(redisKey, verifyCode, 300, TimeUnit.SECONDS);
    }

在这里插入图片描述

2.2.2.4 自动注册

校验验证码完成customer服务根据手机号查询数据库,如果用户冻结则认证失败,如果用户不存在则自动注册。

@Override
public LoginResDTO loginForVerify(LoginForWorkReqDTO loginForWorkReqDTO) {

    // 数据校验
    if(StringUtils.isEmpty(loginForWorkReqDTO.getVeriryCode())){
        throw new BadRequestException("验证码错误,请重新获取");
    }
    //远程调用publics服务校验验证码是否正确
    boolean verifyResult = smsCodeApi.verify(loginForWorkReqDTO.getPhone(), SmsBussinessTypeEnum.SERVE_STAFF_LOGIN, loginForWorkReqDTO.getVeriryCode()).getIsSuccess();
    if(!verifyResult) {
        throw new BadRequestException("验证码错误,请重新获取");
    }
    // 登录校验
    // 根据手机号和用户类型获取服务人员或机构信息
    ServeProvider serveProvider = serveProviderService.findByPhoneAndType(loginForWorkReqDTO.getPhone(), loginForWorkReqDTO.getUserType());
    // 账号禁用校验
    if(serveProvider != null && CommonStatusConstants.USER_STATUS_FREEZE == serveProvider.getStatus()) {
        throw new CommonException(ErrorInfo.Code.ACCOUNT_FREEZED, serveProvider.getAccountLockReason());
    }
    // 自动注册
    if(serveProvider == null) {
        serveProvider = serveProviderService.add(loginForWorkReqDTO.getPhone(), UserType.WORKER, null);
    }

    // 生成登录token
    String token = jwtTool.createToken(serveProvider.getId(), serveProvider.getName(), serveProvider.getAvatar(), loginForWorkReqDTO.getUserType());
    return new LoginResDTO(token);
}

服务人员和机构都存储到serve_provider表,结果如下:

create table `jzo2o-customer`.serve_provider
(
    id                  bigint                             not null comment '主键'
        constraint `PRIMARY`
        primary key,
    code                varchar(255)                       null comment '编号',
    type                int                                not null comment '类型,2:服务人员,3:服务机构',
    name                varchar(255)                       null comment '姓名',
    phone               varchar(255)                       not null comment '电话',
    avatar              varchar(255)                       null comment '头像',
    status              int                                not null comment '状态,0:正常,1:冻结',
    settings_status     int      default 0                 null comment '首次设置状态,0:未完成设置,1:已完成设置',
    password            varchar(255)                       null comment '机构登录密码',
    account_lock_reason varchar(255)                       null comment '账号冻结原因',
    score               double                             null comment '综合评分',
    good_level_rate     varchar(50)                        null comment '好评率',
    create_time         datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time         datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    is_deleted          int      default 0                 not null comment '是否已删除,0:未删除,1:已删除',
    constraint serve_provider_phone_type_uindex
        unique (phone, type)
)
    comment '服务人员/机构表' charset = utf8mb4;

最后生成token返回给前端。

3 实战功能

3.1 机构端账号密码认证测试

机构和管理端的认证方式都是账号密码认证方式,本作业限定为机构端账号密码认证,具体要求如下:

部署机构端前端并将认证流程测试通过

从课程资料的源码目录拷贝project-xzb-PC-vue3-java.zip到自己的代码目录,并解压到project-xzb-PC-vue3-java目录。

修改根目录的vite.config.ts文件中网关地址配置

修改后端地址

在这里插入图片描述

安装依赖包(如果已经安装依赖包则不用安装):

cmd进入project-xzb-PC-vue3-java目录运行 :

npm install || yarn  或 cnpm install || yarn

安装依赖包完成运行:npm run dev 运行前端工程,如下图:

在这里插入图片描述

前端默认的账号:15896123123,密码为:888itcast.CN764%…

机构信息存储在jzo2o-customer数据库的serve_provider表中,可从serve_provider表获取账号。

机构端账号密码认证接口请求customer服务的接口:

代码如下:

@PostMapping("/worker")
@ApiOperation("服务人员/机构人员登录接口")
public LoginResDTO loginForWorker(@RequestBody LoginForWorkReqDTO loginForWorkReqDTO) {

    //机构人员登录
    if(UserType.INSTITUTION == loginForWorkReqDTO.getUserType()){
        return loginService.loginForPassword(loginForWorkReqDTO);
    }else{
        //服务人员登录
        return loginService.loginForVerify(loginForWorkReqDTO);
    }
}

登录成功:

在这里插入图片描述

3.2 完成机构注册功能开发

界面原型:

进入登录页面,点击“去注册”进入注册页面

在这里插入图片描述

在这里插入图片描述

接口定义如下:

接口地址:POST/customer/open/serve-provider/institution/register

在这里插入图片描述

在这里插入图片描述

3.2.1 设计须知

参考服务端自动注册的代码实现。

在这里插入图片描述

在这里插入图片描述

注意:机构端注册和服务端注册完成要向serve_provider表写入数据,具体查阅上图的方法。

密码加密方式:使用BCrypt方式,BCrypt是一种密码哈希函数,通常用于存储用户密码的安全性。它是基于 Blowfish 密码算法的一种单向哈希函数

在这里插入图片描述

测试方法:

public static void main(String[] args) {
    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    /**
     $2a$10$1sp7I0OdYH3Azs/2lK8YYeuiaGZzOGshGT9j.IYArZftsGNsXqlma
     $2a$10$m983E2nmJ7ITlesbXzjbzO/M7HL2wP8EgpgX.pPACDm1wG38Lt.na
     $2a$10$rZvathrW98vVPenLhOnl0OMpUtRTdBkWJ45IkIsTebITS9AFgKqGK
     $2a$10$2gaMKWCRoKdc42E0jsq7b.munjzOSPOM4yr3GG9M6194E7dOH5LyS
     $2a$10$I/n93PIKpKL8m4O3AuT5kuZncZhfqV51bfx5sJrplnYoM7FimdboC
     */
    for (int i = 0; i < 5; i++) {
        //对密码进行哈希
        String encode = passwordEncoder.encode("11111");
        System.out.println(encode);
    }
    //校验哈希串和密码是否匹配
    boolean matches = passwordEncoder.matches("11111", "$2a$10$m983E2nmJ7ITlesbXzjbzO/M7HL2wP8EgpgX.pPACDm1wG38Lt.na");
    System.out.println(matches);
}

根据上边的测试代码可知,BCrypt的使用方法如下:

用户输入密码,通过passwordEncoder.encode(“输入的密码”)得到哈希串,将哈希串存储到数据库。

用户登录校验密码,从数据库取出哈希串,连同用户输入的密码,调用下边的方法:

passwordEncoder.matches(“用户输入的密码”, “从数据库查询的密码哈希串”);

3.2.2 mapper

单表查询,用mybatisplus即可

3.2.3 service

在com.jzo2o.customer.service.IServeProviderService中

接口:

ServeProvider registerInstitution(InstitutionRegisterReqDTO institutionRegisterReqDTO);

实现:

@Override
public ServeProvider registerInstitution(InstitutionRegisterReqDTO institutionRegisterReqDTO) {
    //1.校验手机验证码是否正确
    //1.1.数据校验
    if(StringUtils.isEmpty(institutionRegisterReqDTO.getVerifyCode())){
        throw new BadRequestException("验证码错误,请重新获取");
    }
    //1.2.远程调用publics服务校验验证码是否正确
    boolean verifyResult = smsCodeApi.verify(institutionRegisterReqDTO.getPhone(), SmsBussinessTypeEnum.INSTITION_REGISTER, institutionRegisterReqDTO.getVerifyCode()).getIsSuccess();
    if(!verifyResult) {
        throw new BadRequestException("验证码错误,请重新获取");
    }
    //2.检查手机号是否被注册过
    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    String encode = passwordEncoder.encode(institutionRegisterReqDTO.getPassword());
    ServeProvider serveProvider = add(institutionRegisterReqDTO.getPhone(), UserType.INSTITUTION, encode);
    return serveProvider;
}

3.2.4 controller

创建com.jzo2o.customer.controller.open.InstitutionRegisterController

@RestController("institutionRegisterController")
@RequestMapping("/open/serve-provider/institution")
@Api(tags = "白名单接口 - 机构人员注册相关接口")
public class InstitutionRegisterController {
    @Resource
    private IServeProviderService iServeProviderService;
    @PostMapping("/register")
    @ApiOperation("机构人员注册")
    public void register(@RequestBody InstitutionRegisterReqDTO institutionRegisterReqDTO) {
        iServeProviderService.registerInstitution(institutionRegisterReqDTO);
    }
}

3.2.5 测试

手机号随便输入

在这里插入图片描述

在这里插入图片描述

成功返回,查看数据库

在这里插入图片描述

输入密码也是成功登录。

3.3 完成忘记密码功能开发

界面原型:

进入登录页面,点击“忘记密码”进入找回密码页面

在这里插入图片描述

在这里插入图片描述

接口定义如下:

接口名称:机构登录密码重置接口

接口路径:POST/customer/agency/serve-provider/institution/resetPassword

在这里插入图片描述

在这里插入图片描述

设计须知:

首先校验验证码是否正确。

校验手机号是否存在数据库。

通过校验最后修改密码,密码的加密方式参考机构注册接口。

3.3.1 mapper

单表查询,用mybatisplus即可

3.3.2 service

在com.jzo2o.customer.service.IServeProviderService中

接口:

ServeProvider resetPassword(InstitutionResetPasswordReqDTO institutionResetPasswordReqDTO);

实现:

@Override
public ServeProvider resetPassword(InstitutionResetPasswordReqDTO institutionResetPasswordReqDTO) {
    //0.校验手机号是否存在
    ServeProvider existServeProvider = lambdaQuery().eq(ServeProvider::getPhone, institutionResetPasswordReqDTO.getPhone())
            .one();
    if (existServeProvider == null) {
        throw new BadRequestException("该账号未注册");
    }
    //1.校验手机验证码是否正确
    //1.1.数据校验
    if(StringUtils.isEmpty(institutionResetPasswordReqDTO.getVerifyCode())){
        throw new BadRequestException("验证码错误,请重新获取");
    }
    //1.2.远程调用publics服务校验验证码是否正确
    boolean verifyResult = smsCodeApi.verify(institutionResetPasswordReqDTO.getPhone(), SmsBussinessTypeEnum.INSTITUTION_RESET_PASSWORD, institutionResetPasswordReqDTO.getVerifyCode()).getIsSuccess();
    if(!verifyResult) {
        throw new BadRequestException("验证码错误,请重新获取");
    }
    //2.修改密码
    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    String encode = passwordEncoder.encode(institutionResetPasswordReqDTO.getPassword());
    boolean update = lambdaUpdate().eq(ServeProvider::getPhone, institutionResetPasswordReqDTO.getPhone())
            .set(ServeProvider::getPassword, encode)
            .update();
    if(!update){
        throw new BadRequestException("重置密码失败");
    }
    existServeProvider.setPassword(encode);
    return existServeProvider;
}

3.3.3 controller

在com.jzo2o.customer.controller.agency.ServeProviderController中

@PostMapping("/institution/resetPassword")
@ApiOperation("机构人员重置密码")
public void resetPassword(@RequestBody InstitutionResetPasswordReqDTO institutionResetPasswordReqDTO) {
    serveProviderService.resetPassword(institutionResetPasswordReqDTO);
}

3.3.4 测试

手机号随便输入未注册的

在这里插入图片描述

修改刚刚的手机号密码为87654321

在这里插入图片描述

登录成功

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1807885.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

虚拟机ping不通主机,但是主机可以ping通虚拟机

我在Windows10系统安装了虚拟机&#xff0c;设置的主机与虚拟机的连接方式是桥接&#xff0c;安装好后&#xff0c;发现虚拟机ping不通主机&#xff0c;但是主机可以ping通虚拟机。 我的操作是&#xff1a;关闭防火墙&#xff0c;发现虚拟机可以ping通主机了。说明是Windows10…

设计高并发秒杀系统:保障稳定性与数据一致性

✨✨谢谢大家捧场&#xff0c;祝屏幕前的小伙伴们每天都有好运相伴左右&#xff0c;一定要天天开心哦&#xff01;✨✨ &#x1f388;&#x1f388;作者主页&#xff1a; 喔的嘛呀&#x1f388;&#x1f388; 目录 引言 一. 系统架构设计 1. 系统架构图 二、 系统流程 三…

AI绘画工具Ideogram测评:和Midjourney不分伯仲的AI图像工具之一

Ideogram 是一款令人印象深刻的人工智能图像工具&#xff0c;但尽管它于去年 8 月推出并具有不可思议的文本渲染能力&#xff0c;但它并没有引起其他一些更引人注目的 GenAI 服务的关注。 随着该公司推出其生成式人工智能模型 1.0 版本&#xff0c;这种情况即将发生改变&#…

详解 Flink 的容错机制

一、检查点 Checkpoint 1. 介绍 有状态流应用中的检查点&#xff08;checkpoint&#xff09;&#xff0c;其实就是所有任务的状态在某个时间点的一个快照&#xff08;一份拷贝&#xff09;&#xff0c;这个时间点应该是所有任务都恰好处理完一个相同的输入数据的时刻。在一个流…

帕友的小贴士,锻炼

帕金森病作为一种慢性神经系统疾病&#xff0c;对患者的生活质量产生了深远的影响。虽然医学界对于帕金森病的治疗仍在不断探索&#xff0c;但合理的锻炼已经被证实是改善患者症状、提高生活质量的有效途径之一。本文旨在为帕金森病患者推荐一些适合的锻炼方法&#xff0c;帮助…

2024 年最佳 iPhone 数据恢复软件

最好的 iPhone 数据恢复软件是什么&#xff1f; 说到 iPhone 数据恢复&#xff0c;拥有合适的软件对于恢复丢失或删除的文件至关重要&#xff0c;无论是照片、视频、消息、联系人还是其他重要数据。那么&#xff0c;最好的 iPhone 数据恢复软件是什么&#xff1f;有几个因素有…

使用C++结合OpenCV进行图像处理与分类

⭐️我叫忆_恒心&#xff0c;一名喜欢书写博客的在读研究生&#x1f468;‍&#x1f393;。 如果觉得本文能帮到您&#xff0c;麻烦点个赞&#x1f44d;呗&#xff01; 近期会不断在专栏里进行更新讲解博客~~~ 有什么问题的小伙伴 欢迎留言提问欧&#xff0c;喜欢的小伙伴给个三…

力扣hot100: 48. 旋转图像

LeetCode&#xff1a;48. 旋转图像 受到力扣hot100&#xff1a;54. 螺旋矩阵的启发&#xff0c;我们可以对旋转图像按层旋转&#xff0c;我们只需要记录四个顶点&#xff0c;并且本题是一个方阵&#xff0c;四个顶点就能完成图像的旋转操作。 1、逐层旋转 注意到&#xff0…

设计随笔 ---- ADR4525 篇

ADR4525一颗超低噪声、高精度2.5V基准电压源&#xff1b; Fluke 17B准确度指标&#xff1a; ADR4525指标&#xff1a; Fluke 17B测试结果&#xff1a; 2.5V的基准&#xff0c;输出只有2.477V&#xff0c;其实这么高精度的电压基准用3位半的万用表来测试本身就是一个错误&#…

3-哈希表-51-四数相加 II-LeetCode454

3-哈希表-51-四数相加 II-LeetCode454 LeetCode: 题目序号454 更多内容欢迎关注我&#xff08;持续更新中&#xff0c;欢迎Star✨&#xff09; Github&#xff1a;CodeZeng1998/Java-Developer-Work-Note 技术公众号&#xff1a;CodeZeng1998&#xff08;纯纯技术文&#xff…

《QT实用小工具·七十》openssl+qt开发的P2P文件加密传输工具

1、概述 源码放在文章末尾 该项目实现了P2P的文件加密传输功能&#xff0c;具体包含如下功能&#xff1a; 1、 多文件多线程传输 2、rsaaes文件传输加密 3、秘钥随机生成 4、断点续传 5、跨域传输引导服务器 项目界面如下所示&#xff1a; 接收界面 发送界面 RSA秘钥生成…

(二)深度学习基础练习题(54道选择题)

本文整理了深度学习基础知识相关的练习题&#xff0c;共54道&#xff0c;适用于想巩固深度学习基础的同学。来源&#xff1a;如荷学数据科学题库&#xff08;技术专项-深度学习&#xff09;。 1&#xff09; 2&#xff09; 3&#xff09; 4&#xff09; 5&#xff09; 6&#…

【CW32F030CxTx StartKit开发板】开发资料

本来是参加21ic的评测活动&#xff0c;不知道为什么评测文章一直被提示有不良内容&#xff0c;所以只好先在此记录一下相关的资料。 此次测试的是CW32F030CxTxStartKit 评估板。该开发板为用户提供一种经济且灵活的方式使用 CW32F030CxTx 芯片构建系统原型&#xff0c;可进行性…

插卡式仪器模块:音频分析模块(插卡式)

• 24 位分辨率 • 192 KHz 采样率 • 支持多种模拟音频信号的输入/输出 应用场景 • 音频信号分析&#xff1a;幅值、频率、信噪比、THD、THDN 等指标 • 模拟音频测试&#xff1a;耳机、麦克风、扬声器测试&#xff0c;串扰测 音频分析仪 输入阻抗10 TΩ10 TΩ输入范围3…

第103天: 权限提升-Linux 系统辅助项目脏牛Dirty内核漏洞SUIDGUID

项目下载地址 综合类探针&#xff1a; https://github.com/liamg/traitor 自动化提权&#xff1a; https://github.com/AlessandroZ/BeRoot 信息收集&#xff1a; https://github.com/rebootuser/LinEnum https://github.com/sleventyeleven/linuxprivchecker 漏洞探针&#xf…

AI网络爬虫:批量爬取豆瓣图书搜索结果

工作任务&#xff1a;爬取豆瓣图书搜索结果页面的全部图书信息 在ChatGPT中输入提示词&#xff1a; 你是一个Python编程专家&#xff0c;要完成一个爬虫Python脚本编写的任务&#xff0c;具体步骤如下&#xff1a; 用 fake-useragent库设置随机的请求头&#xff1b; 设置chr…

【小程序】WXML模板语法

目录 数据绑定 数据绑定的基本原则 在data中定义页面的数据 Mustache语法的格式 Mustache语法的应用场景 事件绑定 什么是事件 小程序中常用的事件 事件对象的属性列表 target和currentTarget的区别 bindtap的语法格式 在事件处理函数中为data中的数据赋值 事件…

【linux】进程控制——进程创建,进程退出,进程等待

个人主页&#xff1a;东洛的克莱斯韦克-CSDN博客 祝福语&#xff1a;愿你拥抱自由的风 相关文章 【Linux】进程地址空间-CSDN博客 【linux】详解linux基本指令-CSDN博客 目录 进程控制概述 创建子进程 fork函数 父子进程执行流 原理刨析 常见用法 出错原因 进程退出 概…

【Linux】进程6——环境变量

1.什么是环境变量 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数 比如&#xff1a;我们在编写C/C代码的时候&#xff0c;在链接的时候&#xff0c;从来不知道我们的所链接的动态静态库在哪里&#xff0c;但是照样可以链接成功&…

TalkingData 是一家专注于提供数据统计和分析解决方案的独立第三方数据智能服务平台

TalkingData 是一家专注于提供数据统计和分析解决方案的独立第三方数据智能服务平台。通过搜索结果&#xff0c;我们可以了解到 TalkingData 的一些关键特性和市场情况&#xff0c;并将其与同类型产品进行比较。 TalkingData 产品特性 数据统计与分析&#xff1a;提供专业的数…