news 2026/10/27 22:54:04

JSP垃圾回收积分商城系统:积分流水、幂等结算与兑换双扣减

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSP垃圾回收积分商城系统:积分流水、幂等结算与兑换双扣减

先说个容易被忽略的事实:这几年我接触过的基层信息化小项目里,JSP 出现的频率远比技术圈讨论的要高。尤其是社区回收点、废品中转站这类场景——办公电脑是七八年前的机器,浏览器还停留在某个国产内核的老版本,网络时断时续,运维预算基本为零。在这种环境里,JSP 那套“服务端渲染、一个 war 包丢进容器就能跑、前端零构建”的特性,反而成了难得的优点。

基于 JSP 实现垃圾回收积分商城系统,说白了就是把两件事缝在一起:一头是回收业务,居民把纸箱、塑料瓶、旧衣物送到服务点,按品类和重量折算成积分;另一头是积分商城,积分能在里面换洗衣液、抽纸、大米这些日用品。听起来简单,真正动起手来,难的不是页面怎么写,而是积分这个“钱”怎么记才不出错。我见过太多类似项目,页面做得挺漂亮,上线三个月账就平不了,最后靠人工 Excel 对账。

这篇内容适合三类人:想拿一个完整业务闭环练手 JavaWeb 的同学、接了类似小项目需要快速落地的开发者、以及被“积分/余额”这类账户模型坑过的同行。下面我不讲教科书式的分层架构,只讲这套系统里真正会咬人的地方。

1. 为什么这套系统适合用 JSP 收口,而不是硬上前后端分离

1.1 运行环境的现实约束比技术潮流更有话语权

我先把选型这件事说透,因为很多人一上手就想套 Vue 加 Spring Boot,结果部署阶段直接卡死。回收点的实际条件是:一台装着老版本运行时的工控机或者普通台式机,屏幕分辨率 1366×768,操作人员是四五十岁的阿姨,只会“点开浏览器、输入地址、点按钮”这三步。

这种条件下,前后端分离带来的收益几乎为零,成本却实实在在:需要额外装 Node 环境打包、需要配静态资源服务、需要处理跨域、需要保证前端路由在旧浏览器上不崩。而 JSP 的方案是——一个 war 包,扔进 Tomcat 的 webapps 目录,启动,访问,完事。升级版本也就替换一个文件。这个差别在有人帮你运维的环境里不算什么,在没人运维的环境里就是生死线。

另一个被低估的点是首屏渲染。回收点的网络是那种“能用但会抽风”的状态,前后端分离的项目一旦接口超时,页面就是一片空白加转圈;JSP 因为是服务端拼好 HTML 再吐出来,即使慢,用户看到的也是完整页面,只是慢一点。对不会刷新重试的用户来说,这个体验差异很大。

所以我的判断标准很朴素:如果使用者不会用开发者工具,运维没人管,那就老老实实服务端渲染。这不是技术保守,是场景匹配。

1.2 一套能撑住业务的目录结构长什么样

JSP 项目最容易烂的地方是目录。我见过把所有 jsp 平铺在 webapp 根目录下的,三十多个文件挤在一起,找goods_list.jsp要翻半天。下面这套结构是我反复用过、比较顺手的:

webapp/ ├── WEB-INF/ │ ├── web.xml │ ├── lib/ # 依赖 jar,别用 Maven 之外的野路子 │ └── views/ # 所有 jsp 藏这里,禁止外部直接访问 │ ├── common/ │ │ ├── header.jsp │ │ └── footer.jsp │ ├── recycle/ # 回收业务 │ │ ├── order_add.jsp │ │ └── order_list.jsp │ ├── mall/ # 积分商城 │ │ ├── goods_list.jsp │ │ └── order_confirm.jsp │ └── admin/ ├── static/ │ ├── css/ │ ├── js/ │ └── img/ └── index.jsp # 只做跳转

