微信小程序+Spring Boot实战:销售项目流程化管理系统设计
2026/9/10 8:26:15 网站建设 项目流程

1. 项目整体设计与技术选型思路

先交代一下背景:这个项目是我做的一个毕业设计,选题是《基于微信小程序的销售项目流程化管理系统》。当时选这个题,倒不是为了赶什么热点,而是我自己在实习的时候深有体会——很多中小公司的销售业务管理,真的还停留在"Excel表格 + 微信群接龙 + 销冠用自己的小本本记录客户"的阶段。客户跟到哪一步了全靠销售自己回忆,管理层想看个数据得挨个催报表,项目丢单了复盘都不知道是哪一环出了问题。所以当时就想,能不能做一套轻量级的、销售人员和主管都愿意用的流程化管理系统,把客户从线索到成交甚至交付的全过程给管起来。

在这套系统里,微信小程序承担的是"人人可用、随时可打开"的角色。相比原生App,小程序最大的优势就是不用安装、适配成本低、微信生态内天然有用户基础,对销售这种经常在外跑的人群来说,打开微信就能写日报、录客户、提审批,接受度比任何独立App都高。而后端我用的是 Spring Boot + MyBatis-Plus + MySQL 这套经典组合,一是因为成熟稳定,二是因为毕业设计答辩时候,这套技术栈老师熟,能讲的内容也多,不容易被问倒。鉴权走的是微信生态最常见的 openid 方案,没有自己做用户名密码体系,这也是小程序端的默认做法。

1.1 为什么用"流程化"来切销售管理

这里必须先说清楚一个容易含糊的点:销售管理和"销售项目流程化管理系统"不是一回事。传统的销售管理侧重客户信息的增删改查,说白了就是一个客户档案库。但流程化管理,核心是"项目 + 阶段 + 流转"这三个词。

我当时的设计思路是这样:把一个销售机会看成一个"项目",这个项目从建立到赢单,会经历若干固定阶段。比如常见的 B2B 销售流程会拆成:初步接洽 → 需求沟通 → 方案报价 → 商务谈判 → 合同签订 → 回款 → 交付服务。每个阶段有固定表单要填、有对应负责人、有预计完成时间。只有当当前阶段的必要信息填写完整,项目才能推进到下一个阶段。这样一来,管理层看到的就不是一盘散沙的客户,而是每个项目"卡在哪个阶段、是谁在负责、停留了多久、下一步是什么"。

这套逻辑放在毕设里好看,放在真实业务里更实用。我自己在实习时经历过的那种"销售离职客户直接带走"的情况,在流程化管理下会大幅减少,因为客户脉络、跟进记录、阶段资料全部沉淀在系统里。

1.2 技术选型里面的几个关键决策

前后端分离是明确的,小程序端原生开发,没有用 uni-app。原因很简单:这个项目的核心在于业务逻辑和流程设计,uni-app 确实能一套代码多端复用,但它的编译层会带来额外的调试成本,而且很多小程序原生 API 在 uni-app 里封装得不够透,出了问题反而不好排查。毕业设计讲究的是可控,我宁可多写几个页面文件,也不想在"为什么这个组件在真机上渲染不出来"上浪费时间。

后端方面,Spring Boot 版本选的 2.7.x,JDK 用的 1.8。这两个版本组合网上资料特别多,遇到问题基本一搜就有答案。数据访问层用 MyBatis-Plus,说实在的,这个框架对业务开发的友好度真的高,单表 CRUD 基本不用写 SQL,条件构造器 QueryWrapper 做动态查询非常方便,能省掉大量重复的 mapper 代码。项目里只有几个复杂的统计报表查询是手写的 SQL,这点也符合实际开发中的主流习惯。

数据库设计走的是经典的单体架构下的多表关联模式。重点表包括:用户表、客户表、销售项目表、项目阶段表、阶段任务表、审批记录表、操作日志表。这里有个经验可以分享:学生做毕设很容易把表设计得特别多、特别碎,追求所谓的"设计感",但实际开发时会发现,表越多关联越乱,光是联表查询就能让你写到怀疑人生。我最终把表控制在 10 张以内,每张表都有明确的业务职责,宁可字段多一点,也不要为了"范式"强行拆表。

1.3 项目目录结构与包结构规划

