新增短链功能

This commit is contained in:
liaozetao
2023-09-20 10:08:43 +08:00
parent ff59407f77
commit 38ad5722c7
24 changed files with 1194 additions and 16 deletions

View File

@@ -0,0 +1,61 @@
package com.accompany.admin.dto.link;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
/**
* @author: liaozetao
* @date: 2023/9/19 18:34
* @description:
*/
@Data
public class ShortLinkAdminDto {
/**
* 短链ID
*/
@ExcelProperty("短链ID")
private Long id;
/**
* 短链名称
*/
@ExcelProperty("短链名称")
private String name;
/**
* 短链
*/
@ExcelProperty("短链")
private String url;
/**
* 二维码图片
*/
@ExcelProperty("二维码图片")
private String qrcode;
/**
* 定向跳转
*/
@ExcelProperty("定向跳转")
private String typeName;
/**
* 生成时间
*/
@ExcelProperty("生成时间")
private String createTimeStr;
/**
* 累计点击数
*/
@ExcelProperty("累计点击数")
private Integer clickNum;
/**
* 累计点击uv
*/
@ExcelProperty("累计点击uv")
private Integer uvNum;
}

View File

@@ -0,0 +1,28 @@
package com.accompany.admin.vo.link;
import com.accompany.business.model.link.ShortLink;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @author: liaozetao
* @date: 2023/9/19 15:42
* @description:
*/
@Data
@ApiModel
public class ShortLinkAdminVo extends ShortLink {
/**
* 累计点击数
*/
@ApiModelProperty("累计点击数")
private Integer clickNum = 0;
/**
* 累计点击uv
*/
@ApiModelProperty("累计点击uv")
private Integer uvNum = 0;
}

View File

@@ -67,6 +67,11 @@
<artifactId>tomcat-embed-core</artifactId> <artifactId>tomcat-embed-core</artifactId>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>${zxing.version}</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -0,0 +1,56 @@
package com.accompany.admin.service.link;
import com.accompany.admin.vo.link.ShortLinkAdminVo;
import com.accompany.business.model.link.ShortLink;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.web.context.request.ServletWebRequest;
/**
* @author: liaozetao
* @date: 2023/9/19 15:35
* @description:
*/
public interface ShortLinkAdminService {
/**
* 分页
*
* @param linkId
* @param linkName
* @param page
* @param pageSize
* @return
*/
Page<ShortLinkAdminVo> page(Long linkId, String linkName, Integer page, Integer pageSize);
/**
* 保存
*
* @param param
*/
void save(ShortLink param);
/**
* 失效
*
* @param linkId
*/
void expire(Long linkId);
/**
* 启用
*
* @param linkId
*/
void enable(Long linkId);
/**
* 导出
*
* @param linkId
* @param linkName
* @param servletWebRequest
*/
void export(Long linkId, String linkName, ServletWebRequest servletWebRequest);
}

View File

