疫情期间小区封闭管理那段时间很多社区还在用微信群接龙买菜几百条消息刷下来订单漏记、数量对不上、配送全靠人工喊。我当时正好在做一个前后端分离的小区购物系统用SpringBootVueMyBatisMySQL这套经典技术栈从需求梳理、数据库设计到部署上线完整跑了一遍。这篇就把整套系统的设计思路、核心代码逻辑和部署过程写清楚适合正在做课程设计、毕业设计或者想拿一个完整企业级前后端分离项目练手的人参考。这个项目不算大但五脏俱全登录鉴权、商品管理、购物车、下单扣库存、订单状态流转、配送分配前后端加起来几十个接口。用到的都是国内公司最常用的那套组合没有花里胡哨的中间件一个MySQL加上两个包的产物就能跑起来特别适合用来理解真实项目的完整落地流程。1. 项目背景拆解这个小区疫情购物系统到底长什么样1.1 疫情封控场景下的核心痛点先说需求从哪来。疫情期间小区封闭管理居民不能出门生活物资全靠社区统一采购配送。一开始大家用微信群接龙下单群里发商品列表居民回复2栋301要土豆3斤楼栋管家手工登记再统一报给采购。这么干的问题很明显——漏单、错单、重复下单没人管得过来库存也没有实时概念采购回来发现某样东西缺货又得挨个打电话通知。所以这个系统的目标很明确把微信接龙变成线上商城把人工喊单变成系统派单。居民登录后能看到当天可购商品、库存数量和价格下单后订单进入管理端管理员确认并分配给配送员配送员送货上门后标记完成。整个过程居民在手机上能实时看到订单走到哪一步管理员在后台能看到统计配送员只处理分配给自己的订单。1.2 角色划分与业务流程系统设计了三个角色覆盖社区物资流转的完整链路角色主要操作说明居民浏览商品、加购物车、下单、查看订单小区内的普通住户管理员商品上下架、维护库存、确认订单、分配配送员社区/物业工作人员配送员查看待配送订单、接单、标记完成志愿者或物业配送人员业务流程我梳理下来是这样的管理员发布当日商品设置库存、价格、图片居民登录系统按分类浏览商品加入购物车居民提交订单默认收货地址就是自己的楼栋单元房号不需要填快递地址订单进入待确认状态管理员看到后确认订单分配给某个配送员配送员在待配送列表里看到订单送货后标记完成居民在我的订单里看到状态变化。1.3 和普通电商系统比这套系统简化了什么开发之前我也想过要不要直接改造一个开源商城但仔细对比发现社区购物场景和普通电商有本质差别硬改反而绕路。普通电商要处理在线支付、多店铺、快递物流、售后、优惠券这一堆东西开发量巨大。而社区购物系统有几个特点配送范围固定在一个小区收货地址就是用户注册时填的楼栋单元房号支付往往线下结算不需要接入微信支付库存是小批量的每天可能就几十单订单状态也不需要发货—运输—签收这么复杂本地配送就三步。所以我的设计思路是做减法不做支付、不做物流对接、不做多商户把核心的商品-订单-配送做到位。这套简化后的架构有一个好处——代码量控制在合理范围内新手能从头到尾看懂而不是迷失在电商的各种复杂规则里。2. 技术选型背后的逻辑SpringBootVueMyBatisMySQL为什么这么搭2.1 前后端分离到底分离了什么选前后端分离而不是传统的JSP最直接的原因是开发和部署都更灵活。前端只用管界面和交互通过接口拿数据后端只管业务逻辑和数据不用关心页面长什么样。开发时可以两个人并行部署时后端一个jar包前端一堆静态文件互不干扰。另外一个现实原因是现在公司的前端Vue已经是标配毕业设计和课程设计用这套方向性更对。Vue的组件化开发比jQuery拼字符串舒服太多商品卡片、订单列表这些重复性高的UI封装成组件后维护成本直线下降。2.2 SpringBoot MyBatis上手快、SQL可控SpringBoot选它不是因为技术多新而是因为省事。内嵌Tomcat不用单独装容器自动配置application.yml里写几行就搞定数据源starter生态想加什么功能直接引依赖。对于一个单体管理系统SpringBoot就是最不折腾的选择。MyBatis的选择我犹豫过一阵JPA在上手速度上确实快但最终还是选了MyBatis理由是SQL可控。订单明细、库存扣减、分页统计这些SQL手写出来一眼能看懂执行逻辑而且MyBatis在面试里出现的频率远高于JPA。项目里复杂查询我放在XML里写简单的增删改查用注解两种方式配合着用。2.3 Vue 2还是Vue 3组件库怎么选如果现在从零开始学直接上Vue 3 Element Plus是更顺应趋势的选择。但我这个项目用的是Vue 2 Element UI不是不会Vue 3而是考虑到教程多、坑少、生态稳定。你要是自己写两个方案都行核心逻辑没有区别只是语法层面有变化。组件库无脑选Element系列表单、表格、弹窗、消息提示都有现成的不需要自己造轮子。管理端那套表格分页搜索的组合Element的el-table配el-pagination写出来非常快。2.4 MySQL够不够用肯定够。社区级的数据量一天几十单商品几十个MySQL的InnoDB引擎扛这点压力毫无问题。唯一要注意的是建表时用utf8mb4字符集别用utf8否则生僻字和表情符号会出乱码。事务用默认的隔离级别不用改等真到了需要读写分离的量级这个系统早就该重构了。3. 数据库建模七张核心表怎么设计才能撑起整个业务3.1 用户表一张表搞定三种角色用户相关的设计我合并成了一张user表靠role字段区分角色没有单独做user_role、role_permission那套权限模型。原因很简单只有三种固定角色权限控制用拦截器判断role值就够过度设计反而增加复杂度。字段类型说明idbigint主键自增usernamevarchar(50)登录账号唯一passwordvarchar(100)BCrypt加密后的密码real_namevarchar(50)真实姓名phonevarchar(20)手机号roletinyint1居民 2管理员 3配送员building_novarchar(20)楼栋号unit_novarchar(20)单元号room_novarchar(20)房号楼栋单元房号这三个字段只有居民需要填管理员和配送员不用。配送的时候配送员能看到居民的完整地址这就是收货地址的全部了不需要单独建地址表。3.2 商品与分类商品表goods是商城模块的基础。字段包括商品名称、分类ID、价格、库存、单位、图片地址、上下架状态。这里有两个细节值得注意。价格字段我用decimal(10,2)不用float或double。浮点数在Java里做金额运算会出现0.10.2不等于0.3的问题线上金额算错是要出事故的。mysql的decimal是精确类型配合Java的BigDecimal一分钱都不会差。库存字段stock是int。疫情期间物资有限每天库存可能就几十份int够用。商品上下架用一个status字段控制管理员把商品下架后居民端就搜不到了但历史订单里的商品数据不受影响。3.3 购物车与订单的拆分设计购物车表cart结构很简单user_id、goods_id、quantity三个核心字段。user_id和goods_id加唯一索引防止同一个商品在购物车里出现多条记录用户重复点击加入购物车时走更新数量的逻辑而不是新增记录。订单的设计是整个数据库模型的核心我拆成了orders主表和order_item明细表两张。orders记录一笔订单的总体信息订单号、下单人、总金额、状态、各种时间戳。order_item记录订单里的每一条商品商品ID、商品名称、单价快照、购买数量。order_item里冗余了goods_name和price两个字段这不是偷懒而是刻意的设计。商品信息是会变的今天土豆卖3块明天可能涨到5块但用户下单时的价格必须固定下来否则结算的时候追溯不清。这叫快照电商系统里都这么干。3.4 配送记录和订单状态配送这块我单独建了delivery表字段包括order_id、deliverer_id、assign_time、finish_time。一个订单对应一条配送记录逻辑上是1对1。为什么订单状态不直接放一个deliverer_id在orders表里因为把配送人员单独拆出来后面如果要加一个配送员一次配送多个订单的批量功能只需要在delivery表上加批次号不用改订单表结构。虽然目前功能简单但表结构上留一点扩展余地成本很低。3.5 关于外键和索引的取舍整库我都没有建物理外键全部用逻辑外键就是字段有对应关系但不做数据库约束。理由和大多数实际项目一样外键约束会影响写入性能而且业务上删除用户、删除商品的时候有外键在会很麻烦。但长度、类型、命名这些还是要严格对应靠应用层保证数据一致性。索引方面orders表建了(user_id, status)联合索引因为我的订单和管理端按状态筛选这两个高频查询都用得上order_item的order_id建普通索引反查明细走索引goods的category_id建普通索引。项目数据量小这些索引足够多了反而浪费。4. 后端核心实现登录鉴权、购物车与订单事务4.1 JWT登录和拦截器鉴权后端我按三层结构组织controller、service、mapper。登录模块用JWT做无状态认证用户登录成功后后端签发一个token前端存localStorage每次请求带着token后端拦截器校验。JWT的好处是服务端不用存session集群部署时也天然支持不需要额外的session共享方案。核心拦截器逻辑是这样的public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 放行预检请求 if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new BusinessException(401, 未登录或登录已过期); } LoginUser user JwtUtil.parseToken(token.substring(7)); // 把用户信息放到ThreadLocal后续Service直接取 UserContext.set(user); return true; } }密码存储用BCrypt而不是MD5。MD5加盐虽然也能用但BCrypt是专业的密码哈希算法自带盐值彩虹表攻击基本无效。Spring Security的crypto包里就有BCryptPasswordEncoder单独引一个依赖就能用不用为了密码存储把整个Security引进来。4.2 商品查询与购物车的实现细节商品查询是居民端最高频的接口。我做了按分类筛选关键字搜索分页用的PageHelper插件分页代码就一行PageHelper.startPage(pageNum, pageSize); ListGoods list goodsMapper.selectAvailable(categoryId, keyword); PageInfoGoods pageInfo new PageInfo(list);加入购物车时要注意校验顺序先查商品是否存在再查是否上架最后查库存是否足够。虽然有库存校验但购物车阶段扣库存没意义真正的库存扣减在提交订单时做。购物车只是个意向清单别在购物车里做太多校验逻辑。4.3 下单事务库存扣减是这个系统的命门下单是全局最重要的接口必须保证原子性要么订单、明细、扣库存、清购物车全部成功要么全部回滚。这里我一律用Transactional并且指定rollbackFor Exception.class。Transactional(rollbackFor Exception.class) public Long createOrder(OrderCreateDTO dto) { ListOrderItem items new ArrayList(); BigDecimal total BigDecimal.ZERO; Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(dto.getUserId()); order.setStatus(OrderStatus.PENDING_CONFIRM); order.setCreateTime(LocalDateTime.now()); for (GoodsBuyDTO buy : dto.getBuyList()) { // 条件更新stock 传入数量才扣返回0说明库存不足 int rows goodsMapper.reduceStock(buy.getGoodsId(), buy.getQuantity()); if (rows 0) { throw new BusinessException(商品库存不足或已下架); } Goods goods goodsMapper.selectById(buy.getGoodsId()); OrderItem item buildOrderItem(buy, goods); items.add(item); total total.add(goods.getPrice().multiply(BigDecimal.valueOf(buy.getQuantity()))); } order.setTotalAmount(total); orderMapper.insert(order); orderItemMapper.batchInsert(items); cartMapper.clearCart(dto.getUserId()); return order.getId(); }reduceStock对应的SQL是关键用了一条条件更新语句UPDATE goods SET stock stock - #{quantity} WHERE id #{goodsId} AND stock #{quantity} AND status 1这句话在并发场景下非常安全。数据库的行锁保证同一时刻只有一个事务能更新这个商品的库存stock quantity这个条件保证不会扣成负数受影响行数为0就直接报错回滚。比起先select再update的写法条件更新既避免了超卖又不用select for update加锁性能更好。订单号的生成规则我用的yyyyMMddHHmmss 6位随机数没有用数据库自增ID直接展示给用户。自增ID容易被猜到订单量明细也不够专业业务订单号还是自己生成比较好。true随机数直接用Math.random拼字符串虽然理论上有碰撞可能但一天几百单的体量完全不用担心。4.4 订单状态机与权限控制订单状态我定义了5个待确认(1)、配送中(2)、已完成(3)、已取消(0)。状态流转不是谁都能改的每个操作都有权限限制操作角色前置状态后置状态提交订单居民-待确认取消订单居民待确认已取消确认并分配管理员待确认配送中标记完成配送员配送中已完成所有状态变更接口都校验当前用户角色管理员不能替居民下单居民不能自己给自己分配配送员。这里我用了Spring的RequestAttribute把拦截器解析出的用户信息传进controllerService里再判断角色简单直接。5. 前端Vue实现细节目录结构、请求封装与页面逻辑5.1 前端项目目录规划前端我用Vue CLI初始化目录结构按功能划分src/ ├── api/ # 按模块拆分的接口定义 │ ├── goods.js │ ├── order.js │ └── user.js ├── router/ # 路由配置 ├── store/ # 用户信息和已登录状态 ├── utils/ │ └── request.js # axios统一封装 └── views/ ├── login/ ├── resident/ # 居民端页面 └── admin/ # 管理后台页面一个原则组件里不直接写请求。所有axios请求都定义在api目录的JS文件里页面只调用方法。这样改接口地址只动一个文件排查问题也方便。5.2 axios封装与统一响应体前后端联调最怕接口格式不统一所以一开始我就跟后端约定了一个统一返回格式{ code: 200, message: success, data: {} }axios的响应拦截器里统一判断code200就返回data非200弹错误消息HTTP 401统一跳登录页。这样业务代码里不用每个请求都写错误处理。service.interceptors.response.use( response { const res response.data if (res.code 200) { return res.data } if (res.code 401) { router.push(/login) } Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) }, error { Message.error(网络异常) return Promise.reject(error) } )请求拦截器里把token塞进headerservice.interceptors.request.use(config { if (store.state.token) { config.headers[Authorization] Bearer store.state.token } return config })5.3 路由守卫控制页面访问权限前端也要做权限控制不能只靠后端。我用路由守卫判断登录状态和角色router.beforeEach((to, from, next) { const token store.state.token if (to.path /login) { next() return } if (!token) { next(/login) return } // /admin开头的路由只有管理员能访问 if (to.path.startsWith(/admin) store.state.role ! 2) { next(/) return } next() })前端控制权限只是体验优化真正的安全还是靠后端接口校验。前端设置随便跳过后端拦截器才是底线这句话写给自己也写给读者。5.4 三个关键页面的实现思路居民端的商品列表页是核心页面用el-tabs做分类切换el-card卡片展示商品图片、名称、价格、库存每个卡片带加入购物车按钮。加入后右上角购物车角标数字要实时更新这里用Vuex管理购物车数量避免各组件之间手动同步。购物车页面是结算前的确认环节支持修改数量、删除商品、实时计算总价。提交订单时弹窗让用户确认一下收货地址——也就是自己的楼栋单元房号地址数据从用户信息里带出来不需要重新填。管理后台的订单管理页是使用频率最高的页面。用el-table展示订单列表每行显示订单号、居民姓名、金额、状态、下单时间状态列用el-tag做成彩色标签待确认的橙色、配送中的蓝色、完成的绿色、取消的灰色。操作列根据状态动态显示按钮待确认显示确认分配配送中显示查看配送员已完成不显示操作。配送员接单页逻辑更简单就是一个待配送订单列表点标记送达调接口成功后订单从列表移除。6. 完整部署流程从开发环境到服务器上线6.1 环境准备清单部署前先把环境列清楚避免装到一半发现版本不兼容软件版本建议说明JDK8或11SpringBoot 2.x都兼容Maven3.6只需在打包机器上装Node.js14Vue 2项目要求不高MySQL5.7或8.0推荐8.0注意驱动差异Nginx1.18托管前端静态资源服务器LinuxCentOS 7 / Ubuntu 20.04都行6.2 数据库初始化把项目里的数据库脚本导入MySQLmysql -uroot -p -e create database community_shopping default character set utf8mb4; mysql -uroot -p community_shopping sql/community_shopping.sql导入成功后检查一下表的数量和字符集。我会故意看几个表的建表语句确认collate是utf8mb4_general_cis字符集不对的坑后面会专门说。然后修改后端配置文件application.yml里的数据库连接spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/community_shopping?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 你的密码注意driver-class-name用com.mysql.cj.jdbc.Driver这是MySQL 8的驱动。如果是MySQL 5.7可以用com.mysql.jdbc.Driver但建议统一用8的驱动向下兼容5.7。6.3 后端打包与启动后端打包很简单在项目根目录执行mvn clean package -DskipTests打包完成后target目录下会生成xxx.jar。启动前先确认端口没被占用然后后台启动nohup java -jar community-shopping-server.jar --server.port8080 app.log 21 看日志确认启动成功tail -f app.log看到Started Application in x seconds就说明启动成功了。这里我推荐用systemd托管服务器重启后服务会自动拉起不用手动敲命令。写一个unit文件[Unit] DescriptionCommunity Shopping Server Afternetwork.target [Service] ExecStart/usr/local/jdk/bin/java -jar /opt/community-shopping/server.jar Restartalways Userroot [Install] WantedBymulti-user.target然后systemctl daemon-reload systemctl start community-shopping比nohup好管理得多。6.4 前端构建与Nginx配置前端构建前先确认接口地址配置。开发的时候前端访问的是/dev-api这种代理地址生产环境走nginx反向代理所以构建时要用环境变量区分。Vue CLI项目我配置了.env.production文件把VUE_APP_BASE_API设为/api。npm install npm run build构建产物在dist目录把它上传到服务器的/opt/community-shopping/dist。然后配置Nginxserver { listen 80; server_name 你的域名或IP; root /opt/community-shopping/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }location /里的try_files是Vue Router history模式必须的否则刷新页面按路径找不到文件直接404。如果路由用的是hash模式不需要这行但hash模式URL里带#号不好看也不利于分享。/api的请求全部代理到后端8080端口前端请求和后端接口同源生产环境根本不需要跨域配置。nginx -t检查配置通过后nginx -s reload浏览器打开IP地址就能看到系统。6.5 部署完成后检查什么部署完别急着收工我会依次检查这几项首页能不能正常打开登录接口通不通用管理员账号登录随便点几个页面看接口是否报错查看后端日志有没有异常特别是数据库连接、SQL报错这类问题服务器防火墙有没有放开80端口外网能不能访问。7. 我已经踩过的坑跨域、时区、中文乱码与并发扣库存7.1 跨域配置的正确姿势开发环境前端跑5173端口后端跑8080端口跨域是必然的。后端加一个全局CORS配置注意allowedOriginPatterns不要用allowedOrigins()因为带cookie凭证的跨域请求不允许origin为。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这里有个容易忽略的细节跨域请求会先发一个OPTIONS预检请求拦截器里必须放行OPTIONS否则前端会看到CORS错误而后端日志里什么都没有。我在JwtInterceptor里特意加了那一行判断。7.2 MySQL时区问题启动报错还是小事数据差8小时才是大坑MySQL 8的驱动对时区校验很严格连接串不写serverTimezone会直接报错。写了Asia/Shanghai还不够如果Java端和数据库端时区配置不一致会出现时间数据相差8小时的问题。我本机测试时一切正常部署到服务器后就发现订单时间不对。原因是服务器的系统时区是UTC而Java应用的默认时区跟随系统LocalDateTime存进去再查出来就错位了。解决办法是application.yml里明确指定spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8同时数据库连接串里serverTimezoneAsia/Shanghai两边对齐问题就没了。时区问题不会第一时间暴露通常用到时间查询、时间排序的页面才发现排查起来特别容易上头。7.3 中文乱码的三个源头中文乱码在前后端分离项目里一般有三个源头挨个排除数据库层面建库建表时没指定utf8mb4默认latin1。解决方法是统一用utf8mb4建库老的库用alter table改字符集连接层面连接串加characterEncodingutf8让JDBC知道用UTF-8传输数据后端响应层面SpringBoot默认UTF-8基本不用管但接口返回的JSON里直接乱码的话检查控制器注解和全局配置。页面上看得到中文乱码基本就是前两个原因数据能存进去但显示乱码多半是连接串缺了characterEncodingutf8。7.4 并发扣库存条件更新帮了大忙这个系统虽然并发量不会很高但遇到社区集中抢购某样商品时同一时间几十个请求打过来很常见。最开始我写的是先SELECT库存判断够不够再UPDATE结果测试时用JMeter模拟100个并发下单库存从30直接扣成负数。改成条件更新语句之后库存再也没出过问题。这类问题写完必须用压测工具验证别觉得代码逻辑没问题就直接上线。我当时的验证方法很简单库存设成50JMeter开100个线程同时下单最终生成的订单数正好50库存归零多余的请求全部返回库存不足。7.5 Nginx部署后页面能打开但接口全挂这是很多新手第一次部署前后端分离项目最容易栽的坑。页面静态文件加载正常但一登录就提示请求失败。原因往往是nginx的proxy_pass配置写错了或者根本没配location /api。我见过有人忘了配代理前端请求跑到nginx自己的8081端口当然404。另外注意location /api/后面的proxy_pass如果写成了proxy_pass http://127.0.0.1:8080/;带个斜杠会把/api前缀去掉后端接口如果定义在/api路径下就会全部404。不带斜杠才是原样转发这个细节踩过一次就记住了。8. 二次开发建议这套架构能扩展到什么程度8.1 目前最值得加的三个功能跑通主流程之后我陆续给这个系统加了几块功能都顺着原有架构扩展不用大改。第一个是Redis缓存。商品列表数据变化不频繁加个缓存减轻数据库压力下单时用Redis的原子自增生成订单号比随机字符串更可靠图片验证码也顺手搬到了Redis里过期时间由Redis管理。第二个是超时自动取消。用Spring的Scheduled在每天固定时间扫描待确认超过N小时的订单批量置为取消状态。不用引入消息队列定时任务就够了。第三个是统计报表。管理端加一个Dashboard页面统计今日订单量、成交金额、热门商品TOP10数据用SQL聚合查询前端用ECharts画柱状图和折线图。这个功能对管理者来说价值最大。8.2 架构层面什么时候才需要升级有人问我要不要上微服务、要不要引入消息队列。我的回答都是不需要。这个系统的业务边界清晰、数据量有限、团队规模极小单体架构就是最优解。SpringBoot单体部署简单、排查问题直接、资源占用低引入微服务纯粹是给自己找麻烦。真到了用户量暴增那天优先做的不是拆微服务而是加Redis缓存、做数据库索引优化、把静态资源放CDN。这些操作收益高、风险小等这些手段都用尽了再谈架构升级。8.3 新手做这类项目我的三点建议代码规范比代码功能更重要。字段命名、注释、接口返回格式这些一开始就按规范来后面维护和扩展都会舒服得多。很多同学把功能跑通就觉得完事了代码可读性一塌糊涂结果加功能时自己都看不懂自己写的代码。先跑通主链路再加花活。登录、下单、改库存、状态流转这条主线如果能完整跑通说明你对业务和技术栈的掌握已经到位。在此基础上再加Redis、加图表、加权限细化每个功能都是增量式的心态不会崩。数据库设计多花半小时想清楚后面省下的调试时间是以天计的。尤其是订单和商品这类核心表字段取舍、快照设计、索引规划都值得提前想明白别等数据进去了再改表。我在实现这套系统的过程中最大的体会是拿到一个需求先别急着写代码把角色、流程、状态、数据模型这四个东西画明白代码只是把它们翻译出来而已。这套项目跑下来从需求分析到数据库建模再到前后端联调最后部署上线每个环节我都踩了坑也填了坑那种把一个完整项目从0到1做出来的踏实感是看多少教程都替代不了的。
阅读完成 · 觉得有帮助?