游记服务的后台管理接口可以参考攻略服务

数据库

游记表

CREATE TABLE `travel` (
`id` bigint NOT NULL AUTO_INCREMENT,
`dest_id` bigint DEFAULT NULL,
`dest_name` varchar(255) DEFAULT NULL,
`author_id` bigint DEFAULT NULL,
`title` varchar(255) DEFAULT NULL,
`summary` varchar(255) DEFAULT NULL,
`cover_url` varchar(255) DEFAULT NULL,
`travel_time` datetime DEFAULT NULL,
`avg_consume` int DEFAULT NULL,
`day` int DEFAULT NULL,
`person` int DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`release_time` datetime DEFAULT NULL,
`last_update_time` datetime DEFAULT NULL,
`ispublic` int DEFAULT NULL,
`viewnum` int DEFAULT NULL,
`replynum` int DEFAULT NULL,
`favornum` int DEFAULT NULL,
`sharenum` int DEFAULT NULL,
`thumbsupnum` int DEFAULT NULL,
`state` int DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC;

实体类

找到模块:trip-article-api ,找到包com.swx.article.domain,创建游记实体类

Travel
/**
* 游记
*/
@Setter
@Getter
@TableName("travel")
public class Travel implements Serializable {

public static final int STATE_NORMAL = 0; //草稿
public static final int STATE_WAITING = 1; //待发布(待审核)
public static final int STATE_RELEASE = 2; //审核通过
public static final int STATE_REJECT = 3; //拒绝

public static final int ISPUBLIC_NO = 0;
public static final int ISPUBLIC_YES = 1;

@TableId(type = IdType.AUTO)
private Long id;
private Long destId; //目的地
private String destName; //目的地
private Long authorId; //作者id
@TableField(exist = false)
private UserInfoDTO author;
private String title; //标题
private String summary;//概要
private String coverUrl; //封面
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date travelTime; //旅游时间
private Integer avgConsume; //人均消费
private Integer day; //旅游天数
private Integer person; //和谁旅游
private Date createTime; //创建时间
private Date releaseTime; //发布时间
private Date lastUpdateTime; //最新更新时间内
private Integer ispublic = ISPUBLIC_NO; //是否发布
private Integer viewnum; //点击/阅读数
private Integer replynum; //回复数
private Integer favornum;//收藏数
private Integer sharenum;//分享数
private Integer thumbsupnum;//点赞数
private Integer state = STATE_NORMAL;//游记状态
@TableField(exist = false)
private TravelContent content; //游记内容
}

基础服务

主键查询

接口信息

路径地址 http://localhost:9000/article/travels/detail
请求方式 GET
请求参数 id
返回结果 Travel

Controller

TravelController
@RestController
@RequestMapping("/travels")
public class TravelController {

private final TravelService travelService;

public TravelController(TravelService travelService) {
this.travelService = travelService;
}

@GetMapping("/detail")
public R<Travel> getById(Long id) {
return R.ok(travelService.getById(id));
}
}

保存游记

在保存攻略类型时,需要填充其对应目的地的名称

接口信息

路径地址 http://localhost:9000/article/travels/save
请求方式 POST
请求参数 Travel
返回结果

Controller

TravelController
@PostMapping("/save")
public R<?> save(Travel travel) {
travelService.save(travel);
return R.ok();
}

更新游记

接口信息

路径地址 http://localhost:9000/article/travels/update
请求方式 POST
请求参数 Travel
返回结果

Controller

TravelController
@PostMapping("/update")
public R<?> update(Travel travel) {
travelService.updateById(travel);
return R.ok();
}

删除游记

接口信息

路径地址 http://localhost:9000/article/travels/delete/{id}
请求方式 POST
请求参数 id
返回结果

Controller

TravelController
@PostMapping("/delete/{id}")
public R<?> delete(@PathVariable Long id) {
travelService.removeById(id);
return R.ok();
}

复杂分页查询

根据目的地分组查询类别

对应的前端页面如下:可以根据:出发时间、人均花费、出行天数过滤,根据时间和浏览量排序

接口信息

路径地址 http://localhost:9000/article/travles/query
请求方式 GET
请求参数 TravelQuery
返回结果 R { code: “”, msg: “”, data:Page }

查询条件

使用了Map做映射,前端只需要传入序号,即可映射为过滤范围

找到模块:trip-article-api,创建包:com.swx.article.qo,包下创建查询类:TravelQuery

TravelQuery
@Getter
@Setter
public class TravelQuery extends QueryObject {

private final List<String> ALLOW_ORDER_BY_COLUMNS = Arrays.asList("viewnum", "create_time");
private final static Map<Integer, TravelRange> TRAVEL_TIME_MAP = new HashMap<>(); // 出发时间
private final static Map<Integer, TravelRange> COST_MAP = new HashMap<>(); // 人均花费
private final static Map<Integer, TravelRange> DAYS_MAP = new HashMap<>(); // 出行天数

static {
// 出发时间
TRAVEL_TIME_MAP.put(1, new TravelRange(1, 2));
TRAVEL_TIME_MAP.put(2, new TravelRange(3, 4));
TRAVEL_TIME_MAP.put(3, new TravelRange(5, 6));
TRAVEL_TIME_MAP.put(4, new TravelRange(7, 8));
TRAVEL_TIME_MAP.put(5, new TravelRange(9, 10));
TRAVEL_TIME_MAP.put(6, new TravelRange(11, 12));

// 人均花费
COST_MAP.put(1, new TravelRange(1, 999));
COST_MAP.put(2, new TravelRange(1000, 5999));
COST_MAP.put(3, new TravelRange(6000, 19999));
COST_MAP.put(4, new TravelRange(20000, Integer.MAX_VALUE));

// 出行天数
DAYS_MAP.put(1, new TravelRange(1, 3));
DAYS_MAP.put(2, new TravelRange(4, 7));
DAYS_MAP.put(3, new TravelRange(8, 14));
DAYS_MAP.put(4, new TravelRange(15, 365));
}

private Long destId; // 目的地ID
private String orderBy; // 排序规则
private TravelRange travelTimeRange;
private TravelRange costRange;
private TravelRange dayRange;

/**
* 更改前端的 travelTimeType 参数设置逻辑
* @param travelTimeType 前端的参数
*/
public void setTravelTimeType(Integer travelTimeType) {
this.travelTimeRange = TRAVEL_TIME_MAP.get(travelTimeType);
}

/**
* 更改前端的 consumeType 参数设置逻辑
* @param consumeType 前端的参数
*/
public void setConsumeType(Integer consumeType) {
this.costRange = COST_MAP.get(consumeType);
}

/**
* 更改前端的 dayType 参数设置逻辑
* @param dayType 前端的参数
*/
public void setDayType(Integer dayType) {
this.dayRange = DAYS_MAP.get(dayType);
}

/**
* 防止SQL注入
* @param orderBy 前端传过来的order by 字段
*/
public void setOrderBy(String orderBy) {
if (ALLOW_ORDER_BY_COLUMNS.contains(orderBy)) {
this.orderBy = orderBy;
}
}
}

Service

找到:TravelService,定义分组查询方法

TravelService
public interface TravelService extends IService<Travel> {
/**
* 条件分页查询游记
*
* @param query 分页查询参数
* @return 游记
*/
Page<Travel> pageList(TravelQuery query);
}

找到:TravelServiceImpl,实现上述方法

TravelServiceImpl
@Slf4j
@Service
public class TravelServiceImpl extends ServiceImpl<TravelMapper, Travel> implements TravelService {

private final UserInfoFeignService userInfoFeignService;
private final ThreadPoolExecutor bizThreadPoolExecutor;
private final TravelContentMapper travelContentMapper;

public TravelServiceImpl(UserInfoFeignService userInfoFeignService, ThreadPoolExecutor bizThreadPoolExecutor, TravelContentMapper travelContentMapper) {
this.userInfoFeignService = userInfoFeignService;
this.bizThreadPoolExecutor = bizThreadPoolExecutor;
this.travelContentMapper = travelContentMapper;
}

@Override
public Travel getById(Serializable id) {
Travel travel = super.getById(id);
if (travel == null) {
return null;
}
// 获取游记内容
TravelContent content = travelContentMapper.selectById(id);
travel.setContent(content);

// 获取作者信息
R<UserInfoDTO> result = userInfoFeignService.getById(travel.getAuthorId());
UserInfoDTO author = result.checkAndGet();
travel.setAuthor(author);

return travel;
}

/**
* 条件分页查询游记
*
* @param query 分页查询参数
* @return 游记
*/
@Override
public Page<Travel> pageList(TravelQuery query) {
QueryWrapper<Travel> wrapper = Wrappers.<Travel>query()
.eq(query.getDestId() != null, "dest_id", query.getDestId());
// 旅行时间条件
if (query.getTravelTimeRange() != null) {
TravelRange timeRange = query.getTravelTimeRange();
wrapper.between("MONTH(travel_time)", timeRange.getMin(), timeRange.getMax());
}
// 人均花费条件
if (query.getCostRange() != null) {
TravelRange costRange = query.getCostRange();
wrapper.between("avg_consume", costRange.getMin(), costRange.getMax());
}
// 出行天数条件
if (query.getDayRange() != null) {
TravelRange dayRange = query.getDayRange();
wrapper.between("day", dayRange.getMin(), dayRange.getMax());
}
// 排序
wrapper.orderByDesc(query.getOrderBy() != null, query.getOrderBy());

LoginUser loginUser = AuthenticationUtil.getLoginUser();
if (loginUser == null) {
// 游客:只能浏览已发布的游记
wrapper.eq("ispublic", Travel.ISPUBLIC_YES)
.eq("state", Travel.STATE_RELEASE);
} else {
// 用户:可以查看游客内容以及自己的游记
wrapper.and(w ->
w.eq("author_id", loginUser.getId())
.or(ww -> ww.eq("ispublic", Travel.ISPUBLIC_YES).eq("state", Travel.STATE_RELEASE))
);
}

Page<Travel> page = super.page(new Page<>(query.getCurrent(), query.getSize()), wrapper);
List<Travel> travels = page.getRecords();

// 计数器,等待
CountDownLatch latch = new CountDownLatch(travels.size());
for (Travel travel : travels) {
// 线程池,多线程执行
bizThreadPoolExecutor.execute(() -> {
try {
R<UserInfoDTO> result = userInfoFeignService.getById(travel.getAuthorId());
if (result.getCode() != R.CODE_SUCCESS) {
log.warn("[游记服务] 查询用户作者失败,返回数据异常: {}", JSON.toJSONString(result));
return;
}
travel.setAuthor(result.getData());
} finally {
// 倒计时数量-1
latch.countDown();
}
});
}
// 返回结果前阻塞等待
try {
latch.await(10, TimeUnit.MINUTES);
} catch (InterruptedException e) {
e.printStackTrace();
}
return page;
}
}

Controller

找到:TravelController,定义分页查询接口

TravelController
@GetMapping("/query")
public R<Page<Travel>> pageList(TravelQuery query) {
return R.ok(travelService.pageList(query));
}

查询浏览量前三的游记

接口信息

路径地址 http://localhost:9000/article/travels/viewnumTop3
请求方式 GET
请求参数 destId
返回结果 R { code: “”, msg: “”, data:List }

Service

找到:TravelService,定义查询的方法

TravelService
/**
* 根据目的地ID,查询浏览量最高的前3篇游记
*
* @param destId 目的地
* @return 浏览量最高的前3篇游记
*/
List<Travel> findViewnumTop3(Long destId);

找到:TravelServiceImpl,实现查询的方法

TravelServiceImpl
/**
* 根据目的地ID,查询浏览量最高的前3篇游记
*
* @param destId 目的地
* @return 浏览量最高的前3篇游记
*/
@Override
public List<Travel> findViewnumTop3(Long destId) {
return super.list(Wrappers.<Travel>lambdaQuery()
.eq(Travel::getDestId, destId)
.orderByDesc(Travel::getViewnum)
.last("limit 3")
);
}

Controller

找到:TravelController,定义查询接口

TravelController
@GetMapping("/viewnumTop3")
public R<List<Travel>> viewnumTop3(Long destId) {
return R.ok(travelService.findViewnumTop3(destId));
}