把 jsp 全部塞进WEB-INF/views是个关键动作。因为WEB-INF目录浏览器无法直接访问,所有页面必须经过 Servlet 转发才能到达。这样做的好处是入口唯一:任何一次页面访问都必然经过控制层,权限校验、参数预处理、登录拦截才有地方挂。否则用户直接敲goods_list.jsp就能绕过登录,这类漏洞在小项目里极其常见。

提示:index.jsp只保留一行<jsp:forward page="/home"/>,不要在里面写业务,它的唯一职责是把根路径请求交给 Servlet。

1.3 JSP 只做展示这件事,必须用纪律守住

JSP 最诱人的地方是能直接写 Java 代码,<% %>一裹,什么逻辑都能塞。这正是它名声变差的原因。我的做法是给自己定三条硬规矩:

第一,JSP 里不出现Connection、不出现 SQL。所有数据在 Servlet 里准备好,通过request.setAttribute传过去。

第二,JSP 里不出现if (user == null)这种判断,登录拦截统一放在 Filter 里做。

第三,金额、积分这类数字的格式化统一走 JSTL 的<fmt:formatNumber>,不在页面里写String.format。

举个最典型的对比。反面写法是在页面上直接查数据:

<% List<Goods> list = GoodsDao.queryAll(); // 绝对不要在 JSP 里干这个 for (Goods g : list) { out.print(g.getName()); } %>

正面写法是 Servlet 转发,JSP 只负责渲染:

// GoodsListServlet.java protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { List<Goods> goods = goodsService.listOnSale(); req.setAttribute("goodsList", goods); req.getRequestDispatcher("/WEB-INF/views/mall/goods_list.jsp").forward(req, resp); }
<c:forEach items="${goodsList}" var="g"> <div class="card"> <span>${g.name}</span> <span><fmt:formatNumber value="${g.pricePoints}" pattern="#,##0"/> 积分</span> </div> </c:forEach>

第二段代码看着笨,但它在半年后你回头改需求时能救你一命——因为页面里没有任何业务逻辑需要理解,改动只发生在 Java 侧。这条纪律看着简单,能坚持的项目不到一半。

2. 把“扔一袋废纸箱”翻译成数据模型:三条主线必须分开

业务建模这一步做错,后面全是补丁。这个系统里,真正跑起来的主线只有三条:回收单、积分流水、兑换订单。三者的关系必须理清楚。

2.1 回收单:业务发生的“事实记录”

回收单记录的是一次物理世界里真实发生的事:某人某时在某服务点交了一袋东西,过秤,多少公斤,什么品类。

品类和单价的配置我建议单独建表,不要写死在代码里。原因是积分标准会变,比如塑料瓶从 8 积分/公斤调到 10 积分/公斤,如果写死在 Java 常量里,改一次就要重新打包部署。做成配置表之后,运营在后台改个数字就完事。

CREATE TABLE t_recycle_category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL COMMENT '纸类/塑料/金属/织物/玻璃', points_per_kg INT NOT NULL COMMENT '每公斤折算积分', status TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用' ); CREATE TABLE t_recycle_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT '业务单号 R+日期+序列', user_id BIGINT NOT NULL, category_id INT NOT NULL, weight_kg DECIMAL(8,2) NOT NULL COMMENT '过秤重量,保留两位', points_per_kg INT NOT NULL COMMENT '下单时快照单价', total_points INT NOT NULL COMMENT '结算积分 = weight * points_per_kg', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待结算 1已结算 2已作废', settle_time DATETIME NULL, operator_id BIGINT NOT NULL COMMENT '录入的操作员', create_time DATETIME NOT NULL, UNIQUE KEY uk_order_no (order_no), KEY idx_user_time (user_id, create_time) );

这里有个细节值得单独说:points_per_kg必须在回收单里存一份快照。很多人图省事,结算时去关联品类表取单价,结果品类调价之后,历史单据的金额全跟着变了,月底对账时发现去年十二月的报表和打印出来的小票对不上——这种事故我见过不止一次。快照字段虽然冗余,但它是业务事实的一部分,不能省。

