微信公众号扫码授权登录思路

news2024/11/30 2:31:48

引言

上学期研究了一下微信登录相关内容,也写了两三篇笔记,但是最后实际登录流程没有写,主要因为感觉功能完成有所欠缺,一直也没有好的思路;这两天我又看了看官方文档,重新构思了一下微信公众号登录相关的内容,可能还是有不足之处,但是基本架子已经搭起来了,在这里我就简单说明一下我的想法;

关于登录方式

登录方式在我看来是必须要设计好的一个关键点,我设计的系统涉及到了手机号验证码登录,每个用户绑定的手机号是唯一的;所以对于初次使用微信登录的用户,必须绑定一个手机号才可以;(如果你只是单纯的微信登录,就会少很多步骤,下面我会细说)

微信中有一个叫openId的参数,这个参数可以说对每个关注公众号的用户都是不一样的,也就是每个用户的唯一性标识;

那么我们可以分析出来,一个用户除了id主键唯一以外,他的手机号是唯一的,微信中对应的openId也是唯一的;

所以数据库中就需要有一个用来存放openId的字段,后期微信登录就要通过该字段来检索用户;

准备工作

想要实现微信公众号扫码登录,首先要知道以下三点的实现:

1,获取微信公众号二维码

2,微信网页授权的实现

3,微信公众号消息接收和回复的实现

这三点我之前已经写过对应文章了,可以结合微信官方文档学习;

  • 微信公众号扫码登录(一)—— 获取微信公众号二维码
  • 微信登录——授权登录获取用户信息
  • 微信公众号被动消息回复实现

下面代码可能会和上面文章有所重复,但也有修改之处,看不懂可以对比来看;

登录流程

我的思路如下:

image-20230213205721283

可以分为以下步骤:

1,判断fromUserName即用户openId在数据库中是否存在
2, 如果存在则通过该openId查询到该用户信息,生成token进行登录操作
3, 如果不存在用户信息则跳转到手机号绑定页面
4, 如果该手机号已经注册有用户,则绑定该openId
5, 如果没有注册,则获取用户微信信息,将openId和手机号绑定


大致思路就是这样,其中当然还有很多小细节,下面用代码来大致演示一下;

代码实现

这个接口其实是实现微信消息接收和推送的接口,上面文章中也有,这里只是提取了service层

/**
 * 接收微信公众号消息(微信登录也经由该接口)
 */
@PostMapping("/callback")
@ResponseBody
public String responseMsg(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    req.setCharacterEncoding("UTF-8");
    String respContent = wxService.responseMsg(req);
    return respContent;
}

service

// 这个方法就是微信的登录方法
@Override
public String responseMsg(HttpServletRequest req) {
    if (req == null) {
        throw new BusinessException(StatusCode.SYSTEM_ERROR);
    }
    String message = "success";
    try {
        // 把微信返回的xml信息转义成map
        Map<String, String> xmlMessage = WxMessageUtil.xmlToMap(req); // 解析微信发来的请求信息
        String fromUserName = xmlMessage.get("FromUserName"); // 这个就该事件的用户openId
        String toUserName = xmlMessage.get("ToUserName"); // 这个开发者微信号
        String msgType = xmlMessage.get("MsgType"); // 消息类型(event或者text)
        String createTime = xmlMessage.get("CreateTime"); // 消息创建时间 (整型)
        log.info("发送方帐号(用户的openId)=>" + fromUserName);
        log.info("开发者微信号=>" + toUserName);
        log.info("消息类型为=>" + msgType);
        log.info("消息创建时间 (整型)=>" + createTime);
        if ("event".equals(msgType)) { // 如果是事件推送
            String eventType = xmlMessage.get("Event"); // 事件类型
            String eventKey = xmlMessage.get("EventKey"); // 获取事件KEY值
            if ("subscribe".equals(eventType)) { // 如果是扫描二维码后订阅消息
                String subscribeContent = "感谢关注";
                // 如果是扫码登录二维码后订阅公众号,则获取该用户信息进行登录操作
                if (!StringUtils.isAnyBlank(eventKey)
                        && WxConstant.LOGIN_QR_ID.toString().equals(eventKey.split("_")[1])) {
                    subscribeContent = dealWithWxLoginUser(fromUserName, subscribeContent);
                }
                String subscribeReturnXml = WxMessageUtil.getWxReturnMsg(xmlMessage, subscribeContent);
                return subscribeReturnXml;
            }
            if ("SCAN".equals(eventType)) { // 如果是扫码消息
                String scanContent = "扫码成功";
                // 如果是扫描登录二维码,则获取该用户信息进行登录操作
                if (!StringUtils.isAnyBlank(eventKey)
                        && WxConstant.LOGIN_QR_ID.toString().equals(eventKey)) {
                    scanContent = dealWithWxLoginUser(fromUserName, scanContent);
                }
                String scanReturnXml = WxMessageUtil.getWxReturnMsg(xmlMessage, scanContent);
                return scanReturnXml;
            }
        }
        if ("text".equals(msgType)) { // 如果是文本消息推送
            String content = xmlMessage.get("Content"); // 接收到的消息内容
            String textReturnXml = WxMessageUtil.getWxReturnMsg(xmlMessage, content);
            return textReturnXml; // 将接收到的文本消息变成xml格式再返回
        }
    } catch (IOException | DocumentException e) {
        throw new RuntimeException(e);
    }
    return message;
}

