数据库

表结构分析

字段类型与长度:

  • 数字类型:长度最大20,数字类型
    • 短整型:int
    • 长整型:bigint
    • 小数:decimal
    • 金额类型的数组:看需求,需要小数点,使用 decimal,否则使用 int 单位为分
  • 字符类型:统一使用varchart,字符长度通常设置为8的倍数
  • 时间类型:不同公司规范不同,DATE/DATETIME/TIME,其他一般设置为 bigint 时间戳

攻略表

CREATE TABLE `strategy` (
`id` bigint NOT NULL AUTO_INCREMENT,
`dest_id` bigint DEFAULT NULL,
`dest_name` varchar(255) DEFAULT NULL,
`theme_id` bigint DEFAULT NULL,
`theme_name` varchar(255) DEFAULT NULL,
`catalog_id` bigint DEFAULT NULL,
`catalog_name` varchar(255) DEFAULT NULL,
`title` varchar(255) DEFAULT NULL,
`sub_title` varchar(255) DEFAULT NULL,
`summary` varchar(255) DEFAULT NULL,
`cover_url` varchar(255) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`isabroad` 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=20 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC;

攻略内容表

CREATE TABLE `strategy_content` (
`id` bigint NOT NULL,
`content` text,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC;

攻略排行表

CREATE TABLE `strategy_rank` (
`id` bigint NOT NULL AUTO_INCREMENT,
`dest_id` bigint DEFAULT NULL,
`dest_name` varchar(255) DEFAULT NULL,
`strategy_id` bigint DEFAULT NULL,
`strategy_title` varchar(255) DEFAULT NULL,
`type` int DEFAULT NULL,
`statis_time` datetime DEFAULT NULL,
`statisnum` bigint DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=11321 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC;

实体类

找到 trip-article-api 模块,创建包:com.swx.article.domain,在该包下创建如下的实体类。

攻略表实体类

Strategy
/**
* 攻略
*/
@Setter
@Getter
@TableName("strategy")
public class Strategy implements Serializable {

public static final int ABROAD_NO = 0; //国内
public static final int ABROAD_YES = 1; //国外

public static final int STATE_NORMAL = 0; //带发布
public static final int STATE_PUBLISH = 1; //发布

@TableId(type = IdType.AUTO)
private Long id;

private Long destId; //关联的目的地
private String destName;
private Long themeId; //关联主题
private String themeName;
private Long catalogId; //关联的分类
private String catalogName;
private String title; //标题
private String subTitle; //副标题
private String summary; //内容摘要
private String coverUrl; //封面
private Date createTime; //创建时间
private Integer isabroad; //是否是国外
private Integer viewnum; //点击数
private Integer replynum; //攻略评论数
private Integer favornum; //收藏数
private Integer sharenum; //分享数
private Integer thumbsupnum; //点赞个数
private Integer state; //状态

@TableField(exist = false)
private boolean favorite;

@TableField(exist = false)
private StrategyContent content; //攻略内容
}

攻略内容实体类

StrategyContent
@Setter
@Getter
@TableName("strategy_content")
public class StrategyContent implements Serializable {
private Long id;
private String content;
}

攻略排行实体类

StrategyRank
/**
* 攻略统计表
*/
@Setter
@Getter
@TableName("strategy_rank")
public class StrategyRank {

public static final int TYPE_ABROAD = 1; //国外
public static final int TYPE_CHINA = 2; //国内
public static final int TYPE_HOT = 3; //热门

private Long id;
private Long destId; //目的地id
private String destName; //目的地名称
private Long strategyId; //攻略id
private String strategyTitle; //攻略标题
private int type; //排行类型
private Date statisTime; //归档统计时间
private Long statisnum; //归档统计数
}

文件上传

阿里云OSS

免费使用地址:https://free.aliyun.com/?crowd=personal

对象存储控制台:https://oss.console.aliyun.com/

创建资源

1、创建Bucket

控制台右侧找到 Bucket 列表,新建Bucket,读写权限选择公共读,其他默认即可

2、创建虚拟账号

阿里云的服务都需要AccessKey和AccessSecret才能访问,主账号拥有所有的权限,所以我们需要创建子账号,给子账号授予访问OSS的权限即可

RAM访问控制:https://ram.console.aliyun.com/overview

在RAM访问控制中右侧找到用户,创建用户,访问方式只勾选OpenAPI 调用访问

注意!创建成功后请立即保存 key 和 secret,不要离开页面,否则需要重新创建key和secret

3、给账号授权OSS

点击用户登录名称/显示名称,即用户表格的第一列,选中刚创建的新用户
在下面的Tab栏中点击权限管理,新增授权

给整个云账号授权,权限搜索OSS,授予:AliyunOSSFullAccess,即所有OSS操作权限。

工具类

1、阿里云OSS工具类

基础的配置信息从配置文件中获取,为此需要从 Spring 容器中拿到其他 Environment 对象

OssUtil
/**
* 阿里云OSS工具类
*
* @author swcode
* @since 2023/10/26 13:30
*/
public class OssUtil {
/**
* 访问域名
*/
private final static String ossAccessDomainUrl;
/**
* bucket 名称
*/
private final static String ossBucketName;
/**
* 封装了 accessKeyId 和 accessKeySecret 的客户端对象
*/
private final static OSSClient client;

static {
// 面试题: 如何实现在一个非Spring容器管理的对象中, 从Spring容器中拿到其他Bean对象.
// Environment 对象的作用就是管理 Spring 容器运行时的各种配置内容
Environment environment = SpringContextUtil.getBean(Environment.class);
String endpointUrl = environment.getProperty("aliyun.oss.endpoint");
ossBucketName = environment.getProperty("aliyun.oss.bucket-name");
ossAccessDomainUrl = environment.getProperty("aliyun.oss.domain");
String keyId = environment.getProperty("aliyun.accessKey.id");
String keySecret = environment.getProperty("aliyun.accessKey.secret");
client = new OSSClient(endpointUrl, CredentialsProviderFactory.newDefaultCredentialProvider(keyId, keySecret),
new ClientConfiguration());
}

/**
* @param fileName 用户文件名称
* @return 实际的cos上文件名称
*/
private static String getRealFileName(String saveFolder, String fileName) {
return StringUtils.isNotEmpty(saveFolder) ? saveFolder + "/" + fileName : fileName;
}

public static String upload(String saveFolder, String contentType, String fileName, InputStream input) {
if (StringUtils.isEmpty(fileName) || StringUtils.isEmpty(contentType) || null == input) {
return null;
}
ObjectMetadata objectMeta = new ObjectMetadata();
objectMeta.setContentType(contentType);
String filePath = getRealFileName(saveFolder, fileName);
try {
client.putObject(ossBucketName, filePath, input, objectMeta);
return ossAccessDomainUrl + filePath;
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
try {
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

public static String upload(String saveFolder, String contentType, String fileName, long contentLength, InputStream input) {
if (StringUtils.isEmpty(fileName) || StringUtils.isEmpty(contentType) || contentLength <= 0 || null == input) {
return null;
}
ObjectMetadata objectMeta = new ObjectMetadata();
objectMeta.setContentLength(contentLength);
objectMeta.setContentType(contentType);
String filePath = getRealFileName(saveFolder, fileName);
try {
client.putObject(ossBucketName, filePath, input, objectMeta);
return ossAccessDomainUrl + filePath;
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
try {
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

public static String upload(String saveFolder, String fileMainName, MultipartFile multipartFile) {
if (null != multipartFile && !multipartFile.isEmpty()) {
try {
String filename = multipartFile.getOriginalFilename();
String extFileName;
if (StringUtils.isNotEmpty(filename)) {
extFileName = filename.substring(filename.lastIndexOf("."));
} else {
extFileName = ".jpg";
}
return upload(saveFolder, multipartFile.getContentType(), fileMainName + extFileName, multipartFile.getSize(), multipartFile.getInputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}

public static String upload4SpecialName(String saveFolder, String fileName, MultipartFile multipartFile) {
if (null != multipartFile && !multipartFile.isEmpty()) {
try {
return upload(saveFolder, multipartFile.getContentType(), fileName, multipartFile.getSize(), multipartFile.getInputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}

public static String uploadImgByBase64(String saveFolder, String fileName, String base64ImgContent) {
if (StringUtils.isEmpty(fileName) || StringUtils.isEmpty(base64ImgContent)) {
return null;
}
try {
Matcher matcher = Pattern.compile("^data.(.*?);base64,").matcher(base64ImgContent);
if (matcher.find()) {
base64ImgContent = base64ImgContent.replace(matcher.group(), "");
}
byte[] bytes = Base64Utils.decodeFromString(base64ImgContent);
return upload(saveFolder, "image/jpg", fileName, bytes.length, new ByteArrayInputStream(bytes));
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

public static void delete(String fileUrl) {
if (StringUtils.isEmpty(fileUrl)) {
return;
}
try {
fileUrl = fileUrl.replaceFirst(ossAccessDomainUrl, "");
client.deleteObject(ossBucketName, fileUrl);
} catch (OSSException | ClientException e) {
e.printStackTrace();
}
}

public static String getByFileName(String pathFile) {
return ossAccessDomainUrl + client.getObject(ossBucketName, pathFile).getKey();
}
}

2、获取Spring上下文对象工具

SpringContextUtil
/**
* Spring工具类,获取Spring上下文对象等
*
* @author swcode
* @since 2023/10/26 13:31
*/
@Component
public class SpringContextUtil implements ApplicationContextAware {

private static ApplicationContext applicationContext = null;

/**
* 1. SpringContextUtil 被 JVM 加载时, applicationContext 作为静态属性, 就被初始化了, 但是此时是 null 值
* 2. 当 Spring 容器初始化以后, 会管理 SpringContextUtil Bean 对象
* 3. 当 Spring 创建 SpringContextUtil 实例对象时,
* 在初始化阶段会自动调用实现了 ApplicationContextAware 的 setApplicationContext 方法,
* 此时该类中原本静态容器属性就从 null 变成了容器对象
* 4. 当容器启动成功后, 其他业务代码通过该类的静态成员, 就可以直接的访问容器对象, 从容器对象中获取其他 Bean 对象
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (SpringContextUtil.applicationContext == null) {
SpringContextUtil.applicationContext = applicationContext;
System.out.println("----------" + applicationContext);
}
}

public static ApplicationContext getApplicationContext() {
return applicationContext;
}

public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}

public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}

public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
}

3、完善配置文件

进入桶的概览页面,找到 endpoint 和 domain

进入Nacos 后台,编辑article-service-dev.yaml配置文件,添加如下的配置信息

aliyun:
oss:
endpoint:
bucket-name:
domain:
accessKey:
id:
secret:

接口信息

路径地址 http://localhost:9000/article/strategies/uploadImg
请求方式 POST
请求参数 MultipartFile upload
返回结果 { “uploaded”: 1, “fileName”: “”, “url”: “”}, {“uploaded”: 0, “error”: {“message”: “失败”}}}

上传Controller

找到 StrategyController,定义上传方法:

StrategyController
@PostMapping("/uploadImg")
public JSONObject uploadImg(MultipartFile upload) {
JSONObject result = new JSONObject();
if (upload == null) {
result.put("uploaded", 0);
JSONObject error = new JSONObject();
error.put("message", "请选择要上传的文件!");
result.put("error", error);
return result;
}
String filename = upload.getOriginalFilename();
String mainFilename = "";
if (StringUtils.hasText(filename)) {
mainFilename = filename.substring(0, filename.lastIndexOf(".")) + "_" + System.currentTimeMillis();
} else {
mainFilename = System.currentTimeMillis() + "";
}
// 返回阿里云可访问的地址
String url = OssUtil.upload("images", mainFilename, upload);

result.put("uploaded", 1);
result.put("fileName", upload.getOriginalFilename());
result.put("url", url);
return result;
}

主键查询攻略

获取攻略时需要从 strategy_content 表中查询内容信息

接口信息

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

Service

找到:StrategyServiceImpl,重写 getById 方法:

@Service
public class StrategyServiceImpl extends ServiceImpl<StrategyMapper, Strategy> implements StrategyService {

private final StrategyContentMapper strategyContentMapper;

public StrategyServiceImpl(StrategyContentMapper strategyContentMapper) {
this.strategyContentMapper = strategyContentMapper;
}

@Override
public Strategy getById(Serializable id) {
Strategy strategy = super.getById(id);
StrategyContent strategyContent = strategyContentMapper.selectById(id);
strategy.setContent(strategyContent);
return strategy;
}
}

Controller

找到:StrategyController,定义查询方法:

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

筛选标签查询

对应的前端页面如下:

接口信息

路径地址 http://localhost:9000/article/strategies/conditions
请求方式 GET
请求参数
返回结果 Map{“chinaCondition”, “abroadCondition”, “themeCondition”}

Mapper

找到:StrategyMapper,定义查询筛选条件的方法:

StrategyMapper
public interface StrategyMapper extends BaseMapper<Strategy> {
List<StrategyCondition> selectDestCondition(@Param("abroad") int abroad);
List<StrategyCondition> selectThemeCondition();
}

找到:StrategyMapper.xml,实现查询筛选条件的方法:

StrategyMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.swx.article.mapper.StrategyMapper">
<select id="selectDestCondition" resultType="com.swx.article.vo.StrategyCondition">
SELECT dest_id refid, dest_name name, count(*) total
FROM strategy
WHERE isabroad = #{abroad}
GROUP BY dest_id, dest_name
ORDER BY total DESC
</select>

<select id="selectThemeCondition" resultType="com.swx.article.vo.StrategyCondition">
SELECT theme_id refid, theme_name name, count(*) total
FROM strategy
GROUP BY theme_id, theme_name
ORDER BY total DESC
</select>
</mapper>

Service

找到:StrategyService,定义查询筛选条件的方法

/**
* 查询目的地过滤条件
* @param abroad 是否国内
* @return 过滤条件
*/
List<StrategyCondition> findDestCondition(int abroad);

/**
* 查询主题过滤条件
* @return 过滤条件
*/
List<StrategyCondition> findThemeCondition();

找到:StrategyServiceImpl,实现查询筛选条件的方法

StrategyServiceImpl
/**
* 查询目的地过滤条件
*
* @param abroad 是否国内
* @return 过滤条件
*/
@Override
public List<StrategyCondition> findDestCondition(int abroad) {
return getBaseMapper().selectDestCondition(abroad);
}

/**
* 查询主题过滤条件
*
* @return 过滤条件
*/
@Override
public List<StrategyCondition> findThemeCondition() {
return getBaseMapper().selectThemeCondition();
}

Controller

@GetMapping("/conditions")
public R<Map<String, List<StrategyCondition>>> getConditions() {
Map<String, List<StrategyCondition>> result = new HashMap<>();
List<StrategyCondition> chinaCondition = strategyService.findDestCondition(Strategy.ABROAD_NO);
List<StrategyCondition> abroadCondition = strategyService.findDestCondition(Strategy.ABROAD_YES);
List<StrategyCondition> themeCondition = strategyService.findThemeCondition();

result.put("chinaCondition", chinaCondition);
result.put("abroadCondition", abroadCondition);
result.put("themeCondition", themeCondition);

return R.ok(result);
}

分页查询攻略

对应的前端页面如下:

接口信息

路径地址 http://localhost:9000/article/strategies/query
请求方式 GET
请求参数 StrategyQuery
返回结果 Page

请求参数

请求参数除了基本参数,还包括分页参数,将公共的参数放到QueryObject中:

在 core 模块中创建包:com.swx.common.core.qo,该包下创建 QueryObject

QueryObject
@Getter
@Setter
public class QueryObject {
private String keyword;
private Integer current = 1;
private Integer size = 10;
}
StrategyQuery
@Getter
@Setter
public class StrategyQuery extends QueryObject {
public static final int CONDITION_THEME = 3;

private Long destId; // 目的地ID
private Long themeId; // 主题ID
private Long refid; // 筛选条件ID => 目的地ID|主题ID
private Integer type; // 筛选条件类型 => 目的地|主题
private String orderBy;
}

Service

找到:DestinationService,添加分页查询方法

StrategyService
/**
* 条件分页查询攻略
*
* @param query 查询条件
* @return 攻略
*/
Page<Strategy> pageStrategy(StrategyQuery query);

找到:StrategyServiceImpl,实现上述方法

StrategyServiceImpl
/**
* 条件分页查询攻略
*
* @param query 查询条件
* @return 攻略
*/
@Override
public Page<Strategy> pageStrategy(StrategyQuery query) {
// 兼容标签查询
if ((query.getType() != null && query.getType() != -1)&& (query.getRefid() != null && query.getRefid() != -1)) {
// 多条件标签筛选,目的地或者主题查询
if (query.getType() == StrategyQuery.CONDITION_THEME) {
query.setThemeId(query.getRefid());
} else {
query.setDestId(query.getRefid());
}
}

// 目的地和主题筛选
return super.page(
new Page<>(query.getCurrent(), query.getSize()),
Wrappers.<Strategy>lambdaQuery()
.eq(query.getDestId() != null, Strategy::getDestId, query.getDestId())
.eq(!Objects.isNull(query.getThemeId()), Strategy::getThemeId, query.getThemeId())
.orderByDesc(query.getOrderBy() != null && query.getOrderBy().equals("viewnum"), Strategy::getViewnum)
.orderByDesc(query.getOrderBy() != null && query.getOrderBy().equals("create_time"), Strategy::getCreateTime)

);
}

Controller

找到:StrategyController,添加分页查询方法

StrategyController
@GetMapping("/query")
public R<Page<Strategy>> pageList(StrategyQuery query) {
return R.ok(strategyService.pageStrategy(query));
}

保存更新攻略

接口信息

保存攻略

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

更新攻略

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

Service

更新或者保存方法

@Service
public class StrategyServiceImpl extends ServiceImpl<StrategyMapper, Strategy> implements StrategyService {

private final StrategyCatalogService strategyCatalogService;
private final DestinationService destinationService;
private final StrategyThemeService strategyThemeService;
private final StrategyContentMapper strategyContentMapper;

public StrategyServiceImpl(StrategyCatalogService strategyCatalogService, DestinationService destinationService, StrategyThemeService strategyThemeService, StrategyContentMapper strategyContentMapper) {
this.strategyCatalogService = strategyCatalogService;
this.destinationService = destinationService;
this.strategyThemeService = strategyThemeService;
this.strategyContentMapper = strategyContentMapper;
}

private Boolean doSaveOrUpdate(Strategy strategy) {
// 有封面数据且是base64时才做上传处理
if (StringUtils.hasText(strategy.getCoverUrl()) && !strategy.getCoverUrl().startsWith("http")) {
// 上传封面图片,得到URL后,重新设置到cover属性中
String fiilename = UUID.randomUUID().toString();
// 可以解析base64得到格式,这里直接写死
String url = OssUtil.uploadImgByBase64("images/strategies", fiilename + ".jpg", strategy.getCoverUrl());
strategy.setCoverUrl(url);

}
// 补充分类名称
StrategyCatalog catalog = strategyCatalogService.getById(strategy.getCatalogId());
if (catalog == null) {
throw new BizException(R.CODE_ERROR_PARAM, "分类参数异常");
}
strategy.setCatalogName(catalog.getName());
// 根据分类目的地id/名称,设置到目的地中
strategy.setDestId(catalog.getDestId());
strategy.setDestName(catalog.getDestName());
// 判断目的地是否是国外
List<Destination> toasts = destinationService.toasts(catalog.getDestId());
if (toasts.get(0).getId() == 1) {
strategy.setIsabroad(Strategy.ABROAD_NO);
} else {
strategy.setIsabroad(Strategy.ABROAD_YES);
}
// 查询主题,填充主题名称
StrategyTheme theme = strategyThemeService.getById(strategy.getThemeId());
if (theme == null) {
throw new BizException(R.CODE_ERROR_PARAM, "主题参数异常");
}
strategy.setThemeName(theme.getName());

if (strategy.getId() == null) {
// 设置创建时间
strategy.setCreateTime(new Date());
// 设置各种数量为0
strategy.setViewnum(0);
strategy.setSharenum(0);
strategy.setThumbsupnum(0);
strategy.setReplynum(0);
strategy.setFavornum(0);
// 重新设置状态
strategy.setState(Strategy.STATE_NORMAL);
// 保存攻略对象,得到自增ID
boolean save = super.save(strategy);
// 将攻略ID设置到内容对象中,保存内容对象
StrategyContent content = strategy.getContent();
content.setId(strategy.getId());
return save && strategyContentMapper.insert(content) > 0;
}
// 更新操作
boolean ret = super.updateById(strategy);
StrategyContent content = strategy.getContent();
content.setId(strategy.getId());
int row = strategyContentMapper.updateById(content);
return ret && row > 0;
}
}

重写保存方法

/**
* 保存攻略
*
* @param strategy 攻略参数对象
*/
@Transactional(rollbackFor = Exception.class)
@Override
public boolean save(Strategy strategy) {
// 同一类,事务调用非事务,事务都生效
return doSaveOrUpdate(strategy);
}

重写更新方法

/**
* 更新攻略
*
* @param strategy 攻略参数对象
*/
@Transactional(rollbackFor = Exception.class)
@Override
public boolean updateById(Strategy strategy) {
return doSaveOrUpdate(strategy);
}

Controller

保存攻略

StrategyController
@PostMapping("/save")
public R<?> save(Strategy strategy) {
return R.ok(strategyService.save(strategy));
}

更新攻略

@PostMapping("/update")
public R<?> update(Strategy strategy) {
strategyService.updateById(strategy);
return R.ok();
}

查询攻略内容

接口信息

路径地址 http://localhost:9000/article/strategies/content
请求方式 GET
请求参数 id
返回结果 StrategyContent

Service

找到:DestinationService,添加分页查询方法

StrategyService
/**
* 根据攻略ID,返回攻略内容
*
* @param id 攻略ID
* @return 攻略内容
*/
StrategyContent getContentById(Long id);

找到:StrategyServiceImpl,实现上述方法

StrategyServiceImpl
/**
* 根据攻略ID,返回攻略内容
*
* @param id 攻略ID
* @return 攻略内容
*/
@Override
public StrategyContent getContentById(Long id) {
return strategyContentMapper.selectById(id);
}

Controller

找到:StrategyController,添加查询方法

StrategyController
@GetMapping("/content")
public R<StrategyContent> getContentById(Long id) {
return R.ok(strategyService.getContentById(id));
}

分组查询攻略

根据攻略类别分组查询指定目的地下的攻略

对应的前端页面如下:

接口信息

路径地址 http://localhost:9000/article/strategies/groups
请求方式 GET
请求参数
返回结果 List

Mapper

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

StrategyMapper
public interface StrategyMapper extends BaseMapper<Strategy> {
List<StrategyCatalog> selectGroupsByDestId(@Param("destId") Long destId);
}

找到:StrategyMapper.xml,实现分组查询的方法:

StrategyMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.swx.article.mapper.StrategyMapper">

<resultMap id="BaseResultMap" type="com.swx.article.domain.Strategy">
<id property="id" column="id" />
<result property="title" column="title" />
</resultMap>

<resultMap id="CatalogGroupMap" type="com.swx.article.domain.StrategyCatalog">
<id property="id" column="catalog_id" />
<result property="name" column="catalog_name" />
<collection property="strategies" resultMap="BaseResultMap" columnPrefix="s_">

</collection>
</resultMap>

<select id="selectGroupsByDestId" resultMap="CatalogGroupMap">
SELECT t.catalog_id,
t.catalog_name,
s.id s_id,
s.title s_title
FROM (SELECT catalog_id, catalog_name
FROM strategy
WHERE dest_id = #{destId}
GROUP BY catalog_id, catalog_name) t
JOIN strategy s ON t.catalog_id = s.catalog_id
</select>
</mapper>

Service

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

StrategyService
/**
* 根据目的地ID,分组查询攻略分类下的攻略
*
* @param destId 目的地ID
* @return 分类及其下的攻略
*/
List<StrategyCatalog> findGroupsByDestId(Long destId);

找到:StrategyServiceImpl,实现查询筛选条件的方法

StrategyServiceImpl
/**
* 根据目的地ID,分组查询攻略分类下的攻略
*
* @param destId 目的地ID
* @return 分类及其下的攻略
*/
@Override
public List<StrategyCatalog> findGroupsByDestId(Long destId) {
return baseMapper.selectGroupsByDestId(destId);
}

Controller

StrategyController
@GetMapping("/groups")
public R<List<StrategyCatalog>> groupByCatalog(Long destId) {
return R.ok(strategyService.findGroupsByDestId(destId));
}

攻略排行榜

攻略排行榜采用收藏数+点赞数的方式,计算导致索引失效,查询效率变低。排行榜的实时性要求不高,因此设计strategy_rank表,用于保存排行榜信息,使用Spring Boot定时任务,每10分钟获取排行数据保存到strategy_rank表中,查询时从该表查询即可。

数据处理模块

新建 trip-data-server 模块,pom依赖如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.swx</groupId>
<artifactId>trip-modules</artifactId>
<version>1.0.0</version>
</parent>

<artifactId>trip-data-server</artifactId>

<dependencies>
<dependency>
<groupId>com.swx</groupId>
<artifactId>trip-article-api</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
</dependencies>

</project>

开启任务调度

/**
* 启用 Spring 任务调度
*/
@EnableScheduling
@MapperScan("com.swx.data.mapper")
@SpringBootApplication
public class DataApplication {
public static void main(String[] args) {
SpringApplication.run(DataApplication.class, args);
}
}

在 trip-data-server 模块下创建 StrategyMapper 和 StrategyRankMapper

查询排行数据

新建 StrategyMapper 文件

public interface StrategyMapper extends BaseMapper<Strategy> {
List<StrategyRank> selectStrategyRankByAbroad(@Param("abroad") Integer abroad);
List<StrategyRank> selectStrategyRankHotList();
}

在 StrategyMapper.xml 中实现SQL

StrategyMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.swx.data.mapper.StrategyMapper">
<select id="selectStrategyRankByAbroad" resultMap="com.swx.data.mapper.StrategyRankMapper.BaseResultMap">
SELECT id strategy_id, title strategy_title, dest_id, dest_name, thumbsupnum + favornum statisnum
FROM strategy
WHERE isabroad = #{abroad}
ORDER BY statisnum desc
limit 10
</select>

<select id="selectStrategyRankHotList" resultMap="com.swx.data.mapper.StrategyRankMapper.BaseResultMap">
SELECT id strategy_id, title strategy_title, dest_id, dest_name, (viewnum * 0.2) + (replynum * 0.4) + (favornum * 0.4) statisnum
FROM strategy
ORDER BY statisnum desc
limit 10
</select>
</mapper>

批量插入排行数据

新建 StrategyRankMapper 文件

StrategyRankMapper
public interface StrategyRankMapper extends BaseMapper<StrategyRank> {
int batchInsert(@Param("strategyRanks") List<StrategyRank> strategyRanks);
}

在 StrategyRankMapper.xml 中实现SQL

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.swx.data.mapper.StrategyRankMapper">
<resultMap id="BaseResultMap" type="com.swx.article.domain.StrategyRank">
<id property="id" column="id"/>
<result property="destId" column="dest_id"/>
<result property="destName" column="dest_name"/>
<result property="strategyId" column="strategy_id"/>
<result property="strategyTitle" column="strategy_title"/>
<result property="type" column="type"/>
<result property="statisTime" column="statis_time"/>
<result property="statisnum" column="statisnum"/>
</resultMap>

<insert id="batchInsert">
INSERT INTO strategy_rank(dest_id, dest_name, strategy_id, strategy_title, type, statis_time, statisnum)
values
<foreach collection="strategyRanks" item="item" separator=",">
(#{item.destId}, #{item.destName}, #{item.strategyId}, #{item.strategyTitle}, #{item.type}, #{item.statisTime}, #{item.statisnum})
</foreach>
</insert>
</mapper>

任务调度Service

创建 StrategyRankStatisticJob

/**
* 攻略排行数据统计任务
*/
@Slf4j
@Service
public class StrategyRankStatisticJob {

private final StrategyMapper strategyMapper;
private final StrategyRankMapper strategyRankMapper;

public StrategyRankStatisticJob(StrategyMapper strategyMapper, StrategyRankMapper strategyRankMapper) {
this.strategyMapper = strategyMapper;
this.strategyRankMapper = strategyRankMapper;
}

/**
* 每10分钟执行统计任务
*/
@Transactional(rollbackFor = Exception.class)
// @Scheduled(cron = "0 */10 * * * *")
@Scheduled(cron = "0 * * * * *")
public void statisticRank() {
log.info("[攻略排行统计] 排行数据统计开始 >>>>>>>>>");
Date now = new Date();
// 删除这一次之前的所有数据
strategyRankMapper.delete(Wrappers.<StrategyRank>lambdaQuery().lt(StrategyRank::getStatisTime, now));
log.info("[攻略排行统计] 删除旧的排行数据 >>>>>>>>>");
// 统计国内
doStatistic(now, StrategyRank.TYPE_CHINA, () -> strategyMapper.selectStrategyRankByAbroad(Strategy.ABROAD_NO));

// 统计国外
doStatistic(now, StrategyRank.TYPE_ABROAD, () -> strategyMapper.selectStrategyRankByAbroad(Strategy.ABROAD_YES));

// 统计热门
doStatistic(now, StrategyRank.TYPE_HOT, strategyMapper::selectStrategyRankHotList);
}

public void doStatistic(Date now, Integer type, Supplier<List<StrategyRank>> rankSupplier) {
List<StrategyRank> strategyRanks = rankSupplier.get();
log.info("[攻略排行统计] 排行数据统计:type={}, ranks={}", type, strategyRanks.size());
for (StrategyRank strategyRank : strategyRanks) {
strategyRank.setType(type);
strategyRank.setStatisTime(now);
}
// 保存到排名表中
strategyRankMapper.batchInsert(strategyRanks);
}
}

查询排行榜

接口信息

路径地址 http://localhost:9000/article/strategies/ranks
请求方式 GET
请求参数
返回结果 Map{“abroadRank”, “chinaRank”, “hotRank”}

Service

新建 StrategyRankService,定义获取排行方法

StrategyRankService
public interface StrategyRankService extends IService<StrategyRank> {
/**
* 根据类型获取排名
*
* @param type 类型
* @return 排名
*/
List<StrategyRank> selectLastRanksByType(int type);
}

实现上述方法

StrategyRankServiceImpl
@Service
public class StrategyRankServiceImpl extends ServiceImpl<StrategyRankMapper, StrategyRank> implements StrategyRankService {

@Override
public List<StrategyRank> selectLastRanksByType(int type) {
return list(Wrappers.<StrategyRank>lambdaQuery()
.eq(StrategyRank::getType, type)
.orderByDesc(StrategyRank::getStatisTime)
.last("limit 10")
);
}
}

Controller

StrategyController
@RestController
@RequestMapping("/strategies")
public class StrategyController {

private final StrategyRankService strategyRankService;

public StrategyController(StrategyRankService strategyRankService) {
this.strategyRankService = strategyRankService;
}

@GetMapping("/ranks")
public R<JSONObject> ranks() {
List<StrategyRank> chinaRank = strategyRankService.selectLastRanksByType(StrategyRank.TYPE_CHINA);
List<StrategyRank> abroadRank = strategyRankService.selectLastRanksByType(StrategyRank.TYPE_CHINA);
List<StrategyRank> hotRank = strategyRankService.selectLastRanksByType(StrategyRank.TYPE_HOT);
JSONObject jsonObject = new JSONObject();
jsonObject.put("abroadRank", abroadRank);
jsonObject.put("chinaRank", chinaRank);
jsonObject.put("hotRank", hotRank);
return R.ok(jsonObject);
}
}

删除攻略

接口信息

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

Controller

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

查询浏览量前三的攻略

接口信息

路径地址 http://localhost:9000/article/strategies/viewnumTop3
请求方式 GET
请求参数 destId
返回结果 List

Service

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

StrategyService
/**
* 根据目的地ID,查询浏览量最高的前3篇攻略
*
* @param destId 目的地
* @return 浏览量最高的前3篇攻略
*/
List<Strategy> findViewnumTop3(Long destId);

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

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

Controller

StrategyController
@GetMapping("/viewnumTop3")
public R<List<Strategy>> viewnumTop3(Long destId) {
return R.ok(strategyService.findViewnumTop3(destId));
}