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 | 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 |
Files:
ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosStoreMapper.javaruoyi-system/src/main/resources/mapper/PosStoreMapper.xmlCreate: 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);
在 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>
创建文件 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);
}
}
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): 夜市附近商家列表接口(含搜索)"
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 中已注入则跳过)。
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): 常购/关注摊位列表接口"
Files:
ruoyi-admin/src/main/java/com/ruoyi/app/nightmarket/NightMarketController.javaruoyi-system/src/main/java/com/ruoyi/system/mapper/PosFoodMapper.javaModify: 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" 代替。
在 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;
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): 摊位详情接口(含商品分类列表、评分、关注状态)"
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/nightmarket/stores |
GET | No | 夜市附近商家列表(支持搜索) |
/nightmarket/followed |
GET | Yes | 关注的摊位列表 |
/nightmarket/frequent |
GET | Yes | 常购的摊位列表 |
/nightmarket/store/detail |
GET | Optional | 摊位详情(含商品) |
GET /nightmarket/stores
nightMarketId, longitude, latitude, page(默认1), keyword(可选), language(默认zh-TW)Page<StoreOutput> — 含摊位信息 + foodListGET /nightmarket/followed
token(header), nightMarketId, longitude, latitude, page, languagePage<StoreOutput> — 用户关注的摊位列表GET /nightmarket/frequent
token(header), nightMarketId, longitude, latitude, page, languagePage<StoreOutput> — 用户常购的摊位列表(按近30天下单次数排序)GET /nightmarket/store/detail
storeId, token(header,可选), language(默认zh-TW)