/**
 * 处理微信登录的用户
 * @param openId 扫码登录用户的openId
 * @param content 处理结果
 * @return 处理信息
 */
private String dealWithWxLoginUser(String openId, String content) {
    if (StringUtils.isAnyBlank(openId, content)) {
        throw new BusinessException(StatusCode.PARAMS_ERROR, "dealWithWxLoginUser方法参数为空");
    }
    // 1,判断fromUserName即用户openId在数据库中是否存在
    User loginUser = userService.getOne(new LambdaQueryWrapper<User>().eq(User::getWechatNum, openId));
    if (loginUser == null) {
        // 如果不存在用户信息则跳转到手机号绑定页面
        // (此时微信登录就和这里的方法没有关系了,登录工作由下面跳转的绑定页面完成,这里链接目的之一是引导用户授权信息)
        // 能访问到该绑定页面只有两种情况,不符合这两种情况不能访问该页面:
        // 1,该openId对应用户未绑定手机号 2,该openId对应用户为空
        content = "[POLAR]该账号未绑定手机号,请绑定手机号后登录\n" +
                "<a href =\"http://内网穿透域名/api/user/wx/redirect\">[绑定手机号]</a>";
    } else {
        // 如果存在则通过该openId查询到该用户信息,生成token(存入redis)进行登录操作
        // 封装用户信息
        UserVo userVo = userUtil.setUserVo(loginUser);
        // 生成token
        String token = JwtUtil.createJWT(loginUser.getId().toString());
        // 将用户信息存入redis
        redisTemplate.opsForValue().set(RedisKey.LOGIN_USER + loginUser.getId(), userVo, 14, TimeUnit.DAYS);
        content = "用户" + userVo.getNickname() + "登录成功\n\n" +
                "登录日期:" + new Date();
    }
    return content;
}

这里实现的就是流程图中的第一步判断openId是否存在,主要判断方法就是dealWithWxLoginUser这个方法,该方法触发的条件就是用户扫码订阅公众号事件或者扫码登录事件(SCAN和subscribe)。

可以看到dealWithWxLoginUser方法中当用户不存在时,则需要引导用户进入手机号绑定界面,这里就是通过公众号向用户发送了手机号绑定超链接;实际效果如图:

image-20230213210702476

然后用户点击下面超链接进行手机号绑定;

如果已经绑定openId,则执行登录操作,效果如图:

image-20230213210649602


接下来就是绑定手机号步骤,绑定手机号链接到的是微信的网页授权接口,这一块在微信授权文章里说过,通过内网穿透映射到对应链接;

代码如下:

// 调用微信授权接口重定向
@GetMapping("/redirect")
@ResponseBody
public String toRedirectUrl(HttpServletResponse response) {
    String redirectUrl = "https://open.weixin.qq.com/connect/oauth2/authorize" +
            "?appid=" + WxConfigurationConstant.APP_ID +
            "&redirect_uri=" + WxConfigurationConstant.REDIRECT_URL +
            "&response_type=code" + "&scope=snsapi_userinfo" + // 只有关注公众号才能获取用户全部信息
            "&state=STATE" + "&connect_redirect=1#wechat_redirect";
    try {
        response.sendRedirect(redirectUrl); // 重定向url
    } catch (IOException e) {
        log.error("获取微信code失败: " + e.getMessage());
    }
    return "重定向成功";
}

