feat(shortlink): 新增短链管理功能
- 新增短链表 `system_short_link` 及其访问记录表 `system_short_link_access_log` - 添加短链相关菜单权限配置,包括新增、修改、删除、查询及统计页面 - 定义短链控制器请求与响应 VO 类,支持分页查询与统计数据展示 - 增加短链设备类型、访问时段、地域分布等统计接口返回结构 - 在 ErrorCodeConstants 中增加短链相关错误码定义 - 提供短链创建、状态管理、过期时间设置等功能校验逻辑 - 支持自定义短码与系统生成短码两种模式 - 实现短链点击次数追踪与最后访问时间记录机制pull/223/head
parent
f4ba70ec5a
commit
45bb4b5459
|
|
@ -2327,6 +2327,13 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
|
|||
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5044, '删除模版消息', 'mp:message-template:delete', 3, 2, 5042, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-11-26 17:00:31', '1', '2025-11-26 18:45:05', b'0');
|
||||
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5045, '同步公众号模板', 'mp:message-template:sync', 3, 3, 5042, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-11-26 17:00:55', '1', '2025-11-26 17:00:55', b'0');
|
||||
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5046, '给粉丝发送模版消息', 'mp:message-template:send', 3, 4, 5042, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-11-26 17:01:11', '1', '2025-11-26 17:01:11', b'0');
|
||||
INSERT INTO `system_menu` (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES (5047, '短链管理', '', 1, 6, 1, '', 'ep:loading', '', '', 0, true, true, true, '1', '2025-11-26 17:01:11', '1', '2025-12-04 17:41:34', false);
|
||||
INSERT INTO `system_menu` (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES (5048, '新增短链', 'system:short-link:create', 3, 1, 5053, '', '', '', '', 0, true, true, true, '1', '2025-11-26 17:01:11', '1', '2025-12-04 17:35:42', false);
|
||||
INSERT INTO `system_menu` (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES (5049, '修改短链', 'system:short-link:update', 3, 2, 5053, '', '', '', '', 0, true, true, true, '1', '2025-11-26 17:01:11', '1', '2025-12-04 17:35:42', false);
|
||||
INSERT INTO `system_menu` (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES (5050, '删除短链', 'system:short-link:delete', 3, 3, 5053, '', '', '', '', 0, true, true, true, '1', '2025-11-26 17:01:11', '1', '2025-12-04 17:35:42', false);
|
||||
INSERT INTO `system_menu` (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES (5051, '查询短链', 'system:short-link:query', 3, 4, 5053, '', '', '', '', 0, true, true, true, '1', '2025-11-26 17:01:11', '1', '2025-12-04 17:35:42', false);
|
||||
INSERT INTO `system_menu` (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES (5052, '统计', '', 2, 2, 5047, 'shortlink/statistics', 'ep:picture-rounded', 'system/shortlink/statistics', 'SystemShortlinkStatistics', 0, true, true, true, '1', '2025-12-04 17:30:04', '1', '2025-12-04 18:11:26', false);
|
||||
INSERT INTO `system_menu` (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES (5053, '短链', '', 2, 1, 5047, 'shortlink/index', 'ep:magic-stick', 'system/shortlink/index', 'SystemShortlink', 0, true, true, true, '1', '2025-12-04 17:31:48', '1', '2025-12-04 17:39:14', false);
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
|
|
@ -3931,6 +3938,80 @@ INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`,
|
|||
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (142, 'test01', '$2a$04$IaR0fGYtalIDURMMdcaD2.4JDWZ15ueQZwap9oPUuxkwSbL66vIRG', 'test01', '', NULL, '[]', '', '19021719925', 1, '', 0, '0:0:0:0:0:0:0:1', '2025-07-29 19:47:17', '1', '2025-07-09 21:07:10', '1', '2025-11-25 19:49:08', b'0', 1);
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for system_short_link
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `system_short_link`;
|
||||
CREATE TABLE `system_short_link` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号,数据库自增',
|
||||
`short_code` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '短码',
|
||||
`original_url` text COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '原始URL',
|
||||
`title` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '链接标题',
|
||||
`description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '链接描述',
|
||||
`domain` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '域名配置',
|
||||
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态(1-启用,0-禁用)',
|
||||
`expire_time` datetime DEFAULT NULL COMMENT '过期时间',
|
||||
`click_count` bigint NOT NULL DEFAULT '0' COMMENT '点击次数',
|
||||
`last_click_time` datetime DEFAULT NULL COMMENT '最后点击时间',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
|
||||
`creator` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
|
||||
`updater` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
|
||||
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
`forever` bit(1) NOT NULL COMMENT '是否永久有效',
|
||||
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_short_code` (`short_code`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_create_time` (`create_time`),
|
||||
KEY `idx_expire_time` (`expire_time`),
|
||||
KEY `idx_tenant_id` (`tenant_id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='短链表';
|
||||
BEGIN;
|
||||
INSERT INTO `system_short_link` (id, short_code, original_url, title, description, domain, status, expire_time, click_count, last_click_time, create_time, update_time, creator, updater, deleted, forever, tenant_id) VALUES (4, '7YyTA2', 'https://gitee.com/wushuaiping/yudao-ui-admin-vue3', '123', '达十点', 'http://localhost:48080/app-api/system/short-link', 1, null, 13, '2025-12-04 18:01:34', '2025-12-04 11:30:31', '2025-12-04 16:43:48', '1', '1', false, true, 1);
|
||||
COMMIT;
|
||||
-- ----------------------------
|
||||
-- Table structure for system_short_link_access_log
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `system_short_link_access_log`;
|
||||
CREATE TABLE `system_short_link_access_log` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号,数据库自增',
|
||||
`short_link_id` bigint NOT NULL COMMENT '短链ID',
|
||||
`short_code` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '短码',
|
||||
`ip` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '访问IP',
|
||||
`user_agent` text COLLATE utf8mb4_unicode_ci COMMENT '用户代理',
|
||||
`referer` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '来源页面',
|
||||
`access_time` datetime NOT NULL COMMENT '访问时间',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
|
||||
`creator` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
|
||||
`updater` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
|
||||
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_short_link_id` (`short_link_id`),
|
||||
KEY `idx_short_code` (`short_code`),
|
||||
KEY `idx_access_time` (`access_time`),
|
||||
KEY `idx_ip` (`ip`),
|
||||
KEY `idx_create_time` (`create_time`),
|
||||
KEY `idx_tenant_id` (`tenant_id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='短链访问记录表';
|
||||
|
||||
BEGIN;
|
||||
INSERT INTO `system_short_link_access_log` (id, short_link_id, short_code, ip, user_agent, referer, access_time, create_time, update_time, creator, updater, deleted, tenant_id) VALUES (13, 4, '7YyTA2', '182.133.161.101', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36', 'http://localhost/', '2025-12-04 16:00:47', '2025-12-04 16:00:47', '2025-12-04 16:25:39', null, null, false, 1);
|
||||
INSERT INTO `system_short_link_access_log` (id, short_link_id, short_code, ip, user_agent, referer, access_time, create_time, update_time, creator, updater, deleted, tenant_id) VALUES (14, 4, '7YyTA2', '182.133.161.101', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36', 'http://localhost/', '2025-12-04 16:02:45', '2025-12-04 16:02:46', '2025-12-04 16:25:39', null, null, false, 1);
|
||||
INSERT INTO `system_short_link_access_log` (id, short_link_id, short_code, ip, user_agent, referer, access_time, create_time, update_time, creator, updater, deleted, tenant_id) VALUES (15, 4, '7YyTA2', '182.133.161.101', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36', 'http://localhost/', '2025-12-04 16:20:13', '2025-12-04 16:20:13', '2025-12-04 16:25:39', null, null, false, 1);
|
||||
INSERT INTO `system_short_link_access_log` (id, short_link_id, short_code, ip, user_agent, referer, access_time, create_time, update_time, creator, updater, deleted, tenant_id) VALUES (16, 4, '7YyTA2', '175.0.199.149', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36', 'http://localhost/', '2025-12-04 16:24:42', '2025-12-04 16:24:42', '2025-12-04 17:05:37', null, null, false, 1);
|
||||
INSERT INTO `system_short_link_access_log` (id, short_link_id, short_code, ip, user_agent, referer, access_time, create_time, update_time, creator, updater, deleted, tenant_id) VALUES (17, 4, '7YyTA2', '183.42.134.99', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36', 'http://localhost/', '2025-12-04 16:24:52', '2025-12-04 16:24:52', '2025-12-04 17:05:29', null, null, false, 1);
|
||||
INSERT INTO `system_short_link_access_log` (id, short_link_id, short_code, ip, user_agent, referer, access_time, create_time, update_time, creator, updater, deleted, tenant_id) VALUES (18, 4, '7YyTA2', '183.42.134.99', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.104.3 Chrome/138.0.7204.251 Electron/37.6.1 Safari/537.36', null, '2025-12-04 16:28:50', '2025-12-04 16:28:50', '2025-12-04 17:05:29', null, null, false, 1);
|
||||
INSERT INTO `system_short_link_access_log` (id, short_link_id, short_code, ip, user_agent, referer, access_time, create_time, update_time, creator, updater, deleted, tenant_id) VALUES (19, 4, '7YyTA2', '39.144.177.47', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.104.3 Chrome/138.0.7204.251 Electron/37.6.1 Safari/537.36', null, '2025-12-04 16:29:09', '2025-12-04 16:29:09', '2025-12-04 17:05:29', null, null, false, 1);
|
||||
INSERT INTO `system_short_link_access_log` (id, short_link_id, short_code, ip, user_agent, referer, access_time, create_time, update_time, creator, updater, deleted, tenant_id) VALUES (20, 4, '7YyTA2', '39.148.224.219', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.104.3 Chrome/138.0.7204.251 Electron/37.6.1 Safari/537.36', 'http://localhost/', '2025-12-04 16:29:20', '2025-12-04 16:29:20', '2025-12-04 17:05:29', null, null, false, 1);
|
||||
INSERT INTO `system_short_link_access_log` (id, short_link_id, short_code, ip, user_agent, referer, access_time, create_time, update_time, creator, updater, deleted, tenant_id) VALUES (21, 4, '7YyTA2', '171.94.147.206', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.104.3 Chrome/138.0.7204.251 Electron/37.6.1 Safari/537.36', null, '2025-12-04 16:29:25', '2025-12-04 16:29:25', '2025-12-04 17:05:29', null, null, false, 1);
|
||||
INSERT INTO `system_short_link_access_log` (id, short_link_id, short_code, ip, user_agent, referer, access_time, create_time, update_time, creator, updater, deleted, tenant_id) VALUES (22, 4, '7YyTA2', '45.201.209.154', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Trae/1.104.3 Chrome/138.0.7204.251 Electron/37.6.1 Safari/537.36', 'http://localhost/', '2025-12-04 16:29:39', '2025-12-04 16:29:39', '2025-12-04 17:05:29', null, null, false, 1);
|
||||
INSERT INTO `system_short_link_access_log` (id, short_link_id, short_code, ip, user_agent, referer, access_time, create_time, update_time, creator, updater, deleted, tenant_id) VALUES (23, 4, '7YyTA2', '123.149.30.187', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36', 'http://localhost/', '2025-12-04 16:43:59', '2025-12-04 16:43:59', '2025-12-04 17:05:29', null, null, false, 1);
|
||||
INSERT INTO `system_short_link_access_log` (id, short_link_id, short_code, ip, user_agent, referer, access_time, create_time, update_time, creator, updater, deleted, tenant_id) VALUES (24, 4, '7YyTA2', '127.0.0.1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36', 'http://localhost/', '2025-12-04 18:01:34', '2025-12-04 18:01:34', '2025-12-04 18:01:34', null, null, false, 1);
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for yudao_demo01_contact
|
||||
-- ----------------------------
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.common.exception.ErrorCode;
|
|||
|
||||
/**
|
||||
* System 错误码枚举类
|
||||
*
|
||||
* <p>
|
||||
* system 系统,使用 1-002-000-000 段
|
||||
*/
|
||||
public interface ErrorCodeConstants {
|
||||
|
|
@ -49,7 +49,7 @@ public interface ErrorCodeConstants {
|
|||
|
||||
// ========== 部门模块 1-002-004-000 ==========
|
||||
ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1_002_004_000, "已经存在该名字的部门");
|
||||
ErrorCode DEPT_PARENT_NOT_EXITS = new ErrorCode(1_002_004_001,"父级部门不存在");
|
||||
ErrorCode DEPT_PARENT_NOT_EXITS = new ErrorCode(1_002_004_001, "父级部门不存在");
|
||||
ErrorCode DEPT_NOT_FOUND = new ErrorCode(1_002_004_002, "当前部门不存在");
|
||||
ErrorCode DEPT_EXITS_CHILDREN = new ErrorCode(1_002_004_003, "存在子部门,无法删除");
|
||||
ErrorCode DEPT_PARENT_ERROR = new ErrorCode(1_002_004_004, "不能设置自己为父部门");
|
||||
|
|
@ -168,4 +168,13 @@ public interface ErrorCodeConstants {
|
|||
// ========== 站内信发送 1-002-028-000 ==========
|
||||
ErrorCode NOTIFY_SEND_TEMPLATE_PARAM_MISS = new ErrorCode(1_002_028_000, "模板参数({})缺失");
|
||||
|
||||
// ========== 短链 1-002-029-000 ==========
|
||||
ErrorCode SHORT_LINK_NOT_EXISTS = new ErrorCode(1_002_029_000, "短链不存在");
|
||||
ErrorCode SHORT_LINK_CODE_IS_EXIST = new ErrorCode(1_002_029_001, "短链已存在");
|
||||
ErrorCode SHORT_LINK_TIME_IS_NULL = new ErrorCode(1_002_029_002, "请选择失效时间");
|
||||
ErrorCode SHORT_LINK_CODE_FORMAT_ERROR = new ErrorCode(1_002_029_003, "自定义短码格式不正确");
|
||||
ErrorCode SHORT_LINK_CODE_GENERATE_ERROR = new ErrorCode(1_002_029_004, "生成短码失败,请重试");
|
||||
ErrorCode SHORT_LINK_DISABLED = new ErrorCode(1_002_029_005, "短链已禁用");
|
||||
ErrorCode SHORT_LINK_EXPIRED = new ErrorCode(1_002_029_006, "短链已过期");
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,211 @@
|
|||
package cn.iocoder.yudao.module.system.controller.admin.shortlink;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
|
||||
import cn.iocoder.yudao.module.system.controller.admin.shortlink.vo.*;
|
||||
import cn.iocoder.yudao.module.system.dal.dataobject.shortlink.ShortLinkDO;
|
||||
import cn.iocoder.yudao.module.system.service.shortlink.ShortLinkService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
/**
|
||||
* 管理后台 - 短链
|
||||
*
|
||||
* @author 吴帅苹
|
||||
*/
|
||||
@Tag(name = "管理后台 - 短链")
|
||||
@RestController
|
||||
@RequestMapping("/system/short-link")
|
||||
@Validated
|
||||
public class ShortLinkController {
|
||||
|
||||
@Resource
|
||||
private ShortLinkService shortLinkService;
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建短链")
|
||||
@PreAuthorize("@ss.hasPermission('system:short-link:create')")
|
||||
public CommonResult<Long> createShortLink(@Valid @RequestBody ShortLinkCreateReqVO createReqVO) {
|
||||
return success(shortLinkService.createShortLink(createReqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/domain")
|
||||
@Operation(summary = "修改短链域名")
|
||||
@PreAuthorize("@ss.hasPermission('system:short-link:update')")
|
||||
public CommonResult<Boolean> updateShortLinkDomain(@RequestParam("newDomain") String newDomain) {
|
||||
shortLinkService.updateShortLinkDomain(newDomain);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/domain")
|
||||
@Operation(summary = "获取短链域名")
|
||||
@PreAuthorize("@ss.hasPermission('system:short-link:query')")
|
||||
public CommonResult<String> getShortLinkDomain() {
|
||||
return success(shortLinkService.getShortLinkDomain());
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新短链")
|
||||
@PreAuthorize("@ss.hasPermission('system:short-link:update')")
|
||||
public CommonResult<Boolean> updateShortLink(@Valid @RequestBody ShortLinkUpdateReqVO updateReqVO) {
|
||||
shortLinkService.updateShortLink(updateReqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除短链")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('system:short-link:delete')")
|
||||
public CommonResult<Boolean> deleteShortLink(@RequestParam("id") Long id) {
|
||||
shortLinkService.deleteShortLink(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获得短链")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('system:short-link:query')")
|
||||
public CommonResult<ShortLinkRespVO> getShortLink(@RequestParam("id") Long id) {
|
||||
ShortLinkDO shortLink = shortLinkService.getShortLink(id);
|
||||
return success(BeanUtils.toBean(shortLink, ShortLinkRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得短链分页")
|
||||
@PreAuthorize("@ss.hasPermission('system:short-link:query')")
|
||||
public CommonResult<PageResult<ShortLinkRespVO>> getShortLinkPage(@Valid ShortLinkPageReqVO pageReqVO) {
|
||||
PageResult<ShortLinkDO> pageResult = shortLinkService.getShortLinkPage(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, ShortLinkRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/export-excel")
|
||||
@Operation(summary = "导出短链 Excel")
|
||||
@PreAuthorize("@ss.hasPermission('system:short-link:export')")
|
||||
public void exportShortLinkExcel(@Valid ShortLinkPageReqVO pageReqVO,
|
||||
HttpServletResponse response) throws IOException {
|
||||
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||
List<ShortLinkDO> list = shortLinkService.getShortLinkPage(pageReqVO).getList();
|
||||
// 导出 Excel
|
||||
ExcelUtils.write(response, "短链.xls", "数据", ShortLinkRespVO.class,
|
||||
BeanUtils.toBean(list, ShortLinkRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/by-code")
|
||||
@Operation(summary = "根据短码获取短链")
|
||||
@Parameter(name = "shortCode", description = "短码", required = true, example = "abc123")
|
||||
@PreAuthorize("@ss.hasPermission('system:short-link:query')")
|
||||
public CommonResult<ShortLinkRespVO> getShortLinkByCode(@RequestParam("shortCode") String shortCode) {
|
||||
ShortLinkDO shortLink = shortLinkService.getShortLinkByCode(shortCode);
|
||||
return success(BeanUtils.toBean(shortLink, ShortLinkRespVO.class));
|
||||
}
|
||||
|
||||
@PostMapping("/batch-delete")
|
||||
@Operation(summary = "批量删除短链")
|
||||
@Parameter(name = "ids", description = "编号数组", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('system:short-link:delete')")
|
||||
public CommonResult<Boolean> deleteShortLinks(@RequestBody Long[] ids) {
|
||||
shortLinkService.deleteShortLinks(ids);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PutMapping("/update-status")
|
||||
@Operation(summary = "修改短链状态")
|
||||
@PreAuthorize("@ss.hasPermission('system:short-link:update')")
|
||||
public CommonResult<Boolean> updateShortLinkStatus(@RequestParam("id") Long id,
|
||||
@RequestParam("status") Integer status) {
|
||||
shortLinkService.updateShortLinkStatus(id, status);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
|
||||
// ==================== 统计分析相关接口 ====================
|
||||
|
||||
@GetMapping("/statistics/overview")
|
||||
@Operation(summary = "获取短链概览统计")
|
||||
@PreAuthorize("@ss.hasPermission('system:short-link:query')")
|
||||
public CommonResult<ShortLinkOverviewStatsRespVO> getShortLinkOverviewStats(
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @RequestParam(value = "startTime", required = false) LocalDateTime startTime,
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @RequestParam(value = "endTime", required = false) LocalDateTime endTime) {
|
||||
return success(shortLinkService.getShortLinkOverviewStats(startTime, endTime));
|
||||
}
|
||||
|
||||
@GetMapping("/statistics/trend")
|
||||
@Operation(summary = "获取短链趋势统计")
|
||||
@PreAuthorize("@ss.hasPermission('system:short-link:query')")
|
||||
public CommonResult<List<ShortLinkTrendStatsRespVO>> getShortLinkTrendStats(@RequestParam(defaultValue = "7") Integer days) {
|
||||
return success(shortLinkService.getShortLinkTrendStats(days));
|
||||
}
|
||||
|
||||
@GetMapping("/statistics/status")
|
||||
@Operation(summary = "获取短链状态分布统计")
|
||||
@PreAuthorize("@ss.hasPermission('system:short-link:query')")
|
||||
public CommonResult<ShortLinkStatusStatsRespVO> getShortLinkStatusStats() {
|
||||
return success(shortLinkService.getShortLinkStatusStats());
|
||||
}
|
||||
|
||||
@GetMapping("/statistics/region")
|
||||
@Operation(summary = "获取短链地域访问统计")
|
||||
@PreAuthorize("@ss.hasPermission('system:short-link:query')")
|
||||
public CommonResult<ShortLinkRegionStatsRespVO> getShortLinkRegionStats(
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @RequestParam(value = "startTime", required = false) LocalDateTime startTime,
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @RequestParam(value = "endTime", required = false) LocalDateTime endTime) {
|
||||
return success(shortLinkService.getShortLinkRegionStats(startTime, endTime));
|
||||
}
|
||||
|
||||
@GetMapping("/statistics/device")
|
||||
@Operation(summary = "获取短链设备类型统计")
|
||||
@PreAuthorize("@ss.hasPermission('system:short-link:query')")
|
||||
public CommonResult<ShortLinkDeviceStatsRespVO> getShortLinkDeviceStats(
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @RequestParam(value = "startTime", required = false) LocalDateTime startTime,
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @RequestParam(value = "endTime", required = false) LocalDateTime endTime) {
|
||||
return success(shortLinkService.getShortLinkDeviceStats(startTime, endTime));
|
||||
}
|
||||
|
||||
@GetMapping("/statistics/hour")
|
||||
@Operation(summary = "获取短链访问时段统计")
|
||||
@PreAuthorize("@ss.hasPermission('system:short-link:query')")
|
||||
public CommonResult<ShortLinkHourStatsRespVO> getShortLinkHourStats(
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd") @RequestParam(value = "date", required = false) LocalDate date) {
|
||||
return success(shortLinkService.getShortLinkHourStats(date));
|
||||
}
|
||||
|
||||
// ==================== 访问日志相关接口 ====================
|
||||
|
||||
@GetMapping("/access-log/page")
|
||||
@Operation(summary = "获得短链访问日志分页")
|
||||
@PreAuthorize("@ss.hasPermission('system:short-link:query')")
|
||||
public CommonResult<PageResult<ShortLinkAccessLogRespVO>> getShortLinkAccessLogPage(@Valid ShortLinkAccessLogPageReqVO pageReqVO) {
|
||||
return success(shortLinkService.getShortLinkAccessLogPage(pageReqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/access-log/export-excel")
|
||||
@Operation(summary = "导出短链访问日志 Excel")
|
||||
@PreAuthorize("@ss.hasPermission('system:short-link:export')")
|
||||
public void exportShortLinkAccessLogExcel(@Valid ShortLinkAccessLogPageReqVO pageReqVO,
|
||||
HttpServletResponse response) throws IOException {
|
||||
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||
List<ShortLinkAccessLogRespVO> list = shortLinkService.getShortLinkAccessLogPage(pageReqVO).getList();
|
||||
// 导出 Excel
|
||||
ExcelUtils.write(response, "短链访问日志.xls", "数据", ShortLinkAccessLogRespVO.class, list);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package cn.iocoder.yudao.module.system.controller.admin.shortlink.vo;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
@Schema(description = "管理后台 - 短链访问记录分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class ShortLinkAccessLogPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "短链ID", example = "1024")
|
||||
private Long shortLinkId;
|
||||
|
||||
@Schema(description = "短码", example = "abc123")
|
||||
private String shortCode;
|
||||
|
||||
@Schema(description = "访问IP", example = "192.168.1.1")
|
||||
private String ip;
|
||||
|
||||
@Schema(description = "访问时间")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] accessTime;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package cn.iocoder.yudao.module.system.controller.admin.shortlink.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "管理后台 - 短链访问记录 Response VO")
|
||||
@Data
|
||||
public class ShortLinkAccessLogRespVO {
|
||||
|
||||
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "短链ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long shortLinkId;
|
||||
|
||||
@Schema(description = "短码", requiredMode = Schema.RequiredMode.REQUIRED, example = "abc123")
|
||||
private String shortCode;
|
||||
|
||||
@Schema(description = "访问IP", example = "192.168.1.1")
|
||||
private String ip;
|
||||
|
||||
@Schema(description = "用户代理", example = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||
private String userAgent;
|
||||
|
||||
@Schema(description = "来源页面", example = "https://www.google.com")
|
||||
private String referer;
|
||||
|
||||
@Schema(description = "国家", example = "中国")
|
||||
private String country;
|
||||
|
||||
@Schema(description = "省份", example = "四川省")
|
||||
private String province;
|
||||
|
||||
@Schema(description = "城市", example = "成都市")
|
||||
private String city;
|
||||
|
||||
@Schema(description = "访问时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime accessTime;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package cn.iocoder.yudao.module.system.controller.admin.shortlink.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.URL;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
@Schema(description = "管理后台 - 短链创建 Request VO")
|
||||
@Data
|
||||
public class ShortLinkCreateReqVO {
|
||||
|
||||
@Schema(description = "原始URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.example.com/very/long/url")
|
||||
@NotBlank(message = "原始URL不能为空")
|
||||
@URL(message = "原始URL格式不正确")
|
||||
@Size(max = 2048, message = "原始URL长度不能超过2048个字符")
|
||||
private String originalUrl;
|
||||
|
||||
@Schema(description = "自定义短码", example = "abc123")
|
||||
@Size(max = 32, message = "自定义短码长度不能超过32个字符")
|
||||
private String customShortCode;
|
||||
|
||||
@Schema(description = "链接标题", example = "示例网站")
|
||||
@Size(max = 255, message = "链接标题长度不能超过255个字符")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "链接描述", example = "这是一个示例网站的描述")
|
||||
@Size(max = 500, message = "链接描述长度不能超过500个字符")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "是否永久生效", example = "true")
|
||||
@NotNull(message = "请选择是否永久生效")
|
||||
private Boolean forever;
|
||||
|
||||
@Schema(description = "域名配置", example = "short.example.com")
|
||||
@Size(max = 100, message = "域名配置长度不能超过100个字符")
|
||||
private String domain;
|
||||
|
||||
@Schema(description = "过期时间", example = "2024-12-31T23:59:59")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime expireTime;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package cn.iocoder.yudao.module.system.controller.admin.shortlink.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 短链设备类型统计 Response VO
|
||||
*
|
||||
* @author 吴帅苹
|
||||
*/
|
||||
@Schema(description = "管理后台 - 短链设备类型统计 Response VO")
|
||||
@Data
|
||||
@Builder
|
||||
public class ShortLinkDeviceStatsRespVO {
|
||||
|
||||
@Schema(description = "设备类型分布数据列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<DeviceData> deviceData;
|
||||
|
||||
@Schema(description = "设备数据项")
|
||||
@Data
|
||||
@Builder
|
||||
public static class DeviceData {
|
||||
|
||||
@Schema(description = "设备类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "Mobile")
|
||||
private String deviceType;
|
||||
|
||||
@Schema(description = "设备名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "移动设备")
|
||||
private String deviceName;
|
||||
|
||||
@Schema(description = "访问量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1234")
|
||||
private Long visitCount;
|
||||
|
||||
@Schema(description = "占比", requiredMode = Schema.RequiredMode.REQUIRED, example = "65.4")
|
||||
private Double percentage;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package cn.iocoder.yudao.module.system.controller.admin.shortlink.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 短链访问时段统计 Response VO
|
||||
*
|
||||
* @author 吴帅苹
|
||||
*/
|
||||
@Schema(description = "管理后台 - 短链访问时段统计 Response VO")
|
||||
@Data
|
||||
@Builder
|
||||
public class ShortLinkHourStatsRespVO {
|
||||
|
||||
@Schema(description = "时段分布数据列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<HourData> hourData;
|
||||
|
||||
@Schema(description = "时段数据项")
|
||||
@Data
|
||||
@Builder
|
||||
public static class HourData {
|
||||
|
||||
@Schema(description = "小时", requiredMode = Schema.RequiredMode.REQUIRED, example = "14")
|
||||
private Integer hour;
|
||||
|
||||
@Schema(description = "时段描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "14:00-15:00")
|
||||
private String hourRange;
|
||||
|
||||
@Schema(description = "访问量", requiredMode = Schema.RequiredMode.REQUIRED, example = "234")
|
||||
private Long visitCount;
|
||||
|
||||
@Schema(description = "占比", requiredMode = Schema.RequiredMode.REQUIRED, example = "8.5")
|
||||
private Double percentage;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package cn.iocoder.yudao.module.system.controller.admin.shortlink.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 短链概览统计 Response VO
|
||||
*
|
||||
* @author 吴帅苹
|
||||
*/
|
||||
@Schema(description = "管理后台 - 短链概览统计 Response VO")
|
||||
@Data
|
||||
@Builder
|
||||
public class ShortLinkOverviewStatsRespVO {
|
||||
|
||||
@Schema(description = "总短链数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1234")
|
||||
private Long totalLinks;
|
||||
|
||||
@Schema(description = "总点击量", requiredMode = Schema.RequiredMode.REQUIRED, example = "56789")
|
||||
private Long totalClicks;
|
||||
|
||||
@Schema(description = "活跃短链数", requiredMode = Schema.RequiredMode.REQUIRED, example = "890")
|
||||
private Long activeLinks;
|
||||
|
||||
@Schema(description = "平均点击率", requiredMode = Schema.RequiredMode.REQUIRED, example = "45.6")
|
||||
private Double avgClickRate;
|
||||
|
||||
@Schema(description = "今日新增短链", requiredMode = Schema.RequiredMode.REQUIRED, example = "23")
|
||||
private Long todayNewLinks;
|
||||
|
||||
@Schema(description = "今日点击量", requiredMode = Schema.RequiredMode.REQUIRED, example = "456")
|
||||
private Long todayClicks;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package cn.iocoder.yudao.module.system.controller.admin.shortlink.vo;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
@Schema(description = "管理后台 - 短链分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class ShortLinkPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "短码", example = "abc123")
|
||||
private String shortCode;
|
||||
|
||||
@Schema(description = "原始URL", example = "https://www.example.com")
|
||||
private String originalUrl;
|
||||
|
||||
@Schema(description = "链接标题", example = "示例网站")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "状态", example = "1")
|
||||
private Boolean status;
|
||||
|
||||
@Schema(description = "创建者", example = "admin")
|
||||
private String creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] createTime;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package cn.iocoder.yudao.module.system.controller.admin.shortlink.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 短链地域访问统计 Response VO
|
||||
*
|
||||
* @author 吴帅苹
|
||||
*/
|
||||
@Schema(description = "管理后台 - 短链地域访问统计 Response VO")
|
||||
@Data
|
||||
@Builder
|
||||
public class ShortLinkRegionStatsRespVO {
|
||||
|
||||
@Schema(description = "地域分布数据列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<RegionData> regionData;
|
||||
|
||||
@Schema(description = "地域数据项")
|
||||
@Data
|
||||
@Builder
|
||||
public static class RegionData {
|
||||
|
||||
@Schema(description = "省份", requiredMode = Schema.RequiredMode.REQUIRED, example = "四川省")
|
||||
private String province;
|
||||
|
||||
@Schema(description = "城市", requiredMode = Schema.RequiredMode.REQUIRED, example = "成都市")
|
||||
private String city;
|
||||
|
||||
@Schema(description = "访问量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1234")
|
||||
private Long visitCount;
|
||||
|
||||
@Schema(description = "占比", requiredMode = Schema.RequiredMode.REQUIRED, example = "25.6")
|
||||
private Double percentage;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package cn.iocoder.yudao.module.system.controller.admin.shortlink.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "管理后台 - 短链 Response VO")
|
||||
@Data
|
||||
public class ShortLinkRespVO {
|
||||
|
||||
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "短码", requiredMode = Schema.RequiredMode.REQUIRED, example = "abc123")
|
||||
private String shortCode;
|
||||
|
||||
@Schema(description = "原始URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.example.com/very/long/url")
|
||||
private String originalUrl;
|
||||
|
||||
@Schema(description = "链接标题", example = "示例网站")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "链接描述", example = "这是一个示例网站的描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "域名配置", example = "short.example.com")
|
||||
private String domain;
|
||||
|
||||
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Boolean status;
|
||||
|
||||
@Schema(description = "过期时间", example = "2024-12-31T23:59:59")
|
||||
private LocalDateTime expireTime;
|
||||
|
||||
@Schema(description = "点击次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
|
||||
private Long clickCount;
|
||||
|
||||
@Schema(description = "是否永久生效", example = "true")
|
||||
private Boolean forever;
|
||||
|
||||
@Schema(description = "最后点击时间", example = "2024-01-15T10:30:00")
|
||||
private LocalDateTime lastClickTime;
|
||||
|
||||
@Schema(description = "创建者", example = "admin")
|
||||
private String creator;
|
||||
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package cn.iocoder.yudao.module.system.controller.admin.shortlink.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 短链状态分布统计 Response VO
|
||||
*
|
||||
* @author 吴帅苹
|
||||
*/
|
||||
@Schema(description = "管理后台 - 短链状态分布统计 Response VO")
|
||||
@Data
|
||||
@Builder
|
||||
public class ShortLinkStatusStatsRespVO {
|
||||
|
||||
@Schema(description = "状态分布数据列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<StatusData> statusData;
|
||||
|
||||
@Schema(description = "状态数据项")
|
||||
@Data
|
||||
@Builder
|
||||
public static class StatusData {
|
||||
|
||||
@Schema(description = "状态名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "启用")
|
||||
private String statusName;
|
||||
|
||||
@Schema(description = "状态值", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Integer statusValue;
|
||||
|
||||
@Schema(description = "数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "890")
|
||||
private Long count;
|
||||
|
||||
@Schema(description = "占比", requiredMode = Schema.RequiredMode.REQUIRED, example = "72.3")
|
||||
private Double percentage;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package cn.iocoder.yudao.module.system.controller.admin.shortlink.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 短链趋势统计 Response VO
|
||||
*
|
||||
* @author 吴帅苹
|
||||
*/
|
||||
@Schema(description = "管理后台 - 短链趋势统计 Response VO")
|
||||
@Data
|
||||
@Builder
|
||||
public class ShortLinkTrendStatsRespVO {
|
||||
|
||||
@Schema(description = "趋势数据列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<TrendData> trendData;
|
||||
|
||||
@Schema(description = "趋势数据项")
|
||||
@Data
|
||||
@Builder
|
||||
public static class TrendData {
|
||||
|
||||
@Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-01-01")
|
||||
private LocalDate date;
|
||||
|
||||
@Schema(description = "新增短链数", requiredMode = Schema.RequiredMode.REQUIRED, example = "23")
|
||||
private Long createCount;
|
||||
|
||||
@Schema(description = "点击量", requiredMode = Schema.RequiredMode.REQUIRED, example = "456")
|
||||
private Long clickCount;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package cn.iocoder.yudao.module.system.controller.admin.shortlink.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
@Schema(description = "管理后台 - 短链更新 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class ShortLinkUpdateReqVO extends ShortLinkCreateReqVO {
|
||||
|
||||
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "编号不能为空")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "状态不能为空")
|
||||
private Boolean status;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package cn.iocoder.yudao.module.system.controller.app.shortlink;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
|
||||
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
|
||||
import cn.iocoder.yudao.module.system.service.shortlink.ShortLinkService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error;
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 用户 APP - 短链访问
|
||||
*
|
||||
* @author 吴帅苹
|
||||
*/
|
||||
@Tag(name = "用户 APP - 短链访问")
|
||||
@RestController
|
||||
@RequestMapping("/system/short-link")
|
||||
@Validated
|
||||
@Slf4j
|
||||
@TenantIgnore
|
||||
public class AppShortLinkController {
|
||||
|
||||
@Resource
|
||||
private ShortLinkService shortLinkService;
|
||||
|
||||
@GetMapping("/{shortCode}")
|
||||
@Operation(summary = "短链跳转")
|
||||
@Parameter(name = "shortCode", description = "短码", required = true, example = "abc123")
|
||||
@PermitAll
|
||||
public CommonResult<Boolean> redirectShortLink(@PathVariable("shortCode") String shortCode,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response) {
|
||||
try {
|
||||
String ip = ServletUtils.getClientIP(request);
|
||||
String userAgent = request.getHeader("User-Agent");
|
||||
String referer = request.getHeader("Referer");
|
||||
|
||||
String originalUrl = shortLinkService.accessShortLink(shortCode, ip, userAgent, referer);
|
||||
response.sendRedirect(originalUrl);
|
||||
} catch (Exception e) {
|
||||
log.error("短链跳转失败,短码:{},错误:{}", shortCode, e.getMessage());
|
||||
return error(HttpServletResponse.SC_NOT_FOUND, "短链不存在或已失效");
|
||||
}
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/preview/{shortCode}")
|
||||
@Operation(summary = "短链预览")
|
||||
@Parameter(name = "shortCode", description = "短码", required = true, example = "abc123")
|
||||
public CommonResult<String> previewShortLink(@PathVariable("shortCode") String shortCode) {
|
||||
try {
|
||||
// 获取短链信息但不记录访问
|
||||
var shortLink = shortLinkService.getShortLinkByCode(shortCode);
|
||||
if (shortLink == null) {
|
||||
return CommonResult.error(404, "短链不存在");
|
||||
}
|
||||
|
||||
if (!shortLink.getStatus()) {
|
||||
return CommonResult.error(400, "短链已禁用");
|
||||
}
|
||||
|
||||
return success(shortLink.getOriginalUrl());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("短链预览失败,短码:{},错误:{}", shortCode, e.getMessage());
|
||||
return CommonResult.error(500, "预览失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package cn.iocoder.yudao.module.system.dal.dataobject.shortlink;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 短链访问记录表
|
||||
* 用于记录短链的访问日志,便于统计分析
|
||||
*
|
||||
* @author 吴帅苹
|
||||
*/
|
||||
@TableName("system_short_link_access_log")
|
||||
@KeySequence("system_short_link_access_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ShortLinkAccessLogDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 编号,数据库自增
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 短链ID
|
||||
*/
|
||||
private Long shortLinkId;
|
||||
|
||||
/**
|
||||
* 短码
|
||||
*/
|
||||
private String shortCode;
|
||||
|
||||
/**
|
||||
* 访问IP
|
||||
*/
|
||||
private String ip;
|
||||
|
||||
/**
|
||||
* 用户代理
|
||||
*/
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 来源页面
|
||||
*/
|
||||
private String referer;
|
||||
|
||||
/**
|
||||
* 访问时间
|
||||
*/
|
||||
private LocalDateTime accessTime;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package cn.iocoder.yudao.module.system.dal.dataobject.shortlink;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 短链表
|
||||
* 用于存储短链接的基本信息和统计数据
|
||||
*
|
||||
* @author 吴帅苹
|
||||
*/
|
||||
@TableName("system_short_link")
|
||||
@KeySequence("system_short_link_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ShortLinkDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 编号,数据库自增
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 短码
|
||||
*/
|
||||
private String shortCode;
|
||||
|
||||
/**
|
||||
* 原始URL
|
||||
*/
|
||||
private String originalUrl;
|
||||
|
||||
/**
|
||||
* 链接标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 链接描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 域名配置
|
||||
*/
|
||||
private String domain;
|
||||
|
||||
/**
|
||||
* 状态(true-启用,false-禁用)
|
||||
*/
|
||||
private Boolean status;
|
||||
|
||||
/**
|
||||
* 永久链接(true-是,false-否)
|
||||
*/
|
||||
private Boolean forever;
|
||||
|
||||
/**
|
||||
* 过期时间
|
||||
*/
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS, insertStrategy = FieldStrategy.ALWAYS)
|
||||
private LocalDateTime expireTime;
|
||||
|
||||
/**
|
||||
* 点击次数
|
||||
*/
|
||||
private Long clickCount;
|
||||
|
||||
/**
|
||||
* 最后点击时间
|
||||
*/
|
||||
private LocalDateTime lastClickTime;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package cn.iocoder.yudao.module.system.dal.mysql.shortlink;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.system.controller.admin.shortlink.vo.ShortLinkAccessLogPageReqVO;
|
||||
import cn.iocoder.yudao.module.system.dal.dataobject.shortlink.ShortLinkAccessLogDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 短链访问记录 Mapper
|
||||
*
|
||||
* @author 吴帅苹
|
||||
*/
|
||||
@Mapper
|
||||
public interface ShortLinkAccessLogMapper extends BaseMapperX<ShortLinkAccessLogDO> {
|
||||
|
||||
default PageResult<ShortLinkAccessLogDO> selectPage(ShortLinkAccessLogPageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<ShortLinkAccessLogDO>()
|
||||
.eqIfPresent(ShortLinkAccessLogDO::getShortLinkId, reqVO.getShortLinkId())
|
||||
.likeIfPresent(ShortLinkAccessLogDO::getShortCode, reqVO.getShortCode())
|
||||
.likeIfPresent(ShortLinkAccessLogDO::getIp, reqVO.getIp())
|
||||
.betweenIfPresent(ShortLinkAccessLogDO::getAccessTime, reqVO.getAccessTime())
|
||||
.orderByDesc(ShortLinkAccessLogDO::getId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计在访问时间范围内的访问数量
|
||||
*/
|
||||
default Long selectCountByAccessTimeBetween(java.time.LocalDateTime start, java.time.LocalDateTime end) {
|
||||
return selectCount(new LambdaQueryWrapperX<ShortLinkAccessLogDO>()
|
||||
.between(ShortLinkAccessLogDO::getAccessTime, start, end));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定访问时间范围内的访问日志列表
|
||||
*/
|
||||
default java.util.List<ShortLinkAccessLogDO> selectListByAccessTimeBetween(java.time.LocalDateTime start, java.time.LocalDateTime end) {
|
||||
return selectList(new LambdaQueryWrapperX<ShortLinkAccessLogDO>()
|
||||
.between(ShortLinkAccessLogDO::getAccessTime, start, end));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package cn.iocoder.yudao.module.system.dal.mysql.shortlink;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.system.controller.admin.shortlink.vo.ShortLinkPageReqVO;
|
||||
import cn.iocoder.yudao.module.system.dal.dataobject.shortlink.ShortLinkDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 短链 Mapper
|
||||
*
|
||||
* @author 吴帅苹
|
||||
*/
|
||||
@Mapper
|
||||
public interface ShortLinkMapper extends BaseMapperX<ShortLinkDO> {
|
||||
|
||||
default PageResult<ShortLinkDO> selectPage(ShortLinkPageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<ShortLinkDO>()
|
||||
.likeIfPresent(ShortLinkDO::getShortCode, reqVO.getShortCode())
|
||||
.likeIfPresent(ShortLinkDO::getOriginalUrl, reqVO.getOriginalUrl())
|
||||
.likeIfPresent(ShortLinkDO::getTitle, reqVO.getTitle())
|
||||
.eqIfPresent(ShortLinkDO::getStatus, reqVO.getStatus())
|
||||
.eqIfPresent(ShortLinkDO::getCreator, reqVO.getCreator())
|
||||
.betweenIfPresent(ShortLinkDO::getCreateTime, reqVO.getCreateTime())
|
||||
.orderByDesc(ShortLinkDO::getId));
|
||||
}
|
||||
|
||||
default ShortLinkDO selectByShortCode(String shortCode) {
|
||||
return selectOne(ShortLinkDO::getShortCode, shortCode);
|
||||
}
|
||||
|
||||
// ==================== 统计相关查询方法 ====================
|
||||
|
||||
/**
|
||||
* 统计总短链数
|
||||
*/
|
||||
default Long selectTotalCount() {
|
||||
return selectCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计活跃短链数(有点击记录的)
|
||||
*/
|
||||
default Long selectActiveCount() {
|
||||
return selectCount(new LambdaQueryWrapperX<ShortLinkDO>()
|
||||
.gt(ShortLinkDO::getClickCount, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计启用状态的短链数
|
||||
*/
|
||||
default Long selectEnabledCount() {
|
||||
return selectCount(ShortLinkDO::getStatus, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计禁用状态的短链数
|
||||
*/
|
||||
default Long selectDisabledCount() {
|
||||
return selectCount(ShortLinkDO::getStatus, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计总点击量
|
||||
*/
|
||||
default Long selectTotalClickCount() {
|
||||
return selectObjs(new LambdaQueryWrapperX<ShortLinkDO>()
|
||||
.select(ShortLinkDO::getClickCount))
|
||||
.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.mapToLong(obj -> (Long) obj)
|
||||
.sum();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计在创建时间范围内的短链数量
|
||||
*/
|
||||
default Long selectCountByCreateTimeBetween(java.time.LocalDateTime start, java.time.LocalDateTime end) {
|
||||
return selectCount(new LambdaQueryWrapperX<ShortLinkDO>()
|
||||
.between(ShortLinkDO::getCreateTime, start, end));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -101,10 +101,34 @@ public interface RedisKeyConstants {
|
|||
|
||||
/**
|
||||
* 小程序订阅模版的缓存
|
||||
*
|
||||
* <p>
|
||||
* KEY 格式:wxa_subscribe_template:{userType}
|
||||
* VALUE 数据格式 String, 模版信息
|
||||
*/
|
||||
String WXA_SUBSCRIBE_TEMPLATE = "wxa_subscribe_template";
|
||||
|
||||
/**
|
||||
* 短链域名的缓存
|
||||
* <p>
|
||||
* KEY 短链域名: short_link_domain:{tenantId}
|
||||
* VALUE 数据格式:String 域名信息
|
||||
*/
|
||||
String SHORT_LINK_DOMAIN = "short_link_domain";
|
||||
|
||||
/**
|
||||
* 短链的缓存
|
||||
* <p>
|
||||
* KEY 短链编号: short_link_code_cache:{code}
|
||||
* VALUE 数据格式:String 短链信息
|
||||
*/
|
||||
String SHORT_LINK_CODE_CACHE = "short_link_code_cache";
|
||||
|
||||
/**
|
||||
* 短链的访问次数的缓存
|
||||
* <p>
|
||||
* KEY 短链编号: short_link_access_rate:{code}
|
||||
* VALUE 数据格式:String 短链的访问次数
|
||||
*/
|
||||
String SHORT_LINK_ACCESS_RATE = "short_link_access_rate";
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,158 @@
|
|||
package cn.iocoder.yudao.module.system.service.shortlink;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.system.controller.admin.shortlink.vo.*;
|
||||
import cn.iocoder.yudao.module.system.dal.dataobject.shortlink.ShortLinkDO;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 短链 Service 接口
|
||||
*
|
||||
* @author 吴帅苹
|
||||
*/
|
||||
public interface ShortLinkService {
|
||||
|
||||
/**
|
||||
* 创建短链
|
||||
*
|
||||
* @param createReqVO 创建信息
|
||||
* @return 编号
|
||||
*/
|
||||
Long createShortLink(@Valid ShortLinkCreateReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 更新短链
|
||||
*
|
||||
* @param updateReqVO 更新信息
|
||||
*/
|
||||
void updateShortLink(@Valid ShortLinkUpdateReqVO updateReqVO);
|
||||
|
||||
/**
|
||||
* 删除短链
|
||||
*
|
||||
* @param id 编号
|
||||
*/
|
||||
void deleteShortLink(Long id);
|
||||
|
||||
/**
|
||||
* 获得短链
|
||||
*
|
||||
* @param id 编号
|
||||
* @return 短链
|
||||
*/
|
||||
ShortLinkDO getShortLink(Long id);
|
||||
|
||||
/**
|
||||
* 获得短链分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @return 短链分页
|
||||
*/
|
||||
PageResult<ShortLinkDO> getShortLinkPage(ShortLinkPageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 根据短码获取短链信息
|
||||
*
|
||||
* @param shortCode 短码
|
||||
* @return 短链信息
|
||||
*/
|
||||
ShortLinkDO getShortLinkByCode(String shortCode);
|
||||
|
||||
/**
|
||||
* 访问短链(重定向并记录访问日志)
|
||||
*
|
||||
* @param shortCode 短码
|
||||
* @param ip 访问IP
|
||||
* @param userAgent 用户代理
|
||||
* @param referer 来源页面
|
||||
* @return 原始URL
|
||||
*/
|
||||
String accessShortLink(String shortCode, String ip, String userAgent, String referer);
|
||||
|
||||
/**
|
||||
* 获得短链访问记录分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @return 访问记录分页
|
||||
*/
|
||||
PageResult<ShortLinkAccessLogRespVO> getShortLinkAccessLogPage(ShortLinkAccessLogPageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 获取短链总览统计
|
||||
*
|
||||
* @param start 开始时间
|
||||
* @param end 结束时间
|
||||
* @return 总览统计信息
|
||||
*/
|
||||
ShortLinkOverviewStatsRespVO getShortLinkOverviewStats(LocalDateTime start, LocalDateTime end);
|
||||
|
||||
/**
|
||||
* 获取短链趋势统计
|
||||
*
|
||||
* @param days 统计天数
|
||||
* @return 趋势统计信息
|
||||
*/
|
||||
List<ShortLinkTrendStatsRespVO> getShortLinkTrendStats(Integer days);
|
||||
|
||||
/**
|
||||
* 获取短链状态分布统计
|
||||
*
|
||||
* @return 状态分布统计信息
|
||||
*/
|
||||
ShortLinkStatusStatsRespVO getShortLinkStatusStats();
|
||||
|
||||
/**
|
||||
* 获取短链地域访问统计
|
||||
*
|
||||
* @return 地域访问统计信息
|
||||
*/
|
||||
ShortLinkRegionStatsRespVO getShortLinkRegionStats(LocalDateTime start, LocalDateTime end);
|
||||
|
||||
/**
|
||||
* 获取短链设备类型统计
|
||||
*
|
||||
* @return 设备类型统计信息
|
||||
*/
|
||||
ShortLinkDeviceStatsRespVO getShortLinkDeviceStats(LocalDateTime start, LocalDateTime end);
|
||||
|
||||
/**
|
||||
* 获取短链访问时段统计
|
||||
*
|
||||
* @return 访问时段统计信息
|
||||
*/
|
||||
ShortLinkHourStatsRespVO getShortLinkHourStats(LocalDate date);
|
||||
|
||||
/**
|
||||
* 批量删除短链
|
||||
*
|
||||
* @param ids 编号数组
|
||||
*/
|
||||
void deleteShortLinks(Long[] ids);
|
||||
|
||||
/**
|
||||
* 更新短链状态
|
||||
*
|
||||
* @param id 编号
|
||||
* @param status 状态
|
||||
*/
|
||||
void updateShortLinkStatus(Long id, Integer status);
|
||||
|
||||
/**
|
||||
* 更新短链域名
|
||||
*
|
||||
* @param newDomain 新域名
|
||||
*/
|
||||
void updateShortLinkDomain(String newDomain);
|
||||
|
||||
/**
|
||||
* 获取短链域名
|
||||
*
|
||||
* @return 域名
|
||||
*
|
||||
*/
|
||||
String getShortLinkDomain();
|
||||
}
|
||||
|
|
@ -0,0 +1,615 @@
|
|||
package cn.iocoder.yudao.module.system.service.shortlink;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.framework.ip.core.Area;
|
||||
import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
|
||||
import cn.iocoder.yudao.framework.ip.core.utils.IPUtils;
|
||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||
import cn.iocoder.yudao.module.system.controller.admin.shortlink.vo.*;
|
||||
import cn.iocoder.yudao.module.system.dal.dataobject.shortlink.ShortLinkAccessLogDO;
|
||||
import cn.iocoder.yudao.module.system.dal.dataobject.shortlink.ShortLinkDO;
|
||||
import cn.iocoder.yudao.module.system.dal.mysql.shortlink.ShortLinkAccessLogMapper;
|
||||
import cn.iocoder.yudao.module.system.dal.mysql.shortlink.ShortLinkMapper;
|
||||
import cn.iocoder.yudao.module.system.util.shrotlink.ShortCodeUtils;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants.*;
|
||||
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* 短链 Service 实现类
|
||||
*
|
||||
* @author 吴帅苹
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor(onConstructor_ = @Lazy)
|
||||
public class ShortLinkServiceImpl implements ShortLinkService {
|
||||
|
||||
private final ShortLinkMapper shortLinkMapper;
|
||||
|
||||
private final ShortLinkAccessLogMapper shortLinkAccessLogMapper;
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
|
||||
@Value("${yudao.short-link.domain}")
|
||||
private String domain;
|
||||
|
||||
/**
|
||||
* 创建短链
|
||||
* - 读取当前租户主域名(Redis 优先,空则回退到配置项)
|
||||
* - 校验失效时间(非永久场景必须填写)
|
||||
* - 生成唯一短码并保存
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createShortLink(ShortLinkCreateReqVO createReqVO) {
|
||||
|
||||
// 获取租户
|
||||
Long requiredTenantId = TenantContextHolder.getRequiredTenantId();
|
||||
|
||||
String domain = redisTemplate.opsForValue().get(SHORT_LINK_DOMAIN + ":" + requiredTenantId);
|
||||
if (StrUtil.isBlank(domain)) {
|
||||
domain = this.domain;
|
||||
}
|
||||
|
||||
// 如果不是永久生效,失效时间必填
|
||||
if (!createReqVO.getForever()) {
|
||||
if (createReqVO.getExpireTime() == null) {
|
||||
throw exception(SHORT_LINK_TIME_IS_NULL);
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 生成短码
|
||||
String shortCode = generateUniqueShortCode(createReqVO.getOriginalUrl(), createReqVO.getCustomShortCode());
|
||||
|
||||
// 2. 构建短链对象
|
||||
ShortLinkDO shortLink = BeanUtils.toBean(createReqVO, ShortLinkDO.class);
|
||||
shortLink.setShortCode(shortCode);
|
||||
shortLink.setStatus(Boolean.TRUE); // 默认启用
|
||||
shortLink.setClickCount(0L);
|
||||
shortLink.setDomain(domain);
|
||||
|
||||
// 3. 插入数据库
|
||||
shortLinkMapper.insert(shortLink);
|
||||
return shortLink.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改短链
|
||||
* - 校验存在并处理永久/失效时间逻辑
|
||||
* - 读取租户主域名并更新
|
||||
* - 清理相关短码缓存
|
||||
*/
|
||||
@Override
|
||||
public void updateShortLink(ShortLinkUpdateReqVO updateReqVO) {
|
||||
// 校验存在
|
||||
validateShortLinkExists(updateReqVO.getId());
|
||||
ShortLinkDO old = shortLinkMapper.selectById(updateReqVO.getId());
|
||||
|
||||
// 如果不是永久生效,失效时间必填
|
||||
if (!updateReqVO.getForever()) {
|
||||
if (updateReqVO.getExpireTime() == null) {
|
||||
throw exception(SHORT_LINK_TIME_IS_NULL);
|
||||
}
|
||||
} else {
|
||||
// 永久生效,清空时间
|
||||
updateReqVO.setExpireTime(null);
|
||||
}
|
||||
|
||||
Long tenantId = TenantContextHolder.getRequiredTenantId();
|
||||
String domain = redisTemplate.opsForValue().get(SHORT_LINK_DOMAIN + ":" + tenantId);
|
||||
if (StrUtil.isBlank(domain)) {
|
||||
domain = this.domain;
|
||||
}
|
||||
// 更新
|
||||
ShortLinkDO updateObj = BeanUtils.toBean(updateReqVO, ShortLinkDO.class);
|
||||
updateObj.setDomain(domain);
|
||||
shortLinkMapper.updateById(updateObj);
|
||||
if (old != null && StrUtil.isNotBlank(old.getShortCode())) {
|
||||
redisTemplate.delete(SHORT_LINK_CODE_CACHE + ":" + tenantId + ":" + old.getShortCode());
|
||||
}
|
||||
if (StrUtil.isNotBlank(updateObj.getShortCode())) {
|
||||
redisTemplate.delete(SHORT_LINK_CODE_CACHE + ":" + tenantId + ":" + updateObj.getShortCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除短链
|
||||
* - 校验存在后删除记录
|
||||
* - 清理短码缓存
|
||||
*/
|
||||
@Override
|
||||
public void deleteShortLink(Long id) {
|
||||
// 校验存在
|
||||
validateShortLinkExists(id);
|
||||
ShortLinkDO old = shortLinkMapper.selectById(id);
|
||||
|
||||
// 删除
|
||||
shortLinkMapper.deleteById(id);
|
||||
Long tenantId = TenantContextHolder.getRequiredTenantId();
|
||||
if (old != null && StrUtil.isNotBlank(old.getShortCode())) {
|
||||
redisTemplate.delete(SHORT_LINK_CODE_CACHE + ":" + tenantId + ":" + old.getShortCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据编号获取短链(按当前租户上下文)
|
||||
*/
|
||||
@Override
|
||||
public ShortLinkDO getShortLink(Long id) {
|
||||
return shortLinkMapper.selectById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询短链(按当前租户上下文)
|
||||
*/
|
||||
@Override
|
||||
public PageResult<ShortLinkDO> getShortLinkPage(ShortLinkPageReqVO pageReqVO) {
|
||||
return shortLinkMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过短码获取短链(支持无租户头访问)
|
||||
* - 先跨租户忽略过滤查询短码
|
||||
* - 解析到租户后切换上下文
|
||||
*/
|
||||
@Override
|
||||
public ShortLinkDO getShortLinkByCode(String shortCode) {
|
||||
return resolveShortLinkIgnoreTenant(shortCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 访问短链并返回原始 URL(记录点击与访问日志)
|
||||
* - 基于短码跨租户解析并切换到对应租户上下文
|
||||
* - 按租户维度计算缓存键,使用缓存/数据库获取短链
|
||||
* - 校验状态与过期时间
|
||||
* - 记录点击次数与最后访问时间
|
||||
* - 简单频控并异步记录访问日志
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String accessShortLink(String shortCode, String ip, String userAgent, String referer) {
|
||||
ShortLinkDO shortLink = resolveShortLinkIgnoreTenant(shortCode);
|
||||
if (shortLink == null) {
|
||||
throw exception(SHORT_LINK_NOT_EXISTS);
|
||||
}
|
||||
Long tenantId = TenantContextHolder.getRequiredTenantId();
|
||||
String cacheKey = SHORT_LINK_CODE_CACHE + ":" + tenantId + ":" + shortCode;
|
||||
|
||||
String cached = redisTemplate.opsForValue().get(cacheKey);
|
||||
ShortLinkDO shortLinkCached;
|
||||
if (StrUtil.isNotBlank(cached)) {
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
shortLinkCached = mapper.readValue(cached, ShortLinkDO.class);
|
||||
} catch (Exception e) {
|
||||
shortLinkCached = shortLinkMapper.selectByShortCode(shortCode);
|
||||
}
|
||||
} else {
|
||||
shortLinkCached = shortLinkMapper.selectByShortCode(shortCode);
|
||||
if (shortLinkCached != null) {
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
redisTemplate.opsForValue().set(cacheKey, mapper.writeValueAsString(shortLinkCached));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shortLinkCached == null) {
|
||||
shortLinkCached = shortLink;
|
||||
}
|
||||
ShortLinkDO effective = shortLinkCached;
|
||||
if (!shortLink.getStatus()) {
|
||||
throw exception(SHORT_LINK_DISABLED);
|
||||
}
|
||||
if (effective.getExpireTime() != null && effective.getExpireTime().isBefore(LocalDateTime.now())) {
|
||||
throw exception(SHORT_LINK_EXPIRED);
|
||||
}
|
||||
|
||||
effective.setClickCount(effective.getClickCount() + 1);
|
||||
effective.setLastClickTime(LocalDateTime.now());
|
||||
shortLinkMapper.updateById(effective);
|
||||
|
||||
String normIp = normalizeIp(ip);
|
||||
String rateKey = SHORT_LINK_ACCESS_RATE + ":" + tenantId + ":" + shortCode + ":" + (normIp == null ? "" : normIp);
|
||||
Boolean allowed = redisTemplate.opsForValue().setIfAbsent(rateKey, "1", 5, TimeUnit.SECONDS);
|
||||
if (Boolean.TRUE.equals(allowed)) {
|
||||
ShortLinkAccessLogDO accessLog = ShortLinkAccessLogDO.builder()
|
||||
.shortLinkId(shortLink.getId())
|
||||
.shortCode(shortCode)
|
||||
.ip(normIp)
|
||||
.userAgent(userAgent)
|
||||
.referer(referer)
|
||||
.accessTime(LocalDateTime.now())
|
||||
.build();
|
||||
shortLinkAccessLogMapper.insert(accessLog);
|
||||
}
|
||||
|
||||
return shortLink.getOriginalUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于短码跨租户解析:
|
||||
* 1. 暂时忽略租户过滤查询短码
|
||||
* 2. 查到后切换到对应租户上下文
|
||||
*/
|
||||
private ShortLinkDO resolveShortLinkIgnoreTenant(String shortCode) {
|
||||
ShortLinkDO shortLink;
|
||||
try {
|
||||
TenantContextHolder.setIgnore(true);
|
||||
shortLink = shortLinkMapper.selectByShortCode(shortCode);
|
||||
} finally {
|
||||
TenantContextHolder.setIgnore(false);
|
||||
}
|
||||
if (shortLink != null && shortLink.getTenantId() != null) {
|
||||
TenantContextHolder.setTenantId(shortLink.getTenantId());
|
||||
}
|
||||
return shortLink;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询短链访问日志(按当前租户上下文)
|
||||
*/
|
||||
@Override
|
||||
public PageResult<ShortLinkAccessLogRespVO> getShortLinkAccessLogPage(ShortLinkAccessLogPageReqVO pageReqVO) {
|
||||
PageResult<ShortLinkAccessLogDO> pageResult = shortLinkAccessLogMapper.selectPage(pageReqVO);
|
||||
List<ShortLinkAccessLogRespVO> respList = new ArrayList<>(pageResult.getList().size());
|
||||
for (ShortLinkAccessLogDO log : pageResult.getList()) {
|
||||
ShortLinkAccessLogRespVO vo = BeanUtils.toBean(log, ShortLinkAccessLogRespVO.class);
|
||||
String country = null, province = null, city = null;
|
||||
if (StrUtil.isNotBlank(log.getIp())) {
|
||||
String nip = normalizeIp(log.getIp());
|
||||
Area cur = IPUtils.getArea(nip);
|
||||
for (int i = 0; i < Byte.MAX_VALUE && cur != null; i++) {
|
||||
if (cur.getType() != null) {
|
||||
if (AreaTypeEnum.COUNTRY.getType().equals(cur.getType()) && country == null) {
|
||||
country = cur.getName();
|
||||
} else if (AreaTypeEnum.PROVINCE.getType().equals(cur.getType()) && province == null) {
|
||||
province = cur.getName();
|
||||
} else if (AreaTypeEnum.CITY.getType().equals(cur.getType()) && city == null) {
|
||||
city = cur.getName();
|
||||
}
|
||||
}
|
||||
cur = cur.getParent();
|
||||
}
|
||||
}
|
||||
vo.setCountry(country);
|
||||
vo.setProvince(province);
|
||||
vo.setCity(city);
|
||||
respList.add(vo);
|
||||
}
|
||||
return new PageResult<>(respList, pageResult.getTotal());
|
||||
}
|
||||
|
||||
/**
|
||||
* 正常化 IP 地址,将 IPv6 转换成 IPv4
|
||||
*/
|
||||
private String normalizeIp(String ip) {
|
||||
if (StrUtil.isBlank(ip)) {
|
||||
return ip;
|
||||
}
|
||||
String v = ip.trim();
|
||||
if ("::1".equals(v) || "0:0:0:0:0:0:0:1".equals(v)) {
|
||||
return "127.0.0.1";
|
||||
}
|
||||
Matcher m = Pattern.compile("(\\d{1,3}(?:\\.\\d{1,3}){3})").matcher(v);
|
||||
if (m.find()) {
|
||||
return m.group(1);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// ==================== 统计相关方法实现 ====================
|
||||
|
||||
|
||||
/**
|
||||
* 概览统计:总短链数、总点击量、活跃短链、平均点击率、区间新增与点击数
|
||||
*/
|
||||
@Override
|
||||
public ShortLinkOverviewStatsRespVO getShortLinkOverviewStats(LocalDateTime start, LocalDateTime end) {
|
||||
Long totalShortLinks = shortLinkMapper.selectTotalCount();
|
||||
Long totalClicks = shortLinkMapper.selectTotalClickCount();
|
||||
Long activeShortLinks = shortLinkMapper.selectActiveCount();
|
||||
|
||||
LocalDateTime s = start != null ? start : LocalDate.now().atStartOfDay();
|
||||
LocalDateTime e = end != null ? end : LocalDateTime.now();
|
||||
Long newLinks = shortLinkMapper.selectCountByCreateTimeBetween(s, e);
|
||||
Long clicks = shortLinkAccessLogMapper.selectCountByAccessTimeBetween(s, e);
|
||||
|
||||
return ShortLinkOverviewStatsRespVO.builder()
|
||||
.totalLinks(totalShortLinks != null ? totalShortLinks : 0L)
|
||||
.totalClicks(totalClicks != null ? totalClicks : 0L)
|
||||
.activeLinks(activeShortLinks != null ? activeShortLinks : 0L)
|
||||
.avgClickRate(totalShortLinks != null && totalShortLinks > 0 ? (double) (totalClicks != null ? totalClicks : 0L) / totalShortLinks * 100 : 0.0)
|
||||
.todayNewLinks(newLinks != null ? newLinks : 0L)
|
||||
.todayClicks(clicks != null ? clicks : 0L)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 趋势统计:按天计算新增与点击趋势
|
||||
*/
|
||||
@Override
|
||||
public List<ShortLinkTrendStatsRespVO> getShortLinkTrendStats(Integer days) {
|
||||
List<ShortLinkTrendStatsRespVO.TrendData> trendDataList = new ArrayList<>();
|
||||
|
||||
LocalDateTime todayStart = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0).withNano(0);
|
||||
for (int i = days - 1; i >= 0; i--) {
|
||||
LocalDateTime dayStart = todayStart.minusDays(i);
|
||||
LocalDateTime dayEnd = dayStart.plusDays(1);
|
||||
Long createCount = shortLinkMapper.selectCountByCreateTimeBetween(dayStart, dayEnd);
|
||||
Long clickCount = shortLinkAccessLogMapper.selectCountByAccessTimeBetween(dayStart, dayEnd);
|
||||
trendDataList.add(ShortLinkTrendStatsRespVO.TrendData.builder()
|
||||
.date(dayStart.toLocalDate())
|
||||
.createCount(createCount != null ? createCount : 0L)
|
||||
.clickCount(clickCount != null ? clickCount : 0L)
|
||||
.build());
|
||||
}
|
||||
|
||||
List<ShortLinkTrendStatsRespVO> result = new ArrayList<>();
|
||||
result.add(ShortLinkTrendStatsRespVO.builder().trendData(trendDataList).build());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态分布统计:启用/禁用占比
|
||||
*/
|
||||
@Override
|
||||
public ShortLinkStatusStatsRespVO getShortLinkStatusStats() {
|
||||
Long enabledCount = shortLinkMapper.selectEnabledCount();
|
||||
Long disabledCount = shortLinkMapper.selectDisabledCount();
|
||||
Long totalCount = enabledCount + disabledCount;
|
||||
|
||||
List<ShortLinkStatusStatsRespVO.StatusData> statusList = new ArrayList<>();
|
||||
|
||||
if (totalCount > 0) {
|
||||
statusList.add(ShortLinkStatusStatsRespVO.StatusData.builder().statusName("启用").statusValue(1).count(enabledCount).percentage(enabledCount * 100.0 / totalCount).build());
|
||||
|
||||
statusList.add(ShortLinkStatusStatsRespVO.StatusData.builder().statusName("禁用").statusValue(0).count(disabledCount).percentage(disabledCount * 100.0 / totalCount).build());
|
||||
}
|
||||
|
||||
return ShortLinkStatusStatsRespVO.builder().statusData(statusList).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 地域统计:基于访问日志 IP 解析省/市维度占比
|
||||
*/
|
||||
@Override
|
||||
public ShortLinkRegionStatsRespVO getShortLinkRegionStats(LocalDateTime start, LocalDateTime end) {
|
||||
end = end != null ? end : LocalDateTime.now();
|
||||
start = start != null ? start : end.minusDays(30);
|
||||
List<ShortLinkAccessLogDO> logs = shortLinkAccessLogMapper.selectListByAccessTimeBetween(start, end);
|
||||
|
||||
java.util.Map<String, Long> counter = new java.util.HashMap<>();
|
||||
long total = 0L;
|
||||
for (ShortLinkAccessLogDO log : logs) {
|
||||
String ip = normalizeIp(log.getIp());
|
||||
if (StrUtil.isBlank(ip)) {
|
||||
continue;
|
||||
}
|
||||
Area area = IPUtils.getArea(ip);
|
||||
if (area == null) {
|
||||
continue;
|
||||
}
|
||||
String province = null;
|
||||
String city = null;
|
||||
Area cur = area;
|
||||
for (int i = 0; i < Byte.MAX_VALUE && cur != null; i++) {
|
||||
if (cur.getType() != null) {
|
||||
if (AreaTypeEnum.PROVINCE.getType().equals(cur.getType()) && province == null) {
|
||||
province = cur.getName();
|
||||
} else if (AreaTypeEnum.CITY.getType().equals(cur.getType()) && city == null) {
|
||||
city = cur.getName();
|
||||
}
|
||||
}
|
||||
cur = cur.getParent();
|
||||
}
|
||||
String key = (province != null ? province : "未知省份") + "|" + (city != null ? city : "未知城市");
|
||||
counter.put(key, counter.getOrDefault(key, 0L) + 1);
|
||||
total++;
|
||||
}
|
||||
|
||||
List<ShortLinkRegionStatsRespVO.RegionData> regionList = new ArrayList<>();
|
||||
for (Map.Entry<String, Long> e : counter.entrySet()) {
|
||||
String[] parts = e.getKey().split("\\|");
|
||||
long count = e.getValue();
|
||||
regionList.add(ShortLinkRegionStatsRespVO.RegionData.builder()
|
||||
.province(parts[0])
|
||||
.city(parts.length > 1 ? parts[1] : "")
|
||||
.visitCount(count)
|
||||
.percentage(total > 0 ? count * 100.0 / total : 0.0)
|
||||
.build());
|
||||
}
|
||||
return ShortLinkRegionStatsRespVO.builder().regionData(regionList).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备统计:简单 UA 解析为 PC/Mobile/Tablet
|
||||
*/
|
||||
@Override
|
||||
public ShortLinkDeviceStatsRespVO getShortLinkDeviceStats(LocalDateTime start, LocalDateTime end) {
|
||||
LocalDateTime e = end != null ? end : LocalDateTime.now();
|
||||
LocalDateTime s = start != null ? start : e.minusDays(30);
|
||||
List<ShortLinkAccessLogDO> logs = shortLinkAccessLogMapper.selectListByAccessTimeBetween(s, e);
|
||||
|
||||
long total = logs.size();
|
||||
long pc = 0, mobile = 0, tablet = 0;
|
||||
|
||||
for (ShortLinkAccessLogDO log : logs) {
|
||||
String ua = log.getUserAgent();
|
||||
String u = ua == null ? "" : ua.toLowerCase();
|
||||
if (u.contains("ipad") || u.contains("tablet")) {
|
||||
tablet++;
|
||||
} else if (u.contains("android") || u.contains("iphone") || u.contains("mobile")) {
|
||||
mobile++;
|
||||
} else {
|
||||
pc++;
|
||||
}
|
||||
}
|
||||
|
||||
List<ShortLinkDeviceStatsRespVO.DeviceData> deviceList = new ArrayList<>();
|
||||
deviceList.add(ShortLinkDeviceStatsRespVO.DeviceData.builder().deviceType("PC").deviceName("PC").visitCount(pc).percentage(total > 0 ? pc * 100.0 / total : 0.0).build());
|
||||
deviceList.add(ShortLinkDeviceStatsRespVO.DeviceData.builder().deviceType("Mobile").deviceName("Mobile").visitCount(mobile).percentage(total > 0 ? mobile * 100.0 / total : 0.0).build());
|
||||
deviceList.add(ShortLinkDeviceStatsRespVO.DeviceData.builder().deviceType("Tablet").deviceName("Tablet").visitCount(tablet).percentage(total > 0 ? tablet * 100.0 / total : 0.0).build());
|
||||
|
||||
return ShortLinkDeviceStatsRespVO.builder().deviceData(deviceList).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 时段统计:当日 24 小时访问量分布
|
||||
*/
|
||||
@Override
|
||||
public ShortLinkHourStatsRespVO getShortLinkHourStats(LocalDate date) {
|
||||
List<ShortLinkHourStatsRespVO.HourData> hourList = new ArrayList<>();
|
||||
LocalDateTime startOfToday = (date != null ? date : LocalDate.now()).atStartOfDay();
|
||||
LocalDateTime endOfToday = startOfToday.plusDays(1);
|
||||
Long totalToday = shortLinkAccessLogMapper.selectCountByAccessTimeBetween(startOfToday, endOfToday);
|
||||
long total = totalToday != null ? totalToday : 0L;
|
||||
|
||||
for (int hour = 0; hour < 24; hour++) {
|
||||
LocalDateTime hStart = startOfToday.plusHours(hour);
|
||||
LocalDateTime hEnd = hStart.plusHours(1);
|
||||
Long count = shortLinkAccessLogMapper.selectCountByAccessTimeBetween(hStart, hEnd);
|
||||
long accessCount = count != null ? count : 0L;
|
||||
String timeDescription = String.format("%02d:00-%02d:59", hour, hour);
|
||||
hourList.add(ShortLinkHourStatsRespVO.HourData.builder()
|
||||
.hour(hour)
|
||||
.hourRange(timeDescription)
|
||||
.visitCount(accessCount)
|
||||
.percentage(total > 0 ? accessCount * 100.0 / total : 0.0)
|
||||
.build());
|
||||
}
|
||||
|
||||
return ShortLinkHourStatsRespVO.builder().hourData(hourList).build();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 批量删除短链
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteShortLinks(Long[] ids) {
|
||||
// 校验存在
|
||||
for (Long id : ids) {
|
||||
validateShortLinkExists(id);
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
shortLinkMapper.deleteBatchIds(Arrays.asList(ids));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新短链状态(启用/禁用)
|
||||
*/
|
||||
@Override
|
||||
public void updateShortLinkStatus(Long id, Integer status) {
|
||||
// 校验存在
|
||||
validateShortLinkExists(id);
|
||||
|
||||
// 更新状态
|
||||
ShortLinkDO updateObj = new ShortLinkDO();
|
||||
updateObj.setId(id);
|
||||
updateObj.setStatus(status == 1);
|
||||
shortLinkMapper.updateById(updateObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前租户的短链主域名(写入 Redis)
|
||||
*/
|
||||
@Override
|
||||
public void updateShortLinkDomain(String newDomain) {
|
||||
Long tenantId = TenantContextHolder.getRequiredTenantId();
|
||||
redisTemplate.opsForValue().set(SHORT_LINK_DOMAIN + ":" + tenantId, newDomain);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前租户短链主域名(Redis 优先,空则回退到配置项)
|
||||
*/
|
||||
@Override
|
||||
public String getShortLinkDomain() {
|
||||
Long tenantId = TenantContextHolder.getRequiredTenantId();
|
||||
String domain = redisTemplate.opsForValue().get(SHORT_LINK_DOMAIN + ":" + tenantId);
|
||||
if (StrUtil.isBlank(domain)) {
|
||||
domain = this.domain;
|
||||
}
|
||||
return domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一的短码
|
||||
*
|
||||
* @param originalUrl 原始URL
|
||||
* @param customShortCode 自定义短码
|
||||
* @return 短码
|
||||
*/
|
||||
private String generateUniqueShortCode(String originalUrl, String customShortCode) {
|
||||
// 如果指定了自定义短码,先检查是否可用
|
||||
if (StrUtil.isNotBlank(customShortCode)) {
|
||||
if (!ShortCodeUtils.isValidShortCode(customShortCode)) {
|
||||
throw exception(SHORT_LINK_CODE_FORMAT_ERROR);
|
||||
}
|
||||
|
||||
ShortLinkDO existingShortLink = shortLinkMapper.selectByShortCode(customShortCode);
|
||||
if (existingShortLink != null) {
|
||||
throw exception(SHORT_LINK_CODE_IS_EXIST);
|
||||
}
|
||||
|
||||
return customShortCode;
|
||||
}
|
||||
|
||||
// 自动生成短码,最多尝试10次避免无限循环
|
||||
for (int i = 0; i < 10; i++) {
|
||||
String shortCode;
|
||||
if (i < 5) {
|
||||
// 前5次尝试使用MD5算法(相同URL生成相同短码)
|
||||
shortCode = ShortCodeUtils.generateByMd5(originalUrl + i);
|
||||
} else {
|
||||
// 后5次使用随机算法
|
||||
shortCode = ShortCodeUtils.generateRandom();
|
||||
}
|
||||
|
||||
// 检查短码是否已存在
|
||||
ShortLinkDO existingShortLink = shortLinkMapper.selectByShortCode(shortCode);
|
||||
if (existingShortLink == null) {
|
||||
return shortCode;
|
||||
}
|
||||
}
|
||||
|
||||
throw exception(SHORT_LINK_CODE_GENERATE_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验短链是否存在
|
||||
*
|
||||
* @param id 短链编号
|
||||
*/
|
||||
private void validateShortLinkExists(Long id) {
|
||||
if (shortLinkMapper.selectById(id) == null) {
|
||||
throw exception(SHORT_LINK_NOT_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
package cn.iocoder.yudao.module.system.util.shrotlink;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* 短码生成工具类
|
||||
* 提供多种短码生成策略
|
||||
*
|
||||
* @author 巴蜀科技
|
||||
*/
|
||||
public class ShortCodeUtils {
|
||||
|
||||
/**
|
||||
* Base62 字符集(包含数字、小写字母、大写字母)
|
||||
*/
|
||||
private static final String BASE62_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
|
||||
/**
|
||||
* 默认短码长度
|
||||
*/
|
||||
private static final int DEFAULT_SHORT_CODE_LENGTH = 6;
|
||||
|
||||
/**
|
||||
* 基于URL的MD5哈希生成短码
|
||||
* 优点:相同URL生成相同短码,节省存储空间
|
||||
* 缺点:可能存在哈希冲突
|
||||
*
|
||||
* @param originalUrl 原始URL
|
||||
* @param length 短码长度
|
||||
* @return 短码
|
||||
*/
|
||||
public static String generateByMd5(String originalUrl, int length) {
|
||||
if (StrUtil.isBlank(originalUrl)) {
|
||||
throw new IllegalArgumentException("原始URL不能为空");
|
||||
}
|
||||
if (length <= 0 || length > 32) {
|
||||
throw new IllegalArgumentException("短码长度必须在1-32之间");
|
||||
}
|
||||
|
||||
String md5 = DigestUtil.md5Hex(originalUrl);
|
||||
return convertToBase62(md5, length);
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于URL的MD5哈希生成短码(默认长度)
|
||||
*
|
||||
* @param originalUrl 原始URL
|
||||
* @return 短码
|
||||
*/
|
||||
public static String generateByMd5(String originalUrl) {
|
||||
return generateByMd5(originalUrl, DEFAULT_SHORT_CODE_LENGTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机生成短码
|
||||
* 优点:生成速度快,冲突概率低
|
||||
* 缺点:相同URL会生成不同短码
|
||||
*
|
||||
* @param length 短码长度
|
||||
* @return 短码
|
||||
*/
|
||||
public static String generateRandom(int length) {
|
||||
if (length <= 0 || length > 32) {
|
||||
throw new IllegalArgumentException("短码长度必须在1-32之间");
|
||||
}
|
||||
|
||||
StringBuilder shortCode = new StringBuilder();
|
||||
ThreadLocalRandom random = ThreadLocalRandom.current();
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
int index = random.nextInt(BASE62_CHARS.length());
|
||||
shortCode.append(BASE62_CHARS.charAt(index));
|
||||
}
|
||||
|
||||
return shortCode.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机生成短码(默认长度)
|
||||
*
|
||||
* @return 短码
|
||||
*/
|
||||
public static String generateRandom() {
|
||||
return generateRandom(DEFAULT_SHORT_CODE_LENGTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于雪花算法ID生成短码
|
||||
* 优点:全局唯一,性能高
|
||||
* 缺点:需要雪花算法支持
|
||||
*
|
||||
* @param snowflakeId 雪花算法生成的ID
|
||||
* @param length 短码长度
|
||||
* @return 短码
|
||||
*/
|
||||
public static String generateBySnowflakeId(long snowflakeId, int length) {
|
||||
if (length <= 0 || length > 32) {
|
||||
throw new IllegalArgumentException("短码长度必须在1-32之间");
|
||||
}
|
||||
|
||||
return convertToBase62(String.valueOf(snowflakeId), length);
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于雪花算法ID生成短码(默认长度)
|
||||
*
|
||||
* @param snowflakeId 雪花算法生成的ID
|
||||
* @return 短码
|
||||
*/
|
||||
public static String generateBySnowflakeId(long snowflakeId) {
|
||||
return generateBySnowflakeId(snowflakeId, DEFAULT_SHORT_CODE_LENGTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证短码格式是否正确
|
||||
*
|
||||
* @param shortCode 短码
|
||||
* @return 是否有效
|
||||
*/
|
||||
public static boolean isValidShortCode(String shortCode) {
|
||||
if (StrUtil.isBlank(shortCode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查长度
|
||||
if (shortCode.length() > 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查字符是否都在Base62字符集中
|
||||
for (char c : shortCode.toCharArray()) {
|
||||
if (BASE62_CHARS.indexOf(c) == -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字符串转换为Base62编码的短码
|
||||
*
|
||||
* @param input 输入字符串
|
||||
* @param length 目标长度
|
||||
* @return Base62编码的短码
|
||||
*/
|
||||
private static String convertToBase62(String input, int length) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
// 使用输入字符串的哈希值作为种子
|
||||
int seed = Math.abs(input.hashCode());
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
int index = seed % BASE62_CHARS.length();
|
||||
result.append(BASE62_CHARS.charAt(index));
|
||||
seed = seed / BASE62_CHARS.length() + i * 31; // 添加位置因子避免重复
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成带前缀的短码
|
||||
*
|
||||
* @param prefix 前缀
|
||||
* @param length 短码部分长度
|
||||
* @return 带前缀的短码
|
||||
*/
|
||||
public static String generateWithPrefix(String prefix, int length) {
|
||||
if (StrUtil.isBlank(prefix)) {
|
||||
throw new IllegalArgumentException("前缀不能为空");
|
||||
}
|
||||
|
||||
return prefix + generateRandom(length);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -157,7 +157,10 @@ wx:
|
|||
# 芋道配置项,设置当前项目所有自定义的配置
|
||||
yudao:
|
||||
demo: true # 开启演示模式
|
||||
|
||||
short-link:
|
||||
# 该短链只是测试用,到你们正式使用时,可以将你们的域名通过Nginx映射到该接口
|
||||
# 比如你们使用的域名是yudao.com,那么你可以通过配置域名a.yudao.com 映射到该接口,然后通过a.yudao.com/{shortCode}访问
|
||||
domain: http://localhost:48080/app-api/system/short-link
|
||||
justauth:
|
||||
enabled: true
|
||||
type:
|
||||
|
|
|
|||
|
|
@ -186,7 +186,10 @@ yudao:
|
|||
env-version: develop # 小程序版本: 正式版为 "release";体验版为 "trial";开发版为 "develop"
|
||||
wxa-subscribe-message:
|
||||
miniprogram-state: developer # 跳转小程序类型:开发版为 “developer”;体验版为 “trial”为;正式版为 “formal”
|
||||
|
||||
short-link:
|
||||
# 该短链只是测试用,到你们正式使用时,可以将你们的域名通过Nginx映射到该接口
|
||||
# 比如你们使用的域名是yudao.com,那么你可以通过配置域名a.yudao.com 映射到该接口,然后通过a.yudao.com/{shortCode}访问
|
||||
domain: http://localhost:48080/app-api/system/short-link
|
||||
justauth:
|
||||
enabled: true
|
||||
type:
|
||||
|
|
|
|||
|
|
@ -152,7 +152,8 @@ yudao:
|
|||
transfer-notify-url: https://yunai.natapp1.cc/admin-api/pay/notify/transfer # 支付渠道的【转账】回调地址
|
||||
demo: false # 开启演示模式
|
||||
tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc
|
||||
|
||||
short-link:
|
||||
domain: http://localhost:48080
|
||||
justauth:
|
||||
enabled: true
|
||||
type:
|
||||
|
|
|
|||
|
|
@ -46,27 +46,20 @@ spring:
|
|||
primary: master
|
||||
datasource:
|
||||
master:
|
||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
||||
# url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X 连接的示例
|
||||
# url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
|
||||
# url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
|
||||
# url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true;useUnicode=true;characterEncoding=utf-8 # SQLServer 连接的示例
|
||||
# url: jdbc:dm://127.0.0.1:5236?schema=RUOYI_VUE_PRO # DM 连接的示例
|
||||
# url: jdbc:kingbase8://127.0.0.1:54321/test # 人大金仓 KingbaseES 连接的示例
|
||||
# url: jdbc:postgresql://127.0.0.1:5432/postgres # OpenGauss 连接的示例
|
||||
url: jdbc:mysql://192.168.67.11:3306/ruoyi_vue_pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
||||
username: root
|
||||
password: 123456
|
||||
password: SDC00hlQxG3j4hH
|
||||
# username: sa # SQL Server 连接的示例
|
||||
# password: Yudao@2024 # SQL Server 连接的示例
|
||||
# username: SYSDBA # DM 连接的示例
|
||||
# password: SYSDBA001 # DM 连接的示例
|
||||
# username: root # OpenGauss 连接的示例
|
||||
# password: Yudao@2024 # OpenGauss 连接的示例
|
||||
slave: # 模拟从库,可根据自己需要修改
|
||||
lazy: true # 开启懒加载,保证启动速度
|
||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
|
||||
username: root
|
||||
password: 123456
|
||||
# slave: # 模拟从库,可根据自己需要修改
|
||||
# lazy: true # 开启懒加载,保证启动速度
|
||||
# url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
|
||||
# username: root
|
||||
# password: 123456
|
||||
# tdengine: # IoT 数据库(需要 IoT 物联网再开启噢!)
|
||||
# url: jdbc:TAOS-WS://127.0.0.1:6041/ruoyi_vue_pro
|
||||
# driver-class-name: com.taosdata.jdbc.rs.RestfulDriver
|
||||
|
|
@ -78,10 +71,9 @@ spring:
|
|||
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
|
||||
data:
|
||||
redis:
|
||||
host: 127.0.0.1 # 地址
|
||||
host: 192.168.67.11 # 地址
|
||||
port: 6379 # 端口
|
||||
database: 0 # 数据库索引
|
||||
# password: dev # 密码,建议生产环境开启
|
||||
database: 4 # 数据库索引
|
||||
|
||||
--- #################### 定时任务相关配置 ####################
|
||||
|
||||
|
|
@ -214,7 +206,8 @@ yudao:
|
|||
wxa-subscribe-message:
|
||||
miniprogram-state: developer # 跳转小程序类型:开发版为 “developer”;体验版为 “trial”为;正式版为 “formal”
|
||||
tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc
|
||||
|
||||
short-link:
|
||||
domain: http://localhost:48080
|
||||
justauth:
|
||||
enabled: true
|
||||
type:
|
||||
|
|
|
|||
Loading…
Reference in New Issue