# 夜市摊位管理 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 为夜市用户(userType=3)提供摊位管理能力,包括创建摊位、管理摊位主账号。 **Architecture:** 在现有 PosStore 和 InfoUser 模型上扩展,PosStore 新增 isStall 字段标识摊位,InfoUser 新增 storeId 字段关联摊位主到摊位。后端新增摊位管理 Controller,前端商家端新增摊位管理菜单页面。 **Tech Stack:** Java / Spring Boot / MyBatis / MySQL / Vue 2 / Element UI --- ## Technical Context **Language/Version:** Java 8+, Vue 2 **Primary Dependencies:** RuoYi framework, MyBatis, Element UI **Storage:** MySQL **Testing:** 手动测试(项目无自动化测试框架) **Target Platform:** Web (商家管理后台) **Project Type:** Web application (前后端分离) **注意事项:** - PosStore 已有 `isNightMarket`(Integer) 和 `nightMarketId`(Long) 字段,可用于关联夜市 - InfoUser 已有 `address`、`longitude`、`latitude` 字段,已有夜市地址支持 - 项目使用 RuoYi 菜单系统(sys_menu 表),菜单通过数据库管理 ## Project Structure ### Documentation (this feature) ```text specs/001-food-stall/ ├── spec.md # 需求文档 └── plan.md # 本文件(实施计划) ``` ### Source Code ```text foodie_server/ ├── ruoyi-admin/src/main/java/com/ruoyi/app/ │ ├── mendian/PosStoreController.java # [修改] 现有店铺 Controller │ ├── stall/StallController.java # [新建] 摊位管理 Controller │ └── user/InfoUserController.java # [修改] 用户注册(可能复用) ├── ruoyi-system/src/main/java/com/ruoyi/system/ │ ├── domain/ │ │ ├── PosStore.java # [修改] 新增 isStall 字段 │ │ └── InfoUser.java # [修改] 新增 storeId 字段 │ ├── mapper/ │ │ ├── PosStoreMapper.java # [修改] 查询方法 │ │ ├── PosStoreMapper.xml # [修改] SQL 语句 │ │ ├── InfoUserMapper.java # [修改] 查询方法 │ │ └── InfoUserMapper.xml # [修改] SQL 语句 │ └── service/ │ ├── IPosStoreService.java # [修改] 接口方法 │ ├── impl/PosStoreServiceImpl.java # [修改] 实现方法 │ ├── IInfoUserService.java # [修改] 接口方法 │ └── impl/InfoUserServiceImpl.java # [修改] 实现方法 foodie-store/ # 商家前端 ├── src/ │ ├── api/stall.js # [新建] 摊位管理 API │ ├── views/ │ │ └── StallManage.vue # [新建] 摊位管理页面 │ ├── components/ │ │ └── Aside.vue # [修改] 侧边栏菜单 │ └── router/index.js # [修改] 路由配置 ``` --- ## Tasks ### Task 1: 数据库变更 — PosStore 新增 isStall 字段 **Files:** - Modify: `ruoyi-system/src/main/resources/mapper/chanting/PosStoreMapper.xml` - Modify: `ruoyi-system/src/main/java/com/ruoyi/system/domain/PosStore.java` - [ ] **Step 1: 执行 SQL 添加字段** ```sql ALTER TABLE pos_store ADD COLUMN is_stall TINYINT DEFAULT 0 COMMENT '是否摊位 0=店铺 1=摊位'; ``` - [ ] **Step 2: PosStore.java 新增 isStall 字段** 在 PosStore.java 中添加: ```java /** 是否摊位 0=店铺 1=摊位 */ private Integer isStall; ``` 添加 getter/setter: ```java public Integer getIsStall() { return isStall; } public void setIsStall(Integer isStall) { this.isStall = isStall; } ``` - [ ] **Step 3: PosStoreMapper.xml 更新** 在 `PosStoreResult` resultMap 中添加: ```xml ``` 在 `selectPosStoreVo` 中添加 `is_stall` 字段。 在 `insertPosStore` 中添加: ```xml is_stall, ... #{isStall}, ``` 在 `updatePosStore` 中添加: ```xml is_stall = #{isStall}, ``` 在 `selectPosStoreList` 的 where 中添加: ```xml and is_stall = #{isStall} ``` - [ ] **Step 4: 验证** 启动项目,确认 PosStore 相关接口正常工作。 --- ### Task 2: 数据库变更 — InfoUser 新增 storeId 字段 **Files:** - Modify: `ruoyi-system/src/main/resources/mapper/system/InfoUserMapper.xml` - Modify: `ruoyi-system/src/main/java/com/ruoyi/system/domain/InfoUser.java` - [ ] **Step 1: 执行 SQL 添加字段** ```sql ALTER TABLE info_user ADD COLUMN store_id BIGINT DEFAULT NULL COMMENT '关联摊位ID(摊位主使用)'; ``` - [ ] **Step 2: InfoUser.java 新增 storeId 字段** ```java /** 关联摊位ID(摊位主使用) */ private Long storeId; ``` 添加 getter/setter。 - [ ] **Step 3: InfoUserMapper.xml 更新** 在 resultMap 中添加 `store_id` 映射,在 insert/update/select 中添加 `storeId` 字段支持。 - [ ] **Step 4: 验证** 启动项目,确认 InfoUser 相关接口正常工作。 --- ### Task 3: 后端 — 摊位管理 Controller **Files:** - Create: `ruoyi-admin/src/main/java/com/ruoyi/app/stall/StallController.java` - [ ] **Step 1: 创建 StallController.java** 提供以下接口: ```java @RestController @RequestMapping("/stall") public class StallController extends BaseController { /** 获取当前夜市用户的摊位列表(isStall=1) */ @GetMapping("/list") public AjaxResult getStallList(@Auth InfoUser user) { PosStore query = new PosStore(); query.setUserId(user.getUserId()); query.setIsStall(1); return AjaxResult.success(posStoreService.selectPosStoreList(query)); } /** 创建摊位(复用 PosStore,isStall=1) */ @PostMapping("/add") @Transactional public AjaxResult addStall(@Auth InfoUser user, @RequestBody PosStore posStore) { posStore.setUserId(user.getUserId()); posStore.setIsStall(1); posStoreService.insertPosStore(posStore); return AjaxResult.success(posStore); } /** 为摊位创建摊位主账号 */ @PostMapping("/addOwner") public AjaxResult addStallOwner(@Auth InfoUser user, @RequestBody InfoUser stallOwner) { // 校验摊位属于当前夜市用户 // 设置 userType=4, storeId, 加密密码 // 创建用户 return AjaxResult.success(); } /** 获取摊位下的摊位主列表 */ @GetMapping("/owners") public AjaxResult getStallOwners(@RequestParam Long storeId) { // 查询 userType=4, storeId=storeId 的用户列表 return AjaxResult.success(); } /** 删除/禁用摊位主 */ @PutMapping("/disableOwner") public AjaxResult disableStallOwner(@RequestParam Long userId) { // 设置 status=1(禁用) return AjaxResult.success(); } } ``` - [ ] **Step 2: 在 InfoUserService/Mapper 中添加按 storeId 和 userType 查询的方法** InfoUserMapper.xml 中添加: ```xml ``` - [ ] **Step 3: 验证** 使用 Postman/浏览器测试各接口。 --- ### Task 4: 前端 — 摊位管理 API 层 **Files:** - Create: `E:\QtwCode\foodie\foodie-store\src\api\stall.js` - [ ] **Step 1: 创建 stall.js** ```javascript import request from '@/router/request' // 获取摊位列表 export function getStallList() { return request({ url: '/stall/list', method: 'get' }) } // 创建摊位 export function addStall(data) { return request({ url: '/stall/add', method: 'post', data }) } // 获取摊位主列表 export function getStallOwners(storeId) { return request({ url: '/stall/owners', method: 'get', params: { storeId } }) } // 创建摊位主 export function addStallOwner(data) { return request({ url: '/stall/addOwner', method: 'post', data }) } // 禁用摊位主 export function disableStallOwner(userId) { return request({ url: '/stall/disableOwner', method: 'put', params: { userId } }) } ``` --- ### Task 5: 前端 — 摊位管理页面 **Files:** - Create: `E:\QtwCode\foodie\foodie-store\src\views\StallManage.vue` - Modify: `E:\QtwCode\foodie\foodie-store\src\router\index.js` - Modify: `E:\QtwCode\foodie\foodie-store\src\components\Aside.vue` - [ ] **Step 1: 创建 StallManage.vue 页面** 页面包含: 1. 摊位列表(表格形式,显示摊位名称、地址、状态、操作) 2. 新建摊位按钮 → 弹窗表单(复用现有店铺创建字段) 3. 每个摊位的「管理摊位主」按钮 → 展开摊位主列表 4. 添加摊位主按钮 → 弹窗表单(手机号、密码、昵称) 5. 禁用摊位主按钮 - [ ] **Step 2: 修改 router/index.js 添加摊位管理路由** ```javascript { path: '/stall-manage', name: 'StallManage', component: () => import('@/views/StallManage.vue') } ``` - [ ] **Step 3: 修改 Aside.vue 添加「摊位管理」菜单项** 在侧边栏菜单中添加摊位管理入口,仅夜市用户(userType=3)可见。 - [ ] **Step 4: 验证** 启动前端项目,登录夜市用户账号,验证: - 摊位管理菜单可见 - 创建摊位功能正常 - 创建/管理摊位主功能正常 --- ### Task 6: 数据库变更 — UserWallet 新增 storeId 字段 **Files:** - Modify: `ruoyi-system/src/main/resources/mapper/system/UserWalletMapper.xml` - Modify: `ruoyi-system/src/main/java/com/ruoyi/system/domain/UserWallet.java` - Modify: `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/UserWalletServiceImpl.java` - [ ] **Step 1: 执行 SQL 添加字段** ```sql ALTER TABLE user_wallet ADD COLUMN store_id BIGINT DEFAULT NULL COMMENT '关联摊位id,非摊位钱包为NULL'; ``` - [ ] **Step 2: UserWallet.java 新增 storeId 字段** ```java /** 关联摊位id,非摊位钱包为NULL */ private Long storeId; ``` - [ ] **Step 3: UserWalletMapper.xml 更新** 在 resultMap 中添加 `store_id` 映射,在 insert/update/select 中添加 `storeId` 字段支持。 - [ ] **Step 4: 修改 StallController 创建摊位逻辑** 在 `addStall` 方法中,创建摊位后自动创建摊位钱包: ```java // 创建摊位后,自动创建摊位钱包 UserWallet wallet = new UserWallet(); wallet.setStoreId(posStore.getId()); wallet.setPointsWallet(0L); wallet.setBalanceWallet(BigDecimal.ZERO); wallet.setBlockedFunds(BigDecimal.ZERO); userWalletService.insertUserWallet(wallet); ``` - [ ] **Step 5: 修改现有钱包查询逻辑** 所有通过 `user_id` 查钱包的地方加上 `store_id IS NULL` 条件,避免查到摊位钱包。新增通过 `store_id` 查摊位钱包的方法。 - [ ] **Step 6: 验证** 创建摊位后确认摊位钱包自动创建,查询用户钱包不包含摊位钱包。 --- ### Task 7: 提交与收尾 - [ ] **Step 1: 更新 spec.md 待确认项** 根据实现过程中确认的细节更新需求文档。 - [ ] **Step 2: 提交代码** ```bash git add -A git commit -m "feat: 夜市摊位管理功能 — 创建摊位、管理摊位主账号" ``` --- ## 增量计划:普通商家新增门店审核门禁(2026-08-24) **目标:** 未经平台审核的商家不能通过 `/chanting/store/addmendian` 新增门店。 **实现方式:** `PosStoreController.addmendian` 显式接收请求头 `token`。当 `PosStore.id == null` 时,从 token 解析当前用户 ID,调用现有 `UserService.checkUserStatus(Long)` 统一校验 `InfoUser.auditStatus`,并将新门店 `userId` 覆盖为当前用户 ID;编辑请求不执行该新增门禁。 **安全边界:** 不信任请求体中的 `userId`;不修改平台后台建店、夜市摊位新增和已有门店编辑逻辑;不新增数据库字段或前端状态。 **测试策略:** 新增 `PosStoreControllerTest`,先验证未实现时测试失败,再覆盖未审核拒绝、已审核创建时绑定 token 用户、编辑不校验三种行为;最后运行定向测试及 `ruoyi-admin` 模块构建。