diff --git a/sql/mysql/ruoyi-vue-pro.sql b/sql/mysql/ruoyi-vue-pro.sql index 3e4ca9ef3..79a6e8bd4 100644 --- a/sql/mysql/ruoyi-vue-pro.sql +++ b/sql/mysql/ruoyi-vue-pro.sql @@ -2341,6 +2341,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; -- ---------------------------- @@ -4014,6 +4021,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 (144, 'aoteman001', '$2a$04$omQOmhz8OyUFBKw77nr8KOtMp6xdvoQ1gWStjk9r8.OYT3Bv6oEYe', 'aoteman001', NULL, 116, NULL, '', '', 0, '', 1, '0:0:0:0:0:0:0:1', '2025-12-01 17:05:27', '1', '2025-12-01 17:05:27', '1', '2025-12-15 15:55:54', 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 -- ---------------------------- diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java index c89a5dcdb..fd0f038f0 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java @@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.common.exception.ErrorCode; /** * System 错误码枚举类 - * + *

* 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, "短链已过期"); + } diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/ShortLinkController.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/ShortLinkController.java new file mode 100644 index 000000000..b58f6d03c --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/ShortLinkController.java @@ -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 createShortLink(@Valid @RequestBody ShortLinkCreateReqVO createReqVO) { + return success(shortLinkService.createShortLink(createReqVO)); + } + + @PutMapping("/domain") + @Operation(summary = "修改短链域名") + @PreAuthorize("@ss.hasPermission('system:short-link:update')") + public CommonResult updateShortLinkDomain(@RequestParam("newDomain") String newDomain) { + shortLinkService.updateShortLinkDomain(newDomain); + return success(true); + } + + @GetMapping("/domain") + @Operation(summary = "获取短链域名") + @PreAuthorize("@ss.hasPermission('system:short-link:query')") + public CommonResult getShortLinkDomain() { + return success(shortLinkService.getShortLinkDomain()); + } + + @PutMapping("/update") + @Operation(summary = "更新短链") + @PreAuthorize("@ss.hasPermission('system:short-link:update')") + public CommonResult 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 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 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> getShortLinkPage(@Valid ShortLinkPageReqVO pageReqVO) { + PageResult 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 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 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 deleteShortLinks(@RequestBody Long[] ids) { + shortLinkService.deleteShortLinks(ids); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "修改短链状态") + @PreAuthorize("@ss.hasPermission('system:short-link:update')") + public CommonResult 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 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> 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 getShortLinkStatusStats() { + return success(shortLinkService.getShortLinkStatusStats()); + } + + @GetMapping("/statistics/region") + @Operation(summary = "获取短链地域访问统计") + @PreAuthorize("@ss.hasPermission('system:short-link:query')") + public CommonResult 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 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 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> 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 list = shortLinkService.getShortLinkAccessLogPage(pageReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "短链访问日志.xls", "数据", ShortLinkAccessLogRespVO.class, list); + } + +} diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkAccessLogPageReqVO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkAccessLogPageReqVO.java new file mode 100644 index 000000000..1ead73ac6 --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkAccessLogPageReqVO.java @@ -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; + +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkAccessLogRespVO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkAccessLogRespVO.java new file mode 100644 index 000000000..c455675e7 --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkAccessLogRespVO.java @@ -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; + +} diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkCreateReqVO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkCreateReqVO.java new file mode 100644 index 000000000..459fc9995 --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkCreateReqVO.java @@ -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; + +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkDeviceStatsRespVO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkDeviceStatsRespVO.java new file mode 100644 index 000000000..b53637f73 --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkDeviceStatsRespVO.java @@ -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; + + @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; + } +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkHourStatsRespVO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkHourStatsRespVO.java new file mode 100644 index 000000000..573d71712 --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkHourStatsRespVO.java @@ -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; + + @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; + } +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkOverviewStatsRespVO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkOverviewStatsRespVO.java new file mode 100644 index 000000000..f80fff173 --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkOverviewStatsRespVO.java @@ -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; + +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkPageReqVO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkPageReqVO.java new file mode 100644 index 000000000..39d3713fd --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkPageReqVO.java @@ -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; + +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkRegionStatsRespVO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkRegionStatsRespVO.java new file mode 100644 index 000000000..9f4516b76 --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkRegionStatsRespVO.java @@ -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; + + @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; + } +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkRespVO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkRespVO.java new file mode 100644 index 000000000..8c1c341b3 --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkRespVO.java @@ -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; + +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkStatusStatsRespVO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkStatusStatsRespVO.java new file mode 100644 index 000000000..4691a6ab5 --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkStatusStatsRespVO.java @@ -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; + + @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; + } +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkTrendStatsRespVO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkTrendStatsRespVO.java new file mode 100644 index 000000000..36058493c --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkTrendStatsRespVO.java @@ -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; + + @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; + } +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkUpdateReqVO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkUpdateReqVO.java new file mode 100644 index 000000000..c07275774 --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/shortlink/vo/ShortLinkUpdateReqVO.java @@ -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; + +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/app/shortlink/AppShortLinkController.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/app/shortlink/AppShortLinkController.java new file mode 100644 index 000000000..e7f3630a9 --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/app/shortlink/AppShortLinkController.java @@ -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 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 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()); + } + } + +} diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/shortlink/ShortLinkAccessLogDO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/shortlink/ShortLinkAccessLogDO.java new file mode 100644 index 000000000..b8b11551f --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/shortlink/ShortLinkAccessLogDO.java @@ -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; + +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/shortlink/ShortLinkDO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/shortlink/ShortLinkDO.java new file mode 100644 index 000000000..66c5f6d4d --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/shortlink/ShortLinkDO.java @@ -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; + +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/shortlink/ShortLinkAccessLogMapper.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/shortlink/ShortLinkAccessLogMapper.java new file mode 100644 index 000000000..967098113 --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/shortlink/ShortLinkAccessLogMapper.java @@ -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 { + + default PageResult selectPage(ShortLinkAccessLogPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .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() + .between(ShortLinkAccessLogDO::getAccessTime, start, end)); + } + + /** + * 查询指定访问时间范围内的访问日志列表 + */ + default java.util.List selectListByAccessTimeBetween(java.time.LocalDateTime start, java.time.LocalDateTime end) { + return selectList(new LambdaQueryWrapperX() + .between(ShortLinkAccessLogDO::getAccessTime, start, end)); + } + +} diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/shortlink/ShortLinkMapper.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/shortlink/ShortLinkMapper.java new file mode 100644 index 000000000..defc34fab --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/shortlink/ShortLinkMapper.java @@ -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 { + + default PageResult selectPage(ShortLinkPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .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() + .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() + .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() + .between(ShortLinkDO::getCreateTime, start, end)); + } + +} diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java index 1cd58306b..d1f3cbdc4 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java @@ -101,10 +101,34 @@ public interface RedisKeyConstants { /** * 小程序订阅模版的缓存 - * + *

* KEY 格式:wxa_subscribe_template:{userType} * VALUE 数据格式 String, 模版信息 */ String WXA_SUBSCRIBE_TEMPLATE = "wxa_subscribe_template"; + /** + * 短链域名的缓存 + *

+ * KEY 短链域名: short_link_domain:{tenantId} + * VALUE 数据格式:String 域名信息 + */ + String SHORT_LINK_DOMAIN = "short_link_domain"; + + /** + * 短链的缓存 + *

+ * KEY 短链编号: short_link_code_cache:{code} + * VALUE 数据格式:String 短链信息 + */ + String SHORT_LINK_CODE_CACHE = "short_link_code_cache"; + + /** + * 短链的访问次数的缓存 + *

+ * KEY 短链编号: short_link_access_rate:{code} + * VALUE 数据格式:String 短链的访问次数 + */ + String SHORT_LINK_ACCESS_RATE = "short_link_access_rate"; + } diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/shortlink/ShortLinkService.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/shortlink/ShortLinkService.java new file mode 100644 index 000000000..155c350c2 --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/shortlink/ShortLinkService.java @@ -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 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 getShortLinkAccessLogPage(ShortLinkAccessLogPageReqVO pageReqVO); + + /** + * 获取短链总览统计 + * + * @param start 开始时间 + * @param end 结束时间 + * @return 总览统计信息 + */ + ShortLinkOverviewStatsRespVO getShortLinkOverviewStats(LocalDateTime start, LocalDateTime end); + + /** + * 获取短链趋势统计 + * + * @param days 统计天数 + * @return 趋势统计信息 + */ + List 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(); +} diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/shortlink/ShortLinkServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/shortlink/ShortLinkServiceImpl.java new file mode 100644 index 000000000..2901a7ee5 --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/shortlink/ShortLinkServiceImpl.java @@ -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 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 getShortLinkAccessLogPage(ShortLinkAccessLogPageReqVO pageReqVO) { + PageResult pageResult = shortLinkAccessLogMapper.selectPage(pageReqVO); + List 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 getShortLinkTrendStats(Integer days) { + List 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 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 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 logs = shortLinkAccessLogMapper.selectListByAccessTimeBetween(start, end); + + java.util.Map 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 regionList = new ArrayList<>(); + for (Map.Entry 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 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 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 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); + } + } + +} diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/util/shrotlink/ShortCodeUtils.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/util/shrotlink/ShortCodeUtils.java new file mode 100644 index 000000000..d04c48eb5 --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/util/shrotlink/ShortCodeUtils.java @@ -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); + } + +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/resources/application-dev.yaml b/yudao-module-system/yudao-module-system-server/src/main/resources/application-dev.yaml index 649cc6ec9..9c27e633b 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/resources/application-dev.yaml +++ b/yudao-module-system/yudao-module-system-server/src/main/resources/application-dev.yaml @@ -159,7 +159,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: diff --git a/yudao-module-system/yudao-module-system-server/src/main/resources/application-local.yaml b/yudao-module-system/yudao-module-system-server/src/main/resources/application-local.yaml index a923b34b4..24f0f5bfa 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/resources/application-local.yaml +++ b/yudao-module-system/yudao-module-system-server/src/main/resources/application-local.yaml @@ -188,7 +188,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: diff --git a/yudao-server/src/main/resources/application-dev.yaml b/yudao-server/src/main/resources/application-dev.yaml index f6c9b7c44..dc8a9d6e2 100644 --- a/yudao-server/src/main/resources/application-dev.yaml +++ b/yudao-server/src/main/resources/application-dev.yaml @@ -154,7 +154,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: diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index 737608859..a539a9a60 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -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 # 数据库索引 --- #################### 定时任务相关配置 #################### @@ -216,7 +208,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: