# 夜市摊位管理 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
<result property="isStall" column="is_stall"/>
```

在 `selectPosStoreVo` 中添加 `is_stall` 字段。

在 `insertPosStore` 中添加：

```xml
<if test="isStall != null">is_stall,</if>
...
<if test="isStall != null">#{isStall},</if>
```

在 `updatePosStore` 中添加：

```xml
<if test="isStall != null">is_stall = #{isStall},</if>
```

在 `selectPosStoreList` 的 where 中添加：

```xml
<if test="isStall != null"> and is_stall = #{isStall}</if>
```

- [ ] **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
<select id="selectStallOwnersByStoreId" parameterType="Long" resultMap="InfoUserResult">
    <include refid="selectInfoUserVo"/>
    where store_id = #{storeId} and user_type = '4' and del_flag = '0'
</select>
```

- [ ] **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` 模块构建。