另外weight_kg用DECIMAL(8,2),别用DOUBLE。浮点数在累加时会出现0.1+0.2=0.30000000000000004这种情况,积分是要乘单价的,误差会被放大。称重保留两位小数已经足够,顺手把乘法结果用整数处理:

// 先按“克”放大成整数再算,避免浮点误差 int totalPoints = (int) Math.round(weightKg.doubleValue() * pointsPerKg);

2.2 积分账户:余额字段只能当缓存,流水才是真相

这是整个系统里最重要的一个认知。新手最常见的做法是:用户表加一个points字段,回收了就+,兑换了就-。这个做法在单机低并发下能用,但一旦出问题就无法追溯——用户说我明明有 3000 积分,你系统显示 2000,你拿什么证明?

正确的模型是双表:一张流水表记录每一笔变动,一条记录对应一次业务;一张账户表存当前余额,纯粹为了查询快,属于缓存性质。

CREATE TABLE t_point_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, biz_type TINYINT NOT NULL COMMENT '1回收入账 2兑换出账 3人工调整 4冲正', biz_no VARCHAR(32) NOT NULL COMMENT '关联业务单号,幂等的关键', change_points INT NOT NULL COMMENT '正数入账,负数出账', balance_after INT NOT NULL COMMENT '变动后余额快照', remark VARCHAR(128), create_time DATETIME NOT NULL, UNIQUE KEY uk_biz (biz_type, biz_no), -- 同一业务只能记一次账 KEY idx_user (user_id, create_time) ); CREATE TABLE t_user_account ( user_id BIGINT PRIMARY KEY, balance INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0, update_time DATETIME );

uk_biz这个唯一索引是整个模型的保险丝。它保证同一张回收单不管被结算请求打到多少次,流水表里都只能有一条记录。数据库层面挡住的错误,比代码里写十个if都可靠。

balance_after看起来冗余,但排查问题的时候极其有用。用户投诉“我的积分少了两百”,你把流水按时间列出来,一眼就能看出哪一笔开始对不上,不用去反推。

2.3 商品与兑换订单:库存和积分是两笔账

商城侧的表结构相对常规,但有一点要注意——商品要有“上架状态”和“库存”两个独立字段,不能合并。运营想临时下架一个商品但保留库存数据,合并字段就做不到了。

CREATE TABLE t_goods ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, cover_img VARCHAR(128), price_points INT NOT NULL COMMENT '兑换所需积分', stock INT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架', sort_no INT NOT NULL DEFAULT 0 ); CREATE TABLE t_exchange_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, goods_id BIGINT NOT NULL, goods_name VARCHAR(64) NOT NULL COMMENT '下单快照', price_points INT NOT NULL COMMENT '下单快照', qty INT NOT NULL DEFAULT 1, total_points INT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0待核销 1已完成 2已取消', pickup_code VARCHAR(16) NOT NULL COMMENT '线下自提码', create_time DATETIME NOT NULL, UNIQUE KEY uk_order_no (order_no) );

goods_name和price_points同样是快照。理由和回收单一样:商品改名、调价之后,历史订单必须保持原样,否则用户翻记录会一脸懵。

三条主线理清楚之后,页面就只是这三张表的不同视角而已。回收端是“写回收单 + 触发入账”,商城端是“读商品 + 写出库单 + 触发扣减”,管理端是“查流水 + 查订单 + 处理异常”。逻辑一下子清爽了。

3. 积分入账这条链路,并发问题藏在你想不到的地方

这一段是我最想展开的,因为它是这类系统唯一真正有技术难度的地方,也是最容易上线后炸的地方。

3.1 一张回收单被结算两次,是怎么发生的

先说一个真实场景。操作员阿姨录完一张回收单,点了“确认结算”,页面转圈,她以为没点上,又点了一次。第一次请求刚好走到一半,两次请求同时到达服务端,于是:

  • 请求 A 读到status=0,判断“未结算”,开始处理
  • 请求 B 也读到status=0,同样判断“未结算”,也开始处理
  • 两个请求各自给账户加了积分,各自写了一条流水

结果就是这位居民白拿了一倍积分。如果当天有五十个人遇到这种情况,账就平不了了。

