一、回顾与概览第五天我们学习了Redis的常用命令以及在Java中常见的相关API并通过Redis完成了我们店铺模块查询营业状态。今天我们主要需要解决的是用户端微信登录功能和商品浏览功能的实现了解微信小程序的开发流程和HttpClient技术。二、微信登录功能1.HttpClient1定义HttpClient是 Apache 提供的一个开源 Java 库官方包org.apache.httpcomponents:httpclient核心作用是让 Java 程序能够像浏览器 / Postman/Apifox 一样主动发送 HTTP/HTTPS 请求并处理服务端返回的响应。没有 HttpClient 时Java 原生的HttpURLConnection功能简陋、代码繁琐有了 HttpClient 后你可以轻松实现 GET/POST/PUT/DELETE 等请求处理请求头、请求体、Cookie、超时、重试等复杂场景且 API 更友好、功能更强大。2应用场景微服务 / 系统间接口调用比如你的小程序后端需要调用第三方支付接口、短信服务接口、地图定位接口等接口测试 / 模拟请求像你现在用 Apifox 测试接口用 HttpClient 也能写代码自动化测试接口比如登录接口返回 Token 后自动带着 Token 调用其他接口爬虫 / 数据抓取程序主动请求第三方网站 / 接口获取数据并解析替代 Postman/Apifox 做自动化请求比如你可以写一个 Java 程序自动调用登录接口获取 Token再调用购物车接口验证 Token 是否生效解决你之前的 401 问题后端主动调用前端 / 其他服务比如你的后端需要主动推送数据到另一个服务的接口。3发送请求步骤导入相关依赖可以看到HttpClient依赖包含在aliyun-sdk-oss下面。1.创建HttpClient对象2.创建Http请求对象3.调用HttpClient的execute方法发送请求核心APIHttpClients、HttpClient、CloseableHttpClient、HttpGet、HttpPost4发送Get请求案例需求发送Get请求请求苍穹外卖用户端的查看店铺营业状态接口。/user/shop/status1.利用HttpClients的createDefault方法创建一个HttpClient对象2.由于是发送Get请求所以new一个HttpGet对象请求路径就是/user/shop/status3.调用HttpClient对象的execute方法发送请求获取响应CloseableHttpResponse4.解析响应数据调用getStatusLine获取状态行getStatusCode获取状态码。也可以调用getEntity方法获取响应体但因为响应体HttpEntity内部是二进制字节流无法直接转成String或JSON来获取业务数据所以调用自定义工具类EntityUtils对HttpEntity进行解析。图中toString就是把响应体解析后转成可见的字符串形式并打印出来。PSHttpResponse、HttpEntity、StatusLine三者之间关系HttpResponse代表了整个 HTTP 响应的所有内容包含内容状态行Status Line比如状态码200 OK、401 Unauthorized响应头Headers比如Content-Type、Content-Length、Set-Cookie等元信息响应体Body通过getEntity()方法获取也就是HttpEntity对象作用它让你能获取整个响应的所有信息包括响应的状态、元数据和实际内容。5发送POST请求案例需求通过HttpClient发送Post请求请求苍穹外卖管理端员工登录接口。/admin/employee/login1.利用HttpClients的createDefault方法创建一个HttpClient对象2.由于是发送POST请求所以new一个HttpPOST对象请求路径就是/admin/employee/login3.构造请求体参数登录请求请求体参数就是username和passwordk-v结构使用Map进行存储。由于要把Map里面的数据传输过去但因为Map是Java内存中的对象无法传输所以使用Fastjson库提供的JSON.toJSONString方法将Map对象序列化成JSON格式的字符串作为Http请求发送。StringEntity的作用就是把JSON格式的字符串封装成请求体。4.请求体StringEntity还需要手动设置请求体编码以及类型编码通过方法setContentEncoding设置为UTF-8请求体参数类型通过方法setContentType设置为json。这一步如果不做的话就会报415异常。5.调用HttpClient对象的execute方法发送请求获取响应CloseableHttpResponse6.解析响应数据调用getStatusLine获取状态行getStatusCode获取状态码。调用getEntity方法获取响应体,利用工具类EntityUtils解析成可见字符串并打印出来。PS常见4开头报错类型404资源找不到405路径找到了但是请求方式不对eg.Get请求写成Post请求401没有登录没有权限导致检查是否校验成功了eg.无下发JWT令牌400参数相关eg.参数写错了或者少写了415服务器不支持请求体数据格式或本身不匹配或者未设置请求头之类的2.微信小程序1用户注册在进行微信小程序开发前必须进行小程序注册。注册地址Click Here注册完成后需要完善小程序信息和小程序类目两处信息。2微信开发者工具随后可以下载微信开发者工具下载地址Click Here创建小程序不需要使用云服务和模板APPID在小程序网页开发管理模块可以看到小程序密钥在后续也会用到。左边为小程序目录结构介绍pages目录下每个文件夹都对应一个页面每个页面通常由js、json、wxml、wxss。js文件是必须的主要写页面逻辑。wxml文件是必须的主要写页面结构json文件不是必须的主要写页面配置wxss文件不是必须的主要写页面样式表。右边需要勾选上不校验合法域名选项这源于微信小程序开发阶段的痛点即微信小程序上线前有严格的安全规则 ---所有网络请求如接口调用、图片加载的域名必须提前在微信公众平台的「服务器域名」里配置否则请求会被拦截。在本地开发时你通常是调用本地后端接口如http://localhost:8080或测试环境域名这些域名还没来得及配置到白名单里。勾选「不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书」后绕过域名白名单校验你可以直接调用本地或测试环境的接口不用频繁去公众平台配置域名。跳过 HTTPS 强制要求开发阶段可以用 HTTP 协议如本地http://localhost不用提前部署 HTTPS 证书。提升开发效率避免了开发阶段因为域名配置问题导致的请求失败让你能专注于功能开发。3小程序代码导入目录选中目标文件夹填入AppID无需使用云服务开发模式选小程序直接创建就成功导入了小程序代码。3.登录流程分析1小程序端获取登录凭证小程序调用wx.login()接口微信会返回一个临时的登录凭证code。这个code是一次性的有效期很短只能用来换取后续的用户信息。2开发者服务器换取用户身份信息小程序通过wx.request()把code发送到你的开发者服务器。开发者服务器拿到code后会带上小程序的appid、appsecret和这个code调用微信接口服务的 “登录凭证校验接口”。微信接口服务验证通过后会返回用户的唯一标识openid和会话密钥session_key。3开发者服务器生成自定义登录态开发者服务器拿到openid和session_key后会生成一个自定义登录态比如一个token或sessionId。这个自定义登录态JWT令牌会和openid、session_key关联起来存储在开发者服务器的数据库或缓存里。最后服务器把这个自定义登录态返回给小程序。4小程序存储自定义登录态小程序收到自定义登录态后会把它存入本地的storage比如wx.setStorageSync()。这样后续发起业务请求时就可以直接从本地取出这个登录态不用每次都重新走登录流程。5小程序发起业务请求当小程序需要获取业务数据时会调用wx.request()并在请求中携带这个自定义登录态。开发者服务器收到请求后会通过这个自定义登录态去查询对应的openid和session_key。验证登录态有效后服务器就会处理业务逻辑并把业务数据返回给小程序。关键设计思路安全隔离session_key不会直接返回给小程序避免了前端暴露敏感信息降低了被窃取的风险。状态维护通过自定义登录态开发者可以灵活控制用户登录状态的有效期和刷新机制。性能优化一次登录生成的自定义登录态可以复用减少了频繁调用微信接口的开销4.接口开发1接口文档和产品原型基于产品原型可以分析出业务规则主要有两条1.基于微信登录实现小程序的登录功能。2.如果是新用户需要自动完成注册。分析接口文档可以看出请求参数是json格式里面只有个小程序传来的临时登录凭证code。返回数据就是用户id开发者服务器端数据库里面存储在User表中的用户id、微信用户唯一标识openid、JWT令牌。2Controller层返回值为自定义类UserLoginVO封装了用户idopenidJWT令牌三个关键信息。第一步调用Service方法传入UserLoginDTO里面封装的临时登录凭证code获取到登录的用户。第二步就是如果登录成功就生成JWT令牌这里的逻辑是先创建一个Map存放载荷信息claimskey-value此处需要存放的载荷信息是USER_ID然后调用工具类JwtiUtil的createJWT方法生成JWT令牌这里主要传入签名密钥、过期时间、载荷信息前两个参数主要在我们的yml文件里面配置即可。第三步就是构造UserLoginVO对象把用户idopenidJWT令牌都封装进去返回即可。3Service层在Service业务层就主要是开发者服务器向微信接口服务发送请求的过程这里可以查看微信官方文档服务端可以看到类似于接口文档的信息这个就是向第三方服务发送请求的解释文档。第一步构造请求参数k-v结构可以使用Map进行存储主要是appid依旧在小程序开发管理模块可以看到secret同上这两个参数都提前配置在了WeChatProperties中了然后是临时登录凭证code这个封装在请求参数UserLoginDTO中第四个参数就是固定值直接填即可。然后调用工具类HttpClientUtil的doGet方法传入目标URL和实现封装好的请求参数。第二步解析响应结果doGet方法返回的是JSON字符串利用JSON的parseObject方法可以把JSON字符串转换成可操作的JSONObject对象轻松提取出里面各个key对应的value。这里先可以提取出openid通过判断openid是否存在如果不存在那就说明登陆失败了即第三方微信接口服务不允许登录那就抛出自定义异常。第三步判断是否为新用户调用Mapper层的selectByOpenid方法如果能查到说明不是新用户如果查不到说明是新用户那就到了第四步。第四步如果user为空那就new一个新的User调用一些set方法最重要的还是setOpenidopenid字段是唯一判断微信用户是否存在的标识然后调用Mapper层插入这个user即可。最后都是返回User对象。4Mapper层Mapper层主要就selectByOpenid和insert两个方法注意遇到新用户调用insert方法插入user到数据库时需要加上Options注解往user里面把主键id填入。因为在Controller层第三步构造UserLoginVO对象的时候用到了user.getId()方法这个id如果时新增的用户那对象user里面的id字段是空的因此需要Options注解。5.用户端拦截器1JWT令牌校验拦截器第一处代码主要是判断请求方式是不是Options预检请求如果是那就直接放行。PS当浏览器要发起一个非简单请求比如带自定义头、PUT/DELETE 方法的请求时会先自动发送一个 Options 请求。它的目的是询问目标服务器“我接下来要发的这个请求你允许吗”服务器会在响应头中返回允许的源、方法、头等信息。浏览器拿到响应后判断是否符合规则再决定是否发送真正的业务请求。第二处代码是从请求头中获取令牌这个UserTokenName在JwtProperties里面事先配置好了。第三处代码就是检验令牌3.1 解析 JWTJwtUtil.parseJWT()会用服务端保存的userSecretKey对令牌进行解密和校验。校验内容包括签名是否正确防止令牌被篡改。令牌是否过期。如果校验通过就会返回一个Claims对象里面包含了生成令牌时存入的所有信息比如USER_ID。3.2 提取用户 ID从Claims中取出USER_ID转换成Long类型的userId。这个userId就是当前登录用户的唯一标识。3.3 存入 ThreadLocalBaseContext.setCurrentId(userId)会把userId存入ThreadLocal。这样在后续的业务代码中就可以随时获取当前用户的 ID不用再从请求头里重复解析。3.4 校验通过 / 不通过的处理校验通过返回true请求继续向后执行进入对应的 Controller 方法。校验不通过会触发catch块设置响应状态码为401未授权并返回false直接拦截请求。2注册拦截器在WebMvcConfiguration类中注册用户端拦截器主要拦截/user/**然后把查询营业状态请求路径/user/shop/status和登录请求路径/user/user/login排除在外其余请求路径均要进行JWT令牌校验即拦截器的拦截。三、商品浏览功能1.查询分类1接口文档和产品原型根据接口文档可知查询分类是个Get请求请求参数只有个type。返回值可以使用Category类封装data显示object[]说明查询出来是很多条数据所以就是ListCategory。2Controller层这里可以直接复用之前写管理端时的categoryService的getByType方法。3Service层复用之前的categoryMapper的getByType方法即可直接return查询结果。4Mapper层就是个查询在售类别如果传入了type就按照type动态查询所有类别的select语句。2.根据分类id查询菜品1接口文档和产品原型请求参数就是分类id返回数据里面不仅有该分类下所有菜品的基本信息也有每个菜品的口味信息有无口味就体现在是“号”还是“选择规格”。2Controller层由于data数据类型为object[]说明根据一个分类id查询了多条菜品所有返回值用ListDishVO接收。3Service层除了查询该分类id下每条菜品的基本信息还要查询每个菜品的口味信息因此需要查询两次。先复用之前dishMapper的select方法动态查询该分类id下每条菜品基本信息由于之前设计的参数是DishDTO所以把categoryId封装进去作为参数。然后new一个ListDIshVO作为返回值。随后遍历刚刚查询得到的ListDish对于每条菜品基本信息都复制到new出来的DishVO中然后复用之前DishFlavorMapper中的selectByDishId方法查询当前遍历菜品的所有口味信息接着手动setFlavors并把处理好的DishVO添加到循环外新建的ListDishVO中最后返回。4Mapper层两个Mapper层的方法都是复用之前写管理端时的方法总结下来就主要是Service层需要遍历多个菜品手动setFlavors与之前的区别在于之前只需要对一个特定菜品set即可以及参数和返回值做一些微调。3.根据分类id查询套餐1接口文档和产品原型这个需求比较简单就是根据分类id查询套餐表基本信息即可无须考虑套餐包含的菜品信息2Controller层直接调用Service层的selectByCategoryId方法返回值用Setmeal实体类封装即可且有多条套餐数据。3Service层Service层没啥业务逻辑调用Mapper层直接返回值就行。4Mapper层一个根据categoryId查询套餐所有数据的select语句。4.根据套餐id查询包含的菜品1接口文档和产品原型请求参数为路径参数返回数据不仅有菜品份数菜品名称而且还有菜品描述和菜品图片路径这几个字段没有同时存在于一个表当中所以需要联表查询主要是菜品份数只有在setmeal_dish表才有产品原型效果就是点击套餐会显示该套餐下的菜品的一些信息。2Controller层返回值的四个字段用DishVO封装返回值是最适合的直接调用Service层完事了。3Service层Service层也没啥业务逻辑直接调用Mapper层。4Mapper层主要份数copies这玩意只有setmeal_dish表才有其他信息又基本都在dish表因此做一个联表查询。四、总结第六天主要完成的是微信登陆功能了解HttpClient的使用和微信小程序开发流程。这里面比较困难的就是微信登录流程分析。后面的商品浏览功能实现跟管理端写接口差不多没什么难度。