简介:一份基于Java、Swing、SWT与SQLServer开发的超市会员管理系统课程设计项目,在Eclipse环境中完成,面向正在完成Java课程设计或想学习桌面数据库应用开发的学生,系统围绕超市业务场景,实现会员建档、商品信息维护、会员卡充值消费以及消费记录查询统计等核心功能。资源以zip格式打包,共270个文件,大小仅4.13MB,其中包含40个Java源文件、152个class编译文件、52个PNG界面图片及13个JAR依赖库,可直接导入Eclipse查看源码和运行界面,也便于对比源代码与编译结果。从编译后的class文件名可以看出,项目按会员首页、后台管理、会员卡数据操作等模块进行划分,层次清楚,能帮助理解Java Swing/SWT界面开发与SQLServer数据库连接的完整流程。目前已有80人浏览学习,适合课程设计、毕业设计参考,也可作为自学Java GUI编程和数据库应用开发的实践项目。
1. 课程设计为什么是超市会员管理系统这个配方
在多数高校的 Java 课程设计里,“超市会员管理系统”经常和图书管理系统并列出现,稳坐选题表前两名。原因很简单:它的业务边界足够清晰——会员信息的新增、修改、积分累计、等级迭代、历史消费记录,几乎把一门 Java 课里最常考的东西全部装进去了:集合操作、事件监听、数据库连接、增删改查、简单的事务回滚。换到面试场景,这套项目又能直接产出一批经典的八股文问题:Swing 的事件分发线程、JDBC 的 PreparedStatement 防注入、SQL Server 的字符串转数字和日期比较,都能在这个项目里找到真实落点。
你可能会问,既然现在 Web 管理系统这么普及,为什么还要用 Java + Swing + SWT + SQL Server 做桌面程序?答案藏在“课程设计”四个字里。课程设计要考察的是你在不依赖互联网框架的前提下,能否独立完成一个从数据库建表到界面事件响应的闭环;而桌面 GUI 恰恰把数据库连接、内存对象、界面刷新这三层的耦合关系暴露得最彻底。使用 Eclipse 开发这类系统,不是为了赶上世纪的老旧时髦,而是因为 Eclipse 本身还保留着 SWT 的官方插件生态,同时又能让你在 JFrame 和 Shell 两种组件模型之间反复横跳。
接下来的六章,我会按“数据模型先行 → JDBC 打通 → 界面逻辑分离 → SQL Server 边界处理 → 打包演示”的顺序,把这个项目从 0 到能运行、能答辩、能加分地拆开。你不用照抄某份神秘源码,只要学会这一套惯用做法,自己写一个复现版也就两三天的事。
2. Swing、SWT 并存怎么选:先把数据模型和 SQL Server 设计方案立住
2.1 同一项目里混用 Swing 与 SWT 的边界
很多课程设计要求会同时写出“Swing、SWT”两个词,但这不代表同学们就得在同一套窗口里把它们强行混写。Swing 的运行依赖 JFrame 的 RootPane 和事件队列恢复,SWT 则基于操作系统原生的 Shell 和 Display。真正把组建嵌入到 JFrame 里写到一个组件树上,虽然技术上能走通,但你要同时处理两套事件循环的互斥,半吊子会直接让 UI 卡死,这是常见的翻车点。
我一般建议的稳妥方案是:主管理界面用 Swing 完成,把 SWT 单独封装成一个“扩展报表窗口”。Swing 主程序通过一个按钮触发SWT 窗口启动,SWT 内部去访问同一个 Service 层,数据库连接复用同一份 JDBC 连接池。这样既能说明你同时掌握了两个 GUI 工具包,又不会陷入两者混用的泥潭。
典型的做法是:
// Swing 按钮的 actionPerformed 里调用 private void openSwtWindow(ActionEvent e) { Display display = Display.getDefault(); // 这里确保在 SWT 线程中创建 Shell display.syncExec(() -> { Shell shell = new Shell(display); // 加载报表,展示会员积分排名 }); }如果你实在想在一个容器里嵌入 SWT 组件,可以靠SWT_AWT桥接,把 SWTComposite放进 AWT 的 Canvas,再塞进 Swing 容器。但这是个两难的方案,事件线程和跨平台渲染都会变复杂,课程设计阶段不建议冒险。
2.2 SQL Server 会员库表结构设计
数据库设计要先于界面编码。我建议起码建四张表:会员表、积分流水表、商品分类表、消费记录表。在实际课程设计里,商品分类表是可选的;如果你想在答辩时演示一个像样的“消费积分联动”,就不要只做单表 CRUD。
下面的建表脚本是一个最小可用版本:
CREATE DATABASE SupermarketMember; GO USE SupermarketMember; GO CREATE TABLE Member ( MemberID INT IDENTITY(1,1) PRIMARY KEY, CardNo VARCHAR(20) NOT NULL UNIQUE, RealName NVARCHAR(20) NOT NULL, Phone VARCHAR(11), BirthDate DATE, Level TINYINT DEFAULT 1, -- 1普通 2银卡 3金卡 Points INT DEFAULT 0, JoinDate DATETIME DEFAULT GETDATE(), Status TINYINT DEFAULT 1 -- 1正常 0冻结 ); CREATE TABLE PointLog ( LogID INT IDENTITY(1,1) PRIMARY KEY, MemberID INT NOT NULL, ChangePoints INT NOT NULL, ChangeReason NVARCHAR(100), OperatorName NVARCHAR(20), ChangeTime DATETIME DEFAULT GETDATE(), FOREIGN KEY (MemberID) REFERENCES Member(MemberID) ); CREATE TABLE SaleRecord ( SaleID INT IDENTITY(1,1) PRIMARY KEY, MemberID INT NULL, GoodsName NVARCHAR(50), Quantity INT, UnitPrice DECIMAL(10,2), SaleTime DATETIME DEFAULT GETDATE() );先做一个简单的关联:会员主表保存总积分,积分流水表只保存增量,消费记录表每次购买商品会同时插入一行。MemberID作为外键贯穿三个表,积分总和不用实时SUM,只在消费时更新Member.Points,这一设计能让后续界面的JTable刷新速度明显更快。
2.3 为什么用整数积分而不是带小数的金额
课程设计答辩时,老师最爱问“你的积分怎么算”。如果你用消费金额乘以一个浮点系数,就会碰上标题热搜里的“字符串转数字”和“数字取整”一类问题。用整数积分是最简单的解决办法:消费满 1 元积 1 分,不足 1 元的按 1 分累积。这个规则看上去不优雅,但避免了RoundingMode与 SQL Server 的decimal精度纠缠。
更合理的方式是把积分计算写在后端 Service 层,而不是写死在 SQL 的 UPDATE 语句里:
private int calcPoints(BigDecimal amount) { return amount.setScale(0, RoundingMode.CEILING).intValue(); }保存会员时如果遇到NumberFormatException,大概率是因为你把Phone或CardNo里的字符串直接Integer.parseInt(),而不是用归一化后的int积分字段。核心规则:数据库存INT,Java 端拿Integer,只在界面层做字符串格式化。
2.4 SQL Server 与 Eclipse 开发环境的 JDK 版本配套
用 Eclipse 2022 之后版本配 JDK 11/17 非常顺,但 SQL Server 驱动通常也支持老旧 JDK。需要注意sqljdbc4.jar和mssql-jdbc-*.jar的包名不同:现代版本类名是com.microsoft.sqlserver.jdbc.SQLServerDriver,旧版本是com.microsoft.jdbc.sqlserver.SQLServerDriver。写错驱动类名是课程设计第一大连接错误来源。
环境配置中,“宝塔安装 sqlserver”或“Windows 安装 sqlserver”这一类热搜词频繁出现,是因为很多同学的 SQL Server 默认没有开 TCP/IP。打开 SQL Server 配置管理器,启用SQLEXPRESS或默认实例的 TCP/IP 协议,端口设为 1433,然后重启服务。这些步骤虽然基础,但值得你先从“SQLServer 安装教程”这类短搜索中确认完再做 JDBC 测试。
3. 在 Eclipse 里打通 SQL Server 连接:驱动配置与连接池
3.1 eclipse 项目结构中放 JDBC 驱动的标准姿势
在 Eclipse 里建普通 Java Project,在项目根目录下新建lib文件夹,然后把mssql-jdbc-12.x.x.jre11.jar拖进去。选中包后右键 Build Path — Add to Build Path。这一步后,项目中会出现Referenced Libraries节点,表示驱动已经编译可见。
因此不要直接双击 jar 包或把它加到系统环境变量 CLASS_PATH,课程设计机房的 Eclipse 是绿色版,重启后会消失,最稳的方式是把 jar 放在项目lib下并随项目一起复制。
3.2 数据库连接工具类 DbUtil 写法
连接工具类写得好,会直接避免“sqlserver服务启动不了错误码17051”这类问题。虽然 17051 一般是 SQL Server 服务状态异常,但你在代码层面单独写一个连接池,能让每次重试都有干净的驱动实例。
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DbUtil { private static String url = "jdbc:sqlserver://localhost:1433;" + "databaseName=SupermarketMember;" + "encrypt=true;trustServerCertificate=true;"; private static String user = "sa"; private static String password = "你的密码"; static { try { Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } }这里有两个容易忽略的参数。encrypt=true只在较新驱动中默认开启,如果你用的是 SQL Server 2022 里的默认证书,必须配合trustServerCertificate=true,否则会报证书校验失败。但老项目里的sqljdbc4.jar不接受这两个参数,所以要根据驱动版本来增删。databaseName和instanceName不能同时在 URL 中出现,命名实例要单独写jdbc:sqlserver://localhost\\SQLEXPRESS;databaseName=...。
3.3 用连接池还是每次新建 Connection
在简单课程设计里,每次操作都新建Connection是可以接受的,毕竟并发量只有答辩现场的几个人。但要想让代码有工程感,可以做成一个小型的单例BasicDataSource,只是不要引入 Apache Commons DBCP 的完整依赖。Swing 的程序中连接生命周期很短,每次关闭Connection、Statement、ResultSet三个对象即可。
一个实用性很强的finally关闭模板:
public void close(ResultSet rs, Statement stmt, Connection conn) { try { if (rs != null) rs.close(); } catch (SQLException e) {} try { if (stmt != null) stmt.close(); } catch (SQLException e) {} try { if (conn != null) conn.close(); } catch (SQLException e) {} }连接关闭的顺序必须从里到外,先结果集,后语句,再连接;如果把连接先关了,驱动会隐式回收 statement,后续想读取延迟加载的ResultSet就会直接报No operations allowed after connection closed。
3.4 验证连接成功的两种方式
清爽的方式是写一个主方法直接跑getConnection(),然后SELECT 1。但更贴合会员系统的验证方式是查询一张空表的行数:
public static void main(String[] args) { try (Connection conn = DbUtil.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT COUNT(*) AS c FROM Member")) { if (rs.next()) { System.out.println("连接成功,会员表行数:" + rs.getInt("c")); } } catch (SQLException e) { System.err.println("连接失败:" + e.getMessage()); } }如果到这里报The driver could not establish a secure connection,先去 SQL Server 配置管理器中确认实例名和协议。如果报Login failed for user 'sa',多半是身份验证模式没改成SQL Server 和 Windows 身份验证模式。这些报错占据 SQL Server 安装教程热搜的大量篇幅,其实都是图形化界面里三个选项的问题。
4. Swing + SWT 会员管理界面编写:登录、会员列表和积分录入
4.1 用 JFrame 搭主壳,把业务逻辑放到 Service 层再给 SWT 窗口复用
课程设计要求“界面不崩溃”,核心是把事件监听器写得短。不要在actionPerformed里直接写几十行 SQL,而是抽象成一个MemberService类。Swing 主窗口只负责收集输入并调用 service,SWT 的销售窗口也调用同一个 service。
用如下代码展示一个最小登录login行为的监听器:
private void onLoginButtonClicked(ActionEvent event) { String cardNo = cardField.getText().trim(); String password = new String(passwordField.getPassword()); // 不在这里直接连数据库 Member member = memberService.login(cardNo, password); if (member != null) { JOptionPane.showMessageDialog(this, "欢迎:" + member.getRealName()); openMainPanel(member); } else { JOptionPane.showMessageDialog(this, "卡号或密码错误"); } }参数说明:cardNo从文本框取出来后立刻trim(),避免把空格存到数据库;password使用getPassword()返回char[],这是 Java 面试题中“为什么不建议用 String 存密码”的常考答案。这一层把字符串校验放在 UI 的监听器里,能阻止 SQL 注入以外的空值问题。
4.2 会员表格模型:不要让 JTable 直接操作数据库
把整个会员列表写到JTable时,最省事的方式是用DefaultTableModel装载Vector数据,但它会让客户端的排序逻辑失效。我比较喜欢自定义一个AbstractTableModel,将 List 作为数据源。
public class MemberTableModel extends AbstractTableModel { private final String[] columns = {"会员ID", "卡号", "姓名", "电话", "积分", "等级"}; private List<Member> data; public MemberTableModel(List<Member> data) { this.data = data; } @Override public int getRowCount() { return data.size(); } @Override public int getColumnCount() { return columns.length; } @Override public Object getValueAt(int rowIndex, int columnIndex) { Member m = data.get(rowIndex); return switch (columnIndex) { case 0 -> m.getId(); case 1 -> m.getCardNo(); case 2 -> m.getRealName(); case 3 -> m.getPhone(); case 4 -> m.getPoints(); case 5 -> "普通卡".equals(m.getLevelName()) ? "普通" : m.getLevelName(); default -> ""; }; } @Override public String getColumnName(int column) { return columns[column]; } }在 JTable 上,不管底层数据来自 SQL Server 还是集合,视图只关心getValueAt返回的字符串或数字。你后续要做的所有“排序、筛选”都能基于这个 Model 进行内存操作,避免每点一列就重新查库。
4.3 SWT 报表窗口显示积分流水
亮点来自把 SWT 嵌入到 Swing 项目中。常见做法是点“积分明细”按钮时,单独开一个 SWT Shell。SWT 的表格控件Table比 Swing 的 JTable 原生感更好,尤其适合列大量字段的流水列表。
public void openSwtPointLog(Member member) { Display display = Display.getDefault(); display.syncExec(() -> { Shell shell = new Shell(display); shell.setText("积分流水 - " + member.getRealName()); shell.setSize(560, 400); Table table = new Table(shell, SWT.BORDER | SWT.FULL_SELECTION); table.setHeaderVisible(true); table.setLinesVisible(true); String[] titles = {"时间", "积分变化", "原因", "操作员"}; for (String title : titles) { TableColumn column = new TableColumn(table, SWT.NONE); column.setText(title); column.setWidth(120); } List<PointLog> logs = pointLogService.listByMember(member.getId()); for (PointLog log : logs) { TableItem item = new TableItem(table, SWT.NONE); item.setText(new String[]{ formatDateTime(log.getChangeTime()), String.valueOf(log.getChangePoints()), log.getChangeReason(), log.getOperatorName() }); } table.pack(); shell.open(); }); }这段代码的关键在于syncExec。SWT 的Display循环必须由主线程或明确控制的 UI 线程驱动;如果在 Swing 事件线程直接调用Shell.open(),会出现 UI 不安定,尤其在高分屏缩放底下直接黑窗口。所以任何“由 Swing 发起、SWT 接收”的操作,都建议用syncExec或asyncExec包装一层。
4.4 消费积分联动中的命名参数与事务
录入一笔消费记录后,你往往要做两件事:插入SaleRecord,更新Member.Points。这两步必须在一个事务里完成;否则程序写到一半崩溃,会出现“消费了但积分没到账”。
public void consumeAndAddPoints(Member member, SaleItem item) throws SQLException { String sql = "UPDATE Member SET Points = Points + ? WHERE MemberID = ?"; String insertSale = "INSERT INTO SaleRecord(MemberID, GoodsName, Quantity, UnitPrice) VALUES (?,?,?,?)"; try (Connection conn = DbUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps1 = conn.prepareStatement(insertSale); PreparedStatement ps2 = conn.prepareStatement(sql)) { ps1.setInt(1, member.getId()); ps1.setString(2, item.getGoodsName()); ps1.setInt(3, item.getQuantity()); ps1.setBigDecimal(4, item.getUnitPrice()); ps1.executeUpdate(); ps2.setInt(1, item.calcPoints()); ps2.setInt(2, member.getId()); ps2.executeUpdate(); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } } }参数说明:Points = Points + ?取代“先查询再 UPDATE”,这避免了多次读一致性问题。PreparedStatement的setBigDecimal必须传BigDecimal而不是double,这是 SQL Server 字符串转数字中最常见的“精度被截断”槽点。事务结束后连接关闭,中间结果只存在数据库的日志里。
5. SQL Server 数据边界处理:字符串转数字、日期函数、LEFT JOIN 取第一条
5.1 字符串转数字的两个安全语义
移除热搜里的“sqlserver字符串转数字”,我们回到会员管理里的实际场景:用户导入 Excel 文本会员名单,电话、积分都来自NVARCHAR字符列。在 SQL Server 中把字符串转成INT,最直接的是CAST(PointsString AS INT),也可以通过CONVERT(INT, PointsString)实现。
但“8000 积分”这类含有空格和单位的数据,CAST会直接报转换异常。两个常用防守手段是TRY_CAST和ISNULL:
SELECT MemberID, RealName, TRY_CAST(Points AS INT) AS Points FROM Member;TRY_CAST转不过去时返回NULL,不会生成异常阻断整体查询。SQL Server 2012 以上都支持这个函数,课程设计环境通常是 2016+,不必担心兼容问题。更保守的清洗是:
SELECT LTRIM(RTRIM(REPLACE(phone, '-', ''))) FROM Member WHERE phone LIKE '%_%';5.2 日期函数处理会员到期与最近消费
会员体系经常要算“180天以上未消费成为沉睡会员”。这时需要DATEDIFF或DATEADD组合。
SELECT MemberID, RealName, DATEDIFF(day, JoinDate, GETDATE()) AS membershipDays FROM Member; SELECT MemberID, RealName FROM Member WHERE LastSaleTime <= DATEADD(day, -180, GETDATE());这里要特别说明DATEDIFF的边界:它只计算两个时间点之间跨越的边界数,不是精确的小时差。想计算“超过 180 天没消费”,最稳的是比较时间点LastSaleTime < DATEADD(day, -180, GETDATE()),不要用DATEDIFF(day, LastSaleTime, GETDATE()) > 180,后者在当天的时间点触发时经常多算一天。
热搜词里“取三个日期中最大的”也出自 SQL Server 日期比较。SQL Server 没有MAX(a, b, c)函数,只能嵌套:
SELECT CASE WHEN JoinDate >= LastSaleTime AND JoinDate >= LastUpdateTime THEN JoinDate WHEN LastSaleTime >= JoinDate AND LastSaleTime >= LastUpdateTime THEN LastSaleTime ELSE LastUpdateTime END AS maxDate FROM Member;更简练的是用IIF嵌套,但课程设计答辩时写CASE更容易换分析分数。
5.3 LEFT JOIN 取第一条:ROW_NUMBER 窗口函数
在处理销售记录和积分流水时,经常需要“每个会员最近一条消费记录”。普通GROUP BY会丢字段,投影出历史不准确记录。惯用做法是用ROW_NUMBER()配合LEFT JOIN:
SELECT m.MemberID, m.RealName, t.GoodsName, t.SaleTime FROM Member m LEFT JOIN ( SELECT MemberID, GoodsName, SaleTime, ROW_NUMBER() OVER (PARTITION BY MemberID ORDER BY SaleTime DESC) AS rn FROM SaleRecord ) t ON t.MemberID = m.MemberID AND t.rn = 1;参数说明:PARTITION BY MemberID把每个会员的数据分成一个窗口,ORDER BY SaleTime DESC让最新消费记录排第一,t.rn = 1取得首条。该写法是 SQL Server 中“分组取第一条”的标准答案。如果你看到别的笔记里用LEFT JOIN+MAX(SaleTime),能查询出风控上需要去重的搭配,那种写法在积分流水明细有重复时间点时不可靠。
5.4 删除重复数据且只保留一条:无 ID 表怎么办
热搜词里“sqlserver删除重复数据只保留一条无id”很适合放在要回答的问题中。如果会员表因为导入故障出现了重复卡号,且表里没有独立 ID,你不能一次删除多条。一个比较稳的方案是先给表加自增列,再按窗口函数删:
ALTER TABLE Member ADD RowID INT IDENTITY(1,1); GO ;WITH cte AS ( SELECT CardNo, ROW_NUMBER() OVER (PARTITION BY CardNo ORDER BY RowID DESC) AS rn FROM Member ) DELETE FROM cte WHERE rn > 1; GOCTE的名字出现在热搜词“sqlserver cte materialized”里。这里需要强调:DELETE FROM cte在 SQL Server 中会被解析为基础表的删除操作,不是原封查询。ORDER BY RowID DESC会保留最新的行;如果想要保留最早的行,改成ORDER BY RowID ASC。
5.5 SQL Server 服务启动异常 17051 的快速处理
标题热搜里有“sqlserver服务启动不了错误码17051”。此问题多见于安装 SQL Server 2022 之后,服务第一次启动失败。原因一般是 SQL Server 配置管理器中的实例权限或轻量池启用异常。在服务管理器中找到 SQL Server(MSSQLSERVER),右键重启;如果失败,打开 SQL Server 配置管理器,进入“SQL Server 服务”选项卡,确认启动模式是“自动”,再确认登录身份不冲突。
部分原因是系统账户密码过期,把登录身份改成NT Service\MSSQLSERVER即可。你的 Eclipse 项目不做任何特殊处理,只要服务恢复且 TCP 端口连通,连接代码无需改动。这也是课程设计里“配置优于编码”的典型例子。
6. 把课程设计打包成可演示的 JAR:构建脚本与排错
6.1 使用 Eclipse Export 生成可执行 JAR
到答辩阶段,你需要一个双击就能跑的程序,而不是只在 Eclipse 里点绿色三角按钮。菜单里选File → Export → Java → Runnable JAR file,启动类选包含main的那个;在Library handling中选第三项Copy required libraries into a sub-folder next to the generated JAR。这种方式会让lib下生成完整驱动包,比“Extract required libraries”更不容易丢失第三方依赖。
导出后,程序会要求 SQL Server 正在监听 1433 端口。若换到现场演示机,最快的做法是让项目根目录下存一个db.properties配置文件,程序启动时动态读取数据库地址和账号:
Properties props = new Properties(); try (InputStream in = new FileInputStream("db.properties")) { props.load(in); String url = props.getProperty("db.url"); String user = props.getProperty("db.user"); String pwd = props.getProperty("db.password"); }好处显而易见:连接字符串、用户名密码都不需要回到 Eclipse 里改代码,把 JAR 和 properties 一起复制到新机器即可。注意:数据库地址不能写localhost就了事,如果 SQL Server 和你的演示机不在同一台机器,必须写 IP 地址或主机名。
6.2 日志与弹窗前先想清楚异常路径
与其把e.printStackTrace()直接刷到控制台,不如在界面展示一个统一的信息框。课程设计时,给关键操作加上 logger 或者写到文本日志,比刻意做“异常处理”拿分更重要。下面是简化版:
catch (SQLException e) { Logger logger = Logger.getLogger("MemberSystem"); logger.severe(e.getMessage()); JOptionPane.showMessageDialog(frame, "数据库操作失败,请查看 log.txt"); }使用java.util.logging的标准Logger就足够,不需要 Log4j 依赖。注意控制 logger 存放位置,否则生成的日志文件会越滚越大,演示时磁盘被刷满造成卡顿。
6.3 答辩前跑一遍核心功能回归
最后 20 分钟必做的一条检查清单:
| 操作 | 期望结果 |
|---|---|
| 插入新会员,会员号重复 | 提示卡号已存在,数据库无重复行 |
| 消费金额 30.5 元,积分增加 31 | 积分字段比之前多 31 |
| 删除处于积分明细中的会员 | 外键关联报错,程序不白屏 |
| 断网或停掉 SQL Server 服务再操作 | 弹出可读错误,不是ClassNotFoundException |
| 关闭主窗口后重新打开程序 | 数据从数据库重新加载,不残留内存脏数据 |
把上面五条做好,演示现场就不会出现“无响应的窗口”这类致命伤。到此,标题里的 Java、Swing、SWT、SQLServer、Eclipse 几个词已经全部落到了一个可持续迭代的课程设计项目上,剩下的就是你端着项目走一遍完整的“新会员 → 消费 → 加分 → 查询流水”路径。
本文还有配套的精品资源,点击获取