# 夜市用户端 API 实现计划 > **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:** 为用户端夜市功能提供后端接口:附近商家列表(含搜索)、常购/关注列表、店铺详情。 **Architecture:** 新建 `NightMarketController` 作为用户端夜市入口,复用已有的 `PosStoreEnrichService`(评分/月售/营业状态)、`PosStoreMapper`(距离计算)、`PosCollectService`(关注)和 `PosFoodService`(商品)。夜市特有的查询(按 nightMarketId 过滤的摊位列表、搜索)通过在 PosStoreMapper 中新增 SQL 实现。 **Tech Stack:** Java 21, Spring Boot 3.3.5, MyBatis-Plus, MySQL ST_Distance 距离计算 --- ## File Structure | File | Action | Responsibility | |------|--------|----------------| | `ruoyi-admin/.../app/nightmarket/NightMarketController.java` | Create | 用户端夜市 API 入口(列表/搜索/关注/详情) | | `ruoyi-system/.../mapper/PosStoreMapper.java` | Modify | 新增夜市摊位列表查询(含搜索、距离排序) | | `ruoyi-system/.../mapper/PosStoreMapper.xml` | Modify | 新增夜市摊位列表 SQL | | `ruoyi-system/.../mapper/PosFoodMapper.java` | Modify | 新增按店铺查商品(含分类) | | `ruoyi-system/.../mapper/PosFoodMapper.xml` | Modify | 新增按店铺查商品 SQL | --- ### Task 1: 夜市附近商家列表(含搜索) **Files:** - Modify: `ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosStoreMapper.java` - Modify: `ruoyi-system/src/main/resources/mapper/PosStoreMapper.xml` - Create: `ruoyi-admin/src/main/java/com/ruoyi/app/nightmarket/NightMarketController.java` - [ ] **Step 1: 在 PosStoreMapper.java 新增夜市摊位列表查询方法** 在 `PosStoreMapper.java` 中新增两个方法: ```java /** * 夜市附近摊位列表(含搜索) * @param longitude 用户经度 * @param latitude 用户纬度 * @param nightMarketId 夜市ID * @param keyword 搜索关键词(可为null) * @param offset 分页偏移 * @return 摊位列表(含距离juli) */ List getNightMarketStalls(@Param("longitude") BigDecimal longitude, @Param("latitude") BigDecimal latitude, @Param("nightMarketId") Long nightMarketId, @Param("keyword") String keyword, @Param("offset") Integer offset); /** * 夜市摊位总数(含搜索) */ int getNightMarketStallsCount(@Param("nightMarketId") Long nightMarketId, @Param("keyword") String keyword); ``` - [ ] **Step 2: 在 PosStoreMapper.xml 新增对应 SQL** 在 `PosStoreMapper.xml` 的 `` 标签前添加: ```xml ``` - [ ] **Step 3: 创建 NightMarketController.java** 创建文件 `ruoyi-admin/src/main/java/com/ruoyi/app/nightmarket/NightMarketController.java`: ```java package com.ruoyi.app.nightmarket; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.app.mendian.PosStoreEnrichService; import com.ruoyi.app.user.dto.StoreOutput; import com.ruoyi.common.annotation.Anonymous; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.system.domain.*; import com.ruoyi.system.mapper.PosStoreMapper; import com.ruoyi.system.service.*; import com.ruoyi.system.utils.Auth; import com.ruoyi.system.utils.JwtUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.math.BigDecimal; import java.util.List; import java.util.stream.Collectors; /** * 用户端夜市Controller */ @RestController @RequestMapping("/nightmarket") public class NightMarketController extends BaseController { @Autowired private PosStoreMapper posStoreMapper; @Autowired private PosStoreEnrichService posStoreEnrichService; @Autowired private IPosCollectService posCollectService; @Autowired private IPosOrderService posOrderService; @Autowired private IInfoUserService infoUserService; @Autowired private INightMarketService nightMarketService; /** * 夜市附近商家列表(含搜索) * @param nightMarketId 夜市ID * @param longitude 用户经度 * @param latitude 用户纬度 * @param page 页码(从1开始) * @param keyword 搜索关键词(可选) * @param language 语言(默认zh-TW) */ @Anonymous @GetMapping("/stores") public AjaxResult nearbyStores(@RequestParam Long nightMarketId, @RequestParam BigDecimal longitude, @RequestParam BigDecimal latitude, @RequestParam(defaultValue = "1") Integer page, @RequestParam(required = false) String keyword, @RequestParam(defaultValue = "zh-TW", required = false) String language) { int offset = (page - 1) * 20; List list = posStoreMapper.getNightMarketStalls(longitude, latitude, nightMarketId, keyword, offset); posStoreEnrichService.enrichStoreList(list); Page result = posStoreEnrichService.buildStoreOutputPage(list, language, page, list.size()); return success(result); } } ``` - [ ] **Step 4: 编译验证** Run: `cd E:\QtwCode\foodie\foodie_server && mvn compile -pl ruoyi-admin -am -q` Expected: BUILD SUCCESS - [ ] **Step 5: 提交** ```bash git add ruoyi-admin/src/main/java/com/ruoyi/app/nightmarket/NightMarketController.java ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosStoreMapper.java ruoyi-system/src/main/resources/mapper/PosStoreMapper.xml git commit -m "feat(nightmarket): 夜市附近商家列表接口(含搜索)" ``` --- ### Task 2: 常购/关注列表 **Files:** - Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/nightmarket/NightMarketController.java` - [ ] **Step 1: 在 PosStoreMapper.java 新增常购/关注查询方法** ```java /** * 夜市下用户关注的摊位列表 */ List getFollowedStalls(@Param("longitude") BigDecimal longitude, @Param("latitude") BigDecimal latitude, @Param("nightMarketId") Long nightMarketId, @Param("userId") Long userId, @Param("offset") Integer offset); /** * 夜市下用户常购的摊位列表(按下单次数排序) */ List getFrequentStalls(@Param("longitude") BigDecimal longitude, @Param("latitude") BigDecimal latitude, @Param("nightMarketId") Long nightMarketId, @Param("userId") Long userId, @Param("offset") Integer offset); ``` - [ ] **Step 2: 在 PosStoreMapper.xml 新增对应 SQL** ```xml ``` - [ ] **Step 3: 在 NightMarketController 新增常购/关注接口** 在 `NightMarketController` 中添加两个接口方法: ```java /** * 夜市关注的摊位列表 */ @Anonymous @Auth @GetMapping("/followed") public AjaxResult followedStores(@RequestHeader String token, @RequestParam Long nightMarketId, @RequestParam BigDecimal longitude, @RequestParam BigDecimal latitude, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "zh-TW", required = false) String language) { JwtUtil jwtUtil = new JwtUtil(); String userId = jwtUtil.getusid(token); int offset = (page - 1) * 20; List list = posStoreMapper.getFollowedStalls(longitude, latitude, nightMarketId, Long.valueOf(userId), offset); posStoreEnrichService.enrichStoreList(list); Page result = posStoreEnrichService.buildStoreOutputPage(list, language, page, list.size()); return success(result); } /** * 夜市常购的摊位列表 */ @Anonymous @Auth @GetMapping("/frequent") public AjaxResult frequentStores(@RequestHeader String token, @RequestParam Long nightMarketId, @RequestParam BigDecimal longitude, @RequestParam BigDecimal latitude, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "zh-TW", required = false) String language) { JwtUtil jwtUtil = new JwtUtil(); String userId = jwtUtil.getusid(token); int offset = (page - 1) * 20; List list = posStoreMapper.getFrequentStalls(longitude, latitude, nightMarketId, Long.valueOf(userId), offset); posStoreEnrichService.enrichStoreList(list); Page result = posStoreEnrichService.buildStoreOutputPage(list, language, page, list.size()); return success(result); } ``` 别忘了在文件顶部增加 `@Autowired` 注入(如 Task 1 中已注入则跳过)。 - [ ] **Step 4: 编译验证** Run: `cd E:\QtwCode\foodie\foodie_server && mvn compile -pl ruoyi-admin -am -q` Expected: BUILD SUCCESS - [ ] **Step 5: 提交** ```bash git add ruoyi-admin/src/main/java/com/ruoyi/app/nightmarket/NightMarketController.java ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosStoreMapper.java ruoyi-system/src/main/resources/mapper/PosStoreMapper.xml git commit -m "feat(nightmarket): 常购/关注摊位列表接口" ``` --- ### Task 3: 店铺详情(含商品分类列表) **Files:** - Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/nightmarket/NightMarketController.java` - Modify: `ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosFoodMapper.java` - Modify: `ruoyi-system/src/main/resources/mapper/PosFoodMapper.xml`(如果存在) - [ ] **Step 1: 在 PosFoodMapper.java 新增按店铺查商品方法** ```java /** * 按店铺查商品列表(审核通过、上架状态) */ List selectFoodsByStoreId(@Param("storeId") Long storeId, @Param("language") String language); ``` - [ ] **Step 2: 在 PosFoodMapper.xml 新增 SQL** 找到 `PosFoodMapper.xml` 并添加: ```xml ``` 如果 PosFoodMapper.xml 中没有 `PosFoodResult` resultMap,使用 `resultType="PosFood"` 代替。 - [ ] **Step 3: 在 NightMarketController 新增店铺详情接口** 在 `NightMarketController` 中新增必要的注入和详情方法: ```java @Autowired private IPosStoreService posStoreService; @Autowired private IOperatingHoursService operatingHoursService; /** * 夜市摊位详情(含商品分类列表) * @param storeId 摊位ID * @param token 用户token(可选,用于判断是否已关注) * @param language 语言 */ @Anonymous @GetMapping("/store/detail") public AjaxResult storeDetail(@RequestParam Long storeId, @RequestHeader(defaultValue = "") String token, @RequestParam(defaultValue = "zh-TW", required = false) String language) { PosStore store = posStoreService.getById(storeId); if (store == null || !"0".equals(store.getDelFlag()) || store.getIsStall() == null || store.getIsStall() != 1) { return error("摊位不存在"); } // 判断是否已关注 boolean collected = false; if (token != null && !token.isEmpty()) { try { JwtUtil jwtUtil = new JwtUtil(); String userId = jwtUtil.getusid(token); QueryWrapper qw = new QueryWrapper<>(); qw.eq("user_id", userId).eq("md_id", storeId); collected = posCollectService.count(qw) > 0; } catch (Exception ignored) {} } // 评分 QueryWrapper reviewQw = new QueryWrapper<>(); reviewQw.eq("md_id", storeId); List reviews = posReviewService.list(reviewQw); double rating = reviews.isEmpty() ? 4.5d : Math.round(reviews.stream().mapToDouble(PosReview::getScore).average().orElse(4.5d) * 2.0) / 2.0; // 月售 QueryWrapper orderQw = new QueryWrapper<>(); orderQw.eq("md_id", storeId).apply("DATE_FORMAT(cretim,'%Y-%m') = DATE_FORMAT(NOW(),'%Y-%m')"); long monthlySales = posOrderService.count(orderQw); // 商品列表 String langCode = "zh-CN".equals(language) ? "2" : ("en-US".equals(language) ? "1" : "3"); List foods = posFoodMapper.selectFoodsByStoreId(storeId, langCode); // 组装返回 com.alibaba.fastjson.JSONObject result = new com.alibaba.fastjson.JSONObject(); result.put("id", store.getId()); result.put("posName", store.getPosName()); result.put("image", store.getImage()); result.put("logo", store.getLogo()); result.put("address", store.getAddress()); result.put("longitude", store.getLongitude()); result.put("latitude", store.getLatitude()); result.put("briefIntroduction", store.getBriefIntroduction()); result.put("telephone", store.getTelephone()); result.put("openBusiness", store.getOpenBusiness()); result.put("windingUp", store.getWindingUp()); result.put("rating", rating); result.put("monthlySales", monthlySales); result.put("collected", collected ? 1 : 0); result.put("foodList", foods); result.put("nightMarketId", store.getNightMarketId()); return success(result); } ``` 需要在类顶部额外注入(如尚未注入): ```java @Autowired private IPosReviewService posReviewService; @Autowired private PosFoodMapper posFoodMapper; ``` 并确保有以下 import: ```java import com.ruoyi.system.domain.PosFood; import com.ruoyi.system.domain.PosReview; import com.ruoyi.system.mapper.PosFoodMapper; import com.alibaba.fastjson.JSONObject; ``` - [ ] **Step 4: 编译验证** Run: `cd E:\QtwCode\foodie\foodie_server && mvn compile -pl ruoyi-admin -am -q` Expected: BUILD SUCCESS - [ ] **Step 5: 提交** ```bash git add ruoyi-admin/src/main/java/com/ruoyi/app/nightmarket/NightMarketController.java ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosFoodMapper.java ruoyi-system/src/main/resources/mapper/PosFoodMapper.xml git commit -m "feat(nightmarket): 摊位详情接口(含商品分类列表、评分、关注状态)" ``` --- ## API Summary | Endpoint | Method | Auth | Description | |----------|--------|------|-------------| | `/nightmarket/stores` | GET | No | 夜市附近商家列表(支持搜索) | | `/nightmarket/followed` | GET | Yes | 关注的摊位列表 | | `/nightmarket/frequent` | GET | Yes | 常购的摊位列表 | | `/nightmarket/store/detail` | GET | Optional | 摊位详情(含商品) | ### Request/Response **GET /nightmarket/stores** - Request: `nightMarketId`, `longitude`, `latitude`, `page`(默认1), `keyword`(可选), `language`(默认zh-TW) - Response: `Page` — 含摊位信息 + foodList **GET /nightmarket/followed** - Request: `token`(header), `nightMarketId`, `longitude`, `latitude`, `page`, `language` - Response: `Page` — 用户关注的摊位列表 **GET /nightmarket/frequent** - Request: `token`(header), `nightMarketId`, `longitude`, `latitude`, `page`, `language` - Response: `Page` — 用户常购的摊位列表(按近30天下单次数排序) **GET /nightmarket/store/detail** - Request: `storeId`, `token`(header,可选), `language`(默认zh-TW) - Response: JSONObject — 摊位详情 + rating + monthlySales + collected + foodList