# 夜市摊位管理 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` 模块构建。