2026-05-08-nightmarket-customer-api.md 18 KB

夜市用户端 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 中新增两个方法:

/**
 * 夜市附近摊位列表(含搜索)
 * @param longitude 用户经度
 * @param latitude 用户纬度
 * @param nightMarketId 夜市ID
 * @param keyword 搜索关键词(可为null)
 * @param offset 分页偏移
 * @return 摊位列表(含距离juli)
 */
List<PosStore> 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</mapper> 标签前添加:

<!-- 夜市附近摊位列表(含搜索、距离排序) -->
<select id="getNightMarketStalls" resultMap="PosStoreResult">
    SELECT *,(st_distance(point(longitude,latitude),point(#{longitude},#{latitude}))*111195/1000) as juli
    FROM pos_store
    WHERE del_flag='0' AND off_shelf='0' AND is_stall=1 AND night_market_id=#{nightMarketId}
    <if test="keyword != null and keyword != ''">
        AND pos_name LIKE CONCAT('%', #{keyword}, '%')
    </if>
    ORDER BY juli ASC
    LIMIT #{offset}, 20
</select>

<!-- 夜市摊位总数(含搜索) -->
<select id="getNightMarketStallsCount" resultType="int">
    SELECT COUNT(*) FROM pos_store
    WHERE del_flag='0' AND off_shelf='0' AND is_stall=1 AND night_market_id=#{nightMarketId}
    <if test="keyword != null and keyword != ''">
        AND pos_name LIKE CONCAT('%', #{keyword}, '%')
    </if>
</select>
  • Step 3: 创建 NightMarketController.java

创建文件 ruoyi-admin/src/main/java/com/ruoyi/app/nightmarket/NightMarketController.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<PosStore> list = posStoreMapper.getNightMarketStalls(longitude, latitude, nightMarketId, keyword, offset);
        posStoreEnrichService.enrichStoreList(list);
        Page<StoreOutput> 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: 提交

    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 新增常购/关注查询方法

    /**
    * 夜市下用户关注的摊位列表
    */
    List<PosStore> getFollowedStalls(@Param("longitude") BigDecimal longitude,
                                  @Param("latitude") BigDecimal latitude,
                                  @Param("nightMarketId") Long nightMarketId,
                                  @Param("userId") Long userId,
                                  @Param("offset") Integer offset);
    
    /**
    * 夜市下用户常购的摊位列表(按下单次数排序)
    */
    List<PosStore> 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

    <!-- 夜市下用户关注的摊位 -->
    <select id="getFollowedStalls" resultMap="PosStoreResult">
    SELECT ps.*, (st_distance(point(ps.longitude,ps.latitude),point(#{longitude},#{latitude}))*111195/1000) as juli
    FROM pos_store ps
    INNER JOIN pos_collect pc ON ps.id = pc.md_id
    WHERE ps.del_flag='0' AND ps.off_shelf='0' AND ps.is_stall=1
      AND ps.night_market_id=#{nightMarketId}
      AND pc.user_id=#{userId}
    ORDER BY pc.cretim DESC
    LIMIT #{offset}, 20
    </select>
    
    <!-- 夜市下用户常购的摊位(按近30天下单次数排序) -->
    <select id="getFrequentStalls" resultMap="PosStoreResult">
    SELECT ps.*, (st_distance(point(ps.longitude,ps.latitude),point(#{longitude},#{latitude}))*111195/1000) as juli
    FROM pos_store ps
    INNER JOIN (
        SELECT md_id, COUNT(*) as order_count
        FROM pos_order
        WHERE user_id=#{userId}
          AND DATE_FORMAT(cretim,'%Y-%m-%d') >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
        GROUP BY md_id
        ORDER BY order_count DESC
    ) po ON ps.id = po.md_id
    WHERE ps.del_flag='0' AND ps.off_shelf='0' AND ps.is_stall=1
      AND ps.night_market_id=#{nightMarketId}
    ORDER BY po.order_count DESC
    LIMIT #{offset}, 20
    </select>
    
  • [ ] Step 3: 在 NightMarketController 新增常购/关注接口

NightMarketController 中添加两个接口方法:

/**
 * 夜市关注的摊位列表
 */
@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<PosStore> list = posStoreMapper.getFollowedStalls(longitude, latitude, nightMarketId, Long.valueOf(userId), offset);
    posStoreEnrichService.enrichStoreList(list);
    Page<StoreOutput> 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<PosStore> list = posStoreMapper.getFrequentStalls(longitude, latitude, nightMarketId, Long.valueOf(userId), offset);
    posStoreEnrichService.enrichStoreList(list);
    Page<StoreOutput> 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: 提交

    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 新增按店铺查商品方法

    /**
    * 按店铺查商品列表(审核通过、上架状态)
    */
    List<PosFood> selectFoodsByStoreId(@Param("storeId") Long storeId,
                                    @Param("language") String language);
    
  • [ ] Step 2: 在 PosFoodMapper.xml 新增 SQL

找到 PosFoodMapper.xml 并添加:

<select id="selectFoodsByStoreId" resultMap="PosFoodResult">
    SELECT id, fl_id, mdid, name, image, price, introduce, recommend, sort, language, stacking_up, to_examine
    FROM pos_food
    WHERE mdid = #{storeId}
      AND to_examine = '1'
      AND stacking_up = '0'
      AND language = #{language}
    ORDER BY sort ASC, id DESC
</select>

如果 PosFoodMapper.xml 中没有 PosFoodResult resultMap,使用 resultType="PosFood" 代替。

  • Step 3: 在 NightMarketController 新增店铺详情接口

NightMarketController 中新增必要的注入和详情方法:

@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<PosCollect> qw = new QueryWrapper<>();
            qw.eq("user_id", userId).eq("md_id", storeId);
            collected = posCollectService.count(qw) > 0;
        } catch (Exception ignored) {}
    }

    // 评分
    QueryWrapper<PosReview> reviewQw = new QueryWrapper<>();
    reviewQw.eq("md_id", storeId);
    List<PosReview> 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<PosOrder> 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<PosFood> 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);
}

需要在类顶部额外注入(如尚未注入):

@Autowired
private IPosReviewService posReviewService;

@Autowired
private PosFoodMapper posFoodMapper;

并确保有以下 import:

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: 提交

    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<StoreOutput> — 含摊位信息 + foodList

GET /nightmarket/followed

  • Request: token(header), nightMarketId, longitude, latitude, page, language
  • Response: Page<StoreOutput> — 用户关注的摊位列表

GET /nightmarket/frequent

  • Request: token(header), nightMarketId, longitude, latitude, page, language
  • Response: Page<StoreOutput> — 用户常购的摊位列表(按近30天下单次数排序)

GET /nightmarket/store/detail

  • Request: storeId, token(header,可选), language(默认zh-TW)
  • Response: JSONObject — 摊位详情 + rating + monthlySales + collected + foodList