这个问题的根因不是“用户手抖”,而是检查与更新之间没有原子性。先查询状态 → 再判断 → 再更新这个模式在并发下必然出问题,因为查询和更新之间有一个时间窗口。

修法有几种,我最推荐的是用一条带条件的 UPDATE 完成“检查+更新”:

UPDATE t_recycle_order SET status = 1, settle_time = NOW() WHERE id = #{orderId} AND status = 0;

然后在 Java 里看影响行数:

int affected = stmt.executeUpdate(); if (affected == 0) { // 说明这张单已经被别人结算过了,或者状态不对 return Result.fail("该回收单已结算,请勿重复操作"); }

数据库在执行这条 UPDATE 时会对该行加锁,两个并发请求只有第一个能看到status=0,第二个进来时status已经是 1,匹配不上,影响行数为 0。这是最简单也最可靠的幂等手段,不需要引入任何分布式组件。

3.2 事务边界划在哪儿:账户、流水、单据必须同生共死

幂等解决的是“不重复”,但还有“不残缺”的问题。一次结算要动三张表:回收单状态、账户余额、积分流水。这三步必须在一个事务里,任何一步失败全部回滚。

Connection conn = null; try { conn = dataSource.getConnection(); conn.setAutoCommit(false); // 第一步:条件更新回收单,顺便完成幂等 int n = updateOrderStatus(conn, orderId); // status 0 -> 1 if (n == 0) { conn.rollback(); return Result.fail("该回收单已结算"); } // 第二步:账户加积分 int m = addBalance(conn, userId, totalPoints); if (m == 0) { conn.rollback(); return Result.fail("积分账户异常"); } // 第三步:读回最新余额,写快照 int after = queryBalance(conn, userId); insertFlow(conn, userId, 1, orderNo, totalPoints, after); conn.commit(); return Result.ok("结算成功,本次入账 " + totalPoints + " 积分"); } catch (Exception e) { if (conn != null) conn.rollback(); log.error("结算失败 orderNo={}", orderNo, e); return Result.fail("结算失败,请重试"); } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } }

这里有个细节:第二步用的是balance = balance + ?这种自增写法,不是“先查出来加完再写回”。前者是数据库层面的原子操作,后者在并发下会丢失更新(两个请求读到同一个旧值,都加了一次,最后只生效一次)。

至于balance_after的读取,虽然是事务内查询,但因为前面的UPDATE已经持有该行的锁,其他事务进不来,读到的就是自己刚写的值,不会串。

注意:MySQL 的默认隔离级别是 RR(可重复读),在同一个事务里第一次SELECT之后,后续SELECT读的是快照。所以读余额这一步必须放在 UPDATE 之后,如果放在事务开始就查,读到的会是旧值。这个坑我踩过一次,排查了两个小时。

3.3 一次“积分翻倍”的完整排查链路

讲个我实际处理过的问题,把排查思路摊开,因为这种问题以后你大概率也会遇到。

现象:某天运营反馈,有十几个用户的积分比预期多了整整一倍,时间集中在下午两点到两点半。

第一步,先看流水表。按用户和时间把这些人的流水列出来:

SELECT biz_type, biz_no, change_points, balance_after, create_time FROM t_point_flow WHERE user_id = 1024 ORDER BY create_time DESC LIMIT 20;

结果发现同一个biz_no(回收单号)有两条记录,change_points相同,但create_time相差不到一秒。这时候第一反应应该是:唯一索引uk_biz没生效?

第二步,验证唯一索引。查表结构,发现索引确实建了。那为什么能插进去两条?再看biz_no,两次的值不一样——一张是R20240612001,另一张是R20240612001(末尾多了一个空格)。

第三步,追到源头。看回收单生成逻辑,单号是用System.currentTimeMillis()拼的,而两次请求时间戳不同,所以生成了两个不同的单号。也就是说,阿姨点两次“新增”,系统真的创建了两张回收单,而不是同单重复结算。

第四步,定位真正的漏洞。问题出在页面的提交按钮没有防重复。第一次点击后按钮没禁用,第二次点击又发了一个POST /recycle/add,服务端老老实实建了新单。

修复分两层:前端点击后立即disabled,这是治标;后端在创建回收单时增加一个**提交令牌(token)**校验,这是治本。令牌的实现很简单:进入录入页时生成一个 UUID 存进 session,同时放进表单隐藏域;提交时比对,无论成功失败都从 session 里删掉。这样第二次提交找不到令牌,直接拒绝。

这套排查走下来,核心经验是:看到重复数据,先别急着改代码,先确认重复的到底是“同一业务”还是“两次业务”。这两种情况的修法完全不同——前者是幂等问题,后者是防重提交问题。

4. 兑换环节的双扣减:库存和积分谁先动,顺序有讲究

回收端是“只加不减”,逻辑相对简单。商城兑换就麻烦了,因为它同时要扣两样东西:商品库存、用户积分。任何一边扣失败,都会出现“扣了积分没拿到货”或者“发了货没扣积分”的烂账。

4.1 先扣库存还是先扣积分:我的选择是先库存

这个问题网上答案不一,我按自己的实践说理由。先扣库存的好处是:库存是全局共享资源,冲突概率高,早点扣能早点暴露问题。积分是单用户资源,冲突概率低。把高冲突的操作放在前面,能让事务持锁时间更短,也更容易在第一时间返回“已售罄”。

流程是这样:

-- 第一步:条件扣库存,stock >= qty 保证不超卖 UPDATE t_goods SET stock = stock - #{qty} WHERE id = #{goodsId} AND status = 1 AND stock >= #{qty}; -- 第二步:条件扣积分,balance >= total 保证不透支 UPDATE t_user_account SET balance = balance - #{total}, version = version + 1 WHERE user_id = #{userId} AND balance >= #{total}; -- 第三步:写兑换订单 INSERT INTO t_exchange_order (...) VALUES (...); -- 第四步:写积分流水(负数) INSERT INTO t_point_flow (...) VALUES (...);

四条语句在一个事务里,前三步任何一步影响行数为 0 就回滚。第二步如果返回 0,说明积分不够,回滚之后库存会自动还回去——这就是为什么必须在同一个事务里,如果分开两个接口调用,库存扣了积分不够,就得写补偿逻辑,复杂度翻倍。

stock >= #{qty}这个条件很多人会忘记写,只写stock = stock - #{qty}。后者在库存为 0 时会变成负数,虽然不会报错,但数据就烂了。有了这个条件,库存不足时影响行数为 0,代码里判断一下就能给出友好提示。

4.2 悲观锁还是乐观锁:这个场景其实都不太需要

有人会问要不要加SELECT ... FOR UPDATE。我的看法是:在条件 UPDATE 已经能解决问题的情况下,不要额外引入锁。

原因很实在。加FOR UPDATE会让事务持锁时间变长,回收点这种并发量几十的小系统倒无所谓,但它会带来死锁风险——如果两个事务分别锁了商品行和账户行,顺序不一致就可能互相等待。而条件 UPDATE 本身就是原子的,且锁的范围就是那一行,写完立刻释放,简单得多。

乐观锁(version字段)在这个场景主要用于账户表的更新,作用是防止“读-改-写”模式下的丢失更新。但我上面用的balance = balance - ?本身就是原子的,version更多是给后续可能的缓存刷新用,属于可选动作。真正需要乐观锁的是那种“先查库存算总价,再更新库存”的流程,而我们这里没有。

一句话总结:能用一条 SQL 表达的条件更新,就别拆成查询加更新。

4.3 JSP 表单重复提交的三种拦法

兑换按钮是用户最容易连点的地方,因为大家都怕“抢不到”。三层防护我一般全上:

第一层,前端禁用。表单onsubmit时把按钮置灰并改文案:

function submitOnce(form, btn) { if (btn.disabled) return false; btn.disabled = true; btn.value = '处理中...'; return true; }

这层只能挡住正常用户,挡不住脚本或者刷新重发。

第二层,令牌校验。进入确认页时生成 token 存 session,表单带隐藏域提交,Servlet 里比对后立即从 session 移除。这是主力。

// 生成 String token = UUID.randomUUID().toString(); session.setAttribute("exchangeToken", token); req.setAttribute("token", token); // 校验 String submitted = req.getParameter("token"); String cached = (String) session.getAttribute("exchangeToken"); session.removeAttribute("exchangeToken"); // 无论成败都删 if (cached == null || !cached.equals(submitted)) { resp.sendRedirect(req.getContextPath() + "/mall/list?msg=repeat"); return; }

第三层,数据库唯一约束。兑换订单号用“用户ID + 商品ID + 秒级时间戳”生成,加唯一索引。极端情况下前两层都被绕过,数据库这一层还能兜住。

三层都上的成本很低,但能省掉后面大量的“用户说扣了积分没收到货”的客服工单。

5. 上线之后才暴露的问题,以及补救手段

开发和上线是两回事。这套系统上线后,我遇到过的问题基本都集中在数据一致性和操作留痕上,跟功能本身关系不大。

5.1 每天凌晨跑一次对账,比任何监控都管用

我的习惯是写一个定时任务,每天凌晨两点跑一遍全量校验:

-- 校验一:账户余额是否等于流水累加 SELECT a.user_id, a.balance, IFNULL(SUM(f.change_points), 0) AS flow_sum FROM t_user_account a LEFT JOIN t_point_flow f ON f.user_id = a.user_id GROUP BY a.user_id, a.balance HAVING a.balance <> IFNULL(SUM(f.change_points), 0);

这条 SQL 应该永远返回空集。一旦有记录,说明某次操作没走完整事务,需要人工介入。跑完之后把结果写进一张t_check_log表,连续几天都是 0 才算稳定。

校验二是核对回收单和流水的一对一关系:

-- 已结算的回收单,必须有且只有一条对应的入账流水 SELECT o.order_no FROM t_recycle_order o LEFT JOIN t_point_flow f ON f.biz_type = 1 AND f.biz_no = o.order_no WHERE o.status = 1 GROUP BY o.order_no HAVING COUNT(f.id) <> 1;

这两条 SQL 我建议写在项目文档最显眼的位置。它们不解决问题,但能第一时间告诉你“出问题了”,比等用户投诉强太多。

5.2 脏数据怎么补:冲正而不是删改

发现数据错了,新手的第一反应是UPDATE改回去。这是大忌。改完之后流水和余额对不上,你连问题是什么时候发生的都查不出来了。

正确做法是冲正:写一条相反方向的流水,把错误抵消掉,同时在备注里写清楚冲正原因和关联的原流水 ID。

INSERT INTO t_point_flow (user_id, biz_type, biz_no, change_points, balance_after, remark, create_time) VALUES (1024, 4, 'REV-20240612-001', -500, 2500, '冲正:回收单R20240612001重复入账', NOW());

同时更新账户余额,biz_type用 4(冲正)区分开。这样整个账户的变动历史是完整可追溯的,任何时候都能从头算一遍验证余额。审计也好、复盘也好,都有据可查。

5.3 操作留痕:谁改的,比改了什么更重要

小项目最容易省掉的就是操作日志,但我吃过亏。有一次一个用户的积分突然少了两千,查流水显示是“人工调整”,但没人承认操作过。因为当时的日志只记了“调整 -2000”,没记操作人。

后来我强制加了一张操作表:

CREATE TABLE t_operate_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, operator_id BIGINT NOT NULL, module VARCHAR(32) NOT NULL COMMENT 'recycle/mall/account', action VARCHAR(32) NOT NULL COMMENT 'settle/exchange/adjust', target_no VARCHAR(32) COMMENT '业务单号', detail VARCHAR(512) COMMENT '关键参数JSON', ip VARCHAR(32), create_time DATETIME NOT NULL, KEY idx_time (create_time) );

关键是ip和detail两个字段。detail里存变更前后的值,用简单 JSON 拼一下就行,不需要引入序列化框架。查询的时候按时间倒序一拉,谁在什么时候干了什么一目了然。

这套日志的成本是一个 AOP 或者手工埋点,收益是出问题时不用猜。我个人觉得这是性价比最高的一块投入。

关于页面权限,我的做法很土但有效:在t_user表加一个role字段(1 居民 / 2 操作员 / 3 管理员),登录成功后把 role 放进 session,Filter 里对/admin/*路径做一次判断。别去搞复杂的 RBAC 五张表,这个规模的系统用不上,反而增加了维护负担。

到最后我想说的是,这类系统的技术含量确实不高,但它对数据严谨性的要求一点都不低 —— 因为积分在用户心里就是钱。我做完这个项目最大的体会是:页面写得丑一点没关系,操作员多等两秒也没关系,但账绝对不能错。每次动手改积分相关的逻辑,我都会先把前面那两条对账 SQL 跑一遍,确认改完之后还是空集,才敢发布。这个习惯帮我省掉了无数次半夜被电话叫起来的麻烦。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 22:51:07

视觉引导智能拣拾系统:工业自动化的关键技术解析

1. 项目概述&#xff1a;当机器学会"看"与"抓"在工业自动化领域&#xff0c;物料拣拾一直是制约生产效率的关键环节。传统机械臂需要严格预设轨迹&#xff0c;遇到物料位置偏差时极易发生抓取失败。我们团队开发的这套视觉引导智能拣拾系统&#xff0c;通过…

作者头像 李华
网站建设 2026/10/27 22:50:42

ORM查询技术全解析:从基础到高级应用

1. ORM查询的本质与价值第一次接触ORM查询时&#xff0c;我误以为它只是个简单的SQL包装器。直到在电商项目中处理用户订单关联查询时&#xff0c;手动拼接的SQL语句膨胀到300多行&#xff0c;才真正理解ORM的价值所在。ORM&#xff08;Object-Relational Mapping&#xff09;查…

作者头像 李华
网站建设 2026/10/27 22:49:18

构建高效第二大脑:Karpathy式知识管理实践

1. 为什么我们需要第二大脑&#xff1f;在信息爆炸的时代&#xff0c;我们每天接触的知识量早已超出人脑的处理能力。作为一名长期与代码和算法打交道的技术从业者&#xff0c;我深刻体会到传统笔记方法的局限性——收藏夹里堆积如山的文章、散落各处的代码片段、会议记录与灵感…

作者头像 李华
网站建设 2026/10/27 22:49:18

Linux进程虚拟地址空间详解与内存管理实践

1. 虚拟地址空间基础概念在32位Linux系统中&#xff0c;每个进程都拥有独立的4GB虚拟地址空间&#xff08;0x00000000到0xFFFFFFFF&#xff09;。这个看似连续的内存空间实际上被划分为多个功能区域&#xff0c;每个区域都有特定的用途和访问权限。现代操作系统通过这种划分实现…

作者头像 李华
网站建设 2026/10/27 22:48:20

SM481(和利时 MACS SM 系列 8 通道非 HART 电流 AI 模块)

硬件基础&#xff1a;SM 系列模拟量输入卡件&#xff0c;8 通道电流采集&#xff0c;通道对系统侧光电隔离、通道之间不隔离&#xff1b;64 针欧式背板接口接入机笼底板&#xff0c;防混销防错装&#xff0c;和带 HART 的 SM480、测温类 SM430/SM472 电路不同&#xff0c;支持热…

作者头像 李华
网站建设 2026/10/27 22:48:14

Jev浏览器Agent实测:自然语言驱动网页自动化的效率革命

从去年年底开始&#xff0c;AI 编程助手赛道突然卷出了新高度。GitHub 上那个叫 Jev 的浏览器 Agent 插件&#xff0c;短短几个月就冲到了 21k star&#xff0c;我一开始觉得又是营销堆出来的热度&#xff0c;直到自己上手跑了几个真实任务才明白&#xff0c;它确实踩中了很多人…

作者头像 李华