// 授权接口重定向回调方法
@GetMapping("/redirect/info")
public String redirectInfo(@RequestParam(value = "code") String code,
                           @RequestParam(value = "state", required = false) String state,
                           HttpServletResponse response,
                           Model model) {
    // 获取登录用户信息
    WxUserInfo loginUserInfo = wxService.getWxLoginUserInfo(code);
    // 判断该用户是否已经绑定手机号(只有用户绑定手机号后恶意访问绑定链接才会触发),已绑定则跳转错误界面
    User user = userService.getOne(new LambdaQueryWrapper<User>()
            .eq(User::getWechatNum, loginUserInfo.getOpenid()));
    if (user != null) {
        return "BindPhoneErrorPage";
    }
    model.addAttribute("loginUserInfo", loginUserInfo);
    // 不响应json数据,返回一个手机号绑定界面
    return "BindPhonePage";
}

第一个/redirect接口就是上面超链接的url,通过该接口进行用户信息授权获取的:

image-20230213211237179

然后该接口重定向到下面的/redirect/info接口,这个接口的作用才是实际获取用户信息的接口,通过getWxLoginUserInfo方法获取扫码用户的微信信息;这里可能会有疑惑,既然/redirect/info接口是获取用户信息的接口,那么要/redirect接口干嘛,这里上面微信授权文章也讲过,目的就是一个:获取code参数,只有重定向方式才能活到到code,有了code参数才能获取用户信息;微信官方文档有写;

/redirect/info接口返回的是一个html页面,即手机号绑定页面,并把授权获取的用户信息loginUserInfo也传到了该页面,这里用的是springboot的thymleaf模板,因为我前端实在是烂,就简单写了个条条框框实现这个功能,后期再完善页面:

BindPhonePage:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8">
    <title>绑定手机号</title>
  </head>
  <body>
    <div>
      <br/>
<!--      <span id="hideWxUserInfo" th:text="${loginUserInfo}" hidden></span>-->
      <input id="phoneNum" placeholder="请输入手机号" /> <br/><br/>
      <input id="code" placeholder="请输入验证码" />
      <button id="sendCode">发送验证码</button><br/><br/>
      <button id="bindClick">绑定</button>
    </div>
  </body>
  <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
  <script type="text/javascript" th:inline="javascript">
    $("#sendCode").click(() => {
      // 发送ajax请求获取验证码
      var phone = $("#phoneNum").val();
      $.ajax({
        url:'http://内网穿透域名/api/user/sms/aliyun/code/' + phone,
        // data:{
        // },
        type:'post',
        // dataType:'json',
        success:function (data) {
          if (data.code == 20000) {
            alert('验证码发送成功')
          } else {
            // 提示信息
            alert(data.message);
          }
        }
      });
    })
    $("#bindClick").click(() => {
      // 发送手机号绑定请求
      var wxUserInfo = [[${loginUserInfo}]];
      var phone = $("#phoneNum").val();
      var code = $("#code").val();
      var allInfo = {
        "phone": phone,
        "code": code,
        "wxUserInfo": wxUserInfo
      }
      $.ajax({
        url:'http://内网穿透域名/api/user/bind/phone',
        data: JSON.stringify(allInfo),
        contentType: "application/json;charset=UTF-8",
        type:'post',
        dataType:'json',
        success:function (data) {
          if (data.code == 20000) {
            alert('绑定成功');
            // TODO 跳转失败不知道为什么
            window.location.href = 'BindPhoneErrorPage.html';
          } else {
            // 提示信息
            alert(data.description);
          }
        }
      });
    })
  </script>
</html>

可以看到绑定按钮点击后会发送一个绑定请求:http://内网穿透域名/api/user/bind/phone,这个接口主要完成流程图的注册或添加openId功能,代码如下:

@PostMapping("/bind/phone")
public BaseResponse<String> bindPhoneAndOpenId(@RequestBody BindPhoneAndOpenIdRequest bindRequest) {
    if (bindRequest == null
            || StringUtils.isAnyBlank(bindRequest.getPhone(), bindRequest.getCode())
            || bindRequest.getWxUserInfo() == null) {
        throw new BusinessException(StatusCode.PARAMS_ERROR);
    }
    String phone = bindRequest.getPhone();
    String code = bindRequest.getCode();
    WxUserInfo wxUserInfo = bindRequest.getWxUserInfo();
    userService.bindPhoneAndOpenId(phone, code, wxUserInfo);
    return ResultUtils.success("绑定成功");
}

service

@Transactional
@Override
public void bindPhoneAndOpenId(String phone, String code, WxUserInfo wxUserInfo) {
    // 参数校验
    if (StringUtils.isAnyBlank(phone, code) || wxUserInfo == null) {
        throw new BusinessException(StatusCode.PARAMS_ERROR, "参数为空");
    }
    RegExpUtil.regExpVerify(RegExpUtil.phoneRegExp, phone, "手机号格式错误");
    // 从redis中获取验证码进行校验
    String phoneCode = (String) redisTemplate.opsForValue().get(RedisKey.SMS_LOGIN_CODE + phone);
    if (StringUtils.isAnyBlank(phoneCode)) {
        throw new BusinessException(StatusCode.OPERATION_ERROR, "验证码不存在或已超时");
    }
    phoneCode = phoneCode.split("_")[0]; // 获取真正的验证码
    if (!code.equals(phoneCode)) {
        throw new BusinessException(StatusCode.OPERATION_ERROR, "验证码错误");
    }
    User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone));
    if (user != null) { // 如果该手机号已经注册有用户,则绑定该openId
        user.setWechatNum(wxUserInfo.getOpenid());
        user.setUpdateTime(null);
        userMapper.updateById(user);
        // 再次查询处理好的user数据
        user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone));
    } else { // 如果没有注册,则获取用户微信信息,将openId和手机号绑定(用户注册流程)
        user = new User();
        user.setPhone(phone);
        user.setNickname(wxUserInfo.getNickname());
        user.setAvatar(wxUserInfo.getHeadimgurl());
        user.setGender(wxUserInfo.getSex());
        user.setWechatNum(wxUserInfo.getOpenid());
        user.setProfile("简单介绍一下自己吧!");
        userMapper.insert(user); // 新增用户
        // 重新获取新增用户信息
        user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone));
        // 设置用户为普通用户
        Role normalRole = roleService.getOne(new LambdaQueryWrapper<Role>().eq(Role::getRoleName, "普通用户"));
        UserRoleRelation userRoleRelation = new UserRoleRelation();
        userRoleRelation.setUserId(user.getId());
        userRoleRelation.setRoleId(normalRole.getId());
        userRoleRelationService.save(userRoleRelation);
    }
    // 封装用户信息
    UserVo userVo = userUtil.setUserVo(user);
    // 生成token
    String token = JwtUtil.createJWT(user.getId().toString());
    // 将用户信息存入redis
    redisTemplate.opsForValue().set(RedisKey.LOGIN_USER + user.getId(), userVo, 14, TimeUnit.DAYS);
    // 登录成功后将验证码清除
    redisTemplate.delete(RedisKey.SMS_LOGIN_CODE + phone);
}

这里就是对应流程图的如下步骤:

image-20230213212511626

前面一大堆就是参数校验和验证码校验,后面if~else才是核心;

其中跳转BindPhoneErrorPage.html失败,没有找到原因,后期完善了补充;

BindPhoneErrorPage页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>ERROR</title>
</head>
  <body>
    <h1>绑定手机号成功</h1>
  </body>
</html>

效果如下:

用户点击绑定手机号会有如下页面进行操作:

image-20230213214548462

获取验证码后点击绑定即可:

image-20230213220811635

image-20230213220842914

绑定成功后查看数据库可以看到完整用户数据,即phone和openId字段都有(我这里openId字段是wechat_num):

image-20230213212839267

第一条openId为空的数据就是没有使用过微信登录只通过手机号验证码登录的用户;第二条就是绑定好的数据;

查看redis:

image-20230213213114940

至此微信登录大致就完成了;

总结

