
1. 项目概述与设计思路1.1 这个选题解决了什么问题先聊点实在的。做毕业设计或者个人项目选型最难的不是实现本身而是“这个题目最后能不能作为一个完整的故事讲出来”。护肤购物系统这个题目名字里三个关键词缺一不可微信小程序、精致护肤、购物系统。少了“微信小程序”这就是个传统电商网站少了“精致护肤”这就是个毫无垂直特色的通用商城少了“购物系统”前面两个词说得再好听落不了地没法实现交易闭环。我见过不少同学做电商类毕设最后做成一个半成品商品列表能看详情页能点但购物车、订单、支付、库存这些核心链路要么没做要么是假数据糊弄过去的。这个题目的真正难点和亮点恰恰是把“护肤”这个垂直领域的业务逻辑和小程序这个特定平台的技术约束结合起来——既不能做得像普通商城那样泛泛而谈又要在小程序只有2MB主包限制的环境下把系统跑通。上一轮交付的是一个编译打包好的小程序源码工程那东西和网页工程有本质区别。网页跑起来是浏览器解释执行改一个JS文件刷新就有结果小程序不行它虽然是前端技术栈但要过微信的编译、上传、审核、发布流程还要区分体验版和正式版。所以这个项目里每一行代码都要考虑“在微信这个平台里它会被怎么执行”这是我反复强调的一点。1.2 为什么选择原生小程序而不是uni-app热门搜索词里出现了“uniapp 微信小程序打包”和“source size 2612kb exceed max limit 2mb”这两条我猜很多人正在用uni-app做小程序然后被主包2MB的限制卡住了。这个话题值得展开讲。uni-app在跨端开发上有优势一套Vue代码可以同时输出H5、小程序、App。但代价是什么是运行时框架层的体积。每个uni-app编译出来的小程序项目里都会带上uni-app的runtime适配层这一层代码最少也要几百KB。再加上Vue运行时、各种内置组件主包很容易超过2MB。项目一旦超过限制就必须做分包而分包配置又是一堆额外的工作。所以我的建议很明确如果这个项目确定只做微信小程序没有多端需求直接用原生小程序框架就是最优解代码写起来反而更可控组件渲染方式和底层API的调用也没有中间层损耗。原生框架还有一个隐藏优势网上能找到的小程序开源项目绝大多数是原生写的。我自己研究过几个开源模板里面很多页面级的布局技巧、下拉刷新和上拉加载的实现方式、自定义导航栏的处理直接复制到原生项目里就能用很少需要改适配逻辑。2. 核心业务模块与技术选型2.1 护肤垂直领域的业务建模普通的购物系统商品表设计就是SPU、SKU两层的简单结构名字、图片、价格、库存、规格。但护肤品的商品建模要复杂得多因为用户买护肤品之前最关心的是“这个适不适合我的肤质”、“里面有没有我过敏的成分”、“主打的功效是什么”。所以在设计商品表结构的时候我给product表额外增加了几个核心字段skin_type适用肤质格式用逗号分隔比如“干性,中性,敏感性”effect_tags功效标签比如“保湿,美白,抗皱”用于列表页的筛选featured_ingredients核心成分用于成分解读展示is_sensitive_friendly是否适合敏感肌这个字段做的是布尔值前端详情页会有大图高亮提示为什么不把这些信息放到普通文本描述里因为你一旦存成文本用户就只能在详情页看到。而护肤用户的需求往往是从“关键词”出发的——“我想找一款敏感肌能用的保湿面霜”。如果把成分、肤质、功效独立成字段就能支持按标签筛选、搜索、推荐三个维度的查询逻辑整个系统的可用性直接上一个台阶。这个思路其实提炼成一句话垂直电商和通用电商的核心差异不是界面长得不一样而是底层的数据模型有没有围绕品类设计。你打开详情页看到敏感肌提示、成分分析、肤质匹配度这些都不是UI层面画出来的而是数据层早就设计好的。2.2 小程序端与后端的技术分工整个系统采用前后端分离的架构。后端我用的是SpringBoot MyBatis Plus MySQL接口设计成RESTful风格返回统一的JSON格式。小程序端只负责页面渲染和用户交互所有的业务判断、库存扣减、订单状态流转都在服务端完成。很多做类似项目的同学会犯一个错误把库存判断、价格计算放在前端做。在小程序里前端代码是可以通过抓包工具看到的接口参数能改页面逻辑能被篡改。如果下单的时候是前端来算价格就会面临严重的逻辑漏洞。后端的代码别人改不了就算改了请求参数服务端还要校验一遍真实数据。接口层我按照业务模块分了五组用户模块/api/auth/**处理登录、token签发、个人资料商品模块/api/product/**处理分类、列表、详情、搜索购物车模块/api/cart/**增删改查订单模块/api/order/**下单、支付回调、状态查询、取消护肤测试模块/api/skin-test/**肤质测评题的提交与结果生成另外管理端我是单独做的一个后台管理页面用于管理员管理商品、处理订单。虽然题目核心是小程序端但完整的购物系统一定要配套管理后台否则商品数据没法维护这笔订单也没法处理。这块在答辩的时候也值得明确出来。2.3 技术栈选择背后的三个理由为什么用SpringBoot而不是Node.js或者Go三个原因第一自己和多数同学最熟悉的技术栈就是Java这意味着项目能做完的概率大很多第二毕业设计这道题目中后端技术栈通常能体现一门主流语言的完整工程实践Java在整个技术栈的表现是最稳妥的网络上的参考资料也是最多的第三SpringBoot的生态能覆盖绝大多数实际要做的功能比如拦截器做token校验、拦截器处理跨域、MyBatis Plus做分页查询这些能力的实现成本极低、非常稳定。小程序端我选的原生框架理由前面已经说了。另外一个技术点UI层面没有引入Vant Weapp之类的第三方组件库原因是不想被组件库的样式风格限制住护肤类应用需要干净、浅色、带一点点高级感的设计自己写样式更容易把控页面调性。后面需要优化的时候再把组件库接进来也不困难。3. 数据模型设计与数据库实现3.1 核心数据表结构详解数据库是整套系统的地基它的设计直接决定系统的扩展能力。这个系统我总共设计了8张核心表下面重点说说几张关键表的设计要点。用户表user常规字段之外我加了skin_type字段用来标识用户测出来的肤质类型代码如下。CREATE TABLE user ( user_id int NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL UNIQUE COMMENT 微信openid, nickname varchar(100) DEFAULT , avatar_url varchar(500) DEFAULT , skin_type varchar(20) DEFAULT NULL COMMENT 肤质dry/oily/combination/sensitive, phone varchar(20) DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;openid是用户在微信体系中的唯一标识这一步很关键后面做微信登录时会具体说。商品表product上面提到过护肤业务字段这里给出完整SQLCREATE TABLE product ( product_id int NOT NULL AUTO_INCREMENT, category_id int DEFAULT NULL, name varchar(200) NOT NULL, subtitle varchar(300) DEFAULT NULL COMMENT 副标题/卖点, main_image varchar(500) DEFAULT NULL, detail_html text COMMENT 富文本详情, price decimal(10,2) NOT NULL, original_price decimal(10,2) DEFAULT NULL, stock int NOT NULL DEFAULT 0, sales_count int DEFAULT 0, skin_type varchar(50) DEFAULT NULL COMMENT 适用肤质逗号分隔, effect_tags varchar(100) DEFAULT NULL COMMENT 功效标签逗号分隔, featured_ingredients varchar(200) DEFAULT NULL COMMENT 核心成分, is_sensitive_friendly tinyint(1) DEFAULT 0, status tinyint(1) DEFAULT 1 COMMENT 1上架 0下架, create_time datetime DEFAULT NULL, PRIMARY KEY (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个细节值得提出来商品详情页我会用富文本编辑器去维护detail_html字段存储的就是格式化之后的HTML片段。小程序端使用rich-text组件来渲染这段内容这个方案比起自己用CSS排版效率和美观度都高很多。购物车表cartCREATE TABLE cart ( cart_id int NOT NULL AUTO_INCREMENT, user_id int NOT NULL, product_id int NOT NULL, quantity int NOT NULL DEFAULT 1, checked tinyint(1) DEFAULT 1 COMMENT 是否选中, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (cart_id), UNIQUE KEY uk_user_product (user_id,product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;购物车表的设计有一个容易忽略的点user_id和product_id的联合唯一索引。用户反复点击加购按钮数据库层就拦截了重复添加的问题而不是应用层先去查一遍有没有再插入。一个SQL层面的限制省掉一次网络IO和一段傻乎乎的查询代码。订单表order和订单明细表order_item订单是电商的核心订单表存订单整体信息明细表存商品快照。CREATE TABLE order ( order_id int NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL UNIQUE COMMENT 订单编号, user_id int NOT NULL, total_amount decimal(10,2) NOT NULL, pay_amount decimal(10,2) NOT NULL, pay_type tinyint(1) DEFAULT 1 COMMENT 1微信支付, status tinyint(1) DEFAULT 0 COMMENT 0待付款 1已付款 2已发货 3已签收 4已取消, address varchar(500) DEFAULT NULL, receiver_name varchar(50) DEFAULT NULL, receiver_phone varchar(20) DEFAULT NULL, pay_time datetime DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单明细表的product_name和product_image存的是商品实时字段的快照这是交易系统里非常经典的快照设计。3.2 事务与库存扣减的实现下单操作是系统里对数据一致性要求最高的环节库存扣减和生成订单不能出现“订单生成成功但库存没扣掉”或者反过来的情况。这里必须用事务同时要注意条件的写法。库存扣减我使用的是数据库层面的安全扣减SQL// 安全扣减库存 int rows productMapper.updateStock(productId, quantity); // 对应SQL UPDATE product SET stock stock - #{quantity}, sales_count sales_count #{quantity} WHERE product_id #{productId} AND stock #{quantity}关键在WHERE stock #{quantity}这一步。这条SQL携带了库存检查能力如果库存不足返回的影响行数是0此时不需要做任何补偿直接在事务里抛出异常回滚。我见过一些同学先要去查一下库存够不够再执行扣减这么做会在高并发下发生超卖问题——两个请求同时查到库存2同时扣减订单都生成库存变成-1。再说事务范围。整个下单事务跨越了三个操作扣库存、生成订单主记录、生成订单明细。我使用Spring的Transactional注解在Service层实现注意需要设置合适的传播行为。这里一个关键经验是不能把无关操作放在事务里。比如把微信支付请求也放进事务中就会出现支付成功回调之前锁数据库的情况导致页面长时间卡顿。支付动作应该是先预下单生成订单待付款状态再调起微信支付支付支付完成之后异步更新订单状态。3.3 订单状态机设计订单状态字段我用一个整数表示后台逻辑只认状态流转图前端不能直接改0 待付款 → 1 已付款 → 2 已发货 → 3 已签收任何一个状态都可以 → 4 已取消但先决条件是待付款状态下用户可以主动取消已付款状态取消需要走退款流程这个项目里简化成后台手动处理已发货之后无论如何不能再取消只能走退货流程。用户取消订单这个操作也要用事务封装因为在更新订单状态的同时还要回补库存。如果只改状态不回库那么被取消订单锁住的商品就永远丢失在里面了。这个细节在代码审查和答辩的时候特别容易被问到提出来应对的能力比别人大半阶。4. 小程序端核心页面与API联动4.1 首页与商品列表的加载逻辑首页信息结构设计为顶部搜索栏 轮播图 分类快捷入口 推荐商品流。用户进入到首页后后端API返回聚合好的首页JSON一次接口调用同时返回轮播列表和分类列表推荐商品流则单独走商品接口做分页。商品列表页是整站访问量最大的页面。通用做法是下拉刷新上拉加载更多。这里有一个容易被忽视的技术细节即小程序Page.onReachBottom()触发时间。官方触发条件是页面滚动到底部但如果你把数据一次性全部渲染这个回调就一直不会触发。所以列表页必须做分页。我定义的分页参数是pageNum和pageSize默认10条每次加载完成后把pageNum加1同时用一个hasMore布尔值标记是否还有更多数据。下拉刷新则调用wx.stopPullDownRefresh()结束动画。列表加载还有一个性能优化点使用wx:key来标识列表渲染的每一项而不是使用wx:for-index默认值。当列表数据更新时框架可以准确知道哪些项需要重新渲染而不是把整列表销毁重来这是页面滑动流畅度差异的重要来源之一。4.2 商品详情页的三个富信息模块商品详情页是吸引用户下单转化最重要的页面。尺寸大了、图多了容易加载慢内容少了用户看不到核心价值。我的详情页包含四个模块主题大图区域。顶部轮播展示商品的实拍图、效果对比图等让用户对商品外观和质感形成感知。主信息区域。显示商品名称、价格、副标题卖点提炼价格区同时展示原价用删除线划掉形成对比。功效与成分区域。这个区域是护肤品详情页的特色——通过icon列表展示保湿、美白、抗皱等功效标签下方用户点击“成分解读”可展开富文本详情。这里需要注意rich-text组件在渲染HTML时宽度需要适配屏幕。有个相对好的做法是给内容外部包一层view设置宽度属性并且在富文本里要求图片自带100%宽度。SKU选择和加入购物车。由于单品通常没有复杂的规格分化容量、版本我把规格选择做成了底部弹出的半屏视图用户选择容量版本后显示实时价格变化点击加入购物车时向后端发送商品ID、用户ID和数量。4.3 搜索与筛选功能实现搜索功能用到了后端的关键词匹配查询。前端搜索栏输入关键词后调用商品搜索接口// 模糊搜索 ListProduct searchProducts(String keyword, Integer pageNum, Integer pageSize) { QueryWrapperProduct wrapper new QueryWrapper(); wrapper.like(name, keyword) .or().like(subtitle, keyword) .or().like(effect_tags, keyword); wrapper.eq(status, 1); // 分页 // 排序优先销量 return productService.page(pageNum, pageSize, wrapper); }在按肤质筛选这个场景我实现了肤质测试结果一键筛选匹配商品。这个功能特别适合护肤垂直电商。用户测完肤质直接点击推荐按钮后端会拿用户的肤质标签和product表的skin_type字段做正则匹配。敏感肌用户自动过滤掉非敏感友好产品干性用户优先看到标有干性适用的商品。考虑到系统规模不大数据库层面的LIKE查询已经完全够了。如果商品量到了十万级再去接Elasticsearch或者引入中文分词都不迟现在是典型的两层架构里做掉就行的场景。5. 微信登录与支付模块全流程5.1 wx.login()与后端Token签发流程微信小程序的登录方式和网页登录不一样。网页登录是用户主动输入账号密码小程序则是无感的静默授权核心API是wx.login()。官方示例代码很简单wx.login({ success: (res) { if (res.code) { // 将 code 发送到后端服务 wx.request({ url: https://api.example.com/api/auth/login, data: { code: res.code } }) } else { console.log(登录失败 res.errMsg) } } })res.code是一个临时凭证有效期只有5分钟。后端拿到这个code之后调用微信的jscode2session接口需要传入小程序AppID和AppSecret换回来用户的openid和session_key。代码示意如下// 调用微信登录凭证校验接口获取openid String url https://api.weixin.qq.com/sns/jscode2session ?appid appId secret appSecret js_code code grant_typeauthorization_code; // 返回结果示例{openid:xxx,session_key:xxx}拿到openid后先在user表查询如果没有这个openid就自动注册新用户。查到了就直接登录成功。后端给前端签发自定义登录态——Token后续所有需要用户身份的接口都带上这个Token作为凭证。这里必须提醒一个安全性问题微信小程序的AppSecret一定不能放在小程序前端代码里。AppSecret是后端和微信服务器之间的通信密钥一旦暴露在源码中别人就能通过你的AppSecret冒充你的小程序后端发起请求。小程序端只需要把code传给自己的后端后端自己持有AppSecret即可。Token的过期时间我设置为7天存放到小程序的wx.setStorageSync(token, token)中。每次请求时从存储里取出Token放到请求头Header里像这样const token wx.getStorageSync(token) wx.request({ url: https://api.example.com/api/cart/list, header: { Authorization: token } })后端用一个拦截器统一解析请求头里的Token校验合法性并识别用户身份。如果Token过期或者无效返回401错误码小程序端识别后跳转到登录页重新走一遍wx.login()流程。5.2 微信支付的完整时序微信支付的流程不像网页支付那样重定向页面而是在小程序内部调起支付界面组件。整体链路如下用户在小程序端点击立即支付向后端发起预支付请求后端生成订单记录状态为待付款同时调用微信统一下单接口获得预支付交易标识prepay_id后端根据prepay_id生成支付参数返回给小程序小程序端调用wx.requestPayment()传入后端返回的参数弹出微信自带的支付窗口用户输入密码确认支付微信服务器发送支付结果回调到后端的回调接口后端在回调里验证签名、确认金额后更新订单状态为已付款后端生成支付参数的伪代码如下MapString, String payParams new HashMap(); payParams.put(appId, appId); payParams.put(timeStamp, String.valueOf(System.currentTimeMillis() / 1000)); payParams.put(nonceStr, RandomUtil.randomString(16)); payParams.put(package, prepay_id prepayId); payParams.put(signType, MD5); payParams.put(paySign, genSign(payParams, mchKey));这里最容易被卡住的点是微信支付的商户号、API密钥和证书配置。做开发的时候如果没有真实的商户号可以用微信提供的沙箱环境来模拟测试。一旦接入真实商家回调地址必须是HTTPS域名并且必须在微信公众平台里配置这个环节一定要提前规划。支付回调的坑特别多。回调是由微信服务器直接发起的HTTP POST请求网络波动可能导致同一笔支付的回调被推送多次所以回调处理逻辑必须做幂等判断——先查订单是否为待付款状态不是就直接返回成功响应不再重复更新。此外支付回调除了更新订单状态外还要给用户增加积分或者发送订阅消息我是在最终完成的阶段加了这个扩展同学在设计系统时可以把它当成一个加分项考虑进来。5.3 订阅消息的授权与推送护肤品的送达周期比一般商品长使用提醒的价值非常高。比如用户购买了一款需要按周使用的安瓶精华第八天是使用提醒的好时间。微信小程序的订阅消息API支持一次性订阅即用户点了一次“允许”就只能收一条提醒消息。我实现的是当订单状态变为“已发货”之后给用户推送消息提醒// 小程序端请求订阅消息授权 wx.requestSubscribeMessage({ tmplIds: [模板ID1], success(res) { // res[模板ID1] accept 代表用户已同意 } })后端则通过调用微信的subscribeMessage.send接口把消息发出去。这个模块实现逻辑比较简单但是用户授权率普遍不高因此可以在下单完成页加一个小提示“现在授权到货第一时间通知您”比起用户随意闭着眼睛点确认话术引导能明显提高授权率。6. 页面布局细节与常见技术坑6.1 顶部导航栏高度适配热门搜索词里有一条“微信小程序顶部导航栏高度”这个问题在对接的时候确实困扰了不少人。原因是微信小程序的导航栏分为两部分顶部状态栏显示时间、电量和下面的导航栏。而不同机型的顶部状态栏高度是不一样的刘海屏和非刘海屏差别很大。如果页面使用了自定义导航栏划出来的高度不对按钮就会跑到屏幕外面或被状态栏挡住。解决办法是通过系统API拿到安全距离const systemInfo wx.getSystemInfoSync() const menuRect wx.getMenuButtonBoundingClientRect() // 导航栏高度计算 const statusBarHeight systemInfo.statusBarHeight // 顶部状态栏高度 const navBarHeight (menuRect.top - statusBarHeight) * 2 menuRect.height这里有一个经验公式自定义导航栏区域的高度可以参考胶囊按钮的位置来推算。最后把这个高度存到全局变量里需要的时候统一取。6.2 列表加载更多的三种实现方式比较列表加载更多是热门搜索词里的内容实现方式大概有三种第一种是按钮式列表到底后展示一个“查看更多”按钮用户点击触发加载。实现简单但是用户体验一般多了一步操作。第二种是滚动式监听页面滚动到接近底部时自动加载。小程序里用onReachBottom()即可但要注意这个回调只有页面滚动才触发如果页面内容不足一屏就不会触发。它的判断逻辑也受页面整体高度影响。第三种是懒加载式数据和分页反馈机制不变但在视觉上做无限滚动优化。推荐第二种与第三种结合也就是你看到很多商城在采用的模式页面滚动到底自动加载加载间隙显示一个loading动画没有更多数据后显示“没有更多了”。实现的核心代码如下onReachBottom() { if (this.data.hasMore !this.data.loading) { this.setData({ loading: true }) this.loadProducts() } }重点在于hasMore和loading这两个布尔值的配合。没有loading的判断用户快速滚动时可能一次发出多个重复请求没有hasMore的判断在最后一页时接口还会反复调白白消耗资源。6.3 字符乱码与编码坑小程序默认是Javascript和WXML对中文的支持没有问题。但如果你把后端返回的JSON通过JSON.parse解析的时候出现中文乱码那80%是后端没有设置正确的响应编码。SpringBoot中设置一下即可RequestMapping(value /api, produces application/json;charsetUTF-8)另一个常见的坑在MySQL建表时已经部分提到——数据库字符集最好使用utf8mb4而不是utf8因为utf8在MySQL里是utf8mb3的别名不支持emoji表情和生僻字。商品详情富文本里包含特殊表情符号时用utf8就会报错整条插入失败用户端表现为数据丢失。6.4 真机调试与模拟器的区别小程序开发过程中模拟器和真机之间存在不少环境差异。比如“模拟器的录音API生成的文件格式”这种问题就是典型的模拟器和真机差异——模拟器可能直接调用PC的录音设备生成的文件格式和真机完全不同。这类问题我也遇到过模拟器上一切正常预览版拉到真机上就白屏或者报错。真机白屏是最头疼的。排查思路按照下面三步走开发环境打开vConsole调试看JS报错先定位到是否存在兼容性问题看网络请求模拟器里能访问的局域网IP真机不一定能访问。真机要求后端接口地址必须是HTTPS的线上域名只在本地跑通的话真机请求会直接失败检查是否有wx.login()调用失败但未处理导致的逻辑中断还有一个常见的真机静态图问题在模拟器里能看到图片真机看不到大概率是图片域名没有加到小程序的downloadFile合法域名白名单里。微信对访问的域名有备案和HTTPS强制要求开发热点是不算数的。7. 常见问题与排查技巧实录7.1 微信开发者工具里的小程序如何发给别人试用这个问题在热搜词里原样出现了说明很多人做完项目之后不知道怎么把它发给团队成员或指导老师体验。我来梳理一下完整的试用步骤。小程序开发完成第一件事是在微信公众平台注册小程序账号拿到AppID。开发者工具里填入AppID之后点击工具右上角的“上传”按钮把当前代码作为一个版本提交到微信后台。然后到小程序公众平台的管理后台找到“版本管理”在开发版本里看到刚提交的版本点击“选为体验版”。此时需要先添加体验成员成员的微信号通过后就能在小程序里搜索到这个体验版并打开试用。体验版和正式版的区别有三个第一体验版只有白名单内的成员能打开其他人搜索不到第二体验版可以访问非HTTPS的请求域名开发者可以把域名设置为本地局域网IP第三体验版不能调用真实微信支付要测试完整支付流程必须提交审核发布正式版。有趣的是正式版小程序上架审核时类目选择很重要。护肤电商涉及的内容如果含有“医疗功效”字眼容易被驳回。审核不过的话可以尝试在商品描述中,避免使用“治疗”“药妆”这类词转为“修护”“舒缓”等更偏护肤领域的描述。7.2 商品图加载失败的三个诱因项目运行中我遇到最频繁的问题是商品图片加载不出来。排查后发现诱因基本是三种第一种是网络图片地址没有加白名单。这不只在个人开发时出现团队里如果换了图片服务器忘记更新域名配置正式版就全军覆没。域名白名单在小程序后台的“开发–开发设置–服务器域名”里配置域名必须是HTTPS且备案。第二种是图片格式问题。有时设计师提供的图是WebP格式在Android端可以正常显示但iOS端的老版本微信不支持WebP就只能看到一个空白占位。安全方案是统一转成JPG或PNG。第三种是图片地址直接写了相对路径。/images/xx.jpg这种在小程序里找的是本地包内的静态资源但商品图属于动态数据除了包内的图标和默认占位图全部必须使用完整URL。7.3 小程序主包超过2MB限制的解决方案这个坑我在第一节提到过一次现在给出完整的实操技巧。我的策略是把整个项目拆分成主包和分包。主包只保留TabBar需要的页面首页、分类页、购物车页、我的页。其余页面统统放到分包里// app.json { pages: [ pages/index/index, pages/category/category ], subPackages: [ { root: pages/product, pages: [ detail/detail, list/list ] }, { root: pages/order, pages: [ confirm/confirm, list/list, detail/detail ] } ] }分包之后主包没达到2MB上限分包虽然会单独计算总体积但仍然有明确的限制标准可以参照。如果还想继续压体积可以看编译产物里的代码分布找到哪些页面都被打进主包但是其实只有少数几个页面在用把它们移入分包后编译一次体积立刻降下来。同时注意图片资源。很多小朋友在写代码的时候习惯把UI设计稿直接拖进项目文件夹一张图片几MB也不管打包时直接把包撑爆。项目里所有的网络图片都应该采取在线存储本地包里只放icon和默认占位图这些图也要通过配置压缩工具比如TinyPNG类工具压缩后再放进去。7.4 首页数据缓存的存取逻辑小程序和普通网页的一个重要区别是用户每天第一次打开小程序时都希望首屏速度很快。每次启动都重新请求首页数据网络慢的场景下首屏会有一段白屏或者骨架屏时间。我的解决思路是设置两级缓存机制。第一级启动时从wx.getStorageSync(home_cache)读取上一次保存的首页数据马上渲染出来。第二级同时发出网络请求拿到最新数据后再更新页面。这个机制和“浏览器cache-first策略”有些相似是移动端首屏加速用的黄金工程模式。第一次启动没有缓存就从网络加载并写入缓存同时给缓存设置一个过期时间比如5分钟过期后前端强制走网络请求防止用户始终看到过期数据。const cached wx.getStorageSync(home_cache) if (cached Date.now() - cached.timestamp 5 * 60 * 1000) { this.setData(cached.data) } else { this.loadHomeData() }在app.js的onLaunch里预加载数据这个做法我试过几次之后反而不建议原因是onLaunch阶段用户尚未进入具体的页面预加载结果如果和用户操作产生时序竞争容易引入页面初始化逻辑的混乱。更好的方案是把加载动作放在首页onLoad里页面一旦创建马上取缓存并异步更新。8. 从毕设走向真实可用的小程序最后再聊几句大实话。把这个护肤购物系统从“能演示”做到“真能用”中间还有一段路我踩过的最大坑是给自己挖太多功能。比如一开始给系统规划了AI推荐、种草社区、视频评测骨架还没打好就想着加足够多的复杂功能最后要么是在加班赶进度要么是卡死在某个环节里。后来停下来重新理顺核心闭环商品浏览、肤质测试推荐、购物车、下单、微信支付。这一串流程跑通之后系统的底子和骨架就立住了其他功能全是锦上添花。第二个体会是做这个项目时保持一个“验收者”心态效率会高很多。每次写完一个模块不要停在“代码能编译”就满足了而是打开开发者工具当作真实用户一步步操作从首页进详情加购改数量选收货地址提交订单再对着后台看看库存扣没扣、订单状态对不对。这种“端到端”的验证方式一次性就能发现流程里大量的逻辑漏洞。写代码的时间越长越觉得“认真走查一遍业务流程”是性价比最高的操作。如果你也想做类似的电商小程序我给你的具体建议是拿到题目之后先定数据模型再画页面交互图最后才开始写代码。顺着这个顺序走项目背后所有功能都有依靠答辩的时候每一个问题都能落到实处。护肤垂直领域往大了说还可以接入肤质测试问卷的个性化推荐、定期护肤计划、皮肤状态记录打卡这些功能都是数据模型铺好路之后自然能扩展的方向。这个项目做完交付之后我最大的收获不是掌握了多少API怎么用而是掌握了设计一套完整线上交易系统的全局思维——从前端交互到后端数据一致性从安全校验到真机调试中间每个环节都有值得沉淀的方案。拿这个思路去面对下一类小程序或下一个商业项目会顺畅得多。