多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

打通小程序端到后端:微信登录与商品浏览全链路实战

打通小程序端到后端:微信登录与商品浏览全链路实战 1. 今日任务拆解打通小程序端到后端的第一条完整链路1.1 Day6到底在做什么项目走到第六天说实话已经到了一个分水岭。前五天我们把管理端的架子搭起来、数据库表建好、后端的基础设施统一返回结果、异常处理、JWT令牌、AOP切面陆续落地这些都是“幕后”工作用户是看不见摸不着的。但Day6不一样今天做的四件事——HttpClient、微信小程序开发、微信登录、商品浏览——串起来之后用户从微信里点开小程序到看到商品列表这条链路就真正通了。这个项目本身就是个外卖系统我习惯叫它苍穹外卖。管理端是运营人员用来维护菜品和订单的用户端则是微信小程序。前者的前端是浏览器页面咱们用的是常规的后端传JSON、前端渲染那一套后者的前端是微信小程序技术栈完全不同它有自己的WXML、WXSS、JS语法而且登录机制依赖微信生态。所以今天的任务本质上可以理解成给用户端小程序造接口顺便把微信登录这套前置流程跑通。四个关键词看着多实际可以压缩成两条业务线。第一条是HttpClient和微信登录这俩绑在一起——小程序要向微信服务器证明“我是谁”后端就得拿着小程序传来的code去找微信服务器换身份信息后端这时候作为客户端发HTTP请求底层用的工具就是HttpClient。第二条是商品浏览这是用户进入小程序后看到的第一个核心页面需要后端给小程序提供分类列表和商品列表两套接口。两条线在今天交汇算得上是最能体现全栈感的一天。1.2 四个任务点怎么串联微信小程序端的登录逻辑和传统的账号密码登录差别很大。传统Web项目是前端把用户名密码发给后端后端查库比对小程序没有密码输入这一步它靠的是微信的授权机制——小程序端调用wx.login()拿到一个临时code这个code的有效期只有五分钟而且只能用一次。后端拿到code之后再拿着它去请求微信的接口换用户的openid也就是用户在某个微信小程序里的唯一ID和session_key。说白了微信帮你完成了身份认证后端只需要信任微信返回的结果把openid作为用户的身份标识存进自己的数据库。这一步就是HttpClient出场的地方。后端要主动发起HTTP请求调微信的官方接口。Java生态里能做这件事的库挺多原生的HttpURLConnection太底层、用起来啰嗦Spring自带RestTemplate虽然能用但连接管理比较弱我选择了Apache HttpClient原因后面详细说。用户浏览商品这条线相对单纯但也不是随便查个列表就完事。外卖App的小程序端首页分类是横向滚动的胶囊栏下面跟着一大块商品列表。这个交互决定了接口的粒度先给一个分类接口返回所有启用的分类再给一个商品接口输入分类ID返回该分类下的商品集合。这两条线在今天的开发顺序上是有讲究的。先做HttpClient和微信登录再做商品浏览。原因很简单小程序端如果连登录都没通你拿着wx.request去请求商品接口后端验JWT那一关就直接把你拦住了根本走不到业务代码。所以登录是今天的地基商品浏览是今天的成果展示。地基没打好后面联调全是白搭。1.3 为什么选择HttpClient而不是RestTemplate后端主动调第三方接口这需求在项目里会越来越常见。今天调微信后面还可能调支付、调地图、调短信。选一个合适的HTTP客户端工具值得花点时间。我对比了几个选项HttpURLConnectionJDK自带零依赖但API设计很原始连接池、超时、重试全要自己手写代码量巨大不适合项目长期维护。RestTemplateSpring官方封装和Spring Boot集成度最高但它在连接管理上比较弱默认走的是SimpleClientHttpRequestFactory每次请求都新建连接没有复用机制高并发场景下连接建立的开销会拖垮接口性能。虽然也可以替换底层工厂但绕一圈不如直接用专业的HTTP库。HttpClientApache出品连接池、路由、超时、重试、拦截器一应俱全配置好了之后性能很稳社区案例也多遇到问题基本搜得到答案。它在Java后端圈子里几乎成了事实标准。最终选了Apache HttpClient。而且我顺手做了一层封装把连接池、超时时间、请求头设置都收敛到一个工具类里后面任何地方要调第三方接口一行代码搞定不用到处new对象这样代码也干净。2. HttpClient后端如何请求微信的接口2.1 封装一个健壮的HttpClient工具类直接拿来就用是我的一贯原则但HttpClient有个坑如果你每次请求都new CloseableHttpClient用完了再close那跟HttpURLConnection没有本质区别连接完全没复用。正确做法是启动时就构建一个带连接池的HttpClient实例全局共享让底层帮你管连接复用。我当时封装的工具类大概长这样public class HttpClientUtil { private static final int CONNECT_TIMEOUT 5000; private static final int CONNECTION_REQUEST_TIMEOUT 5000; private static final int SOCKET_TIMEOUT 10000; private static final int MAX_TOTAL 200; private static final int MAX_PER_ROUTE 50; private static CloseableHttpClient httpClient; static { PoolingHttpClientConnectionManager cm new PoolingHttpClientConnectionManager(); cm.setMaxTotal(MAX_TOTAL); cm.setDefaultMaxPerRoute(MAX_PER_ROUTE); RequestConfig config RequestConfig.custom() .setConnectTimeout(CONNECT_TIMEOUT) .setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT) .setSocketTimeout(SOCKET_TIMEOUT) .build(); httpClient HttpClients.custom() .setConnectionManager(cm) .setDefaultRequestConfig(config) .build(); } public static String doGet(String url, MapString, String paramMap) { // 构造URI拼接参数 // 执行GET请求解析Entity为String返回 } public static String doPost(String url, String jsonBody) { // 构造POST请求Content-Type设为application/json // 携带JSON体执行请求 } }超时时间这里我解释一下。connectTimeout是建立TCP连接的超时如果微信服务器连不上5秒就该放弃connectionRequestTimeout是从连接池获取连接的超时池子里没空闲连接时等5秒还等不到就直接报错防止线程全堵在那里socketTimeout是两次数据包之间的间隔超时设成10秒微信接口正常响应都在几百毫秒10秒绰绰有余。注意连接池这两个参数别随便拍脑袋MAX_TOTAL200表示整个池子最多200个连接MAX_PER_ROUTE50表示到同一个目标主机最多50个连接。微信接口的QPS并不高50完全够用。如果哪天要调高并发的外部接口这两个值需要重新压测评估不是越大越好。2.2 用工具类换取openid完整过程微信登录的核心一步就是后端拿code换openid。微信官方接口的URL和参数是固定的你只需要按文档拼好URL、发起GET请求、解析JSON响应就行。这个接口我用代码写出来是这样的public static final String WX_LOGIN_URL https://api.weixin.qq.com/sns/jscode2session; public static final String WX_APPID 你的小程序AppID; public static final String WX_SECRET 你的小程序AppSecret; public static final String WX_GRANT_TYPE authorization_code; MapString, String paramMap new HashMap(); paramMap.put(appid, WX_APPID); paramMap.put(secret, WX_SECRET); paramMap.put(js_code, code); paramMap.put(grant_type, WX_GRANT_TYPE); // 返回的是一个JSON字符串 String response HttpClientUtil.doGet(WX_LOGIN_URL, paramMap);响应体长这样正常情况下{ openid: oABC123xxx, session_key: t0qE3mZ9xK2lR8vU4yN6sW1c, unionid: xxx }有openid就说明身份验证通过了。这个openid就是用户在小程序体系里的唯一标识一个用户对一个openid不会变。session_key是微信帮你生成的会话密钥后面如果要做数据解密才用得上今天登录只需要openid。有个关键细节大厂接口返回的Content-Type有时候是text/plain不是application/json所以HttpClient工具类里解析响应时不要按Content-Type判断直接拿到字符串再用JSON.parseObject去解析否则会莫名其妙报转换异常。这是我之前踩过的坑提一句。2.3 连接池、超时与重试的细节再补几个日常不太会注意但关键时刻要命的细节。第一HttpClient默认不会帮你重试。微信接口偶尔抽风返回500如果你不做重试用户登录就直接失败了。我当时加了一层简单的重试逻辑第一次请求失败后休息200毫秒再试一次最多重试两次但还是失败就抛出业务异常。注意这里重试的对象是“网络异常/服务端5xx”而不是“业务失败”——如果微信返回的是40001这种业务错误码重试多少次都没用问题的根源是参数错了。第二连接池的“连接泄漏”问题。HttpClient的响应流必须关闭CloseableHttpResponse要放到finally块或者用try-with-resources关掉否则连接永远不归还连接池跑个几千请求之后池子满了后面的请求全部卡在connectionRequestTimeout上接口表现为假死。我见过太多人栽在这上面日志里全是Timeout waiting for connection from pool排查一天发现就是响应没关。第三微信接口的域名是HTTPSHttpClient需要处理SSL证书验证。微信这种大厂证书是正规CA签发的JDK的信任库默认就信任你什么都不用做。但如果你用的是自签名证书的测试环境就需要额外配置SSLContext跳过校验否则会报SSLHandshakeException。这个知识点今天暂时用不上但做企业级项目联调时大概率会碰到。3. 微信登录从code到用户身份的完整闭环3.1 登录流程梳理小程序端、后端、微信三方如何配合登录这件事最怕脑子一热就开写。我建了个简单的流程图逻辑先梳理再动手。小程序端发起登录时流程拆成五步小程序端调用wx.login()获取临时code。这个code的有效期只有5分钟且只能用一次。小程序端把code通过wx.request发到咱们后端的/user/user/login接口同时附带用户的昵称和头像。后端拿到code通过HttpClient请求微信的jscode2session接口换取openid和session_key。后端拿openid去数据库查用户表。查到了说明是老用户登录查不到就自动注册一个新用户用openid作为唯一标识。后端生成JWT令牌把令牌返回给小程序端。小程序端把令牌存到wx.setStorageSync里后续所有请求都在Header里带上这个令牌。这个过程里有几个点容易忽略。wx.login()的code是跟着用户走的同一个用户每次wx.login()拿到的code都不一样不要试图缓存code。后端换openid这一步如果微信返回了errcode比如40029code无效或45011频率限制一定要把错误原样记到日志里方便排查。最让我无语的一种错误是开发者在后端把appid和secret写反了微信返回40125这类低级错误排查起来真的很费时间。另外为什么不用openid直接当令牌因为JWT有两个作用一是标识用户身份二是防篡改。如果只用openid别人只要拿到你的openid就能伪装成你调用接口太危险。JWT是带签名的后端每次都能校验令牌有没有被篡改这才安全。3.2 后端登录接口实现Controller、Service、Mapper三层怎么写接口设计先定下来。登录接口返回的数据结构我用了DTO和VO分离的方式Data public class UserLoginDTO { private String code; private String nickname; private String avatar; } Data public class UserLoginVO { private Long id; private String openid; private String token; }Controller层长这样注意返回类型是之前封装好的统一结果ResultRestController RequestMapping(/user/user) Slf4j public class UserController { Autowired private UserService userService; PostMapping(/login) public ResultUserLoginVO login(RequestBody UserLoginDTO userLoginDTO) { log.info(微信用户登录{}, userLoginDTO.getCode()); UserLoginVO userLoginVO userService.wxLogin(userLoginDTO); return Result.success(userLoginVO); } }Service层是核心逻辑。完整代码不贴了我把关键流程写出来public UserLoginVO wxLogin(UserLoginDTO userLoginDTO) { String openid getOpenidByCode(userLoginDTO.getCode()); if (openid null || openid.isEmpty()) { throw new LoginFailedException(微信登录失败获取openid为空); } User user userMapper.getByOpenid(openid); if (user null) { user User.builder() .openid(openid) .nickname(userLoginDTO.getNickname()) .avatar(userLoginDTO.getAvatar()) .build(); userMapper.insert(user); } MapString, Object claims new HashMap(); claims.put(userId, user.getId()); String token JwtUtil.createJWT(claims); return UserLoginVO.builder() .id(user.getId()) .openid(openid) .token(token) .build(); }一个容易被忽略的细节新用户第一次登录时nickname和avatar来自小程序端传过来的值但老用户再次登录时如果微信资料更新了不会自动覆盖。所以老用户那条分支可以顺手加一个逻辑如果昵称或头像有变化更新用户表。这属于体验细节不加也能跑但做产品就得把这些考虑进去。判断新老用户的核心是我在业务里保证openid在用户表中必须唯一。这个唯一性光靠Java代码判断不够数据库层面也要加唯一索引防止并发请求下同一用户被插入两条记录。这个坑也是真实存在的一旦并发量上来不加唯一索引迟早出事。3.3 小程序端登录怎么调、怎么存、怎么带小程序端的代码我第一次写的时候觉得语法很别扭后来发现核心就那几个API用顺手了就好。登录相关的逻辑封装在一个JS文件里const login () { return new Promise((resolve, reject) { wx.login({ success: (res) { if (res.code) { wx.request({ url: BASE_URL /user/user/login, method: POST, data: { code: res.code, nickname: wx.getStorageSync(nickname) || 微信用户, avatar: wx.getStorageSync(avatar) || }, success: (loginRes) { const { token } loginRes.data.data; wx.setStorageSync(token, token); resolve(token); }, fail: reject }); } else { reject(new Error(获取code失败 res.errMsg)); } } }); }); };这里面有个体验问题wx.login()拿到的code不能提前放到请求体里因为它是一次性的而且必须是后端拿它去换openid才有效。如果你在A处调了wx.login()拿到code然后过了几分钟才用它去请求后端code早就过期了。所以正确顺序是点进小程序想看商品 → 先调wx.login()→ 立刻拿code请求登录接口 → 拿到token → 再请求商品接口。请求拦截器的逻辑也要配好最简单粗暴但够用的做法const request (url, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method: method, data: data, header: { Authorization: Bearer wx.getStorageSync(token) }, success: resolve, fail: reject }); }); };这里有个前后端协作的坑后端校验JWT时拦截器里是从请求头拿Authorization字段的但前后端商量的是加Bearer前缀还是不加必须提前约定清楚。如果后端没处理前缀前端带了Bearer就会校验失败要是后端明确要Bearer前端忘了带也会报401。这种问题联调时一查一个准但第一次碰到会让人怀疑人生。我当时是后端在校验逻辑里顺手把Bearer前缀处理了前端爱带不带兼容两种情况省心。3.4 JWT、Redis与openid之间的分工这里稍微展开一下登录态的完整设计。很多人第一次做微信登录会混淆openid是数据库主键吗JWT存的是什么Redis在这里有没有活干我先说结论openid是用户身份的唯一业务标识它存在用户表里是自然的用户ID映射JWT是登录凭证它里面装的是userId由后端签名生成Redis在这个项目里更多用来存短信验证码、购物车这类短生命周期数据登录态本身有JWT就够了不强制依赖Redis。那JWT和session到底有什么区别传统session是服务端存一份、客户端存一个sessionId服务端挂了session就丢了JWT是无状态的令牌自身携带了用户信息后端只需要验签即可不需要在内存里维护会话状态。这对海量用户的外卖项目来说是刚需因为服务端没必要为每个会话保存状态横向扩展也会变得很容易。不过无状态也有代价JWT一旦签发到期之前都没法主动作废。如果用户被封禁了token还能用。所以有的项目会把JWT的jti存到Redis做黑名单机制。今天这个阶段没必要但你要知道有这种设计后面做到管理端才能动手。4. 商品浏览用户端第一次真实看到商品数据4.1 需求理解与接口设计约定商品浏览是整个小程序端的门面功能。外卖App见过吧首页打开是最上面一个轮播图下面是一排可横向滑动的分类胶囊再下面是对应分类的商品列表。咱们今天做的商品浏览主要就是后两部分。管理端之前已经建好了category分类表和dish菜品表关键字段大概是-- 分类表 id, name, type, sort, status, create_time, update_time -- type: 1表示菜品分类2表示套餐分类 -- 菜品表 id, name, category_id, price, image, description, status, create_time, update_time用户端小程序只展示status1启用的数据禁用掉的菜品绝对不能出现在用户端。这个过滤逻辑必须在后端SQL里做完不能把全量数据查到内存里再过滤——数据量一大这种写法就是性能隐患。接口设计我定了两个GET /user/category/list?type1查询启用的菜品分类列表按sort升序排列。GET /user/dish/list?categoryIdxx根据分类ID查询启用菜品列表。这里有过一个方案讨论要不要做一个聚合接口一次返回所有分类和各自的菜品从减少请求次数的角度聚合接口确实更高效。但聚合接口的代价是一旦某个分类下的菜品特别多比如几十个首屏数据量就变得不可控并且接口的缓存粒度也变粗了。我最后选了分开的两个接口这样做的好处是后续可以针对每个接口做Redis缓存商品列表也可以方便地做分页。4.2 分类列表接口实现分类列表的查询逻辑很简单但有几个字段需要处理。数据库里sort字段是用来控制分类展示顺序的比如“热销”排第一“主食”排第二必须Order By sort。还有一个细节分类表里type字段区分了菜品分类和套餐分类用户端首页展示菜品分类就要where type 1。Mapper层可以用注解SQL搞定Select(select * from category where type #{type} and status 1 order by sort asc) ListCategory listByType(Integer type);Controller层GetMapping(/list) public ResultListCategory list(Integer type) { ListCategory list categoryService.listByType(type); return Result.success(list); }看似简单实际上有个隐藏问题这条SQL返回的字段直接是整个Category实体但实体里带createTime、updateTime这些管理端才需要的时间字段。用户端小程序拿到这些字段没有任何用处白浪费带宽还暴露了后端结构。所以更专业的做法是返回一个精简的VOid、name、image、sort。做大项目时接口返回结构不是“能用就行”而是“够用就好”这是后端工程师的职业素养。4.3 根据分类查询商品列表的实现菜品查询也一样核心SQL是Select(select * from dish where category_id #{categoryId} and status 1 order by sort asc) ListDish listByCategoryId(Long categoryId);这里业务上有一个值得考虑的点一份菜品可能对应多个口味比如“鱼香肉丝”有大份、小份或者辣度选择。口味数据存在dish_flavor表里跟dish是一对多的关系。如果直接查询菜品列表再把口味数据一并查出来有两种做法一种是循环查询写起来简单但会产生N1问题另一种是先用一条SQL查出菜品再用一条SQL查出所有关联口味最后在Java代码里做内存拼接。我采用的是第二种方案因为外卖项目的菜品数量级不是特别大内存拼接完全顶得住避免了N1查询。核心逻辑ListDish dishList dishMapper.listByCategoryId(categoryId); ListLong dishIds dishList.stream().map(Dish::getId).collect(Collectors.toList()); if (dishIds.isEmpty()) { return new ArrayList(); } ListDishFlavor flavors flavorMapper.listByDishIds(dishIds); MapLong, ListDishFlavor flavorMap flavors.stream() .collect(Collectors.groupingBy(DishFlavor::getDishId)); ListDishVO voList dishList.stream().map(dish - { DishVO vo new DishVO(); BeanUtils.copyProperties(dish, vo); vo.setFlavors(flavorMap.getOrDefault(dish.getId(), new ArrayList())); return vo; }).collect(Collectors.toList());这样两次SQL搞定所有数据比循环查询不知道高到哪里去了。当然如果你的菜品表数据量真的很大比如几十万条那就要考虑分页和缓存了。外卖场景单个分类下的菜品一般不会超过50个不做分页也没大问题但你要在代码注释里写明这个前提。4.4 联调要点与返回结构优化后端接口写完之后最重要的事就是用小程序开发者工具去调。这一步最容易暴露问题我把常见的联调顺序列一下先用Apifox/Postman直接调后端接口确认参数和返回结构都正常这一步不受小程序端影响排错最快。再在小程序开发者工具里打开调试模式在Network面板查看请求是否发出、Header是否带上。确认后端的JWT拦截器没有拦截登录接口本身。很多新手把登录接口也放进需要token才能访问的白名单之外导致小程序端一调登录接口就被401拒绝整个流程直接卡死。登录接口必须配到“无需认证”的路径列表里。登录成功后用小程序请求商品接口这时候Header里带上token后端校验通过数据正常返回链路就算通了。联调时还要注意一个安全问题微信小程序的网络请求域名必须在微信公众平台配置白名单否则线上环境请求会直接失败。开发阶段可以在开发者工具里勾选“不校验合法域名”但上线前必须把后端域名加到合法域名列表里否则用户端会一片空白。这个坑几乎每个做小程序的人都会踩一遍。5. 踩坑与排查实录5.1 登录后请求商品接口还是401这个问题我记得特别深当时小程序端登录成功了token也都存到本地了但是请求商品分类接口后端一直返回401。日志里能看到JWT解析失败提示SignatureException。排查了半天最后发现是前端在Header里拼token的时候多了一个空格// 错误写法 header: { Authorization: Bearer token } // 正确写法 header: { Authorization: Bearer token }你没看错就是差一个空格。Bearer和token之间必须有空格这是JWT规范里的标准格式少了空格后端解析的时候会把整个字符串当成签名来校验必挂。这种东西你不会想到是空格问题只有把请求头原样打出来才看得出。这里也提醒一个规范多环境联调时让后端把请求头完整记到日志里。如果日志里有Authorization: Bearertoken一眼就能看出问题在哪不用来回猜。5.2 HttpClient连接池耗尽导致接口假死有次本地测试登录之后连续刷新页面大概跑了几百个请求后登录接口突然卡住不动了等了10秒左右才报超时。控制台的日志里全是Timeout waiting for connection from pool。我当时的代码长这样你感受一下哪里有问题CloseableHttpClient httpClient HttpClients.createDefault(); CloseableHttpResponse response httpClient.execute(request); String result EntityUtils.toString(response.getEntity());这段代码无论执行成功还是失败httpClient和response都没有被关闭。HttpClients.createDefault()每次调用都会创建新的HTTP客户端这个客户端底层自己有连接池但池子里的连接被创建之后从来没归还过。前几百个请求把池子占满后面的请求就只能等。正确写法是每次请求后用try-with-resources或者把response.close()放在finally里try (CloseableHttpResponse response httpClient.execute(request)) { return EntityUtils.toString(response.getEntity()); }如果用的是上一版封装的全局httpClient只要保证每次execute的response被关闭连接就会自动归还到连接池永远不会出现耗尽问题。这个小坑属于典型的“写完能跑跑久必炸”排查难度不大但记忆深刻。5.3 商品接口联调发现字段对不上我当时把菜品列表接口写好联调时发现小程序端拿到数据后图片加载不出来。查了网络面板数据确实返回了图片URL是/common/download?namexxx.jpg这样的相对路径小程序端拿这个路径拼到image的src上根本没拼域名。这个问题本质上是前后端对“图片URL字段的语义”没对齐。管理端上传图片后返回的是相对路径用户端小程序需要的是完整的HTTPS地址。解决方案有两种一种是后端在返回用户端的VO里动态拼上服务器域名返回完整URL另一种是小程序端在拼接时统一加域名前缀。我建议用第一种因为前端如果到处拼域名一旦域名变化就得全局替换后端统一处理更干净。改完这个又发现一个小问题返回的价格字段是BigDecimal序列化成JSON后变成20.00小程序端直接渲染没问题但如果要做计算比如购物车合计最好转成整数分或者字符串避免JS的浮点数精度问题。这个属于经典问题外卖项目里绕不开。5.4 数据库唯一索引的重要性前面提到openid要有唯一索引我再展开说两句。当时设计用户表的时候我就跟团队商量过openid这列一定加唯一索引。因为多线程并发场景下两个请求同时拿着同一个用户的新code来登录两次都查不到用户就会同时执行insert如果没有唯一索引用户表里就出现两条相同的openid记录后续所有逻辑都会错乱。有了唯一索引第二次insert会抛DuplicateKeyException你捕获这个异常后重新查一次就能拿到已存在的用户。代码可以这样写try { userMapper.insert(user); } catch (DuplicateKeyException e) { user userMapper.getByOpenid(openid); }这种“先查插不进再查一次”的写法比加锁优雅得多。做高并发业务数据库约束永远是最后一道防线必须靠它兜底。6. 最后说几个今天没做但必须想的事今天的代码跑通了不代表这个功能就能直接上线。有四个“明天的事”我建议在继续开发之前先想清楚不然后面返工成本会很高。第一登录接口目前返回的token时效性建议设为2小时甚至更短。外卖场景用户不会一直停留在小程序上token有效期短一点更安全。但如果设太短用户用着用着突然要重新登录体验又不好。实务做法是给token设置合适的过期时间同时小程序端在请求返回401时自动触发一次静默登录刷新token。第二微信登录的session_key目前拿到了但没有存。如果后续要做“手机号快捷登录”或者“微信加密数据解密”就需要存起来。这个字段属于敏感信息建议单独存到Redis并设置过期时间不要落库。第三商品列表接口现在每次请求都查数据库。当用户量上来以后这个接口就是性能瓶颈。下一步一定要引入Redis缓存查询前先查缓存查不到再查DB并且把缓存做好过期时间——商品如果下架了缓存最多持续几分钟不至于让用户一直看到已下架的商品。第四接口的返回结构目前是Result统一包装但异常时的ErrorCode字段还不够细化。后面做小程序端错误提示时如果每种异常都有明确的错误码前端就不需要靠猜状态码来判断是“网络异常”“token过期”还是“商品不存在”了。提前把错误码规划好能省下后面大量的联调沟通成本。我个人做项目最大的体会就是功能跑通只是第一步把边界条件、并发问题、缓存策略、异常规范想清楚才算是真正把这个功能吃透了。今天这篇文章里的每个坑都对应着一个真实的调试过程每个经验都值一次白头发。希望你在自己的项目里能少踩几个。今天的日记就到这儿。商品浏览通了之后下一个环节大概率就是购物车和下单。那段逻辑里的并发控制和状态流转又会有新的坑等着我们去踩。到时候再聊。
返回列表