其实我这里实现的仅仅是后端部分,前端如何判断用户是否扫码,如何获取用户登录的token都是待解决的问题,这里我可以提供两个思路,前端可以通过和后端长连接websocket通信进行判断用户是否扫码从而进一行下一步操作;或者是很多网站都是用的前端通过轮询的方式定时向后端发送请求查看后端是否登录完成;

以上思路和代码仅仅是我个人想法,稳定性和效率上没有经过测试,希望能給你提供一个思路,如果有问题请指点一二;

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

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

相关文章

操作系统综合实验

实验目的 加深对进程概念理解&#xff0c;进一步认识进程并发执行掌握Linux系统的进程创建和终止操作掌握文件系统调用及文件标准子例程的编程方法掌握Linux下终端图形编程方法&#xff0c;能编写基于文本的图形界面掌握proc文件系统的使用 相关知识 Linux C编程中的头文件 …

知识点整合

⭐面试 自我介绍&#xff08;优势岗位匹配度&#xff09; 为什么来我们公司&#xff08;对公司的了解&#xff09; 讲讲做的项目&#xff08;为什么这么做&#xff0c;思路和贡献&#xff09; 跨部门涉案的业务流程 我们跨部门涉案业务主要是本系统、配合物流系统和罚没系…

二战字节跳动成功上岸,准备了小半年,拿27k也算不上很高吧~

先说下我基本情况&#xff0c;本科不是计算机专业&#xff0c;现在是学通信&#xff0c;然后做图像处理&#xff0c;可能面试官看我不是科班出身没有问太多计算机相关的问题&#xff0c;因为第一次找工作&#xff0c;字节的游戏专场又是最早开始的&#xff0c;就投递了&#xf…

SpringMVC传值

实现步骤 先看后台代码如何获取前端传过来的数据&#xff0c;直接在方法的参数列表中添加RequestParam(xxx)&#xff0c;再在后面加上参数列表即可 不过这样的方式会出现一个问题&#xff1a;当前端页面没有提交相应的数据过来的时候&#xff0c;后台会出现异常&#xff0c;所…

Elasticsearch7.8.0版本进阶——数据读流程

目录一、数据读流程概述二、数据读流程步骤2.1、数据读流程图2.2、数据读流程步骤&#xff08;从主分片或者副本分片检索文档的步骤顺序&#xff09;2.3、数据读流程注意事项一、数据读流程概述 从主分片或者从其它任意副本分片检索文档。 二、数据读流程步骤 2.1、数据读流…

5_机试_递归和分治

一、递归 本章介绍程序设计中的另一个非常重要的思想一递归策略。递归是指函数直接或间接调用自身的一种方法&#xff0c;它通常可把一个复杂的大型问题层层转化为与原问题相似但规模较小的问题来求解。递归策略只需少量的程序就可描述解题过程所需的多次重复计算&#xff0c;…

谈谈Java多线程离不开的AQS

如果你想深入研究Java并发的话&#xff0c;那么AQS一定是绕不开的一块知识点&#xff0c;Java并发包很多的同步工具类底层都是基于AQS来实现的&#xff0c;比如我们工作中经常用的Lock工具ReentrantLock、栅栏CountDownLatch、信号量Semaphore等&#xff0c;而且关于AQS的知识点…

DDR4 信号说明

SDRAM Differential Clock :Differential clocks signal pairs , pair perrank . The crossing of the positive edgeand the negative edge of theircomplement are used to sample thecommand and control signals on theSDRAMSDRAM差分时钟&#xff1a;差分时钟信号对&#…

MagicThoughts|让ChatGPT变得更智能的Finetuned数据集

近两个月&#xff0c;ChatGPT无疑都是AI领域最炙手可热的话题。而它的成功&#xff0c;也引发了行业内外对于对话式AI、LLM模型商业化应用可能性的思考。诚然&#xff0c;尽管就目前来看ChatGPT对大部分问答都能基本做到“对答如流”。但是&#xff0c;ChatGPT本质上依旧是预训…

Flutter Modul集成到IOS项目

Flutter Modul集成到IOS项目中1. 创建一个Flutter Modul2.在既有应用中集成Flutter Modul2.1 Flutter的构建模式选择2.1.1 debug模式2.1.2 Release模式2.1.3 Podfile 模式2.2 Cocoapods管理依赖库集成方式2.3 直接在Xcode中集成framework2.4 Local Network Privacy Permissions…