@@ -0,0 +1,186 @@
package com.accompany.admin.service.link.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.util.StrUtil;
import com.accompany.admin.dto.link.ShortLinkAdminDto;
import com.accompany.admin.service.link.ShortLinkAdminService;
import com.accompany.admin.util.QrCodeUtil;
import com.accompany.admin.vo.link.ShortLinkAdminVo;
import com.accompany.business.enums.link.ShortLinkTypeEnum;
import com.accompany.business.model.link.ShortLink;
import com.accompany.business.model.link.ShortLinkRecord;
import com.accompany.business.mybatismapper.link.ShortLinkMapper;
import com.accompany.business.mybatismapper.link.ShortLinkRecordMapper;
import com.accompany.business.service.api.QinniuService;
import com.accompany.common.constant.Constant;
import com.accompany.common.model.PageReq;
import com.accompany.core.service.SysConfService;
import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.ServletWebRequest;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
/**
* @author: liaozetao
* @date: 2023/9/19 15:35
* @description:
*/
@Slf4j
@Service
public class ShortLinkAdminServiceImpl implements ShortLinkAdminService {
@Autowired
private ShortLinkMapper shortLinkMapper;
@Autowired
private ShortLinkRecordMapper shortLinkRecordMapper;
@Autowired
private QinniuService qinniuService;
@Autowired
private SysConfService sysConfService;
@Override
public Page<ShortLinkAdminVo> page(Long linkId, String linkName, Integer currentPage, Integer pageSize) {
IPage<ShortLink> page = shortLinkMapper.selectPage(new Page<>(currentPage, pageSize), Wrappers.<ShortLink>lambdaQuery()
.eq(linkId != null, ShortLink::getId, linkId)
.like(StrUtil.isNotEmpty(linkName), ShortLink::getName, linkName)
.orderByDesc(ShortLink::getCreateTime));
Page<ShortLinkAdminVo> iPage = new Page<>(currentPage, pageSize);
List<ShortLinkAdminVo> admins = new ArrayList<>();
List<ShortLink> records = page.getRecords();
if (CollectionUtil.isNotEmpty(records)) {
for (ShortLink record : records) {
ShortLinkAdminVo admin = new ShortLinkAdminVo();
BeanUtils.copyProperties(record, admin);
admin.setClickNum(shortLinkRecordMapper.selectCount(Wrappers.<ShortLinkRecord>lambdaQuery()
.eq(ShortLinkRecord::getLinkId, record.getId())));
//UV
admin.setUvNum(shortLinkRecordMapper.getUvCount(record.getId()));
admins.add(admin);
}
}
iPage.setTotal(page.getTotal());
iPage.setRecords(admins);
return iPage;
}
@Override
public void save(ShortLink param) {
String linkUrl = sysConfService.getSysConfValueById(Constant.SysConfId.SHORT_LINK_URL);
Long id = param.getId();
Date now = new Date();
param.setUpdateTime(now);
if (id == null) {
Random random = new Random();
String code;
String qrcode = StrUtil.EMPTY;
for (code = Integer.toHexString(random.nextInt(900001) + 100000);
shortLinkMapper.selectCount(Wrappers.<ShortLink>lambdaQuery()
.eq(ShortLink::getCode, code)) > 0; code = Integer.toHexString(random.nextInt(900001) + 100000))
;
code = code.toLowerCase();
String url = linkUrl + code;
InputStream inputStream = null;
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
QrCodeUtil.encode(url, 200, 200, outputStream);
inputStream = new ByteArrayInputStream(outputStream.toByteArray());
qrcode = qinniuService.uploadByStream(inputStream);
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
IOUtils.closeQuietly(inputStream);
}
param.setUrl(url);
param.setCode(code);
param.setQrcode(qrcode);
param.setCreateTime(now);
shortLinkMapper.insert(param);
} else {
shortLinkMapper.updateById(param);
}
}
@Override
public void expire(Long linkId) {
ShortLink shortLink = shortLinkMapper.selectById(linkId);
if (shortLink == null) {
return;
}
shortLink.setIsEnabled(Constant.Yes1No0.NO);
shortLink.setExpireTime(new Date());
shortLinkMapper.updateById(shortLink);
}
@Override
public void enable(Long linkId) {
ShortLink shortLink = shortLinkMapper.selectById(linkId);
if (shortLink == null) {
return;
}
shortLink.setIsEnabled(Constant.Yes1No0.YES);
shortLink.setExpireTime(null);
shortLinkMapper.updateById(shortLink);
}
@Override
public void export(Long linkId, String linkName, ServletWebRequest servletWebRequest) {
PageReq req = new PageReq();
req.setPage(1);
req.setPageSize(1000000);
List<ShortLinkAdminDto> datas = new ArrayList<>();
Page<ShortLinkAdminVo> page = page(linkId, linkName, 1, 100000);
List<ShortLinkAdminVo> records = page.getRecords();
if (CollectionUtil.isNotEmpty(records)) {
for (ShortLinkAdminVo record : records) {
Integer type = record.getType();
Date createTime = record.getCreateTime();
ShortLinkAdminDto admin = new ShortLinkAdminDto();
BeanUtils.copyProperties(record, admin);
String typeName = "应用商店";
if (type != null) {
if (type == ShortLinkTypeEnum.CUSTOM.ordinal()) {
typeName = "自定义URL:" + record.getCustomValue();
}
}
admin.setTypeName(typeName);
if (createTime != null) {
admin.setCreateTimeStr(DateFormatUtils.format(createTime, DatePattern.NORM_DATETIME_PATTERN));
}
datas.add(admin);
}
}
if (servletWebRequest.getResponse() != null) {
try {
//这里注意 有同学反应使用swagger 会导致各种问题请直接用浏览器或者用postman
servletWebRequest.getResponse().setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
servletWebRequest.getResponse().setCharacterEncoding("utf-8");
//这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("短链记录", "UTF-8").replaceAll("\\+", "%20");
servletWebRequest.getResponse().setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
EasyExcel.write(servletWebRequest.getResponse().getOutputStream(), ShortLinkAdminDto.class).sheet("短链记录").doWrite(datas);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
}

View File

@@ -0,0 +1,42 @@
package com.accompany.admin.util;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Hashtable;
import java.util.Map;
/**
* 二维码生成工具类
*/
public class QrCodeUtil {
/**
* 生成二维码
* David
*
* @param url url 网址
* @param width 二维码宽度
* @param height 二维码高度
*/
public static void encode(String url, int width, int height, OutputStream outputStream) {
Map<EncodeHintType, Object> hints = new Hashtable<>();
// 指定纠错等级
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
// 指定编码格式
hints.put(EncodeHintType.CHARACTER_SET, "UTF8");
try {
BitMatrix bitMatrix = new MultiFormatWriter().encode(url, BarcodeFormat.QR_CODE, width, height, hints);
MatrixToImageWriter.writeToStream(bitMatrix, "png", outputStream);
} catch (Exception ignored) {
}
}
}

View File

@@ -0,0 +1,101 @@
package com.accompany.admin.controller.link;
import com.accompany.admin.service.link.ShortLinkAdminService;
import com.accompany.admin.vo.link.ShortLinkAdminVo;
import com.accompany.business.model.link.ShortLink;
import com.accompany.common.model.PageReq;
import com.accompany.common.result.BusiResult;
import com.accompany.common.result.PageResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author: liaozetao
* @date: 2023/9/19 15:41
* @description:
*/
@Api(tags = "短链管理")
@RestController
@RequestMapping("/admin/short/link")
public class ShortLinkAdminController {
@Autowired
private ShortLinkAdminService shortLinkAdminService;
/**
* 分页
*
* @param linkId
* @param linkName
* @param req
* @return
*/
@ApiOperation("分页")
@GetMapping("page")
public PageResult<ShortLinkAdminVo> page(Long linkId, String linkName, PageReq req) {
return new PageResult<>(shortLinkAdminService.page(linkId, linkName, req.getPage(), req.getPageSize()));
}
/**
* 保存
*
* @param param
* @return
*/
@ApiOperation("保存")
@PostMapping("save")
public BusiResult<Void> save(ShortLink param) {
shortLinkAdminService.save(param);
return BusiResult.success();
}
/**
* 失效
*
* @param linkId
* @return
*/
@ApiOperation("失效")
@GetMapping("expire")
public BusiResult<Void> expire(Long linkId) {
shortLinkAdminService.expire(linkId);
return BusiResult.success();
}
/**
* 启用
*
* @param linkId
* @return
*/
@ApiOperation("启用")
@GetMapping("enable")
public BusiResult<Void> enable(Long linkId) {
shortLinkAdminService.enable(linkId);
return BusiResult.success();
}
/**
* 导出
*
* @param linkId
* @param linkName
* @param request
* @param response
*/
@ApiOperation("导出")
@PostMapping("export")
public void export(Long linkId, String linkName, HttpServletRequest request, HttpServletResponse response) {
shortLinkAdminService.export(linkId, linkName, new ServletWebRequest(request, response));
}
}

View File

@@ -0,0 +1,282 @@
<section class="content">
<div class="box box-primary">
<div class="box-body">
<!-- Content Header (Page header) -->
<section class="content-header">
<h1 id="itemTitle"></h1>
</section>
<!-- .content -->
<div id="table"></div>
<div id="toolbar">
<form id="searchForm" action="/admin/short/link/export" method="get" target="_blank">
<div class="col-sm-12">
<label for="linkId" class="col-sm-2 control-label">短链id:</label>
<div class="col-sm-2">
<input type="text" class="form-control" name="linkId" id="linkId">
</div>
<label for="linkName" class="col-sm-2 control-label">短链名称:</label>
<div class="col-sm-2">
<input type="text" class="form-control" name="linkName" id="linkName">
</div>
</div>
</form>
<button id="btnSearch" class="btn btn-default">
<i class="glyphicon glyphicon-search"></i>查询
</button>
<button id="btnExport" class="btn btn-default">
<i class="glyphicon glyphicon-Export"></i>导出
</button>
<button id="btnAdd" class="btn btn-default">
<i class="glyphicon glyphicon-plus-sign"></i>生成
</button>
</div>
</div>
</div>
</section>
<div class="modal fade" id="editModal" tabindex="-1" role="dialog" aria-labelledby="modalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="modalLabel">生成短链</h4>
</div>
<div class="modal-body">
<form class="form-horizontal">
<input type="hidden" name="id" id="id"/>
<div class="form-group">
<label for="name" class="col-sm-3 control-label">短链名称:</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="name">
</div>
</div>
<label for="type" class="col-sm-3 control-label">定向跳转:</label>
<div class="col-sm-9">
<select name="type" id="type" class="form-control validate[required]">
<option value="0">应用商店</option>
<option value="1">自定义URL</option>
</select>
</div>
<div class="form-group">
<label for="customValue" class="col-sm-3 control-label">自定义:</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="customValue">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="save">生成</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="tipModal" tabindex="-1" role="dialog" aria-labelledby="modalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">提示信息</h4>
</div>
<div class="modal-body" id="tipMsg"></div>
</div>
</div>
</div>
<script>
$(function() {
$('#table').bootstrapTable('destroy');
$('#table').bootstrapTable({
columns: [
{field: 'id', title: '短链ID', align: 'center', width: '5%'},
{field: 'name', title: '短链名称', align: 'center', width: '5%'},
{field: 'url', title: '短链', align: 'center', width: '5%'},
{
field: 'qrcode',
title: '二维码图片',
align: 'center',
width: '5%',
formatter: function (val, row, index) {
return "<img src='"+val+"' width='40' height='40'>";
}
},
{
field: 'type',
title: '定向跳转',
align: 'center',
width: '5%',
formatter: function (val, row, index) {
var value = '';
if (type == 0) {
value = '应用商店';
} else {
value = '自定义URL<br/>' + row.customValue;
}
return value;
}
},
{field: 'createTime', title: '生成时间', align: 'center', width: '5%'},
{field: 'clickNum', title: '累计点击数', align: 'center', width: '5%'},
{field: 'uvNum', title: '累计点击uv', align: 'center', width: '5%'},
{
field: 'id',
title: '操作',
align: 'center',
width: '5%',
valign: 'middle',
formatter: function (val, row, index) {
var value = '<button class="btn btn-sm btn-default opt-edit" data-index="' + index + '">编辑</button>';
if (row.isEnabled == 0) {
value += '<button class="btn btn-sm btn-default opt-enable" data-index="' + index + '">启用</button>';
} else if (row.isEnabled == 1) {
value += '<button class="btn btn-sm btn-default opt-expire" data-index="' + index + '">失效</button>';
}
return value;
}
}
],
cache: false,
striped: true,
showRefresh: false,
pageSize: 20,
pagination: true,
pageList: [20, 50, 100],
search: false,
sidePagination: "server", //表示服务端请求
//设置为undefined可以获取pageNumberpageSizesearchTextsortNamesortOrder
//设置为limit可以获取limit, offset, search, sort, order
queryParamsType: "undefined",
queryParams: function queryParams(params) { //设置查询参数
var param = {
page: params.pageNumber,
pageSize: params.pageSize,
linkId: $('#linkId').val(),
linkName: $('#linkName').val(),
};
return param;
},
toolbar: '#toolbar',
url: '/admin/short/link/page',
onLoadSuccess: function () { //加载成功时执行
console.log("load success");
},
onLoadError: function () { //加载失败时执行
console.log("load fail");
}
});
//导出功能
$("#btnExport").on('click',function () {
$("#searchForm").submit();
});
// 查询刷新
$('#btnSearch').on('click', function () {
TableHelper.doRefresh('#table');
});
$('#btnAdd').on('click', function () {
$("#id").val('');
$("#name").val('');
$("#type").val('');
$("#customValue").val('');
$("#editModal").modal('show');
});
$('#table').on('click', '.opt-edit', function () {
const currentData = $('#table').bootstrapTable('getData')[$(this).data('index')];
$('#id').val(currentData.id);
$('#name').val(currentData.name);
$('#type').val(currentData.type);
$('#customValue').val(currentData.customValue);
$("#editModal").modal('show');
});
$("#save").click(function () {
const msg = '确定要保存吗?';
if (confirm(msg)) {
const data = {
id: $('#id').val(),
name: $('#name').val(),
type: $('#type').val(),
customValue: $('#customValue').val(),
}
$.ajax({
type: "post",
url: "/admin/short/link/save",
data: data,
dataType: "json",
success: function (json) {
if (json.success == 'true' || json.code == 200) {
$("#tipMsg").text("保存成功");
$("#tipModal").modal('show');
TableHelper.doRefresh("#table");
$("#editModal").modal('hide');
} else {
$("#tipMsg").text("保存失败." + json.message);
$("#tipModal").modal('show');
$("#editModal").modal('hide');
}
}
});
}
});
// 失效
$('#table').on('click', '.opt-expire', function () {
const currentData = $('#table').bootstrapTable('getData')[$(this).data('index')];
var id = currentData.id;
const msg = '确定要失效吗?'
if (confirm(msg)) {
$.ajax({
type: "get",
url: "/admin/short/link/expire?linkId=" + id,
dataType: "json",
success: function (json) {
if (json.success == 'true' || json.code == 200) {
$("#tipMsg").text("操作成功");
$("#tipModal").modal('show');
TableHelper.doRefresh("#table");
} else {
$("#tipMsg").text("操作失败." + json.message);
$("#tipModal").modal('show');
}
},
error: function (e){
serverError(e);
}
});
}
});
// 启用
$('#table').on('click', '.opt-enable', function () {
const currentData = $('#table').bootstrapTable('getData')[$(this).data('index')];
var id = currentData.id;
const msg = '确定要启用吗?'
if (confirm(msg)) {
$.ajax({
type: "get",
url: "/admin/short/link/enable?linkId=" + id,
dataType: "json",
success: function (json) {
if (json.success == 'true' || json.code == 200) {
$("#tipMsg").text("操作成功");
$("#tipModal").modal('show');
TableHelper.doRefresh("#table");
} else {
$("#tipMsg").text("操作失败." + json.message);
$("#tipModal").modal('show');
}
},
error: function (e){
serverError(e);
}
});
}
});
});
</script>

View File

@@ -1,16 +0,0 @@
/**
* Simplified Chinese translation for bootstrap-datetimepicker
* Yuan Cheung <advanimal@gmail.com>
*/
;(function($){
$.fn.datetimepicker.dates['zh-CN'] = {
days: ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"],
daysShort: ["周日", "周一", "周二", "周三", "周四", "周五", "周六", "周日"],
daysMin: ["日", "一", "二", "三", "四", "五", "六", "日"],
months: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
monthsShort: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
today: "今天",
suffix: [],
meridiem: ["上午", "下午"]
};
}(jQuery));

View File

@@ -1824,6 +1824,10 @@ public class Constant {
*/ */
public static final String SMS_SDK_TYPE = "sms_sdk_type"; public static final String SMS_SDK_TYPE = "sms_sdk_type";
/**
* 短链
*/
public static final String SHORT_LINK_URL = "short_link_url";
} }
public static class ActiveMq { public static class ActiveMq {

View File

@@ -0,0 +1,19 @@
package com.accompany.business.enums.link;
/**
* @author: liaozetao
* @date: 2023/9/19 17:03
* @description:
*/
public enum ShortLinkTypeEnum {
/**
* 应用商店
*/
STORE,
/**
* 自定义链接
*/
CUSTOM;
}

View File

@@ -0,0 +1,91 @@
package com.accompany.business.model.link;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
* @author: liaozetao
* @date: 2023/9/19 15:15
* @description:
*/
@Data
@TableName("short_link")
public class ShortLink {
/**
* 主键
*/
@ApiModelProperty("主键")
@TableId(type = IdType.AUTO)
private Long id;
/**
* 编码
*/
@ApiModelProperty("编码")
private String code;
/**
* 名称
*/
@ApiModelProperty("名称")
private String name;
/**
* 短链
*/
@ApiModelProperty("短链")
private String url;
/**
* 二维码
*/
@ApiModelProperty("二维码")
private String qrcode;
/**
* 类型
*/
@ApiModelProperty("类型")
private Integer type;
/**
* 自定义URL
*/
@ApiModelProperty("自定义URL")
private String customValue;
/**
* 是否启用 0 失效 1 启用
*/
@ApiModelProperty("是否启用 0 失效 1 启用")
private Integer isEnabled;
/**
* 失效时间
*/
@ApiModelProperty("失效时间")
private Date expireTime;
/**
* 创建时间
*/
@ApiModelProperty("创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
/**
* 更新时间
*/
@ApiModelProperty("更新时间")
private Date updateTime;
}

View File

@@ -0,0 +1,77 @@
package com.accompany.business.model.link;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
* @author: liaozetao
* @date: 2023/9/19 15:15
* @description:
*/
@Data
@TableName("short_link_record")
public class ShortLinkRecord {
/**
* 主键
*/
@ApiModelProperty("主键")
@TableId(type = IdType.AUTO)
private Long id;
/**
* 短链ID
*/
@ApiModelProperty("短链ID")
private Long linkId;
/**
* 系统
*/
@ApiModelProperty("系统")
private String os;
/**
* 系统版本
*/
@ApiModelProperty("系统版本")
private String osVersion;
/**
* 设备
*/
@ApiModelProperty("设备")
private String model;
/**
* IP
*/
@ApiModelProperty("IP")
private String clientIp;
/**
* 客户端时间
*/
@ApiModelProperty("客户端时间")
private Date clientTime;
/**
* 创建时间
*/
@ApiModelProperty("创建时间")
private Date createTime;
/**
* 更新时间
*/
@ApiModelProperty("更新时间")
private Date updateTime;
}

View File

@@ -88,6 +88,12 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test-autoconfigure</artifactId> <artifactId>spring-boot-test-autoconfigure</artifactId>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/eu.bitwalker/UserAgentUtils -->
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
<version>${bitwalker.version}</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -0,0 +1,12 @@
package com.accompany.business.mybatismapper.link;
import com.accompany.business.model.link.ShortLink;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @author: liaozetao
* @date: 2023/9/19 15:26
* @description:
*/
public interface ShortLinkMapper extends BaseMapper<ShortLink> {
}

View File

@@ -0,0 +1,21 @@
package com.accompany.business.mybatismapper.link;
import com.accompany.business.model.link.ShortLinkRecord;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
/**
* @author: liaozetao
* @date: 2023/9/19 15:26
* @description:
*/
public interface ShortLinkRecordMapper extends BaseMapper<ShortLinkRecord> {
/**
* 获取uv数
* @param linkId
* @return
*/
Integer getUvCount(@Param("linkId") Long linkId);
}

View File

@@ -0,0 +1,12 @@
package com.accompany.business.service.link;
import com.accompany.business.model.link.ShortLinkRecord;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* @author: liaozetao
* @date: 2023/9/19 15:27
* @description:
*/
public interface ShortLinkRecordService extends IService<ShortLinkRecord> {
}

View File

@@ -0,0 +1,20 @@
package com.accompany.business.service.link;
import com.accompany.business.model.link.ShortLink;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* @author: liaozetao
* @date: 2023/9/19 15:27
* @description:
*/
public interface ShortLinkService extends IService<ShortLink> {
/**
* 点击
* @param code
* @param clientTime
*/
Integer click(String code, String clientTime);
}

View File

@@ -0,0 +1,18 @@
package com.accompany.business.service.link.impl;
import com.accompany.business.model.link.ShortLinkRecord;
import com.accompany.business.mybatismapper.link.ShortLinkRecordMapper;
import com.accompany.business.service.link.ShortLinkRecordService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* @author: liaozetao
* @date: 2023/9/19 15:28
* @description:
*/
@Slf4j
@Service
public class ShortLinkRecordServiceImpl extends ServiceImpl<ShortLinkRecordMapper, ShortLinkRecord> implements ShortLinkRecordService {
}

View File

@@ -0,0 +1,100 @@
package com.accompany.business.service.link.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.util.StrUtil;
import com.accompany.business.enums.link.ShortLinkTypeEnum;
import com.accompany.business.model.link.ShortLink;
import com.accompany.business.model.link.ShortLinkRecord;
import com.accompany.business.mybatismapper.link.ShortLinkMapper;
import com.accompany.business.mybatismapper.link.ShortLinkRecordMapper;
import com.accompany.business.service.link.ShortLinkService;
import com.accompany.common.utils.DateTimeUtil;
import com.accompany.common.utils.IPUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import eu.bitwalker.useragentutils.Browser;
import eu.bitwalker.useragentutils.OperatingSystem;
import eu.bitwalker.useragentutils.UserAgent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.List;
/**
* @author: liaozetao
* @date: 2023/9/19 15:28
* @description:
*/
@Slf4j
@Service
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLink> implements ShortLinkService {
@Autowired
private ShortLinkRecordMapper shortLinkRecordMapper;
@Override
public Integer click(String code, String clientTimeStr) {
int shortLinkType = ShortLinkTypeEnum.CUSTOM.ordinal();
try {
Long linkId = null;
List<ShortLink> shortLinks = baseMapper.selectList(Wrappers.<ShortLink>lambdaQuery()
.eq(ShortLink::getCode, code));
if (CollectionUtil.isNotEmpty(shortLinks)) {
ShortLink shortLink = shortLinks.get(0);
linkId = shortLink.getId();
shortLinkType = shortLink.getType();
}
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
return shortLinkType;
}
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = servletRequestAttributes.getRequest();
String os = StrUtil.EMPTY;
String osVersion = StrUtil.EMPTY;
String model = StrUtil.EMPTY;
//设备
String userAgentStr = request.getHeader(HttpHeaders.USER_AGENT);
if (StrUtil.isNotEmpty(userAgentStr)) {
UserAgent userAgent = UserAgent.parseUserAgentString(userAgentStr);
OperatingSystem operatingSystem = userAgent.getOperatingSystem();
os = operatingSystem.getName();
osVersion = String.valueOf(operatingSystem.getId());
model = operatingSystem.getDeviceType().getName();
}
//客户端时间
Date clientTime = new Date();
if (StrUtil.isNotEmpty(clientTimeStr)) {
try {
clientTime = DateTimeUtil.convertStrToDate(clientTimeStr, DatePattern.NORM_DATETIME_PATTERN);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
//IP
String ipAddress = IPUtils.getRealIpAddress(request);
//记录
ShortLinkRecord record = new ShortLinkRecord();
record.setLinkId(linkId);
record.setClientIp(ipAddress);
record.setOs(os);
record.setOsVersion(osVersion);
record.setModel(model);
record.setClientTime(clientTime);
record.setCreateTime(new Date());
record.setUpdateTime(new Date());
shortLinkRecordMapper.insert(record);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return shortLinkType;
}
}

View File

@@ -0,0 +1,5 @@
<?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.accompany.business.mybatismapper.link.ShortLinkMapper" >
</mapper>

View File

@@ -0,0 +1,11 @@
<?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.accompany.business.mybatismapper.link.ShortLinkRecordMapper">
<select id="getUvCount" resultType="java.lang.Integer">
select
count(distinct concat(slr.client_ip, '_', slr.model))
from
short_link_record as slr
where slr.link_id = #{linkId}
</select>
</mapper>

View File

@@ -0,0 +1,35 @@
package com.accompany.business.controller.link;
import com.accompany.business.service.link.ShortLinkService;
import com.accompany.common.result.BusiResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author: liaozetao
* @date: 2023/9/19 19:00
* @description:
*/
@Api(tags = "短链")
@RestController
@RequestMapping("/short/link")
public class ShortLinkController {
@Autowired
private ShortLinkService shortLinkService;
/**
* 点击
* @return
*/
@ApiOperation("点击")
@GetMapping("click")
public BusiResult<Void> click(String code, String clientTime) {
shortLinkService.click(code, clientTime);
return BusiResult.success();
}
}

View File

@@ -97,6 +97,8 @@
<hippo4j-core.version>1.5.0</hippo4j-core.version> <hippo4j-core.version>1.5.0</hippo4j-core.version>
<android-json.version>0.0.20131108.vaadin1</android-json.version> <android-json.version>0.0.20131108.vaadin1</android-json.version>
<bcprov-jdk15on.version>1.64</bcprov-jdk15on.version> <bcprov-jdk15on.version>1.64</bcprov-jdk15on.version>
<zxing.version>3.2.0</zxing.version>
<bitwalker.version>1.20</bitwalker.version>
</properties> </properties>
<dependencyManagement> <dependencyManagement>