前端页面作品集页面以及后端对应代码实现1.0
实现点赞,标签,悬浮通知
This commit is contained in:
parent
475e0461e2
commit
1f379b9c21
@ -30,38 +30,113 @@
|
||||
<java.version>21</java.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<!-- Spring Boot JDBC数据访问启动器 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jdbc</artifactId>
|
||||
</dependency>
|
||||
<!-- Spring Boot Redis数据访问启动器 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<!-- Spring Boot JDBC启动器 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||
</dependency>
|
||||
<!-- Spring Boot Web启动器 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL连接器 -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<!-- Lombok注解处理器 -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<!-- Spring Boot测试启动器 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里云OSS对象存储依赖 -->
|
||||
<dependency>
|
||||
<groupId>com.aliyun.oss</groupId>
|
||||
<artifactId>aliyun-sdk-oss</artifactId>
|
||||
<version>3.15.1</version>
|
||||
</dependency>
|
||||
<!-- FastJSON JSON处理库 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<version>2.0.31</version>
|
||||
</dependency>
|
||||
<!-- MyBatis-Plus启动器 -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<version>3.5.7</version>
|
||||
</dependency>
|
||||
<!-- Druid数据库连接池 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid-spring-boot-starter</artifactId>
|
||||
<version>1.2.18</version>
|
||||
</dependency>
|
||||
<!-- Redis客户端依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<!-- AOP依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 日志框架 -->
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON处理 -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- IP地址解析 -->
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.20</version>
|
||||
</dependency>
|
||||
|
||||
<!-- IP地址库 -->
|
||||
<dependency>
|
||||
<groupId>org.lionsoul</groupId>
|
||||
<artifactId>ip2region</artifactId>
|
||||
<version>2.6.4</version>
|
||||
</dependency>
|
||||
<!-- Spring Boot 3 需要使用 jakarta.servlet -->
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<version>6.0.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
@ -1,11 +1,12 @@
|
||||
package com.nailart;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
@MapperScan("com.nailart.mapper")
|
||||
public class NaCoreApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(NaCoreApplication.class, args);
|
||||
}
|
||||
|
91
na-core/src/main/java/com/nailart/aop/RequestLogAspect.java
Normal file
91
na-core/src/main/java/com/nailart/aop/RequestLogAspect.java
Normal file
@ -0,0 +1,91 @@
|
||||
package com.nailart.aop;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nailart.dto.RequestLogDTO;
|
||||
import com.nailart.utils.IpAddressUtil;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @Description 实现日志记录
|
||||
* @Classname RequestLogDTOAspect
|
||||
* @Date 2025/7/11 0:05
|
||||
* @Created by 21616
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class RequestLogAspect {
|
||||
@Autowired
|
||||
private IpAddressUtil ipAddressUtil;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final String logFilePath = "request_logs.json"; // 日志文件路径
|
||||
|
||||
@Pointcut("execution(* com.nailart.controller..*(..))")
|
||||
public void requestPointcut() {}
|
||||
|
||||
@Around("requestPointcut()")
|
||||
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
long startTime = System.currentTimeMillis();
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
|
||||
RequestLogDTO log = new RequestLogDTO();
|
||||
log.setRequestTime(new Date());
|
||||
log.setIp(getClientIp(request));
|
||||
log.setCity(ipAddressUtil.getCityInfo(log.getIp()));
|
||||
log.setUrl(request.getRequestURL().toString());
|
||||
log.setHttpMethod(request.getMethod());
|
||||
log.setMethod(joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
|
||||
|
||||
try {
|
||||
return joinPoint.proceed();
|
||||
} finally {
|
||||
log.setResponseTime(System.currentTimeMillis() - startTime);
|
||||
// 直接将日志写入文件,不使用日志框架
|
||||
writeLogToFile(log);
|
||||
}
|
||||
}
|
||||
|
||||
private String getClientIp(HttpServletRequest request) {
|
||||
String ip = request.getHeader("x-forwarded-for");
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
if (ip != null && ip.contains(",")) {
|
||||
ip = ip.split(",")[0].trim();
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
private void writeLogToFile(RequestLogDTO log) {
|
||||
try (PrintWriter writer = new PrintWriter(new FileWriter(logFilePath, true))) {
|
||||
// 将日志对象序列化为JSON并写入文件
|
||||
writer.println(objectMapper.writeValueAsString(log));
|
||||
} catch (IOException e) {
|
||||
// 处理写入异常(可以选择记录到标准输出或忽略)
|
||||
System.err.println("Failed to write request log: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
31
na-core/src/main/java/com/nailart/config/CorsConfig.java
Normal file
31
na-core/src/main/java/com/nailart/config/CorsConfig.java
Normal file
@ -0,0 +1,31 @@
|
||||
package com.nailart.config;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* @Description 跨域处理
|
||||
* @Classname CorsConfig
|
||||
* @Date 2025/7/11 0:01
|
||||
* @Created by 21616
|
||||
*/
|
||||
// 案例 一
|
||||
@Configuration
|
||||
public class CorsConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
//是否发送Cookie
|
||||
.allowCredentials(false)
|
||||
//放行哪些原始域
|
||||
.allowedOrigins("*")
|
||||
.allowedMethods(new String[]{"GET", "POST", "PUT", "DELETE"})
|
||||
.allowedHeaders("*")
|
||||
.exposedHeaders("*");
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package com.nailart.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @Classname my
|
||||
* @Description TODO
|
||||
* @Date 2025/7/10 18:50
|
||||
* @Created by 21616
|
||||
*/
|
||||
@Configuration
|
||||
public class MyBatisPlusConfig {
|
||||
/**
|
||||
* 分页插件配置
|
||||
*
|
||||
* @return MybatisPlusInterceptor
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
// 向MyBatis-Plus的过滤器链中添加分页拦截器,需要设置数据库类型(主要用于分页方言)
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
||||
return interceptor;
|
||||
}
|
||||
}
|
37
na-core/src/main/java/com/nailart/config/RedisConfig.java
Normal file
37
na-core/src/main/java/com/nailart/config/RedisConfig.java
Normal file
@ -0,0 +1,37 @@
|
||||
package com.nailart.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* @Description redis配置类
|
||||
* @Classname RedisConfig
|
||||
* @Date 2025/7/10 22:23
|
||||
* @Created by 21616
|
||||
*/
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(redisConnectionFactory);
|
||||
|
||||
// 设置键的序列化方式
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
// 设置值的序列化方式
|
||||
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
|
||||
|
||||
// 设置哈希键的序列化方式
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
// 设置哈希值的序列化方式
|
||||
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
package com.nailart.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.nailart.dataobj.PortfolioDO;
|
||||
import com.nailart.enums.RespCodeEnum;
|
||||
import com.nailart.service.PortfolioService;
|
||||
import com.nailart.vo.RespVO;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Classname PortfolioController
|
||||
* @Description 作品集控制类
|
||||
* @Date 2025/7/10 18:48
|
||||
* @Created by 21616
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/portfolio")
|
||||
public class PortfolioController {
|
||||
private final PortfolioService portfolioService;
|
||||
|
||||
public PortfolioController(PortfolioService portfolioService) {
|
||||
this.portfolioService = portfolioService;
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
public RespVO getPortfolioList(@RequestParam("page") int page, @RequestParam("size") int size) {
|
||||
RespVO respVO = new RespVO();
|
||||
if (page <= 0 || size <= 0) {
|
||||
respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode());
|
||||
respVO.setMsg(RespCodeEnum.BAD_REQUEST.getMessage());
|
||||
return respVO;
|
||||
}
|
||||
try {
|
||||
IPage<PortfolioDO> portfolioDOIPage = portfolioService.listByPage(page, size);
|
||||
respVO.setCode(RespCodeEnum.SUCCESS.getCode());
|
||||
respVO.setMsg(RespCodeEnum.SUCCESS.getMessage());
|
||||
respVO.setData(portfolioDOIPage);
|
||||
} catch (Exception e) {
|
||||
respVO.setCode(RespCodeEnum.INTERNAL_SERVER_ERROR.getCode());
|
||||
respVO.setMsg(RespCodeEnum.INTERNAL_SERVER_ERROR.getMessage());
|
||||
}
|
||||
return respVO;
|
||||
|
||||
}
|
||||
|
||||
@PostMapping("/add")
|
||||
public RespVO addPortfolio(PortfolioDO portfolioDO) {
|
||||
RespVO respVO = new RespVO();
|
||||
if (portfolioDO == null) {
|
||||
respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode());
|
||||
respVO.setMsg(RespCodeEnum.BAD_REQUEST.getMessage());
|
||||
return respVO;
|
||||
}
|
||||
try {
|
||||
Integer i = portfolioService.addPortfolio(portfolioDO);
|
||||
respVO.setCode(RespCodeEnum.SUCCESS.getCode());
|
||||
respVO.setMsg(RespCodeEnum.SUCCESS.getMessage());
|
||||
respVO.setData(i);
|
||||
} catch (Exception e) {
|
||||
respVO.setCode(RespCodeEnum.INTERNAL_SERVER_ERROR.getCode());
|
||||
respVO.setMsg(RespCodeEnum.INTERNAL_SERVER_ERROR.getMessage());
|
||||
}
|
||||
return respVO;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@PostMapping("/update")
|
||||
public RespVO updatePortfolio(PortfolioDO portfolioDO) {
|
||||
RespVO respVO = new RespVO();
|
||||
if (portfolioDO == null) {
|
||||
respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode());
|
||||
respVO.setMsg(RespCodeEnum.BAD_REQUEST.getMessage());
|
||||
return respVO;
|
||||
}
|
||||
try {
|
||||
Integer i = portfolioService.updatePortfolio(portfolioDO);
|
||||
respVO.setCode(RespCodeEnum.SUCCESS.getCode());
|
||||
respVO.setMsg(RespCodeEnum.SUCCESS.getMessage());
|
||||
respVO.setData(i);
|
||||
} catch (Exception e) {
|
||||
respVO.setCode(RespCodeEnum.INTERNAL_SERVER_ERROR.getCode());
|
||||
respVO.setMsg(RespCodeEnum.INTERNAL_SERVER_ERROR.getMessage());
|
||||
}
|
||||
return respVO;
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
public RespVO deletePortfolio(@RequestParam("id") Integer id) {
|
||||
RespVO respVO = new RespVO();
|
||||
if (id == null) {
|
||||
respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode());
|
||||
respVO.setMsg(RespCodeEnum.BAD_REQUEST.getMessage());
|
||||
return respVO;
|
||||
}
|
||||
try {
|
||||
Integer i = portfolioService.deletePortfolio(id);
|
||||
respVO.setCode(RespCodeEnum.SUCCESS.getCode());
|
||||
respVO.setMsg(RespCodeEnum.SUCCESS.getMessage());
|
||||
respVO.setData(i);
|
||||
} catch (Exception e) {
|
||||
respVO.setCode(RespCodeEnum.INTERNAL_SERVER_ERROR.getCode());
|
||||
respVO.setMsg(RespCodeEnum.INTERNAL_SERVER_ERROR.getMessage());
|
||||
}
|
||||
return respVO;
|
||||
}
|
||||
|
||||
@PostMapping("/addLoves")
|
||||
public RespVO addLoves(@RequestParam("id") Integer id,@RequestParam Integer changes) {
|
||||
RespVO respVO = new RespVO();
|
||||
if (id == null) {
|
||||
respVO.setCode(RespCodeEnum.BAD_REQUEST.getCode());
|
||||
respVO.setMsg(RespCodeEnum.BAD_REQUEST.getMessage());
|
||||
return respVO;
|
||||
}
|
||||
try {
|
||||
Integer i = portfolioService.addLoves(id, changes);
|
||||
respVO.setCode(RespCodeEnum.SUCCESS.getCode());
|
||||
respVO.setMsg(RespCodeEnum.SUCCESS.getMessage());
|
||||
respVO.setData(i);
|
||||
} catch (Exception e) {
|
||||
respVO.setCode(RespCodeEnum.INTERNAL_SERVER_ERROR.getCode());
|
||||
respVO.setMsg(RespCodeEnum.INTERNAL_SERVER_ERROR.getMessage());
|
||||
}
|
||||
return respVO;
|
||||
}
|
||||
}
|
54
na-core/src/main/java/com/nailart/dataobj/PortfolioDO.java
Normal file
54
na-core/src/main/java/com/nailart/dataobj/PortfolioDO.java
Normal file
@ -0,0 +1,54 @@
|
||||
package com.nailart.dataobj;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
@Data
|
||||
@TableName("portfolio_info")
|
||||
public class PortfolioDO {
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
private Long id;
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
private String title;
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private String description;
|
||||
/**
|
||||
* 点赞数
|
||||
*/
|
||||
private Integer loves;
|
||||
/**
|
||||
* 图片地址
|
||||
*/
|
||||
private String imgUrl;
|
||||
/**
|
||||
* 图片文件名
|
||||
*/
|
||||
private String imgFilename;
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
private String tag;
|
||||
/**
|
||||
* 标签样式
|
||||
*/
|
||||
private String tagStyle;
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private String createTime;
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
private Integer status;
|
||||
/**
|
||||
* 价格
|
||||
*/
|
||||
private Integer amt;
|
||||
}
|
33
na-core/src/main/java/com/nailart/dto/RequestLogDTO.java
Normal file
33
na-core/src/main/java/com/nailart/dto/RequestLogDTO.java
Normal file
@ -0,0 +1,33 @@
|
||||
package com.nailart.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @Description 日志实体类
|
||||
* @Classname RequestLogDTO
|
||||
* @Date 2025/7/11 0:04
|
||||
* @Created by 21616
|
||||
*/
|
||||
|
||||
|
||||
@Data
|
||||
public class RequestLogDTO {
|
||||
// 请求IP
|
||||
private String ip;
|
||||
// IP对应的城市
|
||||
private String city;
|
||||
// 请求时间
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS", timezone = "GMT+8")
|
||||
private Date requestTime;
|
||||
// 请求方法(类名+方法名)
|
||||
private String method;
|
||||
// 请求URL
|
||||
private String url;
|
||||
// HTTP方法(GET/POST等)
|
||||
private String httpMethod;
|
||||
// 响应时间(ms)
|
||||
private long responseTime;
|
||||
}
|
80
na-core/src/main/java/com/nailart/enums/RespCodeEnum.java
Normal file
80
na-core/src/main/java/com/nailart/enums/RespCodeEnum.java
Normal file
@ -0,0 +1,80 @@
|
||||
package com.nailart.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @Description 统一响应状态码枚举类
|
||||
* @Classname ResptCodeEnum
|
||||
* @Date 2025/7/10 22:34
|
||||
* @Created by 21616
|
||||
*/
|
||||
@Getter
|
||||
public enum RespCodeEnum {
|
||||
|
||||
// ===================== 成功状态码 =====================
|
||||
/** 成功 */
|
||||
SUCCESS(200, "操作成功"),
|
||||
/** 创建成功 */
|
||||
CREATED(201, "资源创建成功"),
|
||||
/** 无内容(适用于删除成功等场景) */
|
||||
NO_CONTENT(204, "操作成功,无返回内容"),
|
||||
|
||||
|
||||
// ===================== 客户端错误 =====================
|
||||
/** 请求参数错误 */
|
||||
BAD_REQUEST(400, "请求参数格式错误或不完整"),
|
||||
/** 未授权(未登录) */
|
||||
UNAUTHORIZED(401, "请先登录"),
|
||||
/** 权限不足 */
|
||||
FORBIDDEN(403, "没有权限执行该操作"),
|
||||
/** 资源不存在 */
|
||||
NOT_FOUND(404, "请求的资源不存在"),
|
||||
/** 请求方法不支持 */
|
||||
METHOD_NOT_ALLOWED(405, "不支持的请求方法(如用GET访问POST接口)"),
|
||||
/** 请求频率过高 */
|
||||
TOO_MANY_REQUESTS(429, "请求过于频繁,请稍后再试"),
|
||||
|
||||
|
||||
// ===================== 服务器错误 =====================
|
||||
/** 服务器内部错误 */
|
||||
INTERNAL_SERVER_ERROR(500, "服务器内部错误,请联系管理员"),
|
||||
/** 服务暂不可用 */
|
||||
SERVICE_UNAVAILABLE(503, "服务暂时不可用,请稍后再试"),
|
||||
/** 数据库错误 */
|
||||
DATABASE_ERROR(5001, "数据库操作失败"),
|
||||
/** 缓存错误 */
|
||||
CACHE_ERROR(5002, "缓存操作失败"),
|
||||
|
||||
|
||||
// ===================== 业务自定义错误 =====================
|
||||
/** 数据重复(如新增已存在的记录) */
|
||||
DATA_DUPLICATE(601, "数据已存在,请勿重复添加"),
|
||||
/** 数据验证失败(如字段格式不符合要求) */
|
||||
DATA_VALIDATION_FAILED(602, "数据验证失败,请检查输入"),
|
||||
/** 第三方接口调用失败 */
|
||||
THIRD_PARTY_SERVICE_ERROR(603, "第三方服务调用失败");
|
||||
|
||||
|
||||
/** 状态码 */
|
||||
private final int code;
|
||||
/** 描述信息 */
|
||||
private final String message;
|
||||
|
||||
// 构造方法
|
||||
RespCodeEnum(int code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据状态码获取枚举实例(可选工具方法)
|
||||
*/
|
||||
public static RespCodeEnum getByCode(int code) {
|
||||
for (RespCodeEnum resultCode : values()) {
|
||||
if (resultCode.code == code) {
|
||||
return resultCode;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.nailart.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.nailart.dataobj.PortfolioDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface PortfolioMapper extends BaseMapper<PortfolioDO> {
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package com.nailart.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.nailart.dataobj.PortfolioDO;
|
||||
|
||||
|
||||
public interface PortfolioService extends IService<PortfolioDO> {
|
||||
IPage<PortfolioDO> listByPage(int page, int size);
|
||||
Integer addPortfolio(PortfolioDO portfolioDO);
|
||||
|
||||
Integer updatePortfolio(PortfolioDO portfolioDO);
|
||||
|
||||
Integer deletePortfolio(Integer id);
|
||||
|
||||
Integer addLoves(Integer id, Integer changes);
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package com.nailart.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.nailart.dataobj.PortfolioDO;
|
||||
import com.nailart.mapper.PortfolioMapper;
|
||||
import com.nailart.service.PortfolioService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
*
|
||||
* @Author: xiaowang
|
||||
* @Date: 2021/5/10 14:48
|
||||
*/
|
||||
@Service
|
||||
public class PortfolioServiceImpl extends ServiceImpl<PortfolioMapper, PortfolioDO> implements PortfolioService {
|
||||
|
||||
private final PortfolioMapper portfolioMapper;
|
||||
|
||||
public PortfolioServiceImpl(PortfolioMapper portfolioMapper) {
|
||||
this.portfolioMapper = portfolioMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param page
|
||||
* @param size
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public IPage<PortfolioDO> listByPage(int page, int size) {
|
||||
Page<PortfolioDO> objectPage = new Page<>(page, size);
|
||||
return portfolioMapper.selectPage(objectPage, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer addPortfolio(PortfolioDO portfolioDO) {
|
||||
return portfolioMapper.insert(portfolioDO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer updatePortfolio(PortfolioDO portfolioDO) {
|
||||
return portfolioMapper.updateById(portfolioDO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer deletePortfolio(Integer id) {
|
||||
return portfolioMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer addLoves(Integer id, Integer changes) {
|
||||
PortfolioDO portfolioDO = portfolioMapper.selectById(id);
|
||||
if (portfolioDO == null) {
|
||||
return 0;
|
||||
}
|
||||
portfolioDO.setLoves(portfolioDO.getLoves() + changes);
|
||||
return portfolioMapper.updateById(portfolioDO);
|
||||
}
|
||||
}
|
313
na-core/src/main/java/com/nailart/utils/AliyunOSSUtils.java
Normal file
313
na-core/src/main/java/com/nailart/utils/AliyunOSSUtils.java
Normal file
@ -0,0 +1,313 @@
|
||||
package com.nailart.utils;
|
||||
|
||||
/**
|
||||
* @Classname AliyunOSSUtils
|
||||
* @Description 阿里云对象存储工具类
|
||||
* @Date 2025/7/10 19:11
|
||||
* @Created by 21616
|
||||
*/
|
||||
|
||||
import com.aliyun.oss.OSS;
|
||||
import com.aliyun.oss.OSSClientBuilder;
|
||||
import com.aliyun.oss.model.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 阿里云OSS操作工具类
|
||||
*/
|
||||
@Component
|
||||
public class AliyunOSSUtils {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AliyunOSSUtils.class);
|
||||
|
||||
@Value("${aliyun.oss.endpoint}")
|
||||
private String endpoint;
|
||||
|
||||
@Value("${aliyun.oss.access-key-id}")
|
||||
private String accessKeyId;
|
||||
|
||||
@Value("${aliyun.oss.access-key-secret}")
|
||||
private String accessKeySecret;
|
||||
|
||||
@Value("${aliyun.oss.bucket-name}")
|
||||
private String bucketName;
|
||||
|
||||
@Value("${aliyun.oss.file-host}")
|
||||
private String fileHost;
|
||||
|
||||
/**
|
||||
* 上传文件到OSS
|
||||
*
|
||||
* @param file 上传的文件
|
||||
* @param directory 存储目录
|
||||
* @return 文件访问URL
|
||||
*/
|
||||
public String uploadFile(MultipartFile file, String directory) {
|
||||
OSS ossClient = null;
|
||||
try {
|
||||
// 生成文件名
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
|
||||
String fileName = UUID.randomUUID().toString().replace("-", "") + fileExtension;
|
||||
|
||||
// 构建完整路径
|
||||
if (directory != null && !directory.isEmpty()) {
|
||||
if (!directory.endsWith("/")) {
|
||||
directory = directory + "/";
|
||||
}
|
||||
fileName = directory + fileName;
|
||||
}
|
||||
|
||||
// 创建OSSClient实例
|
||||
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
|
||||
|
||||
// 上传文件
|
||||
ossClient.putObject(bucketName, fileName, new ByteArrayInputStream(file.getBytes()));
|
||||
|
||||
// 返回访问URL
|
||||
return fileHost + "/" + fileName;
|
||||
} catch (Exception e) {
|
||||
logger.error("上传文件失败: {}", e.getMessage(), e);
|
||||
return null;
|
||||
} finally {
|
||||
// 关闭OSSClient
|
||||
if (ossClient != null) {
|
||||
ossClient.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传本地文件到OSS
|
||||
*
|
||||
* @param filePath 文件路径
|
||||
* @param targetFileName 目标文件名
|
||||
* @return 文件访问URL
|
||||
*/
|
||||
public String uploadLocalFile(String filePath, String targetFileName) {
|
||||
OSS ossClient = null;
|
||||
try {
|
||||
// 创建OSSClient实例
|
||||
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
|
||||
|
||||
// 上传文件
|
||||
ossClient.putObject(bucketName, targetFileName, new File(filePath));
|
||||
|
||||
// 返回访问URL
|
||||
return fileHost + "/" + targetFileName;
|
||||
} catch (Exception e) {
|
||||
logger.error("上传本地文件失败: {}", e.getMessage(), e);
|
||||
return null;
|
||||
} finally {
|
||||
// 关闭OSSClient
|
||||
if (ossClient != null) {
|
||||
ossClient.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
*
|
||||
* @param objectName 对象名称
|
||||
* @param localFilePath 本地文件路径
|
||||
* @return 是否下载成功
|
||||
*/
|
||||
public boolean downloadFile(String objectName, String localFilePath) {
|
||||
OSS ossClient = null;
|
||||
try {
|
||||
// 创建OSSClient实例
|
||||
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
|
||||
|
||||
// 下载OSS文件到本地文件
|
||||
ossClient.getObject(new GetObjectRequest(bucketName, objectName), new File(localFilePath));
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
logger.error("下载文件失败: {}", e.getMessage(), e);
|
||||
return false;
|
||||
} finally {
|
||||
// 关闭OSSClient
|
||||
if (ossClient != null) {
|
||||
ossClient.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*
|
||||
* @param objectName 对象名称
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
public boolean deleteFile(String objectName) {
|
||||
OSS ossClient = null;
|
||||
try {
|
||||
// 创建OSSClient实例
|
||||
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
|
||||
|
||||
// 删除文件
|
||||
ossClient.deleteObject(bucketName, objectName);
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
logger.error("删除文件失败: {}", e.getMessage(), e);
|
||||
return false;
|
||||
} finally {
|
||||
// 关闭OSSClient
|
||||
if (ossClient != null) {
|
||||
ossClient.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除文件
|
||||
*
|
||||
* @param objectNames 对象名称列表
|
||||
* @return 删除结果
|
||||
*/
|
||||
public DeleteObjectsResult deleteFiles(List<String> objectNames) {
|
||||
OSS ossClient = null;
|
||||
try {
|
||||
// 创建OSSClient实例
|
||||
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
|
||||
|
||||
// 批量删除文件
|
||||
DeleteObjectsRequest deleteObjectsRequest = new DeleteObjectsRequest(bucketName)
|
||||
.withKeys(objectNames);
|
||||
return ossClient.deleteObjects(deleteObjectsRequest);
|
||||
} catch (Exception e) {
|
||||
logger.error("批量删除文件失败: {}", e.getMessage(), e);
|
||||
return null;
|
||||
} finally {
|
||||
// 关闭OSSClient
|
||||
if (ossClient != null) {
|
||||
ossClient.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件URL(带签名,用于临时访问)
|
||||
*
|
||||
* @param objectName 对象名称
|
||||
* @param expires 过期时间(秒)
|
||||
* @return 带签名的URL
|
||||
*/
|
||||
public String getSignedUrl(String objectName, long expires) {
|
||||
OSS ossClient = null;
|
||||
try {
|
||||
// 创建OSSClient实例
|
||||
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
|
||||
|
||||
// 设置URL过期时间
|
||||
Date expiration = new Date(System.currentTimeMillis() + expires * 1000);
|
||||
|
||||
// 生成签名URL
|
||||
URL url = ossClient.generatePresignedUrl(bucketName, objectName, expiration);
|
||||
|
||||
return url.toString();
|
||||
} catch (Exception e) {
|
||||
logger.error("获取签名URL失败: {}", e.getMessage(), e);
|
||||
return null;
|
||||
} finally {
|
||||
// 关闭OSSClient
|
||||
if (ossClient != null) {
|
||||
ossClient.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断文件是否存在
|
||||
*
|
||||
* @param objectName 对象名称
|
||||
* @return 是否存在
|
||||
*/
|
||||
public boolean doesObjectExist(String objectName) {
|
||||
OSS ossClient = null;
|
||||
try {
|
||||
// 创建OSSClient实例
|
||||
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
|
||||
|
||||
// 判断文件是否存在
|
||||
return ossClient.doesObjectExist(bucketName, objectName);
|
||||
} catch (Exception e) {
|
||||
logger.error("检查文件是否存在失败: {}", e.getMessage(), e);
|
||||
return false;
|
||||
} finally {
|
||||
// 关闭OSSClient
|
||||
if (ossClient != null) {
|
||||
ossClient.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件元信息
|
||||
*
|
||||
* @param objectName 对象名称
|
||||
* @return 文件元信息
|
||||
*/
|
||||
public ObjectMetadata getObjectMetadata(String objectName) {
|
||||
OSS ossClient = null;
|
||||
try {
|
||||
// 创建OSSClient实例
|
||||
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
|
||||
|
||||
// 获取文件元信息
|
||||
return ossClient.getObjectMetadata(bucketName, objectName);
|
||||
} catch (Exception e) {
|
||||
logger.error("获取文件元信息失败: {}", e.getMessage(), e);
|
||||
return null;
|
||||
} finally {
|
||||
// 关闭OSSClient
|
||||
if (ossClient != null) {
|
||||
ossClient.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出指定目录下的文件
|
||||
*
|
||||
* @param prefix 前缀(目录)
|
||||
* @param maxKeys 最大返回数量
|
||||
* @return 文件列表
|
||||
*/
|
||||
public ListObjectsV2Result listObjects(String prefix, int maxKeys) {
|
||||
OSS ossClient = null;
|
||||
try {
|
||||
// 创建OSSClient实例
|
||||
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
|
||||
|
||||
// 构造ListObjects请求
|
||||
ListObjectsV2Request listObjectsV2Request = new ListObjectsV2Request();
|
||||
listObjectsV2Request.setBucketName(bucketName);
|
||||
listObjectsV2Request.setPrefix(prefix);
|
||||
listObjectsV2Request.setMaxKeys(maxKeys);
|
||||
|
||||
// 列出文件
|
||||
return ossClient.listObjectsV2(listObjectsV2Request);
|
||||
} catch (Exception e) {
|
||||
logger.error("列出文件失败: {}", e.getMessage(), e);
|
||||
return null;
|
||||
} finally {
|
||||
// 关闭OSSClient
|
||||
if (ossClient != null) {
|
||||
ossClient.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
199
na-core/src/main/java/com/nailart/utils/DateUtils.java
Normal file
199
na-core/src/main/java/com/nailart/utils/DateUtils.java
Normal file
@ -0,0 +1,199 @@
|
||||
package com.nailart.utils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 日期处理工具类,基于Java 8+的java.time包
|
||||
*/
|
||||
public class DateUtils {
|
||||
// 默认日期格式
|
||||
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
|
||||
// 默认日期时间格式
|
||||
public static final String DEFAULT_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
|
||||
// 默认时间格式
|
||||
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
|
||||
|
||||
// 日期格式化器
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT);
|
||||
// 日期时间格式化器
|
||||
private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_DATETIME_FORMAT);
|
||||
// 时间格式化器
|
||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT);
|
||||
|
||||
/**
|
||||
* 获取当前日期(yyyy-MM-dd)
|
||||
* @return 当前日期字符串
|
||||
*/
|
||||
public static String getCurrentDate() {
|
||||
return LocalDate.now().format(DATE_FORMATTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前日期时间(yyyy-MM-dd HH:mm:ss)
|
||||
* @return 当前日期时间字符串
|
||||
*/
|
||||
public static String getCurrentDateTime() {
|
||||
return LocalDateTime.now().format(DATETIME_FORMATTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时间(HH:mm:ss)
|
||||
* @return 当前时间字符串
|
||||
*/
|
||||
public static String getCurrentTime() {
|
||||
return LocalTime.now().format(TIME_FORMATTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期转字符串
|
||||
* @param date 日期
|
||||
* @param pattern 格式模式
|
||||
* @return 格式化后的日期字符串
|
||||
*/
|
||||
public static String formatDate(LocalDate date, String pattern) {
|
||||
return date.format(DateTimeFormatter.ofPattern(pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期时间转字符串
|
||||
* @param dateTime 日期时间
|
||||
* @param pattern 格式模式
|
||||
* @return 格式化后的日期时间字符串
|
||||
*/
|
||||
public static String formatDateTime(LocalDateTime dateTime, String pattern) {
|
||||
return dateTime.format(DateTimeFormatter.ofPattern(pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串转日期
|
||||
* @param dateStr 日期字符串
|
||||
* @param pattern 格式模式
|
||||
* @return 解析后的LocalDate
|
||||
*/
|
||||
public static LocalDate parseDate(String dateStr, String pattern) {
|
||||
return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern(pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串转日期时间
|
||||
* @param dateTimeStr 日期时间字符串
|
||||
* @param pattern 格式模式
|
||||
* @return 解析后的LocalDateTime
|
||||
*/
|
||||
public static LocalDateTime parseDateTime(String dateTimeStr, String pattern) {
|
||||
return LocalDateTime.parse(dateTimeStr, DateTimeFormatter.ofPattern(pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个日期之间的天数差
|
||||
* @param startDate 开始日期
|
||||
* @param endDate 结束日期
|
||||
* @return 天数差(正数表示结束日期晚于开始日期)
|
||||
*/
|
||||
public static long daysBetween(LocalDate startDate, LocalDate endDate) {
|
||||
return ChronoUnit.DAYS.between(startDate, endDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个日期时间之间的小时差
|
||||
* @param startDateTime 开始日期时间
|
||||
* @param endDateTime 结束日期时间
|
||||
* @return 小时差
|
||||
*/
|
||||
public static long hoursBetween(LocalDateTime startDateTime, LocalDateTime endDateTime) {
|
||||
return ChronoUnit.HOURS.between(startDateTime, endDateTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个日期时间之间的分钟差
|
||||
* @param startDateTime 开始日期时间
|
||||
* @param endDateTime 结束日期时间
|
||||
* @return 分钟差
|
||||
*/
|
||||
public static long minutesBetween(LocalDateTime startDateTime, LocalDateTime endDateTime) {
|
||||
return ChronoUnit.MINUTES.between(startDateTime, endDateTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定日期上增加/减少天数
|
||||
* @param date 基准日期
|
||||
* @param days 增加(正数)/减少(负数)的天数
|
||||
* @return 计算后的日期
|
||||
*/
|
||||
public static LocalDate plusDays(LocalDate date, int days) {
|
||||
return date.plusDays(days);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定日期时间上增加/减少小时
|
||||
* @param dateTime 基准日期时间
|
||||
* @param hours 增加(正数)/减少(负数)的小时
|
||||
* @return 计算后的日期时间
|
||||
*/
|
||||
public static LocalDateTime plusHours(LocalDateTime dateTime, int hours) {
|
||||
return dateTime.plusHours(hours);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定日期时间上增加/减少分钟
|
||||
* @param dateTime 基准日期时间
|
||||
* @param minutes 增加(正数)/减少(负数)的分钟
|
||||
* @return 计算后的日期时间
|
||||
*/
|
||||
public static LocalDateTime plusMinutes(LocalDateTime dateTime, int minutes) {
|
||||
return dateTime.plusMinutes(minutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Date转LocalDateTime
|
||||
* @param date Date对象
|
||||
* @return LocalDateTime对象
|
||||
*/
|
||||
public static LocalDateTime toLocalDateTime(Date date) {
|
||||
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* LocalDateTime转Date
|
||||
* @param dateTime LocalDateTime对象
|
||||
* @return Date对象
|
||||
*/
|
||||
public static Date toDate(LocalDateTime dateTime) {
|
||||
return Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一天的开始时间(00:00:00)
|
||||
* @param date 日期
|
||||
* @return 当天开始时间的LocalDateTime
|
||||
*/
|
||||
public static LocalDateTime getStartOfDay(LocalDate date) {
|
||||
return date.atStartOfDay();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一天的结束时间(23:59:59)
|
||||
* @param date 日期
|
||||
* @return 当天结束时间的LocalDateTime
|
||||
*/
|
||||
public static LocalDateTime getEndOfDay(LocalDate date) {
|
||||
return LocalDateTime.of(date, LocalTime.MAX);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断日期是否在范围内
|
||||
* @param targetDate 目标日期
|
||||
* @param startDate 开始日期
|
||||
* @param endDate 结束日期
|
||||
* @return true-在范围内;false-不在范围内
|
||||
*/
|
||||
public static boolean isDateInRange(LocalDate targetDate, LocalDate startDate, LocalDate endDate) {
|
||||
return !targetDate.isBefore(startDate) && !targetDate.isAfter(endDate);
|
||||
}
|
||||
}
|
59
na-core/src/main/java/com/nailart/utils/IpAddressUtil.java
Normal file
59
na-core/src/main/java/com/nailart/utils/IpAddressUtil.java
Normal file
@ -0,0 +1,59 @@
|
||||
package com.nailart.utils;
|
||||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import org.lionsoul.ip2region.xdb.Searcher;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Description IP 解析工具类,使用 ip2region 实现 IP 地址解析城市功能
|
||||
* @Classname IpAddressUtil
|
||||
* @Date 2025/7/11 0:04
|
||||
* @Created by 21616
|
||||
*/
|
||||
|
||||
@Component
|
||||
public class IpAddressUtil {
|
||||
private static Searcher searcher;
|
||||
|
||||
// 初始化IP数据库
|
||||
static {
|
||||
try {
|
||||
// 从资源文件中读取ip2region.xdb
|
||||
InputStream inputStream = ResourceUtil.getStream("ip2region.xdb");
|
||||
byte[] dbContent = new byte[inputStream.available()];
|
||||
inputStream.read(dbContent);
|
||||
searcher = Searcher.newWithBuffer(dbContent);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("初始化IP地址解析器失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据IP获取城市信息
|
||||
*
|
||||
* @param ip IP地址
|
||||
* @return 城市信息(格式:国家|区域|省份|城市|ISP)
|
||||
*/
|
||||
public String getCityInfo(String ip) {
|
||||
if (ip == null || ip.isEmpty()) {
|
||||
return "未知IP";
|
||||
}
|
||||
|
||||
// 本地IP处理
|
||||
if ("127.0.0.1".equals(ip) || "localhost".equals(ip)) {
|
||||
return "本地主机";
|
||||
}
|
||||
|
||||
try {
|
||||
String result = searcher.search(ip);
|
||||
return result != null ? result : "未知地址";
|
||||
} catch (IOException e) {
|
||||
return "解析失败";
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
665
na-core/src/main/java/com/nailart/utils/RedisUtils.java
Normal file
665
na-core/src/main/java/com/nailart/utils/RedisUtils.java
Normal file
@ -0,0 +1,665 @@
|
||||
package com.nailart.utils;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.connection.RedisStringCommands;
|
||||
import org.springframework.data.redis.core.*;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Redis工具类,封装常用的Redis操作
|
||||
*/
|
||||
@Component
|
||||
public class RedisUtils {
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Autowired
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
// =============================common============================
|
||||
|
||||
/**
|
||||
* 指定缓存失效时间
|
||||
*
|
||||
* @param key 键
|
||||
* @param time 时间(秒)
|
||||
* @return true成功 false失败
|
||||
*/
|
||||
public boolean expire(String key, long time) {
|
||||
try {
|
||||
if (time > 0) {
|
||||
redisTemplate.expire(key, time, TimeUnit.SECONDS);
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据key获取过期时间
|
||||
*
|
||||
* @param key 键 不能为null
|
||||
* @return 时间(秒) 返回0代表为永久有效
|
||||
*/
|
||||
public long getExpire(String key) {
|
||||
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断key是否存在
|
||||
*
|
||||
* @param key 键
|
||||
* @return true存在 false不存在
|
||||
*/
|
||||
public boolean hasKey(String key) {
|
||||
try {
|
||||
return redisTemplate.hasKey(key);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存
|
||||
*
|
||||
* @param key 可以传一个值 或多个
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public void del(String... key) {
|
||||
if (key != null && key.length > 0) {
|
||||
if (key.length == 1) {
|
||||
redisTemplate.delete(key[0]);
|
||||
} else {
|
||||
redisTemplate.delete(Arrays.asList(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================String=============================
|
||||
|
||||
/**
|
||||
* 普通缓存获取
|
||||
*
|
||||
* @param key 键
|
||||
* @return 值
|
||||
*/
|
||||
public Object get(String key) {
|
||||
return key == null ? null : redisTemplate.opsForValue().get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通缓存放入
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @return true成功 false失败
|
||||
*/
|
||||
public boolean set(String key, Object value) {
|
||||
try {
|
||||
redisTemplate.opsForValue().set(key, value);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 普通缓存放入并设置时间
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
|
||||
* @return true成功 false失败
|
||||
*/
|
||||
public boolean set(String key, Object value, long time) {
|
||||
try {
|
||||
if (time > 0) {
|
||||
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
|
||||
} else {
|
||||
set(key, value);
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递增
|
||||
*
|
||||
* @param key 键
|
||||
* @param delta 要增加几(大于0)
|
||||
* @return 增加后的值
|
||||
*/
|
||||
public long incr(String key, long delta) {
|
||||
if (delta < 0) {
|
||||
throw new RuntimeException("递增因子必须大于0");
|
||||
}
|
||||
return redisTemplate.opsForValue().increment(key, delta);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递减
|
||||
*
|
||||
* @param key 键
|
||||
* @param delta 要减少几(大于0)
|
||||
* @return 减少后的值
|
||||
*/
|
||||
public long decr(String key, long delta) {
|
||||
if (delta < 0) {
|
||||
throw new RuntimeException("递减因子必须大于0");
|
||||
}
|
||||
return redisTemplate.opsForValue().increment(key, -delta);
|
||||
}
|
||||
|
||||
// ================================Map=================================
|
||||
|
||||
/**
|
||||
* HashGet
|
||||
*
|
||||
* @param key 键 不能为null
|
||||
* @param item 项 不能为null
|
||||
* @return 值
|
||||
*/
|
||||
public Object hget(String key, String item) {
|
||||
return redisTemplate.opsForHash().get(key, item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取hashKey对应的所有键值
|
||||
*
|
||||
* @param key 键
|
||||
* @return 对应的多个键值
|
||||
*/
|
||||
public Map<Object, Object> hmget(String key) {
|
||||
return redisTemplate.opsForHash().entries(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* HashSet
|
||||
*
|
||||
* @param key 键
|
||||
* @param map 对应多个键值
|
||||
* @return true 成功 false 失败
|
||||
*/
|
||||
public boolean hmset(String key, Map<String, Object> map) {
|
||||
try {
|
||||
redisTemplate.opsForHash().putAll(key, map);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HashSet 并设置时间
|
||||
*
|
||||
* @param key 键
|
||||
* @param map 对应多个键值
|
||||
* @param time 时间(秒)
|
||||
* @return true成功 false失败
|
||||
*/
|
||||
public boolean hmset(String key, Map<String, Object> map, long time) {
|
||||
try {
|
||||
redisTemplate.opsForHash().putAll(key, map);
|
||||
if (time > 0) {
|
||||
expire(key, time);
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向一张hash表中放入数据,如果不存在将创建
|
||||
*
|
||||
* @param key 键
|
||||
* @param item 项
|
||||
* @param value 值
|
||||
* @return true 成功 false 失败
|
||||
*/
|
||||
public boolean hset(String key, String item, Object value) {
|
||||
try {
|
||||
redisTemplate.opsForHash().put(key, item, value);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向一张hash表中放入数据,如果不存在将创建
|
||||
*
|
||||
* @param key 键
|
||||
* @param item 项
|
||||
* @param value 值
|
||||
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
|
||||
* @return true 成功 false 失败
|
||||
*/
|
||||
public boolean hset(String key, String item, Object value, long time) {
|
||||
try {
|
||||
redisTemplate.opsForHash().put(key, item, value);
|
||||
if (time > 0) {
|
||||
expire(key, time);
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除hash表中的值
|
||||
*
|
||||
* @param key 键 不能为null
|
||||
* @param item 项 可以使多个 不能为null
|
||||
*/
|
||||
public void hdel(String key, Object... item) {
|
||||
redisTemplate.opsForHash().delete(key, item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断hash表中是否有该项的值
|
||||
*
|
||||
* @param key 键 不能为null
|
||||
* @param item 项 不能为null
|
||||
* @return true 存在 false不存在
|
||||
*/
|
||||
public boolean hHasKey(String key, String item) {
|
||||
return redisTemplate.opsForHash().hasKey(key, item);
|
||||
}
|
||||
|
||||
/**
|
||||
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
|
||||
*
|
||||
* @param key 键
|
||||
* @param item 项
|
||||
* @param by 要增加几(大于0)
|
||||
* @return 增加后的值
|
||||
*/
|
||||
public double hincr(String key, String item, double by) {
|
||||
return redisTemplate.opsForHash().increment(key, item, by);
|
||||
}
|
||||
|
||||
/**
|
||||
* hash递减
|
||||
*
|
||||
* @param key 键
|
||||
* @param item 项
|
||||
* @param by 要减少记(大于0)
|
||||
* @return 减少后的值
|
||||
*/
|
||||
public double hdecr(String key, String item, double by) {
|
||||
return redisTemplate.opsForHash().increment(key, item, -by);
|
||||
}
|
||||
|
||||
// ============================set=============================
|
||||
|
||||
/**
|
||||
* 根据key获取Set中的所有值
|
||||
*
|
||||
* @param key 键
|
||||
* @return Set集合
|
||||
*/
|
||||
public Set<Object> sGet(String key) {
|
||||
try {
|
||||
return redisTemplate.opsForSet().members(key);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据value从一个set中查询,是否存在
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @return true 存在 false不存在
|
||||
*/
|
||||
public boolean sHasKey(String key, Object value) {
|
||||
try {
|
||||
return redisTemplate.opsForSet().isMember(key, value);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数据放入set缓存
|
||||
*
|
||||
* @param key 键
|
||||
* @param values 值 可以是多个
|
||||
* @return 成功个数
|
||||
*/
|
||||
public long sSet(String key, Object... values) {
|
||||
try {
|
||||
return redisTemplate.opsForSet().add(key, values);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将set数据放入缓存
|
||||
*
|
||||
* @param key 键
|
||||
* @param time 时间(秒)
|
||||
* @param values 值 可以是多个
|
||||
* @return 成功个数
|
||||
*/
|
||||
public long sSetAndTime(String key, long time, Object... values) {
|
||||
try {
|
||||
Long count = redisTemplate.opsForSet().add(key, values);
|
||||
if (time > 0) expire(key, time);
|
||||
return count;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取set缓存的长度
|
||||
*
|
||||
* @param key 键
|
||||
* @return 长度数值
|
||||
*/
|
||||
public long sGetSetSize(String key) {
|
||||
try {
|
||||
return redisTemplate.opsForSet().size(key);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除值为value的
|
||||
*
|
||||
* @param key 键
|
||||
* @param values 值 可以是多个
|
||||
* @return 移除的个数
|
||||
*/
|
||||
public long setRemove(String key, Object... values) {
|
||||
try {
|
||||
Long count = redisTemplate.opsForSet().remove(key, values);
|
||||
return count;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================list=================================
|
||||
|
||||
/**
|
||||
* 获取list缓存的内容
|
||||
*
|
||||
* @param key 键
|
||||
* @param start 开始
|
||||
* @param end 结束 0 到 -1代表所有值
|
||||
* @return List集合
|
||||
*/
|
||||
public List<Object> lGet(String key, long start, long end) {
|
||||
try {
|
||||
return redisTemplate.opsForList().range(key, start, end);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取list缓存的长度
|
||||
*
|
||||
* @param key 键
|
||||
* @return 长度数值
|
||||
*/
|
||||
public long lGetListSize(String key) {
|
||||
try {
|
||||
return redisTemplate.opsForList().size(key);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过索引 获取list中的值
|
||||
*
|
||||
* @param key 键
|
||||
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
|
||||
* @return 值对象
|
||||
*/
|
||||
public Object lGetIndex(String key, long index) {
|
||||
try {
|
||||
return redisTemplate.opsForList().index(key, index);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将list放入缓存
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @return 操作结果
|
||||
*/
|
||||
public boolean lSet(String key, Object value) {
|
||||
try {
|
||||
redisTemplate.opsForList().rightPush(key, value);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将list放入缓存
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @param time 时间(秒)
|
||||
* @return 操作结果
|
||||
*/
|
||||
public boolean lSet(String key, Object value, long time) {
|
||||
try {
|
||||
redisTemplate.opsForList().rightPush(key, value);
|
||||
if (time > 0) expire(key, time);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将list放入缓存
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值列表
|
||||
* @return 操作结果
|
||||
*/
|
||||
public boolean lSet(String key, List<Object> value) {
|
||||
try {
|
||||
redisTemplate.opsForList().rightPushAll(key, value);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将list放入缓存
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值列表
|
||||
* @param time 时间(秒)
|
||||
* @return 操作结果
|
||||
*/
|
||||
public boolean lSet(String key, List<Object> value, long time) {
|
||||
try {
|
||||
redisTemplate.opsForList().rightPushAll(key, value);
|
||||
if (time > 0) expire(key, time);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据索引修改list中的某条数据
|
||||
*
|
||||
* @param key 键
|
||||
* @param index 索引
|
||||
* @param value 值
|
||||
* @return 操作结果
|
||||
*/
|
||||
public boolean lUpdateIndex(String key, long index, Object value) {
|
||||
try {
|
||||
redisTemplate.opsForList().set(key, index, value);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除N个值为value
|
||||
*
|
||||
* @param key 键
|
||||
* @param count 移除多少个
|
||||
* @param value 值
|
||||
* @return 移除的个数
|
||||
*/
|
||||
public long lRemove(String key, long count, Object value) {
|
||||
try {
|
||||
Long remove = redisTemplate.opsForList().remove(key, count, value);
|
||||
return remove;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================HyperLogLog=============================
|
||||
|
||||
/**
|
||||
* 添加元素到HyperLogLog
|
||||
*
|
||||
* @param key 键
|
||||
* @param values 值
|
||||
* @return 添加结果
|
||||
*/
|
||||
public long pfAdd(String key, String... values) {
|
||||
return redisTemplate.opsForHyperLogLog().add(key, (Object[]) values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取HyperLogLog的基数估算值
|
||||
*
|
||||
* @param key 键
|
||||
* @return 基数估算值
|
||||
*/
|
||||
public long pfCount(String key) {
|
||||
return redisTemplate.opsForHyperLogLog().size(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并多个HyperLogLog
|
||||
*
|
||||
* @param destKey 目标键
|
||||
* @param sourceKeys 源键
|
||||
* @return 合并结果
|
||||
*/
|
||||
public long pfMerge(String destKey, String... sourceKeys) {
|
||||
return redisTemplate.opsForHyperLogLog().union(destKey, sourceKeys);
|
||||
}
|
||||
|
||||
// ============================BitMap=============================
|
||||
|
||||
/**
|
||||
* 设置BitMap的位值
|
||||
*
|
||||
* @param key 键
|
||||
* @param offset 偏移量
|
||||
* @param value 值
|
||||
* @return 设置结果
|
||||
*/
|
||||
public boolean setBit(String key, long offset, boolean value) {
|
||||
return redisTemplate.opsForValue().setBit(key, offset, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取BitMap的位值
|
||||
*
|
||||
* @param key 键
|
||||
* @param offset 偏移量
|
||||
* @return 位值
|
||||
*/
|
||||
public boolean getBit(String key, long offset) {
|
||||
return redisTemplate.opsForValue().getBit(key, offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计BitMap中为1的位的数量
|
||||
*
|
||||
* @param key 键
|
||||
* @return 为1的位的数量
|
||||
*/
|
||||
public long bitCount(String key) {
|
||||
return stringRedisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(key.getBytes()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计BitMap中指定范围内为1的位的数量
|
||||
*
|
||||
* @param key 键
|
||||
* @param start 起始字节
|
||||
* @param end 结束字节
|
||||
* @return 为1的位的数量
|
||||
*/
|
||||
public long bitCount(String key, long start, long end) {
|
||||
return stringRedisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(key.getBytes(), start, end));
|
||||
}
|
||||
|
||||
/**
|
||||
* 对多个BitMap执行位运算
|
||||
*
|
||||
* @param op 运算类型
|
||||
* @param destKey 目标键
|
||||
* @param keys 源键
|
||||
* @return 运算结果的长度
|
||||
*/
|
||||
public long bitOp(RedisStringCommands.BitOperation op, String destKey, String... keys) {
|
||||
byte[][] bytes = new byte[keys.length][];
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
bytes[i] = keys[i].getBytes();
|
||||
}
|
||||
return stringRedisTemplate.execute((RedisCallback<Long>) con ->
|
||||
con.bitOp(op, destKey.getBytes(), bytes));
|
||||
}
|
||||
}
|
16
na-core/src/main/java/com/nailart/vo/RespVO.java
Normal file
16
na-core/src/main/java/com/nailart/vo/RespVO.java
Normal file
@ -0,0 +1,16 @@
|
||||
package com.nailart.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @Description 所有的请求返回封装体
|
||||
* @Classname RespVO
|
||||
* @Date 2025/7/10 22:32
|
||||
* @Created by 21616
|
||||
*/
|
||||
@Data
|
||||
public class RespVO {
|
||||
private Integer code;
|
||||
private String msg;
|
||||
private Object data;
|
||||
}
|
@ -1 +0,0 @@
|
||||
spring.application.name=na-core
|
76
na-core/src/main/resources/application.yml
Normal file
76
na-core/src/main/resources/application.yml
Normal file
@ -0,0 +1,76 @@
|
||||
server:
|
||||
port: 8090
|
||||
servlet:
|
||||
context-path: /api # 应用访问路径前缀
|
||||
|
||||
# 数据源配置
|
||||
spring:
|
||||
datasource:
|
||||
type: com.alibaba.druid.pool.DruidDataSource # 使用Druid连接池
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver # MySQL驱动类
|
||||
url: jdbc:mysql://wcy111.top:3306/nailart?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC # 数据库连接地址
|
||||
username: root # 数据库用户名
|
||||
password: 122503wcy # 数据库密码
|
||||
druid: # Druid连接池配置
|
||||
initial-size: 5 # 初始化连接数
|
||||
min-idle: 5 # 最小空闲连接数
|
||||
max-active: 20 # 最大活跃连接数
|
||||
max-wait: 60000 # 获取连接时的最大等待时间(毫秒)
|
||||
time-between-eviction-runs-millis: 60000 # 间隔多久进行一次检测,检测需要关闭的空闲连接(毫秒)
|
||||
min-evictable-idle-time-millis: 300000 # 一个连接在池中最小生存的时间(毫秒)
|
||||
validation-query: SELECT 1 FROM DUAL # 验证连接是否有效的SQL语句
|
||||
test-while-idle: true # 空闲时是否检测连接有效性
|
||||
test-on-borrow: false # 借出连接时是否检测有效性
|
||||
test-on-return: false # 归还连接时是否检测有效性
|
||||
pool-prepared-statements: true # 是否缓存PreparedStatement
|
||||
max-pool-prepared-statement-per-connection-size: 20 # 每个连接缓存的PreparedStatement数量
|
||||
filters: stat,wall,log4j # 配置监控统计拦截的filters
|
||||
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 # 连接属性配置(合并SQL、慢SQL阈值)
|
||||
# Redis配置
|
||||
redis:
|
||||
host: wcy111.top # Redis服务器地址
|
||||
port: 6379 # Redis服务器端口
|
||||
password: 122503wcy # Redis密码
|
||||
timeout: 10000ms # 连接超时时间
|
||||
database: 0 # 数据库索引
|
||||
lettuce: # Lettuce连接池配置
|
||||
pool:
|
||||
max-active: 8 # 最大连接数
|
||||
max-wait: -1ms # 最大阻塞等待时间(负数表示无限制)
|
||||
max-idle: 8 # 最大空闲连接
|
||||
min-idle: 0 # 最小空闲连接
|
||||
|
||||
# MyBatis-Plus配置
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath:mapper/*.xml # Mapper XML文件存放路径
|
||||
type-aliases-package: com.nailart.do # 实体类别名包路径
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: auto # 主键生成策略(AUTO=数据库ID自增)
|
||||
field-strategy: not_null # 字段验证策略(NOT_NULL=非NULL判断)
|
||||
table-underline: false # 表名是否使用下划线命名
|
||||
logic-delete-field: deleted # 全局逻辑删除字段名
|
||||
logic-not-delete-value: 0 # 逻辑未删除值
|
||||
logic-delete-value: 1 # 逻辑已删除值
|
||||
configuration:
|
||||
map-underscore-to-camel-case: false # 是否开启下划线转驼峰命名
|
||||
cache-enabled: false # 是否开启二级缓存
|
||||
call-setters-on-nulls: true # 查询结果是否返回null字段
|
||||
jdbc-type-for-null: 'null' # 空值的JDBC类型映射
|
||||
|
||||
# 阿里云OSS配置
|
||||
aliyun:
|
||||
oss:
|
||||
endpoint: oss-cn-chengdu.aliyuncs.com # OSS服务端点
|
||||
access-key-id: LTAI5tLbzuS6P8rGQ4Rw9rN9 # 访问密钥ID
|
||||
access-key-secret: R3zbklnTmEMks4HEiKRaxlXwBJvY8T # 访问密钥Secret
|
||||
bucket-name: yuminailart # 存储桶名称
|
||||
file-host: yuminailart.oss-cn-chengdu.aliyuncs.com # 文件主机地址
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
root: info # 根日志级别
|
||||
com.example.mapper: debug # Mapper接口日志级别(调试用)
|
||||
file:
|
||||
name: logs/application.log # 日志文件存储路径
|
BIN
na-core/src/main/resources/ip2region.xdb
Normal file
BIN
na-core/src/main/resources/ip2region.xdb
Normal file
Binary file not shown.
106
na-frontend/package-lock.json
generated
106
na-frontend/package-lock.json
generated
@ -8,6 +8,7 @@
|
||||
"name": "na-frontend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.10.0",
|
||||
"core-js": "^3.8.3",
|
||||
"element-ui": "^2.15.14",
|
||||
"less-loader": "^12.3.0",
|
||||
@ -3442,6 +3443,12 @@
|
||||
"babel-runtime": "6.x"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/at-least-node": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz",
|
||||
@ -3490,6 +3497,17 @@
|
||||
"postcss": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.10.0.tgz",
|
||||
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-helper-vue-jsx-merge-props": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz",
|
||||
@ -3903,7 +3921,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@ -4238,6 +4255,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz",
|
||||
@ -5075,6 +5104,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
|
||||
@ -5253,7 +5291,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
@ -5428,7 +5465,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -5438,7 +5474,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -5455,7 +5490,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
@ -5464,6 +5498,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
|
||||
@ -6385,7 +6434,6 @@
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@ -6402,6 +6450,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.3.tgz",
|
||||
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@ -6485,7 +6549,6 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@ -6522,7 +6585,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
@ -6547,7 +6609,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
@ -6666,7 +6727,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -6732,7 +6792,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -6741,6 +6800,21 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hash-sum": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-2.0.0.tgz",
|
||||
@ -6752,7 +6826,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
@ -8024,7 +8097,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -8138,7 +8210,6 @@
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
@ -8148,7 +8219,6 @@
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
@ -9804,6 +9874,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prr": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/prr/-/prr-1.0.1.tgz",
|
||||
|
@ -8,6 +8,7 @@
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.10.0",
|
||||
"core-js": "^3.8.3",
|
||||
"element-ui": "^2.15.14",
|
||||
"less-loader": "^12.3.0",
|
||||
|
@ -6,9 +6,9 @@
|
||||
<!-- 中间内容区:包裹路由视图,控制动画范围 -->
|
||||
<div class="content-container">
|
||||
<!-- 根据路由方向动态切换过渡类 -->
|
||||
<transition :name="transitionName" mode="out-in">
|
||||
<!-- <transition name="van-slide-left"> -->
|
||||
<router-view></router-view>
|
||||
</transition>
|
||||
<!-- </transition> -->
|
||||
</div>
|
||||
|
||||
<!-- 固定底部TabBar -->
|
||||
@ -48,22 +48,12 @@ export default {
|
||||
|
||||
/* 固定顶部导航栏 */
|
||||
.nav-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
height: 45px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 固定底部TabBar */
|
||||
.tab-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
height: 50px;
|
||||
background: #fff;
|
||||
}
|
||||
@ -71,9 +61,7 @@ export default {
|
||||
/* 中间内容区:避开上下Bar的位置 */
|
||||
.content-container {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
padding-top: 50px;
|
||||
padding-bottom: 60px;
|
||||
height: calc(100vh - 110px);
|
||||
overflow: hidden;
|
||||
/* 防止内容溢出 */
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="nav-bar">
|
||||
<van-nav-bar title="屿" left-text="" :placeholder="true" :safe-area-inset-top="true" z-index="99">
|
||||
<van-nav-bar title="屿" left-text="" :placeholder="true" :safe-area-inset-top="true" z-index="50" :fixed="true">
|
||||
<template #right>
|
||||
<van-icon name="search" size="18" />
|
||||
</template>
|
||||
|
@ -2,8 +2,7 @@ import Vue from "vue";
|
||||
import Vuex from "vuex";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
import axios from './utils/request'
|
||||
import { Tabbar, TabbarItem } from "vant";
|
||||
import { NavBar } from "vant";
|
||||
import { Toast } from "vant";
|
||||
@ -14,7 +13,16 @@ import { Image as VanImage } from "vant";
|
||||
import { Lazyload } from "vant";
|
||||
import { Card } from "vant";
|
||||
import { Tag } from "vant";
|
||||
import { Swipe, SwipeItem } from 'vant';
|
||||
import { Loading } from 'vant';
|
||||
import { NoticeBar } from 'vant';
|
||||
import { Sticky } from 'vant';
|
||||
|
||||
Vue.use(Sticky);
|
||||
Vue.use(NoticeBar);
|
||||
Vue.use(Loading);
|
||||
Vue.use(Swipe);
|
||||
Vue.use(SwipeItem);
|
||||
Vue.use(Tag);
|
||||
Vue.use(Card);
|
||||
Vue.use(Lazyload);
|
||||
@ -27,7 +35,10 @@ Vue.use(NavBar);
|
||||
Vue.use(Tabbar);
|
||||
Vue.use(TabbarItem);
|
||||
Vue.use(Vuex);
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
// 全局挂载 axios
|
||||
Vue.prototype.$axios = axios
|
||||
new Vue({
|
||||
router,
|
||||
render: (h) => h(App),
|
||||
|
@ -1,50 +1,60 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="page-container">
|
||||
<!-- 使用原生 sticky 定位的公告栏 -->
|
||||
<div class="sticky-notice">
|
||||
<van-notice-bar left-icon="volume-o" :scrollable="true" text="所有标价均为款式价格,如需延长需补差价喔~~~(半贴:¥90,浅贴:¥130)" />
|
||||
</div>
|
||||
|
||||
<!-- 内容容器 -->
|
||||
<div class="cards-container">
|
||||
<div class="card-grid">
|
||||
<!-- 卡片列表 -->
|
||||
<div
|
||||
v-for="(card, index) in cards"
|
||||
:key="index"
|
||||
class="card"
|
||||
:class="{ 'card-active': activeCardIndex === index }"
|
||||
@touchstart="handleTouchStart(index)"
|
||||
@touchend="handleTouchEnd(index)"
|
||||
@click="handleCardClick(card)"
|
||||
>
|
||||
<div v-for="(card, index) in cards" :key="index" class="card"
|
||||
:class="{ 'card-active': activeCardIndex === index }" @touchstart="handleTouchStart(index)"
|
||||
@touchend="handleTouchEnd(index)" @click="handleCardClick(card)">
|
||||
|
||||
<div class="card-image-container">
|
||||
<img
|
||||
:src="card.image"
|
||||
:alt="card.title"
|
||||
class="card-image"
|
||||
>
|
||||
<div
|
||||
v-if="card.tag"
|
||||
class="card-tag"
|
||||
:style="card.tagStyle"
|
||||
>
|
||||
{{ card.tag }}
|
||||
</div>
|
||||
<van-swipe :autoplay="3000">
|
||||
<van-swipe-item v-for="(image, imgIndex) in card.images" :key="imgIndex">
|
||||
<van-image lazy-load fit="contain" :src="image">
|
||||
<template v-slot:loading>
|
||||
<van-loading type="spinner" size="20" />
|
||||
</template>
|
||||
</van-image>
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<h2 class="card-title">{{ card.title }}</h2>
|
||||
<p class="card-description">{{ card.description }}</p>
|
||||
|
||||
<div v-if="card.tag" class="card-tag">
|
||||
<van-tag round type="primary">{{ card.tag }}</van-tag>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<span class="card-meta">
|
||||
<i class="fa fa-calendar-o mr-1"></i> {{ card.duration }}
|
||||
<span>款式价格:¥</span><i class="fa fa-calendar-o mr-1"></i> {{ card.amt }}
|
||||
</span>
|
||||
<button
|
||||
class="card-button"
|
||||
@click.stop="handleButtonClick(card)"
|
||||
>
|
||||
了解更多
|
||||
</button>
|
||||
<div class="heart" @click.stop>
|
||||
<label class="action-btn heart-label">
|
||||
<input type="checkbox" class="heart-checkbox" v-model="card.checked"
|
||||
@change="handleClickLove(card)">
|
||||
<span class="heart-btn">
|
||||
<span class="heart-icon"></span>
|
||||
</span>
|
||||
</label>
|
||||
<span class="like-count">{{ card.loves }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="loading" v-show="loading">
|
||||
<van-loading type="spinner" color="#1989fa" />
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -52,233 +62,382 @@ export default {
|
||||
name: 'PortfolioPage',
|
||||
data() {
|
||||
return {
|
||||
cards: [
|
||||
{
|
||||
title: "探索自然之美",
|
||||
description: "深入原始森林,感受大自然的鬼斧神工。这里有清澈的溪流、茂密的树木和各种珍稀野生动物。",
|
||||
image: "https://picsum.photos/seed/card1/600/800",
|
||||
duration: "3天2晚",
|
||||
tag: "热门",
|
||||
tagStyle: {
|
||||
backgroundColor: "#F97316",
|
||||
color: "#fff"
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "城市探险之旅",
|
||||
description: "穿梭于现代都市与历史古迹之间,体验城市独特的文化氛围和美食特色。",
|
||||
image: "https://picsum.photos/seed/card2/600/800",
|
||||
duration: "2天1晚",
|
||||
tag: "热门",
|
||||
tagStyle: {
|
||||
backgroundColor: "#F97316",
|
||||
color: "#fff"
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "美食文化体验",
|
||||
description: "跟随当地厨师学习传统美食的制作方法,品尝地道佳肴,感受美食背后的文化故事。",
|
||||
image: "https://picsum.photos/seed/card3/600/800",
|
||||
duration: "1天",
|
||||
tag: "新品",
|
||||
tagStyle: {
|
||||
backgroundColor: "#22c55e",
|
||||
color: "#fff"
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "海洋生态探索",
|
||||
description: "潜入蔚蓝深海,探索神秘的海底世界,与海龟、珊瑚和热带鱼共舞。",
|
||||
image: "https://picsum.photos/seed/card4/600/800",
|
||||
duration: "4天3晚",
|
||||
tag: "推荐",
|
||||
tagStyle: {
|
||||
backgroundColor: "#3b82f6",
|
||||
color: "#fff"
|
||||
}
|
||||
}
|
||||
],
|
||||
activeCardIndex: -1
|
||||
cards: [],
|
||||
activeCardIndex: -1,
|
||||
page: 1,
|
||||
pageSize: 2,
|
||||
loading: false,
|
||||
changes: [],
|
||||
totalPages: 0
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async getCarts(pageNum, size) {
|
||||
try {
|
||||
this.loading = true;
|
||||
let res = await this.$axios.get('/portfolio/list', {
|
||||
params: {
|
||||
page: pageNum,
|
||||
size: size
|
||||
}
|
||||
});
|
||||
|
||||
if (res.code !== 200 && res.data.code !== 200) {
|
||||
const errorMsg = res.msg || res.data.msg || '获取数据失败';
|
||||
this.$toast.fail(errorMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
const records = res.data?.data?.records || [];
|
||||
if (records.length === 0) {
|
||||
this.$toast('没有更多数据了');
|
||||
return;
|
||||
}
|
||||
|
||||
const formattedCards = records.map(item => {
|
||||
const images = item.imgFilename
|
||||
? item.imgFilename.split('&&').map(filename => `${item.imgUrl}/${filename}`)
|
||||
: [];
|
||||
|
||||
const tagStyle = item.tagStyle
|
||||
? { backgroundColor: item.tagStyle.split('&&')[0], color: item.tagStyle.split('&&')[1] }
|
||||
: { backgroundColor: "#eeeeee", color: "#fff" };
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
description: item.description,
|
||||
images,
|
||||
duration: '',
|
||||
tag: item.tag || '热门',
|
||||
tagStyle,
|
||||
loves: item.loves || 0,
|
||||
changes: 0,
|
||||
checked: false,
|
||||
amt: item.amt || 0,
|
||||
};
|
||||
});
|
||||
|
||||
this.cards = [...this.cards, ...formattedCards];
|
||||
this.loading = false;
|
||||
this.totalPages = res.data.data.pages;
|
||||
} catch (error) {
|
||||
console.error('获取卡片数据失败:', error);
|
||||
this.loading = false;
|
||||
this.$toast.fail('网络错误,请稍后重试');
|
||||
}
|
||||
},
|
||||
handleTouchStart(index) {
|
||||
this.activeCardIndex = index;
|
||||
},
|
||||
|
||||
handleTouchEnd() {
|
||||
setTimeout(() => {
|
||||
this.activeCardIndex = -1;
|
||||
}, 300);
|
||||
},
|
||||
|
||||
handleCardClick(card) {
|
||||
console.log('点击了卡片:', card.title);
|
||||
},
|
||||
handleClickLove(card) {
|
||||
if (card.checked) {
|
||||
card.loves++;
|
||||
card.changes++;
|
||||
} else {
|
||||
if (card.loves > 0) {
|
||||
card.loves--;
|
||||
card.changes--;
|
||||
}
|
||||
}
|
||||
this.changes.push({
|
||||
id: card.id,
|
||||
changes: card.changes
|
||||
});
|
||||
},
|
||||
async savaLoveChanes() {
|
||||
for (const change of this.changes) {
|
||||
try {
|
||||
await this.$axios.post('/portfolio/addLoves', null, {
|
||||
params: {
|
||||
id: change.id,
|
||||
changes: change.changes
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('保存点赞变化失败:', error);
|
||||
this.$toast.fail('保存点赞状态失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
},
|
||||
handleScroll() {
|
||||
const container = document.querySelector('.cards-container');
|
||||
if (!container) return;
|
||||
|
||||
handleButtonClick(card) {
|
||||
console.log('点击了了解更多:', card.title);
|
||||
const scrollTop = container.scrollTop;
|
||||
const clientHeight = container.clientHeight;
|
||||
const scrollHeight = container.scrollHeight;
|
||||
|
||||
if (scrollTop + clientHeight >= scrollHeight - 50) {
|
||||
if (!this.loading && this.page < this.totalPages) {
|
||||
this.page++;
|
||||
this.getCarts(this.page, this.pageSize || 2);
|
||||
console.log('加载更多数据,当前页:', this.page);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 处理按钮触摸反馈
|
||||
const buttons = document.querySelectorAll('.card-button');
|
||||
buttons.forEach(button => {
|
||||
button.addEventListener('touchstart', () => {
|
||||
button.classList.add('button-active');
|
||||
});
|
||||
|
||||
button.addEventListener('touchend', () => {
|
||||
setTimeout(() => {
|
||||
button.classList.remove('button-active');
|
||||
}, 200);
|
||||
});
|
||||
});
|
||||
this.getCarts(this.page, this.pageSize || 2);
|
||||
this.scrollContainer = document.querySelector('.cards-container');
|
||||
if (this.scrollContainer) {
|
||||
this.scrollHandler = () => this.handleScroll();
|
||||
this.scrollContainer.addEventListener('scroll', this.scrollHandler);
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.scrollContainer && this.scrollHandler) {
|
||||
this.scrollContainer.removeEventListener('scroll', this.scrollHandler);
|
||||
}
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
next();
|
||||
this.savaLoveChanes();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 基础样式 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
<style lang="less" scoped>
|
||||
// 变量定义
|
||||
@base-bg: #f9fafb;
|
||||
@card-bg: white;
|
||||
@text-primary: #1f2937;
|
||||
@text-secondary: #6b7280;
|
||||
@accent-color: #3b82f6;
|
||||
@accent-dark: #2563eb;
|
||||
@danger-color: #ef4444;
|
||||
@gray-light: #d1d5db;
|
||||
@shadow-base: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
@shadow-active: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
|
||||
body {
|
||||
background-color: #f9fafb;
|
||||
min-height: 100vh;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
/* 容器样式 - 限制最大宽度为800px(双列布局的合理宽度) */
|
||||
.container {
|
||||
max-width: 800px; /* 双列布局的最佳最大宽度 */
|
||||
// 页面容器(最外层)
|
||||
.page-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: clamp(1.5rem, 3vw, 2rem);
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
// 粘性公告栏
|
||||
.sticky-notice {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* 卡片网格布局 - 固定双列 */
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr); /* 固定双列布局 */
|
||||
gap: 24px; /* 卡片间距 */
|
||||
}
|
||||
// 卡片容器(滚动区域)
|
||||
.cards-container {
|
||||
overflow: auto;
|
||||
padding-bottom: 20px;
|
||||
|
||||
/* 移动端适配 - 屏幕小于600px时改为单列 */
|
||||
@media (max-width: 600px) {
|
||||
// 卡片网格布局
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 24px;
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 600px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.card {
|
||||
background-color: white;
|
||||
// 卡片样式(核心内容区)
|
||||
.card {
|
||||
background-color: @card-bg;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
box-shadow: @shadow-base;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
// 悬停与激活状态
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
box-shadow: @shadow-active;
|
||||
}
|
||||
|
||||
.card-active {
|
||||
&.card-active {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
box-shadow: @shadow-active;
|
||||
}
|
||||
|
||||
/* 卡片图片 */
|
||||
.card-image-container {
|
||||
// 卡片图片容器
|
||||
.card-image-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-image {
|
||||
width: 100%;
|
||||
display: block;
|
||||
/* 3:4 比例 */
|
||||
aspect-ratio: 3 / 4;
|
||||
object-fit: cover;
|
||||
}
|
||||
// 卡片标签(图片右上角)
|
||||
.card-tag {
|
||||
|
||||
/* 卡片标签 */
|
||||
.card-tag {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 9999px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 卡片内容 */
|
||||
.card-content {
|
||||
// 卡片内容区(标题、描述、底部)
|
||||
.card-content {
|
||||
padding: 20px;
|
||||
}
|
||||
position: relative;
|
||||
|
||||
.card-title {
|
||||
// 卡片标题
|
||||
.card-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
color: @text-primary;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-description {
|
||||
color: #6b7280;
|
||||
// 卡片描述
|
||||
.card-description {
|
||||
color: @text-secondary;
|
||||
text-wrap: balance;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.5;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 卡片底部 */
|
||||
.card-footer {
|
||||
// 卡片底部(价格与点赞)
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 12px;
|
||||
}
|
||||
position: relative;
|
||||
bottom: 0px;
|
||||
|
||||
.card-meta {
|
||||
color: #3b82f6;
|
||||
// 价格信息
|
||||
.card-meta {
|
||||
color: #9932CC;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.card-button {
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
font-size: 20px;
|
||||
background-color: #D8BFD8;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
padding: 0 5px 0 5px;
|
||||
|
||||
// 日历图标
|
||||
.fa {
|
||||
margin-right: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
// 点赞区域
|
||||
.heart {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
// 点赞数量
|
||||
.like-count {
|
||||
font-size: 13px;
|
||||
color: @text-secondary;
|
||||
}
|
||||
|
||||
// 点赞按钮容器
|
||||
.action-btn {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// 隐藏原生复选框
|
||||
.heart-checkbox {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
// 点赞按钮样式
|
||||
.heart-btn {
|
||||
position: relative;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
transition: transform 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
// 爱心图标
|
||||
.heart-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -5px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: color 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&::before {
|
||||
content: '❤';
|
||||
font-size: 1.25rem;
|
||||
color: @gray-light;
|
||||
transition: color 0.3s, transform 0.3s;
|
||||
position: relative;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
transform-origin: center;
|
||||
}
|
||||
}
|
||||
|
||||
// 选中状态(点赞效果)
|
||||
.heart-checkbox:checked~.heart-btn .heart-icon::before {
|
||||
color: @danger-color;
|
||||
animation: heartBeat 0.5s cubic-bezier(0.17, 0.89, 0.32, 1.49);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载动画(备用)
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-button:hover {
|
||||
background-color: #2563eb;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
// 爱心动画
|
||||
@keyframes heartBeat {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.button-active {
|
||||
transform: scale(0.95);
|
||||
25% {
|
||||
transform: scale(1.3);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,6 +1,41 @@
|
||||
<template>
|
||||
<div class="sample-collection-page">
|
||||
<h1>Sample Collection Page</h1>
|
||||
<div class="card-grid">
|
||||
<!-- 卡片列表 -->
|
||||
<div v-for="(card, index) in cards" :key="index" class="card"
|
||||
:class="{ 'card-active': activeCardIndex === index }" @touchstart="handleTouchStart(index)"
|
||||
@touchend="handleTouchEnd(index)" @click="handleCardClick(card)">
|
||||
|
||||
<div class="card-image-container">
|
||||
<van-swipe :autoplay="3000">
|
||||
<van-swipe-item v-for="(image, imgIndex) in card.images" :key="imgIndex">
|
||||
<van-image :src="image" lazy-load>
|
||||
<template v-slot:loading>
|
||||
<van-loading type="spinner" size="20" />
|
||||
</template>
|
||||
</van-image>
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
<div v-if="card.tag" class="card-tag" :style="card.tagStyle">
|
||||
{{ card.tag }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<h2 class="card-title">{{ card.title }}</h2>
|
||||
<p class="card-description">{{ card.description }}</p>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="employee">
|
||||
<van-tag round type="primary" v-for="employee in card.employee.split('&&')"
|
||||
:key="employee">{{ employee
|
||||
}}</van-tag>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -10,16 +45,277 @@ export default {
|
||||
components: {},
|
||||
props: {},
|
||||
data() {
|
||||
|
||||
return {
|
||||
cards: [
|
||||
{
|
||||
title: "探索自然之美",
|
||||
description: "深入原始森林,感受大自然的鬼斧神工。这里有清澈的溪流、茂密的树木和各种珍稀野生动物。",
|
||||
images: ["https://picsum.photos/seed/card1/600/800", "https://picsum.photos/seed/card2/600/800"],
|
||||
|
||||
tag: "热门",
|
||||
tagStyle: {
|
||||
backgroundColor: "#F97316",
|
||||
color: "#fff"
|
||||
},
|
||||
status: 0,
|
||||
employee: "小吕&&寒冰射手",
|
||||
neddTime: 2.0
|
||||
}
|
||||
],
|
||||
activeCardIndex: -1
|
||||
};
|
||||
},
|
||||
watch: {},
|
||||
computed: {
|
||||
|
||||
},
|
||||
methods: {},
|
||||
methods: {
|
||||
handleTouchStart(index) {
|
||||
this.activeCardIndex = index;
|
||||
},
|
||||
|
||||
handleTouchEnd() {
|
||||
setTimeout(() => {
|
||||
this.activeCardIndex = -1;
|
||||
}, 300);
|
||||
},
|
||||
|
||||
handleCardClick(card) {
|
||||
console.log('点击了卡片:', card.title);
|
||||
},
|
||||
|
||||
handleButtonClick(card) {
|
||||
console.log('点击了了解更多:', card.title);
|
||||
},
|
||||
|
||||
|
||||
},
|
||||
mounted() {
|
||||
// 处理按钮触摸反馈
|
||||
const buttons = document.querySelectorAll('.card-button');
|
||||
buttons.forEach(button => {
|
||||
button.addEventListener('touchstart', () => {
|
||||
button.classList.add('button-active');
|
||||
});
|
||||
|
||||
button.addEventListener('touchend', () => {
|
||||
setTimeout(() => {
|
||||
button.classList.remove('button-active');
|
||||
}, 200);
|
||||
});
|
||||
});
|
||||
},
|
||||
created() { },
|
||||
mounted() { }
|
||||
|
||||
};
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
||||
<style lang="less" scoped>
|
||||
// 变量定义
|
||||
@base-bg: #f9fafb;
|
||||
@card-bg: white;
|
||||
@text-primary: #1f2937;
|
||||
@text-secondary: #6b7280;
|
||||
@accent-color: #3b82f6;
|
||||
@accent-dark: #2563eb;
|
||||
@danger-color: #ef4444;
|
||||
@gray-light: #d1d5db;
|
||||
@shadow-base: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
@shadow-active: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
|
||||
// 基础样式
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: @base-bg;
|
||||
min-height: 100vh;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
// 容器样式
|
||||
.sample-collection-page {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
|
||||
.page-title {
|
||||
font-size: clamp(1.5rem, 3vw, 2rem);
|
||||
font-weight: bold;
|
||||
color: @text-primary;
|
||||
margin-bottom: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// 卡片网格布局
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 24px;
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 600px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片样式
|
||||
.card {
|
||||
background-color: @card-bg;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: @shadow-base;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: @shadow-active;
|
||||
}
|
||||
|
||||
&.card-active {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: @shadow-active;
|
||||
}
|
||||
|
||||
// 卡片图片容器
|
||||
.card-image-container {
|
||||
position: relative;
|
||||
|
||||
// 卡片标签
|
||||
.card-tag {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 9999px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片内容
|
||||
.card-content {
|
||||
padding: 20px;
|
||||
|
||||
.card-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: @text-primary;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
color: @text-secondary;
|
||||
text-wrap: balance;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.5;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// 卡片底部
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 12px;
|
||||
|
||||
.card-meta {
|
||||
color: @accent-color;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
|
||||
.fa {
|
||||
margin-right: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
// 爱心按钮区域
|
||||
.heart {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.like-count {
|
||||
font-size: 13px;
|
||||
color: @text-secondary;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.heart-checkbox {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.heart-btn {
|
||||
position: relative;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
transition: transform 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.heart-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -5px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: color 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&::before {
|
||||
content: '❤';
|
||||
font-size: 1.25rem;
|
||||
color: @gray-light;
|
||||
transition: color 0.3s, transform 0.3s;
|
||||
position: relative;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
transform-origin: center;
|
||||
}
|
||||
}
|
||||
|
||||
// 选中状态样式
|
||||
.heart-checkbox:checked~.heart-btn .heart-icon::before {
|
||||
color: @danger-color;
|
||||
animation: heartBeat 0.5s cubic-bezier(0.17, 0.89, 0.32, 1.49);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.employee {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
.van-tag {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 9999px;
|
||||
background-color: #7B68EE;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
66
na-frontend/src/utils/request.js
Normal file
66
na-frontend/src/utils/request.js
Normal file
@ -0,0 +1,66 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// 创建 axios 实例
|
||||
const service = axios.create({
|
||||
baseURL: "http://127.0.0.1:8090/api",
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
// 存储请求的防抖定时器(key: 请求标识, value: 定时器ID)
|
||||
const debounceTimers = new Map();
|
||||
// 防抖时间(毫秒,可根据需求调整)
|
||||
const DEBOUNCE_DELAY = 500;
|
||||
|
||||
// 生成请求唯一标识(URL + 参数 + 方法)
|
||||
const getRequestKey = (config) => {
|
||||
const { url, method, params, data } = config;
|
||||
// 序列化参数,确保相同参数生成相同key
|
||||
const paramsStr = params ? JSON.stringify(params) : '';
|
||||
const dataStr = data ? JSON.stringify(data) : '';
|
||||
return `${method}:${url}:${paramsStr}:${dataStr}`;
|
||||
};
|
||||
// 请求拦截器:添加防抖逻辑
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
// 1. 检查POST请求体和参数是否都为空
|
||||
if (config.method === 'post') {
|
||||
// 检查请求体是否为空
|
||||
const bodyEmpty =
|
||||
config.data === undefined ||
|
||||
config.data === null ||
|
||||
(typeof config.data === 'object' && Object.keys(config.data).length === 0) ||
|
||||
(typeof config.data === 'string' && config.data.trim().length === 0);
|
||||
|
||||
// 检查URL参数是否为空
|
||||
const paramsEmpty =
|
||||
config.params === undefined ||
|
||||
config.params === null ||
|
||||
(typeof config.params === 'object' && Object.keys(config.params).length === 0);
|
||||
|
||||
// 如果请求体和参数都为空,则打印提示
|
||||
if (bodyEmpty && paramsEmpty) {
|
||||
console.log('提示:POST请求的请求体和参数均为空');
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 防抖逻辑
|
||||
return new Promise((resolve) => {
|
||||
const requestKey = getRequestKey(config);
|
||||
// 清除之前的定时器
|
||||
if (debounceTimers.has(requestKey)) {
|
||||
clearTimeout(debounceTimers.get(requestKey));
|
||||
}
|
||||
// 设置新定时器
|
||||
const timer = setTimeout(() => {
|
||||
debounceTimers.delete(requestKey);
|
||||
resolve(config); // 延迟后执行请求
|
||||
}, DEBOUNCE_DELAY);
|
||||
debounceTimers.set(requestKey, timer);
|
||||
});
|
||||
},
|
||||
error => {
|
||||
console.error('请求拦截器错误:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
export default service;
|
@ -2,7 +2,7 @@ const { defineConfig } = require('@vue/cli-service')
|
||||
module.exports = defineConfig({
|
||||
transpileDependencies: true,
|
||||
devServer: {
|
||||
host: 'localhost',
|
||||
host: '0.0.0.0',
|
||||
port: 8080,
|
||||
historyApiFallback: true,
|
||||
allowedHosts: "all"
|
||||
|
Loading…
Reference in New Issue
Block a user