diff --git a/src/main/java/com/gxwebsoft/app/controller/AppResourceController.java b/src/main/java/com/gxwebsoft/app/controller/AppResourceController.java new file mode 100644 index 0000000..35a8273 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/controller/AppResourceController.java @@ -0,0 +1,145 @@ +package com.gxwebsoft.app.controller; + +import com.gxwebsoft.app.entity.AppResource; +import com.gxwebsoft.app.param.AppResourceParam; +import com.gxwebsoft.app.service.AppResourceService; +import com.gxwebsoft.common.core.annotation.OperationLog; +import com.gxwebsoft.common.core.web.ApiResult; +import com.gxwebsoft.common.core.web.BaseController; +import com.gxwebsoft.common.core.web.PageResult; +import com.gxwebsoft.common.system.entity.User; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +/** + * 开发者资源管理控制器(服务器/数据库/云存储/域名/SSL) + * + * @author 科技小王子 + * @since 2026-03-31 + */ +@Slf4j +@Tag(name = "开发者资源管理") +@RestController +@RequestMapping("/api/_modules/app/developer-resource") +public class AppResourceController extends BaseController { + + @Resource + private AppResourceService appResourceService; + + // ─── 查询接口 ───────────────────────────────────────────────── + + @Operation(summary = "分页查询资源列表") + @GetMapping("/page") + public ApiResult> page(AppResourceParam param) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录", null); + // 普通开发者只能查自己的资源 + param.setUserId(loginUser.getUserId()); + param.setTenantId(loginUser.getTenantId()); + return success(appResourceService.pageRel(param)); + } + + @Operation(summary = "查询资源列表(不分页)") + @GetMapping + public ApiResult> list(AppResourceParam param) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录", null); + param.setUserId(loginUser.getUserId()); + param.setTenantId(loginUser.getTenantId()); + return success(appResourceService.listRel(param)); + } + + @Operation(summary = "获取资源详情") + @GetMapping("/{resourceId}") + public ApiResult get(@PathVariable Long resourceId) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录", null); + AppResource resource = appResourceService.getByIdRel(resourceId); + if (resource == null) return fail("资源不存在", null); + if (!resource.getUserId().equals(loginUser.getUserId())) return fail("无权访问此资源", null); + return success(resource); + } + + @Operation(summary = "统计各类型资源数量") + @GetMapping("/stats") + public ApiResult> stats() { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录", null); + return success(appResourceService.countByType(loginUser.getUserId(), loginUser.getTenantId())); + } + + // ─── 新增/修改接口 ──────────────────────────────────────────── + + @OperationLog + @Operation(summary = "新增资源") + @PostMapping + public ApiResult save(@RequestBody AppResource resource) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录", null); + if (resource.getResourceType() == null || resource.getResourceType().isEmpty()) { + return fail("资源类型不能为空", null); + } + if (resource.getName() == null || resource.getName().isEmpty()) { + return fail("资源名称不能为空", null); + } + resource.setTenantId(loginUser.getTenantId()); + try { + AppResource result = appResourceService.addResource(resource, loginUser.getUserId()); + return success("添加成功", result); + } catch (Exception e) { + return fail(e.getMessage(), null); + } + } + + @OperationLog + @Operation(summary = "修改资源") + @PutMapping + public ApiResult update(@RequestBody AppResource resource) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录", null); + try { + AppResource result = appResourceService.updateResource(resource); + return success("修改成功", result); + } catch (Exception e) { + return fail(e.getMessage(), null); + } + } + + // ─── 删除接口 ───────────────────────────────────────────────── + + @OperationLog + @Operation(summary = "删除资源(逻辑删除)") + @DeleteMapping("/{resourceId}") + public ApiResult remove(@PathVariable Long resourceId) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录"); + try { + appResourceService.removeResource(resourceId, loginUser.getUserId()); + return success("删除成功"); + } catch (Exception e) { + return fail(e.getMessage()); + } + } + + @OperationLog + @Operation(summary = "批量删除资源") + @DeleteMapping("/batch") + public ApiResult removeBatch(@RequestBody List ids) { + User loginUser = getLoginUser(); + if (loginUser == null) return fail("请先登录"); + try { + for (Long id : ids) { + appResourceService.removeResource(id, loginUser.getUserId()); + } + return success("批量删除成功"); + } catch (Exception e) { + return fail(e.getMessage()); + } + } +} diff --git a/src/main/java/com/gxwebsoft/app/entity/AppResource.java b/src/main/java/com/gxwebsoft/app/entity/AppResource.java new file mode 100644 index 0000000..2027876 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/entity/AppResource.java @@ -0,0 +1,121 @@ +package com.gxwebsoft.app.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 开发者资源(服务器/数据库/云存储/域名/SSL证书) + * + * @author 科技小王子 + * @since 2026-03-31 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("app_resource") +@Schema(name = "AppResource对象", description = "开发者资源") +public class AppResource implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description = "资源ID") + @TableId(value = "resource_id", type = IdType.AUTO) + private Long resourceId; + + @Schema(description = "资源类型: server/database/storage/domain/ssl") + private String resourceType; + + @Schema(description = "资源名称") + private String name; + + @Schema(description = "服务商: tencent/aliyun/huawei/other") + private String provider; + + @Schema(description = "关联应用ID(可选)") + private Long websiteId; + + @Schema(description = "关联应用名称(冗余)") + private String websiteName; + + // ─── 服务器字段 ─────────────────────────────────────── + @Schema(description = "IP地址(服务器用)") + private String ip; + + // ─── 数据库字段 ─────────────────────────────────────── + @Schema(description = "数据库类型: MySQL/PostgreSQL/Redis/MongoDB(数据库用)") + private String dbType; + + @Schema(description = "连接主机地址(数据库用)") + private String host; + + @Schema(description = "连接端口(数据库用)") + private Integer port; + + // ─── 云存储字段 ─────────────────────────────────────── + @Schema(description = "地区/Region(云存储用)") + private String region; + + @Schema(description = "访问权限: public-read/private(云存储用)") + private String acl; + + @Schema(description = "已用空间(字节,云存储用)") + private Long usedBytes; + + // ─── 域名字段 ───────────────────────────────────────── + @Schema(description = "域名(域名用)") + private String domain; + + @Schema(description = "注册商(域名用)") + private String registrar; + + @Schema(description = "是否已备案(域名用)") + private Boolean icp; + + @Schema(description = "ICP备案号(域名用)") + private String icpNo; + + @Schema(description = "是否已绑定SSL(域名用,冗余)") + private Boolean sslBound; + + // ─── SSL证书字段 ────────────────────────────────────── + @Schema(description = "证书类型: DV/OV/EV(SSL用)") + private String certType; + + @Schema(description = "颁发机构(SSL用)") + private String issuer; + + // ─── 通用字段 ───────────────────────────────────────── + @Schema(description = "状态: running/stopped/expired/pending") + private String status; + + @Schema(description = "到期时间") + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDate expireAt; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "所属用户ID") + private Integer userId; + + @Schema(description = "租户ID") + private Integer tenantId; + + @Schema(description = "是否删除: 0否 1是") + private Integer deleted; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/gxwebsoft/app/mapper/AppResourceMapper.java b/src/main/java/com/gxwebsoft/app/mapper/AppResourceMapper.java new file mode 100644 index 0000000..94847b6 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/mapper/AppResourceMapper.java @@ -0,0 +1,35 @@ +package com.gxwebsoft.app.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.gxwebsoft.app.entity.AppResource; +import com.gxwebsoft.app.param.AppResourceParam; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 开发者资源 Mapper + * + * @author 科技小王子 + * @since 2026-03-31 + */ +public interface AppResourceMapper extends BaseMapper { + + /** + * 分页查询(关联应用名称) + */ + List selectPageRel(@Param("page") IPage page, + @Param("param") AppResourceParam param); + + /** + * 查询全部列表(关联应用名称) + */ + List selectListRel(@Param("param") AppResourceParam param); + + /** + * 统计各类型资源数量,返回 [{resourceType, cnt}] + */ + List> countByType(@Param("userId") Integer userId, + @Param("tenantId") Integer tenantId); +} diff --git a/src/main/java/com/gxwebsoft/app/mapper/xml/AppResourceMapper.xml b/src/main/java/com/gxwebsoft/app/mapper/xml/AppResourceMapper.xml new file mode 100644 index 0000000..292b82c --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/mapper/xml/AppResourceMapper.xml @@ -0,0 +1,75 @@ + + + + + + + SELECT a.*, w.website_name + FROM app_resource a + LEFT JOIN cms_website w ON a.website_id = w.website_id AND w.deleted = 0 + + a.deleted = 0 + + AND a.resource_id = #{param.resourceId} + + + AND a.resource_type = #{param.resourceType} + + + AND a.website_id = #{param.websiteId} + + + AND a.provider = #{param.provider} + + + AND a.status = #{param.status} + + + AND a.user_id = #{param.userId} + + + AND a.tenant_id = #{param.tenantId} + + + AND ( + a.name LIKE CONCAT('%', #{param.keywords}, '%') + OR a.ip LIKE CONCAT('%', #{param.keywords}, '%') + OR a.domain LIKE CONCAT('%', #{param.keywords}, '%') + OR a.host LIKE CONCAT('%', #{param.keywords}, '%') + ) + + + AND a.create_time >= #{param.createTimeStart} + + + AND a.create_time <= #{param.createTimeEnd} + + + ORDER BY a.create_time DESC + + + + + + + + + + + + diff --git a/src/main/java/com/gxwebsoft/app/param/AppResourceParam.java b/src/main/java/com/gxwebsoft/app/param/AppResourceParam.java new file mode 100644 index 0000000..bc18e73 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/param/AppResourceParam.java @@ -0,0 +1,54 @@ +package com.gxwebsoft.app.param; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.gxwebsoft.common.core.annotation.QueryField; +import com.gxwebsoft.common.core.annotation.QueryType; +import com.gxwebsoft.common.core.web.BaseParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 开发者资源查询参数 + * + * @author 科技小王子 + * @since 2026-03-31 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(name = "AppResourceParam对象", description = "开发者资源查询参数") +public class AppResourceParam extends BaseParam { + private static final long serialVersionUID = 1L; + + @Schema(description = "资源ID") + @QueryField(type = QueryType.EQ) + private Long resourceId; + + @Schema(description = "资源类型: server/database/storage/domain/ssl") + @QueryField(type = QueryType.EQ) + private String resourceType; + + @Schema(description = "关联应用ID") + @QueryField(type = QueryType.EQ) + private Long websiteId; + + @Schema(description = "服务商") + @QueryField(type = QueryType.EQ) + private String provider; + + @Schema(description = "状态") + @QueryField(type = QueryType.EQ) + private String status; + + @Schema(description = "所属用户ID") + @QueryField(type = QueryType.EQ) + private Integer userId; + + @Schema(description = "租户ID") + @QueryField(type = QueryType.EQ) + private Integer tenantId; + + @Schema(description = "关键词(名称/IP/域名/Host模糊搜索)") + private String keywords; +} diff --git a/src/main/java/com/gxwebsoft/app/service/AppResourceService.java b/src/main/java/com/gxwebsoft/app/service/AppResourceService.java new file mode 100644 index 0000000..76f4057 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/service/AppResourceService.java @@ -0,0 +1,39 @@ +package com.gxwebsoft.app.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.gxwebsoft.app.entity.AppResource; +import com.gxwebsoft.app.param.AppResourceParam; +import com.gxwebsoft.common.core.web.PageResult; + +import java.util.List; +import java.util.Map; + +/** + * 开发者资源 Service + * + * @author 科技小王子 + * @since 2026-03-31 + */ +public interface AppResourceService extends IService { + + /** 分页查询(关联应用名称) */ + PageResult pageRel(AppResourceParam param); + + /** 列表查询 */ + List listRel(AppResourceParam param); + + /** 详情(关联查询) */ + AppResource getByIdRel(Long resourceId); + + /** 新增资源 */ + AppResource addResource(AppResource resource, Integer userId); + + /** 更新资源 */ + AppResource updateResource(AppResource resource); + + /** 删除资源(逻辑删除) */ + void removeResource(Long resourceId, Integer userId); + + /** 按资源类型统计数量 [{resourceType, cnt}] */ + Map countByType(Integer userId, Integer tenantId); +} diff --git a/src/main/java/com/gxwebsoft/app/service/impl/AppResourceServiceImpl.java b/src/main/java/com/gxwebsoft/app/service/impl/AppResourceServiceImpl.java new file mode 100644 index 0000000..dc3aa96 --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/service/impl/AppResourceServiceImpl.java @@ -0,0 +1,109 @@ +package com.gxwebsoft.app.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.gxwebsoft.app.entity.AppResource; +import com.gxwebsoft.app.mapper.AppResourceMapper; +import com.gxwebsoft.app.param.AppResourceParam; +import com.gxwebsoft.app.service.AppResourceService; +import com.gxwebsoft.common.core.web.PageParam; +import com.gxwebsoft.common.core.web.PageResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 开发者资源 Service 实现 + * + * @author 科技小王子 + * @since 2026-03-31 + */ +@Slf4j +@Service +public class AppResourceServiceImpl extends ServiceImpl + implements AppResourceService { + + @Override + public PageResult pageRel(AppResourceParam param) { + PageParam page = new PageParam<>(param); + page.setDefaultOrder("create_time desc"); + List list = baseMapper.selectPageRel(page, param); + return new PageResult<>(list, page.getTotal()); + } + + @Override + public List listRel(AppResourceParam param) { + return baseMapper.selectListRel(param); + } + + @Override + public AppResource getByIdRel(Long resourceId) { + AppResourceParam param = new AppResourceParam(); + param.setResourceId(resourceId); + List list = baseMapper.selectListRel(param); + return list.isEmpty() ? null : list.get(0); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public AppResource addResource(AppResource resource, Integer userId) { + resource.setUserId(userId); + resource.setDeleted(0); + resource.setCreateTime(LocalDateTime.now()); + resource.setUpdateTime(LocalDateTime.now()); + // 默认状态 + if (resource.getStatus() == null) { + resource.setStatus("running"); + } + save(resource); + log.info("新增资源成功, type={}, name={}, userId={}", resource.getResourceType(), resource.getName(), userId); + return resource; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public AppResource updateResource(AppResource resource) { + if (resource.getResourceId() == null) { + throw new RuntimeException("资源ID不能为空"); + } + resource.setUpdateTime(LocalDateTime.now()); + updateById(resource); + return getByIdRel(resource.getResourceId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeResource(Long resourceId, Integer userId) { + AppResource resource = getById(resourceId); + if (resource == null) { + throw new RuntimeException("资源不存在"); + } + if (!resource.getUserId().equals(userId)) { + throw new RuntimeException("无权操作此资源"); + } + resource.setDeleted(1); + resource.setUpdateTime(LocalDateTime.now()); + updateById(resource); + log.info("删除资源成功, resourceId={}, userId={}", resourceId, userId); + } + + @Override + public Map countByType(Integer userId, Integer tenantId) { + List> raw = baseMapper.countByType(userId, tenantId); + Map result = new HashMap<>(); + // 初始化所有类型为 0 + for (String type : new String[]{"server", "database", "storage", "domain", "ssl"}) { + result.put(type, 0L); + } + for (Map row : raw) { + String type = (String) row.get("resourceType"); + Long cnt = ((Number) row.get("cnt")).longValue(); + result.put(type, cnt); + } + return result; + } +} diff --git a/src/main/java/com/gxwebsoft/app/sql/app_resource.sql b/src/main/java/com/gxwebsoft/app/sql/app_resource.sql new file mode 100644 index 0000000..7f18c0b --- /dev/null +++ b/src/main/java/com/gxwebsoft/app/sql/app_resource.sql @@ -0,0 +1,54 @@ +-- ---------------------------- +-- 开发者资源管理表 +-- 统一存放服务器/数据库/云存储/域名/SSL证书等开发基础设施资源 +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `app_resource` ( + `resource_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '资源ID', + `resource_type` VARCHAR(20) NOT NULL COMMENT '资源类型: server/database/storage/domain/ssl', + `name` VARCHAR(100) NOT NULL COMMENT '资源名称', + `provider` VARCHAR(30) DEFAULT NULL COMMENT '服务商: tencent/aliyun/huawei/other', + + -- 服务器字段 + `ip` VARCHAR(50) DEFAULT NULL COMMENT 'IP地址(服务器用)', + + -- 数据库字段 + `db_type` VARCHAR(30) DEFAULT NULL COMMENT '数据库类型: MySQL/PostgreSQL/Redis/MongoDB', + `host` VARCHAR(200) DEFAULT NULL COMMENT '连接主机地址(数据库用)', + `port` INT DEFAULT NULL COMMENT '连接端口(数据库用)', + + -- 云存储字段 + `region` VARCHAR(50) DEFAULT NULL COMMENT '地区/Region(云存储用)', + `acl` VARCHAR(30) DEFAULT 'private' COMMENT '访问权限: public-read/private(云存储用)', + `used_bytes` BIGINT DEFAULT 0 COMMENT '已用空间(字节,云存储用)', + + -- 域名字段 + `domain` VARCHAR(200) DEFAULT NULL COMMENT '域名(域名/SSL用)', + `registrar` VARCHAR(100) DEFAULT NULL COMMENT '注册商(域名用)', + `icp` TINYINT(1) DEFAULT 0 COMMENT '是否已备案: 0否 1是(域名用)', + `icp_no` VARCHAR(100) DEFAULT NULL COMMENT 'ICP备案号(域名用)', + `ssl_bound` TINYINT(1) DEFAULT 0 COMMENT '是否已绑定SSL(域名冗余标记)', + + -- SSL证书字段 + `cert_type` VARCHAR(10) DEFAULT NULL COMMENT '证书类型: DV/OV/EV(SSL用)', + `issuer` VARCHAR(100) DEFAULT NULL COMMENT '颁发机构(SSL用)', + + -- 通用字段 + `status` VARCHAR(20) NOT NULL DEFAULT 'running' COMMENT '状态: running/stopped/expired/pending', + `website_id` BIGINT DEFAULT NULL COMMENT '关联应用ID(可选)', + `expire_at` DATE DEFAULT NULL COMMENT '到期时间', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `user_id` INT NOT NULL COMMENT '所属用户ID', + `tenant_id` INT DEFAULT NULL COMMENT '租户ID', + `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0否 1是', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + + PRIMARY KEY (`resource_id`), + KEY `idx_resource_type` (`resource_type`), + KEY `idx_user_id` (`user_id`), + KEY `idx_tenant_id` (`tenant_id`), + KEY `idx_website_id` (`website_id`), + KEY `idx_status` (`status`), + KEY `idx_expire_at` (`expire_at`), + KEY `idx_deleted` (`deleted`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='开发者资源管理(服务器/数据库/云存储/域名/SSL)';