一个好的包结构对后期开发效率影响非常大。当时我的后端包结构是这样划分的:

  • controller:接收前端请求,只做参数校验和结果返回
  • service:业务逻辑层,项目流转、权限判断这些核心逻辑都在这一层
  • mapper:数据层接口,配合 MyBatis-Plus
  • entity:数据库实体类
  • dto:前端请求参数的封装对象
  • vo:返回给前端的数据视图对象
  • config:配置类,包含微信小程序配置、拦截器配置等
  • utils:工具类,比如 JWT 工具、日期工具、微信请求工具

这里要重点说一下 entity、dto、vo 的分离,很多同学觉得麻烦,一个 User 类走天下。但实际写下来你会发现,数据库实体类里有createTimeupdateTimedeleted这类字段,前端可能根本不需要返回;前端提交的数据未必能和数据库字段一一对应。分层之后,controller 层接收 dto,service 层做转换,外层返回 vo,每一层的字段变化都限制在局部,不至于改一个需求整个项目受伤。

小程序端的目录则是按页面和组件划分的:

  • pages/login:登录页
  • pages/index:首页
  • pages/project:项目列表和详情
  • pages/task:待办任务
  • pages/customer:客户管理
  • pages/mine:个人中心
  • components/:自定义组件
  • utils/:请求封装、工具函数

另外每个页面配套四个文件这一结构,其实可以理解为"页面结构文件 + 样式文件 + 逻辑文件 + 配置声明",小程序端页面就是一个多面体,把结构、样式、逻辑、配置拆开管理,各自高内聚,这是小程序框架本身的设计红利,用好了页面代码会非常清爽。

2. 数据库设计与流程状态机核心逻辑

2.1 核心数据表设计详解

先说说最关键的表。客户表(customer),我设计的字段包括:客户名称、联系人、联系电话、客户来源(转介绍、线上广告、展会、自拓)、所属行业、客户等级(A/B/C)、状态(潜在、跟进中、已成交、已流失)、备注、创建人、创建时间、更新时间。客户表是销售系统的地基,它和销售项目表是一对多的关系,也就是说一个客户可以对应多个销售项目,这符合真实业务——同一个客户,可能同时跟你买产品 A 和产品 B,这是两个独立的项目。

销售项目表(sales_project),这个表是整个系统的核心,字段包括:项目名称、客户ID、负责人(销售员ID)、项目金额、当前阶段编码、预计成交时间、状态(进行中、已赢单、已丢单、暂停)、阶段推进历史、赢单概率、备注等。这里有个细节值得琢磨——current_stage字段存的是阶段编码,而不是阶段名称。阶段名称是可能变化的,而阶段编码是固定的,这样即使前端展示文案变化,后端流程逻辑也不会受影响。

项目阶段配置表(project_stage_config),这张表存的是流程模板。字段有:阶段编码、阶段名称、阶段序号、所需必填字段配置、允许推进到下一阶段的角色。为什么要把阶段配置做成表而不是写死在代码里?因为不同公司的销售流程是不一样的,有的公司五个阶段、有的公司七个阶段,写死的话每次调整流程都要改代码重新发布,做成配置表之后,管理员在后台就能调整流程,代码一行不用改。对毕设来说这也多了一个功能性亮点,答辩的时候可以拿出来说"流程可配置"。

阶段推进记录表则是一个任务清单,每个阶段可能拆出若干项待办,比如"发送产品资料""预约演示""提交报价单",完成一项勾选一项,全部完成后项目才能进入下一阶段。这张表的存在,让"流程化"这三个字落地了——管理者不再只能看到一句话的项目状态,而是能看到阶段内所有事情的完成度。

2.2 状态流转的核心逻辑与实现

项目的状态流转逻辑是这个系统的大脑,也是我当时写代码时最谨慎的部分。项目状态我设计成一个枚举类ProjectStageEnum,每个阶段的编码、名称、序号都在这里定义,和数据库中的配置表对应:

public enum ProjectStageEnum { STAGE_INITIAL_CONTACT(1, "初步接洽", "INITIAL_CONTACT"), STAGE_NEEDS_COMMUNICATION(2, "需求沟通", "NEEDS_COMMUNICATION"), STAGE_SOLUTION_QUOTATION(3, "方案报价", "SOLUTION_QUOTATION"), STAGE_NEGOTIATION(4, "商务谈判", "NEGOTIATION"), STAGE_CONTRACT(5, "合同签订", "CONTRACT"), STAGE_PAYMENT(6, "回款阶段", "PAYMENT"), STAGE_DELIVERY(7, "交付服务", "DELIVERY"); private final int seq; private final String name; private final String code; }

推进项目的核心方法,我把它封装在了ProjectService中,调用链路上会做三次校验:

第一道校验:当前用户是否是这个项目的负责人。不是负责人无权操作。

第二道校验:当前阶段的所有任务是否已经完成。有任何一个任务没勾选完成,系统会直接提示"请先完成当前阶段的全部待办事项",项目无法推进。

第三道校验:目标阶段是否合法。只能从第 N 阶段推进到第 N+1 阶段,不允许跳阶段操作。回退操作倒是可以,但会记录操作日志,防止有人恶意回退数据。

public Result<String> advanceProject(Long projectId, Long operatorId) { SalesProject project = salesProjectMapper.selectById(projectId); // 校验1:操作者是否是项目负责人 if (!project.getOwnerId().equals(operatorId)) { return Result.error("只有项目负责人才能推进流程"); } // 校验2:当前阶段任务是否全部完成 Integer unfinishedCount = taskMapper .selectCount(new LambdaQueryWrapper<ProjectTask>() .eq(ProjectTask::getProjectId, projectId) .eq(ProjectTask::getStageCode, project.getCurrentStage()) .eq(ProjectTask::getCompleted, 0)); if (unfinishedCount > 0) { return Result.error("当前阶段还有未完成的待办任务,无法推进"); } // 校验3:目标阶段是否使当前阶段的下一阶段 ProjectStageEnum current = ProjectStageEnum.getByCode(project.getCurrentStage()); ProjectStageEnum next = ProjectStageEnum.getBySeq(current.getSeq() + 1); if (next == null) { return Result.error("当前已经是最终阶段,无需推进"); } project.setCurrentStage(next.getCode()); project.setUpdateTime(new Date()); salesProjectMapper.updateById(project); return Result.success("流程已推进至:" + next.getName()); }

这里有一个斩钉截铁的经验:流程状态机的校验逻辑必须放在后端,绝不能让前端只靠按钮禁用或隐藏来控制。小程序端请求接口是可以被任意人构造的,如果前面只做了界面上的推进限制而接口本身不校验,任何知道接口地址的人都能绕过界面直接把项目推到终态,这在真实系统里是不可接受的。

2.3 权限体系的坑和应对方案

权限体系这个事,我在做这个项目之前以为很简单,做完之后发现里面的坑相当多。

最简单粗暴的做法是每个用户表里加一个角色字段:1 表示销售,2 表示主管。但实际业务场景中,一个人可能既是销售又是主管,他既需要录入自己的项目,也需要查看团队的数据。直接改成多对多关系,用户表、角色表、用户角色关联表三张表,代码写起来又明显复杂,对毕设来说有点头重脚轻。

我最终的做法是"单用户多角色并存,取最高权限"。用户表里用一个roleLevel字段,1 表示普通销售,2 表示销售主管,3 表示系统管理员。主管可以查看自己负责团队所有销售的项目看板,但操作权限仍然受限——主管可以编辑项目信息,但推进流程的操作只允许项目负责人执行。管理员则拥有全部配置权限,包括管理用户、配置阶段流程、查看全公司项目数据。

这样划分之后,大部分场景都能覆盖,权限逻辑也清楚:菜单按角色级别动态加载,接口按角色做拦截。拦截器的实现其实很简单,定义一个自定义注解标记在需要权限校验的接口上,然后实现 HandlerInterceptor 做一个统一的鉴权处理,代码量不大,效果却很直观。我当时还特意在小程序前端的登录逻辑里做了角色判断,主管登录之后底部 TabBar 会多一个"团队看板"入口,这个细节在演示的时候很加分。

3. 小程序端核心功能实现要点

3.1 登录与 openid 获取全流程

小程序端登录是一个看起来很基础、但很多人搞不清楚完整链路的环节。尤其容易出错的是"为什么我的后端拿不到用户信息"。

登录链路分三步:

第一步,前端调用wx.login(),这个接口会返回一个临时凭证code。注意,这个 code 有效期只有 5 分钟,而且只能用一次,用完之后作废,所有拿到 code 一定要尽快传给后端。

第二步,后端拿到 code 之后,调用微信的接口https://api.weixin.qq.com/sns/jscode2session,用 code 换取用户的 openid 和 session_key。这个接口需要四个参数:小程序的 appid、secret、刚才传过来的 code,以及固定值authorization_code

第三步,后端拿到 openid 之后,先去数据库查这个 openid 是否已经存在。如果不存在,说明是新用户,自动创建账号;如果存在,直接基于用户信息生成一个自定义登录态返回给前端。

关于自定义登录态,我这里用的是 JWT,生成的 token 里包含 userId 和 roleLevel,设置 7 天有效期,前端每次请求都放在 header 里带过来,后端拦截器解析。之所以不用后端 session 方案,是因为小程序一旦关闭重新打开,session 的持久化往往不可靠,而 JWT 天然无状态,服务端不存任何会话记录,压力小且天然适合前后端分离。

还要特别提醒一个容易踩的坑:尽量不要在小程序端用wx.getUserProfile()直接拿用户头像昵称存入自己的数据库。从某次微信版本更新之后,这个接口返回的昵称已经统一变成"微信用户",头像也是灰色默认头像,真正的用户信息已经被微信收走了。对于需要用户昵称头像展示的业务,常规做法是让用户去"个人中心"里手动上传头像、填写昵称,或者走微信提供的头像昵称填写能力组件,这个组件会弹出微信自己的选择页,让用户选择微信头像昵称或者自定义填写。我当时系统里直接提供了编辑资料页面,用户进来自己设置,没有纠结实时拿微信头像这件事。

3.2 项目列表、详情与流程推进页

项目列表页是整个销售日常使用频率最高的页面。我做了三个维度切换:"我负责的"、"我参与的"、"团队全部",对应查询的时候就是按 ownerId 筛选、按 team 关联筛选、按数据权限全查。列表的每一项卡片展示项目名称、客户名称、当前阶段、项目金额、赢单概率,配合一个进度条样式的组件,视觉上直观展示项目走到哪一步了。

这里有个代码细节可以分享:项目列表接口必须做分页,而且最好用"滚动触底加载下一页"的方式,而不是一次性拉全部数据。销售项目一多,一次性渲染几百条数据小程序端会明显卡顿。我当时是用的onReachBottom生命周期函数实现触底加载,分页大小为 10 条,配合后端用 MyBatis-Plus 的Page分页参数,效果很平滑。

项目详情页则是流程化管理的核心展示区。上半部分展示项目基本信息,下半部分用时间线组件展示当前项目的阶段推进记录。时间线每个节点显示阶段名称、负责人、推进时间,当前阶段高亮显示。再往下是阶段任务清单,任务是勾选式的,用户勾选完成后,任务的完成状态立即回写到后端。这种"所见即所得"的交互方式,是我在原型设计阶段就定下来的——销售在外面跑,没有耐心在密密麻麻的表单里填一堆东西,好的交互应该把信息录入量降到最低。

流程推进按钮放在详情页的最底部,按钮文案会跟随当前阶段动态变化。比如当前阶段是"初步接洽",按钮文案就是"推进至:需求沟通",用户点一下,弹出确认框,显示"确认将项目推进至下一阶段?",点击确认后调用后端接口,校验通过则整个时间线刷新,新的阶段出现在时间线上。

3.3 顶部导航栏高度与页面适配

小程序顶部导航栏的高度问题,看起来很细枝末节,但真机调试时一旦没处理好,页面就会明显歪掉,体验非常拉胯,而且这个坑几乎所有第一次做小程序的人都会踩一遍。

微信小程序的顶部区域由两部分组成:状态栏(显示手机电量、时间的那一条)和导航栏(显示页面标题的那一条)。状态栏高度在不同手机上不一样,iPhone 的刘海屏和普通安卓全面屏高度都有差异。导航栏的默认高度一般是 44px 或 48px,但在自定义导航栏模式下,这个高度完全由你的代码决定。

如果你在页面的app.json或页面的.json文件里配置了"navigationStyle": "custom",那么页面顶部就完全由自己绘制,这时候需要动态获取导航栏高度来做占位。获取方式是这样的:

const systemInfo = wx.getSystemInfoSync(); const menuButton = wx.getMenuButtonBoundingClientRect(); const statusBarHeight = systemInfo.statusBarHeight; const navBarHeight = (menuButton.top - statusBarHeight) * 2 + menuButton.height;

这里面的逻辑是:wx.getMenuButtonBoundingClientRect()返回的是右上角胶囊按钮的位置信息,胶囊按钮的垂直居中位置大致就是导航栏的垂直中心,所以导航栏总高度等于"胶囊按钮顶部到状态栏底部的距离的两倍加上胶囊按钮本身的高度"。这套公式是官方社区里比较通用的算法,实测下来各机型误差很小。

我的建议是不要给自己的毕设项目增加这种自定义导航栏的工作量,直接用微信自带的基础导航栏,把标题配置好,省事儿又不容易出错。但如果你的项目有那种"首页需要放一个大搜索框"之类的需求,必须自定义导航栏,那就把上面的适配代码封装成一个工具函数放到 utils 里,在每个使用自定义导航栏的页面 onLoad 时调用一次,把返回的高度值存到 data 里,然后在 wxml 的占位 view 上绑定这个高度。千万别忘记这一步,否则你会在 iPhone 上测试没问题、换到安卓上标题就顶到状态栏里去了。

4. 微信支付 V3 对接全流程与避坑记录

4.1 V2 与 V3 的核心区别

这个项目里我用到了微信支付,过程属实是踩了不少坑,特别是小程序微信支付V3的对接,值得单独写一段。先说一下为什么直接选 V3 不选 V2,这也是很多人的疑惑。

V2 和 V3 最核心的区别在两点。第一是接口风格,V2 使用的是 XML 格式的数据交互,V3 则全面改用 JSON,这对接 Java 体系非常友好,不需要再做 XML 的序列化与反序列化。第二是签名机制,V2 用的是 MD5 或 HMAC-SHA256,V3 用的是微信支付平台证书 + 商户私钥的双向签名和验签,安全性强很多,但配置复杂度也上了一个台阶。微信官方已在推动商户迁移到 V3,新商户默认只能接 V3,所以这个方向是正确的。

V3 对接需要准备的核心材料有这四样:

  • 商户号(mchid):申请微信支付成功后分配的一串数字。
  • 商户 API 私钥:在商户平台生成,相当于你作为商户的"身份证印章",所有需要商户签名的请求都要用它。
  • 商户证书序列号:商户 API 证书里的序列号,微信用来识别你的证书。
  • APIv3 密钥:在商户平台手动设置的一串 32 字符密钥,主要用于回调数据解密或平台证书下载时的加密。

其中经常出问题的就是第四项——APIv3 密钥。很多人在商户平台设置的时候没注意,设置完之后就找不到了。这个密钥和微信支付商户平台登录密码不一样,它是在"API安全"页面设置的,设置后不可找回,只能重置。我当时就因为这个问题卡了半天,最后打了客服电话才搞清楚,有种"守着宝藏钥匙却死活找不到门"的感觉。

4.2 小程序支付下单与回调完整流程

小程序端支付的核心流程是这样的:

第一步,用户在小程序端点击"去支付",小程序把订单号发送给自己的后端服务器。

第二步,后端接收支付请求,根据订单号在本地数据库查询订单信息,包括订单金额、商品描述、用户 openid,然后调用微信支付的下单接口。

第三步,微信支付接口返回一个prepay_id(预支付交易会话标识)。后端拿到这个 prepay_id 之后,需要生成一个新的签名,然后返回给前端以下五个参数:

appId timeStamp nonceStr package(值为 `prepay_id=xxx`) signType(RSA)

这里的 signType 是重点中的重点。V3 的签名算法是 RSA,和 V2 时代的 MD5 完全不同。很多人对接的时候报了sign error或者invalid signature,绝大多数是因为这里的签名算法弄错了。

第四步,小程序端拿到这五个参数以后,调用wx.requestPayment()拉起支付界面。用户输入密码、支付成功,微信会以异步的方式发送支付结果通知到你的回调地址。

第五步,后端拿到回调通知之后,需要先做验签,再解密报文拿到支付结果,更新数据库里的订单状态。这一步也是 V3 区别于 V2 最明显的地方:回调通知的报文是加密的,需要用你的 APIv3 密钥做 AES-256-GCM 解密。

整个流程中我有一个特别深刻的教训:回调接口必须要有幂等处理。因为微信的支付回调不保证只发送一次,极端情况下同一个支付结果会回调多次。如果你在回调里执行的是"更新订单状态为已支付",这个操作本身是幂等的,没关系;但如果你不小心在回调里执行"给用户增加余额"这样的操作,回调重复一次用户余额就会多出一份。所以回调处理的代码里,一定要先查订单状态,判断这个订单是否已经处理过了,如果已处理,直接返回成功应答,不再重复执行业务逻辑。

@PostMapping("/notify/pay") public String payNotify(@RequestBody String requestBody, @RequestHeader("Wechatpay-Timestamp") String timestamp, @RequestHeader("Wechatpay-Nonce") String nonce, @RequestHeader("Wechatpay-Signature") String signature, @RequestHeader("Wechatpay-Serial") String serial) { // 1. 验签,确认请求来自微信支付 boolean verify = wxPayService.verifyNotifySignature(requestBody, timestamp, nonce, signature, serial); if (!verify) { return failResponse(); } // 2. 解密通知报文 WxPayNotifyResult notifyResult = wxPayService.decryptNotifyData(requestBody); String outTradeNo = notifyResult.getOutTradeNo(); // 3. 幂等处理:先查订单状态 Order order = orderMapper.selectByOutTradeNo(outTradeNo); if (order != null && "PAID".equals(order.getStatus())) { return successResponse(); } // 4. 更新订单状态,记录支付流水号 order.setStatus("PAID"); order.setTransactionId(notifyResult.getTransactionId()); orderMapper.updateById(order); return successResponse(); }

这个小节的代码逻辑,是我在整个毕设里最骄傲的部分,因为它在真实业务上也是完全能跑通的写法。每次答辩或者给同学演示的时候,我会特意把这段代码拉出来详细讲一下为什么要先查状态再更新,既能展示业务思考,也能体现工程素养。

4.3 平台证书与"无可用的平台证书"报错

微信支付V3这个"无可用的平台证书"报错,是我当时搜遍全网才搞定的一个问题,现在看到这个关键字都觉得亲切。这个问题出现的背景是这样的:V3 接口在回调验签和解密时,正常情况下应该使用微信支付平台证书。但这个证书的有效期只有一年,过期之后需要更新。

很多用 SDK 的同学会遇到这个报错,是因为在初始化 SDK 的时候下载了平台证书并保存在本地,但证书过期后,本地文件没有更新,SDK 拿着过期的证书去验签或解密,就会提示平台证书不可用。

我当时用微信官方推荐的wechatpay-javaSDK,这个问题可以通过配置wechatpay4j的方式来解决——在商户平台下载新的平台证书,放到项目的src/main/resources/cert/目录下,然后在配置文件里指定证书路径。注意,平台证书和商户证书是两回事,商户证书是证明"你是你",平台证书是证明"微信是微信",配置的时候别放颠倒,文件名也别写错。

另一个更加省心的思路是用"自动更新平台证书"方案:SDK 启动时先从微信服务器动态拉取最新的平台证书,而不是用本地文件。这种方式避免了手动更新证书的烦恼,但要求首次启动时网络环境能正常访问微信支付服务器,部署到内网服务器时需要额外放通白名单。

4.4 小程序违规与支付功能被限制的应对

这个场景虽然不太愉快,但确实值得记录一下。我开发的系统在测试阶段经历过一次"小程序违规,支付功能暂时无法使用"的限制,原因是小程序的主体资质没有完成微信认证,支付权限被临时关闭。当时用户一打开支付页面,就出现"由于小程序违规,支付功能暂时无法使用"的提示。

这里要重点说明一下,这个提示并不一定是真的"违规",更多情况下是主体的微信认证没完成、支付权限没有成功开通,或者开通了但没通过审核。排查路径是:打开微信公众平台 → 左侧菜单"支付" → 查看支付权限状态。如果是"未开通"状态,需要先完成小程序认证(个人主体无法认证,必须企业或个体工商户主体),然后申请微信支付,提交营业执照和银行账户信息,审核通常需要一至三天。

如果你在开发测试阶段还没申请到正式的微信支付商户号,可以暂时在开发者工具里使用虚拟支付模拟体验:在开发者工具中点击"编译模式"旁边的下拉箭头,选择"添加编译模式",然后勾选"模拟支付接口"选项,这样无需真实的商户号也能调通整个支付流程,等到正式上线前再替换成真实参数就行。这个功能对毕设演示和开发联调来说简直救命,我当时就是靠这个方式在没有商户号的情况下把前端支付流程全部调通的。

5. 常见问题与排查技巧实录

这部分我整理一下整个项目开发过程中遇到的高频问题和对应的排查思路,这些问题有些是我踩过的,有些是我帮同学排查过的,每一类都很有代表性。

5.1 图片保存到相册失败

uniApp或原生小程序中调用wx.saveImageToPhotosAlbum()时提示saveImageToPhotosAlbum:fail,这个报错的常见原因有两个:

第一个原因是缺少权限声明。在小程序的app.json文件中,需要配置"permission"字段,尤其是安卓平台,读取相册权限必须在配置文件里声明。同时,在调用保存图片之前,最好先通过wx.getSetting检查当前用户的权限状态,如果用户拒绝了授权,要引导用户去wx.openSetting里手动开启权限,这一步不能省略。

第二个原因是图片格式问题。saveImageToPhotosAlbum要求图片必须是本地路径或临时路径,如果传入的是网络图片 URL,会直接失败。正确的做法是先用wx.downloadFile把网络图片下载到本地临时路径,拿到临时路径之后再调用保存接口。如果是 base64 格式的图片,还得先通过wx.getFileSystemManager().writeFile写成临时文件才能保存。

5.2 手机软键盘遮挡输入框

真机调试时,在小程序里填写表单,软键盘弹起来后把输入框或查询内容挡住,是一个极其常见但也极其影响体验的问题。原因在于页面底部固定元素(如提交按钮)没有随软键盘的出现而上移。

我从这个项目里总结出来的常规方案是这样:不要把按钮设计成固定在页面底部的形式,取而代之把底部操作区放在内容流里,让整页内容自然滚动。如果业务上确实需要底部固定按钮,可以使用adjust-position属性来控制页面整体上移,或者监听键盘高度wx.onKeyboardHeightChange(),手动给底部按钮设置一个动态的bottom值,等于键盘高度。

当然,更粗暴也更好用的办法是,在小程序页面的.json配置文件里把"disableScroll": false,同时使用cursor-spacing属性给输入框设置光标与键盘之间的间距。像搜索查询场景,只要给输入框设置了合理的cursor-spacing="100",软键盘弹出后遮住查询内容的概率就会降低很多。

5.3 蓝牙搜索部分手机收不到设备

如果你的销售系统场景里用到了蓝牙搜索设备(比如连接便携打印机),会遇到"有的手机能搜索到设备,有的手机搜索不到"的问题,这个问题的根源往往不在代码,而在手机系统的蓝牙权限策略。

排查思路分三步走。第一步,确认小程序已在app.json中声明蓝牙相关权限;第二步,确认手机系统授予了微信应用"位置信息"权限,这一点尤其在 iOS 设备上非常严格——蓝牙扫描会用到位置权限,没有位置权限,蓝牙扫描大概率失效;第三步,在代码中开启蓝牙适配器之前,先检查手机蓝牙是否已经打开以及系统权限是否已经授好。

wx.openBluetoothAdapter({ success: function (res) { wx.startBluetoothDevicesDiscovery({ allowDuplicatesKey: false, interval: 2000, success: function () { console.log('开始搜索蓝牙设备'); } }); }, fail: function (err) { console.log('蓝牙适配器初始化失败', err); } });

如果你确定权限都开了还是搜索不到,可以在同样的位置用"微信->发现->小程序"里的官方蓝牙调试工具对比测试,如果官方工具也搜不到,那基本可以排除代码问题,判断为手机系统或硬件兼容性问题。

5.4 微信开发者工具与真机表现不一致

真机调试和开发者工具表现不一致,这个属于"经典中的经典"。常见表现有:开发者工具上样式正常,真机上布局错乱;开发者工具上接口请求正常,真机上请求失败;开发者工具上支付模拟正常,真机上无法调起支付。

排查顺序是这样的:

先看详情 -> 本地设置里有没有勾选"不效验合法域名"。开发者工具默认对请求域名做合法性校验,如果没有在微信公众平台配置合法服务器域名,就必须勾选这个选项才能调试。但真机上没有这个开关,真机运行时必须配置合法域名才行,否则请求直接被拦截。

再看wx.request的请求地址是不是用了 localhost。开发者工具里 localhost 指向电脑本机,真机上 localhost 指向手机自己,完全不是一回事。正确的联调方式是让电脑和手机处于同一个局域网,电脑端启动后端服务监听 0.0.0.0,然后真机调试时把请求地址改成电脑的局域网 IP,比如http://192.168.1.100:8080

最后看有没有用到真机才有的原生能力。像微信支付、蓝牙、扫码这类能力,在开发者工具里只是模拟,很多细节在真机上行为完全不同。这类问题没有捷径,建议在每个真机调试环节集中完整测一遍,并写一个自测清单,避免反复打包发现遗漏。

5.5 小程序源码保护与反编译话题

开发过程中还有一个绕不开的话题,是"已经部署的小程序能不能拿到源码"。我必须先说结论:可以,而且难度远比你想象的低。小程序代码包在发布时会打包成wxapkg文件,网络上流传的反编译工具可以还原出大部分前端代码,包括页面的 wxml、wxss、js 逻辑。

这给我们的警示是:永远不要把敏感信息写在客户端代码里。我在这个项目中特别注意了一点,小程序端的代码不包含任何后端接口密钥、数据库连接信息、加密私钥,后端接口所有敏感操作都必须有鉴权校验。如果你的项目里有管理员的默认密码,或者微信支付的 APIv3 密钥,一旦被反编译提取到,后果就是整个系统裸奔。

从开发规范上讲,前端代码要假设"任何人能看到",所以前端的代码要干净、无密钥、无敏感逻辑。需要保密的业务逻辑应该永远放在后端服务中。对毕设来说,这个安全习惯不仅是在作业里体现专业度,也是未来进入工作时最基础的安全红线。

6. 这套系统的价值延伸与扩展方向

做完整个毕设,我最大的感受是:毕业设计的价值不在于代码量有多少,而在于它是否能真正解决一个具体场景的问题。销售项目流程化管理系统这个小程序,切入点不大,但业务链路完整——从客户信息维护、项目阶段流转、待办任务管理,到支付流程、统计看板,每一条链路都有实际业务场景做支撑,这比浮于表面的"XX管理系统"扎实得多。

如果在现有基础上继续扩展,有几个方向非常值得做。

第一个方向是数据分析看板。项目数据沉淀下来之后,可以统计每个销售人员的项目转化率、平均成交周期、阶段停留时长,甚至能推出"哪个阶段最容易丢单"这样对管理真正有用的结论。我当时只做了基础的按状态统计,这个已经能在答辩时展示得不错了。

第二个方向是流程模板的可视化配置。目前阶段配置虽然做成了数据库表,但调整规则还是需要后端代码更新。如果做一个管理端页面,让管理员通过拖拽的方式配置流程阶段、必填字段、审批人,这套系统的适应面会从销售场景扩展到更多行业。当然这部分完整做下来工作量不小,适合作为二期规划。

第三个方向是消息通知的智能触达。比如某个项目在需求沟通阶段停留超过七天,系统自动给对应销售推送一条"该跟进了"的提醒;比如客户长时间未跟进时,系统自动给主管发送预警。这类智能化提醒对销售业务的提效非常明显,实现也不复杂,就是在定时任务里查一下阶段状态和更新时间,触发对应的订阅消息发送。

这三个方向里,我当时选了第三个作为进阶功能做了一个简化版本:订阅消息发送。说实在的,微信订阅消息本身比想象中麻烦一点,主要在于小程序需要用户主动订阅授权,而且每次授权只能推送一条内容。但即便如此,"系统主动找用户"这个体验,还是要比"用户自己打开小程序查看"高一个量级。这个功能当时在答辩演示环节提供了不少亮点,老师们普遍觉得这个不是玩具,是真的有使用场景。

7. 写在最后的几点真心话

项目做完了,答辩也顺利过了,回头再看整个过程,有几句话想对准备做类似项目的同学说。

第一,毕业设计的功能宁做深不做宽。我当时其实想过加考勤打卡、加销售排行榜、加日报周报,但后来都砍掉了,只保留了客户、项目流程、任务、支付、统计这五个和"销售项目流程化"最相关的核心模块。功能越聚焦,每个模块能打磨的细节越多,答辩的时候展示起来也越有底气。

第二,技术选型不要盲目追新。Spring Boot 2.7 足够稳定,MyBatis-Plus 足够顺手,小程序原生语法虽然啰嗦但资料多。追求最新的技术栈不是不行,但毕业设计的时间成本摆在那里,与其花两周踩新版本的坑,不如把这些时间用在把业务流程打磨得更完整上。

第三,数据设计要反复推敲。状态字段用字符串还是数字、项目金额用什么类型、时间字段如何统一格式,这些细节表面上不起眼,但等到联调阶段出现各种类型转换问题、筛选条件对不上数据的时候,你就会明白前期设计的重要性。

第四,也是最重要的一条,一定要在开发之前把业务流程画清楚。我当时画了一张完整的业务流程图贴在自己桌子前面,每个阶段做什么事、谁能操作、数据怎么流动,都写清楚以后才开始写代码。事实证明,这个先行的设计过程帮我省去了大量的返工时间。整个过程做下来,写代码其实是最简单的一环,难的是想清楚各个业务节点之间的约束和关系。

最后说个私心的话。毕设并不是"做完交了答辩就完事儿"的阶段性产物,你在选题时如果可以贴近真实应用场景,做完之后的收获会比做一个"题库管理"或者"宿舍报修"大得多。因为真实的业务场景会逼你想清楚权限、状态机、幂等、数据一致性问题,做完了,你就真的会用这些概念了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询