采用 spring 配置文件管理Bean

文章目录采用 spring 配置文件管理Bean一、安装配置Maven二、Spring 框架1、Spring 官网三、Spring 容器演示-采用Spring配置文件管理Bean1、创建Manev项目2、添加Spring依赖3、创建杀龙骑士类4、创建勇敢骑士类5、采用传统方式让勇敢骑士完成杀龙任务6、采用Spring 容器让勇敢…

创建Ubuntu虚拟机与Windows共享的文件夹

目录 1、Windows创建一个共享文件夹 2、在虚拟机的设置中选择Windows下的共享文件夹 3、在Ubuntu中查看共享文件夹 1、Windows创建一个共享文件夹 该共享文件夹可以被Windows和Ubuntu访问&#xff0c;需要注意的是&#xff0c;Ubuntu在共享目录下的一些操作会受到限制&…

图解经典电路之OCL差分功放-三极管分立器件电路分析

下面从简到繁,从框架到细节的顺序讲解电路。即先讲框架,然后逐渐添加电路细节,所以大家跟上思路。 1、第一步,尽可能的抽象这个电路,等效如下: 图二 OCL等效电路 整个OCL电路,可以等效为一个大功率的运放,加上几个电阻电容构成了一个同向放大器,就是这么简单。 为了便…

Linux常用命令---系统常用命令

Linux系统常用命令场景一&#xff1a; 查看当前系统内核版本相关信息场景二&#xff1a; sosreport 命令场景三&#xff1a; 如何定位并确定命令&#xff1f;场景四&#xff1a;查看当前系统运行负载怎场景五&#xff1a; 查看当前系统的内存可用情况场景六&#xff1a;查看网卡…

【DOTA】目标检测数据集介绍与使用

every blog every motto: You can do more than you think. https://blog.csdn.net/weixin_39190382?typeblog 0. 前言 DOTA 数据集简单介绍 1. 正文 1.1 简介 数据集包含来自不同的传感器和平台的航拍图。每张图像的像素尺寸在 800 800 到 20,000 20,000 之间&#xf…

如何编写接口测试用例?

接口测试用例如何编写&#xff1f;下面简单给大家讲解一下。 接口测试用例是目前软件开发中不可或缺的一个重要部分&#xff0c;因此编写接口测试用例同样重要。 接口测试用例的作用非常明显&#xff0c;它能够帮助我们了解产品正在考验、调整它如何表现在特定情境之下、产品是…

2023金三银四,测试人还能找到好工作吗?

按照往年的惯例&#xff0c;春节后复工的 3 月、4 月是人员跳槽最频繁的时候&#xff0c;俗称“金三银四”。然而&#xff0c;市场大环境的影响&#xff0c;很多行业感受到了一丝寒冷的气息。我们以为受影响比较轻的互联网行业&#xff0c;头上也充满乌云&#xff0c;所谓互联网…

2023年浙江交安安全员考试题库及答案

百分百题库提供交安安全员考试试题、交安安全员考试真题、交安安全员证考试题库等&#xff0c;提供在线做题刷题&#xff0c;在线模拟考试&#xff0c;助你考试轻松过关。 50.根据《建设工程安全生产管理条例》第65条规定&#xff0c;施工单位有下列&#xff08;&#xff09;行…

JavaScript高级程序设计读书分享之3章——3.5操作符

JavaScript高级程序设计(第4版)读书分享笔记记录 适用于刚入门前端的同志 目录 操作符 一元操作符 递增/递减操作符 一元加和减 布尔操作符 逻辑非 逻辑与 逻辑或 乘性操作符 乘法操作符 除法操作符 取模操作符 加性操作符 加法操作符 减法操作符 关系操作符 相等操…

使用python将EXCEL表格中数据转存到数据库

使用Python将excel表格中数据转存到数据库 1. 思路&#xff1a; 1&#xff09; 使用python读取excel表格中数据 2&#xff09;根据数据生成sql语句 3&#xff09;批量运行sql语句 2. 代码&#xff1a; import pandas as pddef readExcel(path, excel_file):return pd.read_e…