首次提交
This commit is contained in:
commit
1101681331
131 changed files with 7017 additions and 0 deletions
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/mvnw text eol=lf
|
||||
*.cmd text eol=crlf
|
||||
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
HELP.md
|
||||
target/
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
102
pom.xml
Normal file
102
pom.xml
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>4.0.1</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.xjhs.findme.merchant</groupId>
|
||||
<artifactId>findme-merchant</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>findme-backend-merchant-java</name>
|
||||
<description>findme-backend-merchant-java</description>
|
||||
<url/>
|
||||
<licenses>
|
||||
<license/>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer/>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection/>
|
||||
<developerConnection/>
|
||||
<tag/>
|
||||
<url/>
|
||||
</scm>
|
||||
<properties>
|
||||
<java.version>25</java.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
100
settings.xml
Normal file
100
settings.xml
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
|
||||
<mirrors>
|
||||
<mirror>
|
||||
<id>mirror</id>
|
||||
<mirrorOf>central,jcenter,!2469005-release-amnWma</mirrorOf>
|
||||
<name>mirror</name>
|
||||
<url>https://maven.aliyun.com/nexus/content/groups/public</url>
|
||||
</mirror>
|
||||
</mirrors>
|
||||
<servers>
|
||||
<server>
|
||||
<id>2469005-release-amnWma</id>
|
||||
<username>5fc457938d483c39b2f94ff6</username>
|
||||
<password>z51TiLCp0Gyp</password>
|
||||
</server>
|
||||
</servers>
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>rdc</id>
|
||||
<properties>
|
||||
|
||||
<altReleaseDeploymentRepository>
|
||||
2469005-release-amnWma::default::https://packages.aliyun.com/663da318da122f1ab35859ca/maven/2469005-release-amnwma
|
||||
</altReleaseDeploymentRepository>
|
||||
|
||||
|
||||
</properties>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>central</id>
|
||||
<url>https://maven.aliyun.com/nexus/content/groups/public</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>snapshots</id>
|
||||
<url>https://maven.aliyun.com/nexus/content/groups/public</url>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>2469005-release-amnWma</id>
|
||||
<url>https://packages.aliyun.com/663da318da122f1ab35859ca/maven/2469005-release-amnwma</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>central</id>
|
||||
<url>https://maven.aliyun.com/nexus/content/groups/public</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
<pluginRepository>
|
||||
<id>snapshots</id>
|
||||
<url>https://maven.aliyun.com/nexus/content/groups/public</url>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
<pluginRepository>
|
||||
<id>2469005-release-amnWma</id>
|
||||
<url>https://packages.aliyun.com/663da318da122f1ab35859ca/maven/2469005-release-amnwma</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
</profile>
|
||||
</profiles>
|
||||
<activeProfiles>
|
||||
<activeProfile>rdc</activeProfile>
|
||||
</activeProfiles>
|
||||
</settings>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.xjhs.findmemerchant;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class FindmeBackendMerchantJavaApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(FindmeBackendMerchantJavaApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
87
src/main/java/com/xjhs/findmemerchant/common/ApiResult.java
Normal file
87
src/main/java/com/xjhs/findmemerchant/common/ApiResult.java
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
package com.xjhs.findmemerchant.common;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 统一数据结构
|
||||
* @param <T> 数据类别
|
||||
*/
|
||||
@Data
|
||||
public class ApiResult<T> {
|
||||
|
||||
public ErrorCode code = ErrorCode.OK;
|
||||
|
||||
private String msg = ErrorCode.OK.getMsg();
|
||||
|
||||
private T data;
|
||||
|
||||
|
||||
public static <T> ApiResult<T> Unauthorized(String msg){
|
||||
var result = new ApiResult<T>();
|
||||
result.code = ErrorCode.Unauthorized;
|
||||
result.msg = msg;
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> ApiResult<PageData<T>> page(long total, List<T> dataList){
|
||||
return data(new PageData<T>(dataList,total));
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static ApiResult<Map<String,String>> returnToken(String token,String refreshToken){
|
||||
return data(Map.of(
|
||||
"accessToken",token,
|
||||
"refreshToken",refreshToken
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
public static <T> ApiResult<T> success(){
|
||||
return new ApiResult<T>();
|
||||
}
|
||||
|
||||
public static ApiResult<Void> success(String msg){
|
||||
var result = new ApiResult<Void>();
|
||||
result.msg = msg;
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> ApiResult<T> data(T data){
|
||||
var result = new ApiResult<T>();
|
||||
result.data = data;
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> ApiResult<T> data(String msg, T data){
|
||||
var result = new ApiResult<T>();
|
||||
result.msg = msg;
|
||||
result.data = data;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public static <T> ApiResult<T> fail(){
|
||||
var result = new ApiResult<T>();
|
||||
result.code = ErrorCode.FAIL;
|
||||
result.msg = ErrorCode.FAIL.getMsg();
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> ApiResult<T> fail(String msg){
|
||||
var result = new ApiResult<T>();
|
||||
result.code = ErrorCode.FAIL;
|
||||
result.msg = msg;
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> ApiResult<T> fail(ErrorCode errorCode){
|
||||
var result = new ApiResult<T>();
|
||||
result.code = errorCode;
|
||||
result.msg = errorCode.getMsg();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
41
src/main/java/com/xjhs/findmemerchant/common/ErrorCode.java
Normal file
41
src/main/java/com/xjhs/findmemerchant/common/ErrorCode.java
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package com.xjhs.findmemerchant.common;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 通用错误码
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum ErrorCode {
|
||||
|
||||
OK(200,"操作成功"),
|
||||
FAIL(500,"fail"),
|
||||
Unauthorized(401,"Unauthorized")
|
||||
|
||||
;
|
||||
|
||||
|
||||
@JsonCreator
|
||||
public static ErrorCode fromCode(int code){
|
||||
for (ErrorCode value : values()) {
|
||||
if (value.code == code) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Invalid ErrorCode: " + code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误号
|
||||
*/
|
||||
@JsonValue
|
||||
private final int code;
|
||||
/**
|
||||
* 错误说明
|
||||
*/
|
||||
private final String msg;
|
||||
}
|
||||
15
src/main/java/com/xjhs/findmemerchant/common/PageData.java
Normal file
15
src/main/java/com/xjhs/findmemerchant/common/PageData.java
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package com.xjhs.findmemerchant.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class PageData<T> {
|
||||
private List<T> list;
|
||||
private long total;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.xjhs.findmemerchant.common.jackson;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@JacksonAnnotationsInside
|
||||
@JsonDeserialize(using = SafeLongDeserializer.class)
|
||||
public @interface JsonDeserializeToLong {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package com.xjhs.findmemerchant.common.jackson;
|
||||
|
||||
public class SafeLongDeserializer {
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package com.xjhs.findmemerchant.common.jpa;
|
||||
|
||||
import com.xjhs.findmemerchant.adapter.id.SnowflakeGenerated;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
import org.springframework.data.annotation.CreatedBy;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedBy;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@MappedSuperclass
|
||||
@Getter
|
||||
@Setter
|
||||
public class AbstractBaseEntity {
|
||||
/**
|
||||
* 主键(雪花 ID)
|
||||
*/
|
||||
@Id
|
||||
@GeneratedValue
|
||||
@SnowflakeGenerated
|
||||
@Column(nullable = false)
|
||||
@Comment("主键ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@CreatedDate
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
@Comment("创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@CreatedBy
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
@Comment("创建人")
|
||||
private String createdBy;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@LastModifiedDate
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
@Comment("更新时间")
|
||||
private LocalDateTime updatedAt;
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
@LastModifiedBy
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
@Comment("更新人")
|
||||
private String updatedBy;
|
||||
/**
|
||||
* 软删除时间(null 表示未删除)
|
||||
*/
|
||||
@Column(name = "deleted_at")
|
||||
@Comment("软删除时间")
|
||||
private LocalDateTime deletedAt;
|
||||
|
||||
/**
|
||||
* 是否已删除
|
||||
*/
|
||||
public boolean isDeleted() {
|
||||
return deletedAt != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记删除
|
||||
*/
|
||||
public void markDeleted() {
|
||||
this.deletedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.xjhs.findmemerchant.adapter.id;
|
||||
|
||||
import org.hibernate.annotations.IdGeneratorType;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static java.lang.annotation.ElementType.METHOD;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@IdGeneratorType(SnowflakeIdGenerator.class)
|
||||
@Target({FIELD, METHOD})
|
||||
@Retention(RUNTIME)
|
||||
public @interface SnowflakeGenerated {
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.xjhs.findmemerchant.adapter.id;
|
||||
|
||||
import org.hibernate.engine.spi.SharedSessionContractImplementor;
|
||||
import org.hibernate.id.IdentifierGenerator;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
|
||||
public class SnowflakeIdGenerator implements IdentifierGenerator {
|
||||
|
||||
// 可以换成通过 Spring 注入的方式,这里简单处理
|
||||
private static final SnowflakeIdWorker WORKER = new SnowflakeIdWorker(1, 1);
|
||||
|
||||
@Override
|
||||
public Serializable generate(SharedSessionContractImplementor session, Object object) {
|
||||
return WORKER.nextId();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.xjhs.findmemerchant.adapter.id;
|
||||
|
||||
/**
|
||||
* 雪花Id生成器
|
||||
*/
|
||||
public class SnowflakeIdWorker {
|
||||
|
||||
|
||||
private static final long START_TIMESTAMP = 1704067200000L;
|
||||
|
||||
private static final long SEQUENCE_BITS = 12L;
|
||||
private static final long WORKER_ID_BITS = 5L;
|
||||
private static final long DATACENTER_ID_BITS = 5L;
|
||||
|
||||
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
|
||||
private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS);
|
||||
private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
|
||||
|
||||
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
|
||||
private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
|
||||
private static final long TIMESTAMP_SHIFT =
|
||||
SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
|
||||
|
||||
private final long workerId;
|
||||
private final long datacenterId;
|
||||
private long sequence = 0L;
|
||||
private long lastTimestamp = -1L;
|
||||
|
||||
public SnowflakeIdWorker(long workerId, long datacenterId) {
|
||||
if (workerId > MAX_WORKER_ID || workerId < 0) throw new IllegalArgumentException();
|
||||
if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) throw new IllegalArgumentException();
|
||||
this.workerId = workerId;
|
||||
this.datacenterId = datacenterId;
|
||||
}
|
||||
|
||||
public synchronized long nextId() {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
if (timestamp < lastTimestamp) throw new RuntimeException("Clock rollback");
|
||||
|
||||
if (timestamp == lastTimestamp) {
|
||||
sequence = (sequence + 1) & SEQUENCE_MASK;
|
||||
if (sequence == 0) {
|
||||
do { timestamp = System.currentTimeMillis(); }
|
||||
while (timestamp <= lastTimestamp);
|
||||
}
|
||||
} else {
|
||||
sequence = 0;
|
||||
}
|
||||
|
||||
lastTimestamp = timestamp;
|
||||
|
||||
return ((timestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
|
||||
| (datacenterId << DATACENTER_ID_SHIFT)
|
||||
| (workerId << WORKER_ID_SHIFT)
|
||||
| sequence;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.xjhs.findmemerchant.adapter.json;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
import jakarta.persistence.Converter;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* HashMap <-> JSON
|
||||
*/
|
||||
@Converter
|
||||
public class HashMapJsonConverter implements AttributeConverter<HashMap<String, Object>, String> {
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public String convertToDatabaseColumn(HashMap<String, Object> attribute) {
|
||||
if (attribute == null || attribute.isEmpty()) {
|
||||
return "{}";
|
||||
}
|
||||
try {
|
||||
return MAPPER.writeValueAsString(attribute);
|
||||
} catch (Exception e) {
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HashMap<String, Object> convertToEntityAttribute(String dbData) {
|
||||
try {
|
||||
if (dbData == null || dbData.isBlank()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
Map<String, Object> map =
|
||||
MAPPER.readValue(dbData, new TypeReference<Map<String, Object>>() {});
|
||||
return new HashMap<>(map);
|
||||
} catch (Exception e) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.xjhs.findmemerchant.adapter.json;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
import jakarta.persistence.Converter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* List<String> <-> JSON
|
||||
*/
|
||||
@Converter
|
||||
public class StringListJsonConverter implements AttributeConverter<List<String>, String> {
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public String convertToDatabaseColumn(List<String> attribute) {
|
||||
if (attribute == null || attribute.isEmpty()) {
|
||||
return "[]";
|
||||
}
|
||||
try {
|
||||
return MAPPER.writeValueAsString(attribute);
|
||||
} catch (Exception e) {
|
||||
return "[]";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> convertToEntityAttribute(String dbData) {
|
||||
try {
|
||||
if (dbData == null || dbData.isBlank()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return MAPPER.readValue(dbData, new TypeReference<List<String>>() {});
|
||||
} catch (Exception e) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.xjhs.findmemerchant.common.jpa.query;
|
||||
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class JpaSpecs {
|
||||
/**
|
||||
* 等于
|
||||
*/
|
||||
public static <T> Specification<T> eq(String field, Object value) {
|
||||
return (root, query, cb) ->
|
||||
value == null ? cb.conjunction() : cb.equal(root.get(field), value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 不等于
|
||||
*/
|
||||
public static <T> Specification<T> notEqual(String field, Object value) {
|
||||
return (root, query, cb) ->
|
||||
value == null ? cb.conjunction() : cb.notEqual(root.get(field), value);
|
||||
}
|
||||
|
||||
/**
|
||||
* like 模糊查询
|
||||
*/
|
||||
public static <T> Specification<T> like(String field, String keyword) {
|
||||
return (root, query, cb) ->
|
||||
(keyword == null || keyword.isEmpty()) ? cb.conjunction()
|
||||
: cb.like(root.get(field), "%" + keyword + "%");
|
||||
}
|
||||
|
||||
/**
|
||||
* in 查询
|
||||
*/
|
||||
public static <T> Specification<T> in(String field, Collection<?> values) {
|
||||
return (root, query, cb) -> {
|
||||
if (values == null || values.isEmpty()) {
|
||||
return cb.conjunction(); // 什么都不查,返回 true 条件
|
||||
}
|
||||
return root.get(field).in(values);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* between 查询
|
||||
*/
|
||||
public static <T, Y extends Comparable<? super Y>> Specification<T> between(
|
||||
String field, Y start, Y end) {
|
||||
return (root, query, cb) -> {
|
||||
if (start == null && end == null) return cb.conjunction();
|
||||
if (start != null && end != null) {
|
||||
return cb.between(root.get(field), start, end);
|
||||
} else if (start != null) {
|
||||
return cb.greaterThanOrEqualTo(root.get(field), start);
|
||||
} else {
|
||||
return cb.lessThanOrEqualTo(root.get(field), end);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 大于等于
|
||||
*/
|
||||
public static <T, Y extends Comparable<? super Y>> Specification<T> ge(String field, Y value) {
|
||||
return (root, query, cb) ->
|
||||
value == null ? cb.conjunction() : cb.greaterThanOrEqualTo(root.get(field), value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 小于等于
|
||||
*/
|
||||
public static <T, Y extends Comparable<? super Y>> Specification<T> le(String field, Y value) {
|
||||
return (root, query, cb) ->
|
||||
value == null ? cb.conjunction() : cb.lessThanOrEqualTo(root.get(field), value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 升序排序
|
||||
*/
|
||||
public static <T> Specification<T> orderByAsc(String field){
|
||||
return (root, query, cb) ->{
|
||||
query.orderBy(cb.asc(root.get(field)));
|
||||
return cb.conjunction();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 降序排序
|
||||
*/
|
||||
public static <T> Specification<T> orderByDesc(String field){
|
||||
return (root, query, cb) ->{
|
||||
query.orderBy(cb.desc(root.get(field)));
|
||||
return cb.conjunction();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package com.xjhs.findmemerchant.common.mvc;
|
||||
|
||||
public class GlobalResponseHandler {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package com.xjhs.findmemerchant.common.mvc;
|
||||
|
||||
public class PageVo {
|
||||
}
|
||||
25
src/main/java/com/xjhs/findmemerchant/config/CorsConfig.java
Normal file
25
src/main/java/com/xjhs/findmemerchant/config/CorsConfig.java
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package com.xjhs.findmemerchant.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
public class CorsConfig {
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.setAllowedOrigins(List.of("*")); // 替换为你前端的地址
|
||||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
config.setAllowedHeaders(List.of("*"));
|
||||
config.setAllowCredentials(false); // 如果需要携带 cookie
|
||||
config.setMaxAge(3600L); // 预检请求缓存时间
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config); // 应用于所有路径
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.xjhs.findmemerchant.config;
|
||||
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
|
||||
|
||||
@Bean
|
||||
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
|
||||
return builder -> {
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
builder.serializers(new LocalDateTimeSerializer(formatter));
|
||||
builder.deserializers(new LocalDateTimeDeserializer(formatter));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package com.xjhs.findmemerchant.config;
|
||||
|
||||
public class JpaConfig {
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.xjhs.findmemerchant.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.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
|
||||
return new StringRedisTemplate(factory);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(factory);
|
||||
|
||||
// key / hash-key
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
|
||||
// value / hash-value
|
||||
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
|
||||
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
}
|
||||
10
src/main/java/com/xjhs/findmemerchant/config/WebConfig.java
Normal file
10
src/main/java/com/xjhs/findmemerchant/config/WebConfig.java
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package com.xjhs.findmemerchant.config;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.xjhs.findmemerchant.constants;
|
||||
|
||||
/**
|
||||
* 角色编码 & 权限常量
|
||||
*/
|
||||
public final class RoleConstants {
|
||||
|
||||
private RoleConstants() {
|
||||
}
|
||||
|
||||
// ===== 角色编码 =====
|
||||
public static final String ROLE_CODE_OWNER = "owner"; // 店长
|
||||
public static final String ROLE_CODE_OPERATOR = "operator"; // 运营
|
||||
public static final String ROLE_CODE_SERVICE = "service"; // 客服
|
||||
public static final String ROLE_CODE_MARKETING = "marketing"; // 营销管理员
|
||||
|
||||
// ===== 权限常量 =====
|
||||
public static final String PERMISSION_ALL = "*";
|
||||
public static final String PERMISSION_STORE_MANAGE = "store:manage";
|
||||
public static final String PERMISSION_STORE_VIEW = "store:view";
|
||||
public static final String PERMISSION_COUPON_MANAGE = "coupon:manage";
|
||||
public static final String PERMISSION_COUPON_VIEW = "coupon:view";
|
||||
public static final String PERMISSION_COUPON_VERIFY = "coupon:verify";
|
||||
public static final String PERMISSION_EMPLOYEE_MANAGE = "employee:manage";
|
||||
public static final String PERMISSION_EMPLOYEE_VIEW = "employee:view";
|
||||
public static final String PERMISSION_ORDER_MANAGE = "order:manage";
|
||||
public static final String PERMISSION_ORDER_VIEW = "order:view";
|
||||
public static final String PERMISSION_ACTIVITY_MANAGE = "activity:manage";
|
||||
public static final String PERMISSION_ACTIVITY_VIEW = "activity:view";
|
||||
public static final String PERMISSION_ANALYTICS_VIEW = "analytics:view";
|
||||
public static final String PERMISSION_MEMBER_MANAGE = "member:manage";
|
||||
public static final String PERMISSION_MEMBER_VIEW = "member:view";
|
||||
public static final String PERMISSION_MESSAGE_VIEW = "message:view";
|
||||
public static final String PERMISSION_CREDENTIAL_MANAGE = "credential:manage";
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
package com.xjhs.findmemerchant.controller;
|
||||
|
||||
import com.xjhs.findmemerchant.common.ApiResult;
|
||||
import com.xjhs.findmemerchant.dto.MerchantDto;
|
||||
import com.xjhs.findmemerchant.dto.auth.RegisterDto;
|
||||
import com.xjhs.findmemerchant.entity.Merchant;
|
||||
import com.xjhs.findmemerchant.mapper.MerchantMapper;
|
||||
import com.xjhs.findmemerchant.redis.TokenBlacklistRedisService;
|
||||
import com.xjhs.findmemerchant.repository.MerchantRepository;
|
||||
import com.xjhs.findmemerchant.security.JwtTokenService;
|
||||
import com.xjhs.findmemerchant.security.RefreshTokenService;
|
||||
import com.xjhs.findmemerchant.security.sms.SmsAuthenticationToken;
|
||||
import com.xjhs.findmemerchant.security.sms.SmsCodeService;
|
||||
import com.xjhs.findmemerchant.security.sms.SmsLoginVo;
|
||||
import com.xjhs.findmemerchant.security.sms.SmsSendVo;
|
||||
import com.xjhs.findmemerchant.service.MerchantService;
|
||||
import com.xjhs.findmemerchant.vo.merchant.MerchantUpdateVo;
|
||||
import com.xjhs.findmemerchant.vo.auth.RegisterVo;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 登录授权管理接口
|
||||
*/
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/auth")
|
||||
public class AuthController {
|
||||
private final SmsCodeService smsCodeService;
|
||||
private final JwtTokenService jwtTokenService;
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final RefreshTokenService refreshTokenService;
|
||||
private final MerchantRepository merchantRepository;
|
||||
private final MerchantMapper merchantMapper;
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
private final TokenBlacklistRedisService tokenBlacklistRedisService;
|
||||
private final MerchantService merchantService;
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*/
|
||||
@PostMapping("/sms/send")
|
||||
public ApiResult<Void> sendCode(@Valid @RequestBody SmsSendVo sendVo) {
|
||||
smsCodeService.sendVerificationCode(sendVo.getPhone(), sendVo.getScene());
|
||||
return ApiResult.success("验证码已发送");
|
||||
}
|
||||
|
||||
/**
|
||||
* 短信验证码登录
|
||||
* @param vo 登录信息
|
||||
* @return 令牌信息
|
||||
*/
|
||||
@PostMapping("/sms/login")
|
||||
public ApiResult<Map<String, String>> login(@Valid @RequestBody SmsLoginVo vo) {
|
||||
try {
|
||||
var authToken = new SmsAuthenticationToken(vo.getPhone(), vo.getCode());
|
||||
var auth = this.authenticationManager.authenticate(authToken);
|
||||
var accessToken = jwtTokenService.generateToken(vo.getPhone());
|
||||
var refreshToken = refreshTokenService.create(vo.getPhone());
|
||||
return ApiResult.returnToken(accessToken, refreshToken);
|
||||
} catch (Exception e) {
|
||||
return ApiResult.fail("登录失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*
|
||||
* @param refreshToken 用于刷新的令牌值
|
||||
* @return 成功返回accessToken访问令牌
|
||||
*/
|
||||
@PostMapping("/refresh")
|
||||
public ApiResult<String> refresh(@RequestParam(name = "refreshToken") String refreshToken) {
|
||||
var mobile = refreshTokenService.validate(refreshToken);
|
||||
if (mobile == null) {
|
||||
return ApiResult.fail("refresh token 失效或不存在");
|
||||
}
|
||||
try {
|
||||
var newAccess = jwtTokenService.generateToken(mobile);
|
||||
return ApiResult.data(newAccess);
|
||||
} catch (Exception e) {
|
||||
return ApiResult.fail("refresh token 失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销登录
|
||||
* @param authHeader @ignore 认证头信息
|
||||
* @return 注销结果
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
public ApiResult<Void> logout(@RequestHeader(HttpHeaders.AUTHORIZATION) String authHeader){
|
||||
if (StringUtils.isEmpty(authHeader) || !authHeader.startsWith("Bearer ")) {
|
||||
return ApiResult.fail("访问令牌错误");
|
||||
}
|
||||
try {
|
||||
var token = authHeader.substring(7);
|
||||
tokenBlacklistRedisService.add(token);
|
||||
return ApiResult.success();
|
||||
} catch (Exception e) {
|
||||
return ApiResult.fail("注销失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 商家注册
|
||||
*
|
||||
* @param registerVo 注册信息对象
|
||||
* @return 注册成功返回登录令牌信息
|
||||
*/
|
||||
@PostMapping("/register")
|
||||
public ApiResult<RegisterDto> register(@Valid @RequestBody RegisterVo registerVo) {
|
||||
try {
|
||||
this.smsCodeService.verifyCode(registerVo.getPhone(), "register", registerVo.getCode());
|
||||
var exists = merchantRepository.existsByPhone(registerVo.getPhone());
|
||||
if (exists) {
|
||||
return ApiResult.fail("手机号已被注册");
|
||||
}
|
||||
var merchant = new Merchant();
|
||||
merchant.setPhone(registerVo.getPhone());
|
||||
this.merchantRepository.save(merchant);
|
||||
return ApiResult.data(
|
||||
new RegisterDto(
|
||||
merchant.getId(),
|
||||
this.jwtTokenService.generateToken(registerVo.getPhone()),
|
||||
this.refreshTokenService.create(registerVo.getPhone())
|
||||
)
|
||||
);
|
||||
} catch (Exception e) {
|
||||
return ApiResult.fail("注册失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录的商户基本信息
|
||||
* @param principal @ignore 登录信息对象
|
||||
* @return 商户信息
|
||||
*/
|
||||
@GetMapping("/profile")
|
||||
public ApiResult<MerchantDto> getProfile(Principal principal) {
|
||||
if (principal instanceof Merchant merchant) {
|
||||
return this.merchantService.getById(merchant.getId())
|
||||
.map(ApiResult::data)
|
||||
.orElse(ApiResult.fail("商户信息不存在"));
|
||||
}
|
||||
return ApiResult.fail("商户信息不存在");
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前登录的商户基本信息
|
||||
* @param principal @ignore 登录信息对象
|
||||
* @param merchantUpdateVo 更新信息对象
|
||||
* @return 商户信息
|
||||
*/
|
||||
@PutMapping("/profile")
|
||||
public ApiResult<MerchantDto> updateProfile(Principal principal,@Valid @RequestBody MerchantUpdateVo merchantUpdateVo) {
|
||||
if (principal instanceof Merchant merchant) {
|
||||
try {
|
||||
var result = this.merchantService.updateMerchant(merchant.getId(),merchantUpdateVo);
|
||||
return ApiResult.data(result);
|
||||
} catch (Exception e) {
|
||||
return ApiResult.fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
return ApiResult.fail("商户信息不存在");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.xjhs.findmemerchant.controller;
|
||||
|
||||
import com.xjhs.findmemerchant.common.ApiResult;
|
||||
import com.xjhs.findmemerchant.dto.MerchantDto;
|
||||
import com.xjhs.findmemerchant.entity.Merchant;
|
||||
import com.xjhs.findmemerchant.service.MerchantService;
|
||||
import com.xjhs.findmemerchant.vo.merchant.MerchantUpdateVo;
|
||||
import com.xjhs.findmemerchant.vo.merchant.MerchantVerifyVo;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/merchant")
|
||||
@RequiredArgsConstructor
|
||||
public class MerchantController {
|
||||
private final MerchantService merchantService;
|
||||
|
||||
@GetMapping
|
||||
public ApiResult<MerchantDto> getMerchant(Principal principal) {
|
||||
if (principal instanceof Merchant merchant) {
|
||||
return this.merchantService.getById(merchant.getId())
|
||||
.map(ApiResult::data)
|
||||
.orElse(ApiResult.fail("商户信息不存在"));
|
||||
}
|
||||
return ApiResult.fail("商户信息不存在");
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
public ApiResult<MerchantDto> updateMerchant(Principal principal,@Valid @RequestBody MerchantUpdateVo merchantUpdateVo) {
|
||||
if (principal instanceof Merchant merchant) {
|
||||
try {
|
||||
var dto = this.merchantService.updateMerchant(merchant.getId(),merchantUpdateVo);
|
||||
return ApiResult.data(dto);
|
||||
} catch (Exception e) {
|
||||
return ApiResult.fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
return ApiResult.fail("商户信息不存在");
|
||||
}
|
||||
|
||||
@PostMapping("/verify")
|
||||
public ApiResult<MerchantDto> verifyMerchant(Principal principal,@Valid @RequestBody MerchantVerifyVo merchantVerifyVo) {
|
||||
if (principal instanceof Merchant merchant) {
|
||||
try {
|
||||
var dto = this.merchantService.verifyMerchant(merchant.getId(),merchantVerifyVo.getIdCardNo(),merchantVerifyVo.getRealName());
|
||||
return ApiResult.data(dto);
|
||||
} catch (Exception e) {
|
||||
return ApiResult.fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
return ApiResult.fail("商户信息不存在");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
package com.xjhs.findmemerchant.controller;
|
||||
|
||||
import com.xjhs.findmemerchant.common.ApiResult;
|
||||
import com.xjhs.findmemerchant.common.PageData;
|
||||
import com.xjhs.findmemerchant.common.jpa.query.JpaSpecs;
|
||||
import com.xjhs.findmemerchant.dto.store.StoreDto;
|
||||
import com.xjhs.findmemerchant.entity.Merchant;
|
||||
import com.xjhs.findmemerchant.entity.Store;
|
||||
import com.xjhs.findmemerchant.mapper.StoreMapper;
|
||||
import com.xjhs.findmemerchant.repository.MerchantRepository;
|
||||
import com.xjhs.findmemerchant.repository.StoreRepository;
|
||||
import com.xjhs.findmemerchant.vo.store.StoreCreateVo;
|
||||
import com.xjhs.findmemerchant.vo.store.StoreUpdateVo;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Objects;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/stores")
|
||||
@RequiredArgsConstructor
|
||||
public class StoreController {
|
||||
private final MerchantRepository merchantRepository;
|
||||
private final StoreRepository storeRepository;
|
||||
private final StoreMapper storeMapper;
|
||||
|
||||
/**
|
||||
* 门店信息(分页查询)
|
||||
*
|
||||
* @param pageable 分页参数
|
||||
* @param principal @ignore 当前登录门店
|
||||
* @return 分页数据信息
|
||||
*/
|
||||
@GetMapping
|
||||
@Transactional(readOnly = true)
|
||||
public ApiResult<PageData<StoreDto>> findPage(Pageable pageable, Principal principal) {
|
||||
var merchant = (Merchant) principal;
|
||||
var pageData = this.storeRepository.findAll(Specification.allOf(
|
||||
JpaSpecs.eq("merchant.id", merchant.getId())
|
||||
), pageable).map(this.storeMapper::toDto);
|
||||
return ApiResult.page(pageData.getTotalElements(), pageData.getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建商户的门店
|
||||
*
|
||||
* @param vo 门店信息
|
||||
* @param principal @ignore 当前登录的商户
|
||||
* @return 门店信息
|
||||
*/
|
||||
@PostMapping
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ApiResult<StoreDto> create(@Valid @RequestBody StoreCreateVo vo, Principal principal) {
|
||||
var merchant = (Merchant) principal;
|
||||
if (Objects.isNull(merchant)) {
|
||||
return ApiResult.fail("商家信息错误");
|
||||
}
|
||||
var store = this.storeMapper.toEntity(vo);
|
||||
merchant.addStore(store);
|
||||
this.storeRepository.save(store);
|
||||
var dto = this.storeMapper.toDto(store);
|
||||
return ApiResult.data(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询门店详情
|
||||
*
|
||||
* @param storeId 门店id
|
||||
* @param principal @ignore 当前登录的商户
|
||||
* @return 门店详情
|
||||
*/
|
||||
@GetMapping("/{storeId}")
|
||||
@Transactional(readOnly = true)
|
||||
public ApiResult<StoreDto> findById(@PathVariable("storeId") String storeId, Principal principal) {
|
||||
var merchant = (Merchant) principal;
|
||||
if (Objects.isNull(merchant)) {
|
||||
return ApiResult.fail("商家信息错误");
|
||||
}
|
||||
var storeIdLong = Long.parseLong(storeId);
|
||||
return merchant.getStores().stream()
|
||||
.filter(x -> Objects.equals(x.getId(), storeIdLong))
|
||||
.map(this.storeMapper::toDto)
|
||||
.map(ApiResult::data)
|
||||
.findFirst()
|
||||
.orElse(ApiResult.fail("门店信息不存在"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新门店信息
|
||||
*
|
||||
* @param storeId 门店id
|
||||
* @param principal @ignore 当前登录的商户
|
||||
* @param vo 更新对象
|
||||
* @return 门店信息
|
||||
*/
|
||||
@PutMapping("/{storeId}")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ApiResult<StoreDto> updateById(@PathVariable("storeId") String storeId, Principal principal, @Valid @RequestBody StoreUpdateVo vo) {
|
||||
var merchant = (Merchant) principal;
|
||||
if (Objects.isNull(merchant)) {
|
||||
return ApiResult.fail("商家信息错误");
|
||||
}
|
||||
var storeIdLong = Long.parseLong(storeId);
|
||||
for (Store store : merchant.getStores()) {
|
||||
if (Objects.equals(storeIdLong, store.getId())) {
|
||||
this.storeMapper.updateFromVo(vo, store);
|
||||
this.storeRepository.save(store);
|
||||
return ApiResult.data(this.storeMapper.toDto(store));
|
||||
}
|
||||
}
|
||||
return ApiResult.fail("门店信息不存在");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除门店
|
||||
*
|
||||
* @param storeId 门店id
|
||||
* @param principal @ignore 当前登录的商户
|
||||
*/
|
||||
@DeleteMapping("/{storeId}")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ApiResult<Void> delteById(@PathVariable("storeId") String storeId, Principal principal) {
|
||||
var merchant = (Merchant) principal;
|
||||
if (Objects.isNull(merchant)) {
|
||||
return ApiResult.fail("商家信息错误");
|
||||
}
|
||||
var storeIdLong = Long.parseLong(storeId);
|
||||
merchant.getStores().removeIf(x -> Objects.equals(storeIdLong, x.getId()));
|
||||
this.merchantRepository.save(merchant);
|
||||
return ApiResult.success("删除成功");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package com.xjhs.findmemerchant.controller;
|
||||
|
||||
public class StoreEmployeeController {
|
||||
}
|
||||
40
src/main/java/com/xjhs/findmemerchant/dto/MerchantDto.java
Normal file
40
src/main/java/com/xjhs/findmemerchant/dto/MerchantDto.java
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package com.xjhs.findmemerchant.dto;
|
||||
|
||||
import com.xjhs.findmemerchant.types.AuthStatus;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 商户信息
|
||||
*/
|
||||
@Data
|
||||
public class MerchantDto {
|
||||
/**
|
||||
* 商户id
|
||||
*/
|
||||
private String id;
|
||||
/**
|
||||
* 手机号码
|
||||
* TODO: 手机号码需要脱敏
|
||||
*/
|
||||
private String phone;
|
||||
/**
|
||||
* 真实姓名
|
||||
*/
|
||||
private String realName;
|
||||
/**
|
||||
* 身份证号(明文)
|
||||
*/
|
||||
private String idCardNo;
|
||||
/**
|
||||
* 认证状态
|
||||
*/
|
||||
private AuthStatus authStatus;
|
||||
/**
|
||||
* 认证状态(说明)
|
||||
*/
|
||||
private String authStatusDesc;
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private String createAt;
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.xjhs.findmemerchant.dto.auth;
|
||||
|
||||
/**
|
||||
* 商家注册响应内容
|
||||
*
|
||||
* @param merchantId 商户id
|
||||
* @param accessToken 访问令牌
|
||||
* @param refreshToken 刷新令牌
|
||||
*/
|
||||
public record RegisterDto(
|
||||
Long merchantId,
|
||||
String accessToken,
|
||||
String refreshToken
|
||||
) {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package com.xjhs.findmemerchant.dto.member;
|
||||
|
||||
public class EmployeeDto {
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.xjhs.findmemerchant.vo.store;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class BusinessPeriodVo {
|
||||
/**
|
||||
* 周几,周天0,...
|
||||
*/
|
||||
@Size(max = 6)
|
||||
@NotNull
|
||||
private Integer dayOfWeek;
|
||||
/**
|
||||
* 开始营业时间 HH:mm 格式
|
||||
*/
|
||||
@NotBlank
|
||||
private String startTime;
|
||||
/**
|
||||
* 结束营业时间 HH:mm 格式
|
||||
*/
|
||||
@NotBlank
|
||||
private String endTime;
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
private Boolean enabled;
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package com.xjhs.findmemerchant.dto.store;
|
||||
|
||||
public class StoreBusinessStatusDto {
|
||||
}
|
||||
106
src/main/java/com/xjhs/findmemerchant/dto/store/StoreDto.java
Normal file
106
src/main/java/com/xjhs/findmemerchant/dto/store/StoreDto.java
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
package com.xjhs.findmemerchant.dto.store;
|
||||
|
||||
import com.xjhs.findmemerchant.types.BusinessStatus;
|
||||
import com.xjhs.findmemerchant.types.CommonStatus;
|
||||
import com.xjhs.findmemerchant.types.StoreAuditStatus;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
public class StoreDto {
|
||||
/**
|
||||
* 门店id
|
||||
*/
|
||||
private String id;
|
||||
/**
|
||||
* 商户id
|
||||
*/
|
||||
private String merchantId;
|
||||
/**
|
||||
* 门店名称
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 门店logo
|
||||
*/
|
||||
private String logo;
|
||||
/**
|
||||
* 联系电话
|
||||
*/
|
||||
private String phone;
|
||||
/**
|
||||
* 省
|
||||
*/
|
||||
private String province;
|
||||
/**
|
||||
* 市
|
||||
*/
|
||||
private String city;
|
||||
/**
|
||||
* 区/县
|
||||
*/
|
||||
private String district;
|
||||
/**
|
||||
* 详细地址
|
||||
*/
|
||||
private String address;
|
||||
/**
|
||||
* 完整地址
|
||||
*/
|
||||
private String fullAddress;
|
||||
|
||||
// TODO: 后续改为 Point location
|
||||
/**
|
||||
* 经度
|
||||
*/
|
||||
private BigDecimal longitude;
|
||||
/**
|
||||
* 纬度
|
||||
*/
|
||||
private BigDecimal latitude;
|
||||
/**
|
||||
* 营业时间描述
|
||||
*/
|
||||
private String businessHours;
|
||||
/**
|
||||
* 营业状态
|
||||
*/
|
||||
private BusinessStatus businessStatus;
|
||||
/**
|
||||
* 营业状态(说明)
|
||||
*/
|
||||
private String businessStatusDesc;
|
||||
/**
|
||||
* 临时打烊原因
|
||||
*/
|
||||
private String tempCloseReason;
|
||||
/**
|
||||
* 临时打烊结束时间
|
||||
*/
|
||||
private String tempCloseUntil;
|
||||
/**
|
||||
* 审核状态
|
||||
*/
|
||||
private StoreAuditStatus auditStatus;
|
||||
/**
|
||||
* 审核状态(说明)
|
||||
*/
|
||||
private String auditStatusDesc;
|
||||
/**
|
||||
* 审核备注
|
||||
*/
|
||||
private String auditRemark;
|
||||
/**
|
||||
* 启用状态
|
||||
*/
|
||||
private CommonStatus status;
|
||||
/**
|
||||
* 启用状态(说明)
|
||||
*/
|
||||
private String statusDesc;
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private String createdAt;
|
||||
}
|
||||
137
src/main/java/com/xjhs/findmemerchant/entity/Activity.java
Normal file
137
src/main/java/com/xjhs/findmemerchant/entity/Activity.java
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import com.xjhs.findmemerchant.types.ActivityStatus;
|
||||
import com.xjhs.findmemerchant.types.ActivityType;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 活动 / 营销活动实体
|
||||
* 对应表:activities
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "activities",
|
||||
indexes = {
|
||||
@Index(name = "idx_activities_merchant_id", columnList = "merchant_id"),
|
||||
@Index(name = "idx_activities_time", columnList = "start_time,end_time"),
|
||||
@Index(name = "idx_activities_status", columnList = "status")
|
||||
}
|
||||
)
|
||||
public class Activity extends AbstractBaseEntity {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "merchant_id")
|
||||
private Merchant merchant;
|
||||
|
||||
/**
|
||||
* 活动名称
|
||||
*/
|
||||
@Column(name = "name", length = 100)
|
||||
@Comment("活动名称")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 活动类型 (1团购 2折扣 3限时优惠)
|
||||
*/
|
||||
@Column(name = "type", length = 15, columnDefinition = "VARCHAR(15)")
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Comment("活动类型")
|
||||
private ActivityType type;
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
@Column(name = "start_time")
|
||||
@Comment("开始时间")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
/**
|
||||
* 结束时间
|
||||
*/
|
||||
@Column(name = "end_time")
|
||||
@Comment("结束时间")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
/**
|
||||
* 库存
|
||||
*/
|
||||
@Column(name = "stock")
|
||||
@Comment("库存总数")
|
||||
private Integer stock = 0;
|
||||
|
||||
/**
|
||||
* 已售数量
|
||||
*/
|
||||
@Column(name = "sold_count")
|
||||
@Comment("已售数量")
|
||||
private Integer soldCount = 0;
|
||||
|
||||
/**
|
||||
* 库存告警阈值
|
||||
*/
|
||||
@Column(name = "alert_threshold")
|
||||
@Comment("库存告警阈值")
|
||||
private Integer alertThreshold;
|
||||
|
||||
/**
|
||||
* 活动状态
|
||||
*/
|
||||
@Column(name = "status", columnDefinition = "VARCHAR(20)")
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Comment("活动状态:0未开始 1进行中 2已结束 3已下架")
|
||||
private ActivityStatus status = ActivityStatus.NOT_STARTED;
|
||||
|
||||
|
||||
|
||||
|
||||
// ================= 业务逻辑 =================
|
||||
|
||||
public boolean isOngoing() {
|
||||
var now = LocalDateTime.now();
|
||||
return this.status == ActivityStatus.ONGOING
|
||||
&& now.isAfter(startTime)
|
||||
&& now.isBefore(endTime);
|
||||
}
|
||||
|
||||
public boolean hasStock() {
|
||||
return stock != null && soldCount != null && soldCount < stock;
|
||||
}
|
||||
|
||||
public int remainingStock() {
|
||||
if (stock == null || soldCount == null) return 0;
|
||||
return stock - soldCount;
|
||||
}
|
||||
|
||||
public boolean isLowStock() {
|
||||
return alertThreshold != null && remainingStock() <= alertThreshold;
|
||||
}
|
||||
|
||||
public String getTypeText() {
|
||||
var e = this.type;
|
||||
return e != null ? e.getDesc() : "未知";
|
||||
}
|
||||
|
||||
public String getStatusText() {
|
||||
var e = this.status;
|
||||
return e != null ? e.getDesc() : "未知";
|
||||
}
|
||||
|
||||
public boolean canEdit() {
|
||||
return this.status == ActivityStatus.NOT_STARTED;
|
||||
}
|
||||
|
||||
public boolean canOffline() {
|
||||
var s = this.status;
|
||||
return s == ActivityStatus.NOT_STARTED || s == ActivityStatus.ONGOING;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
109
src/main/java/com/xjhs/findmemerchant/entity/BankCard.java
Normal file
109
src/main/java/com/xjhs/findmemerchant/entity/BankCard.java
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import com.xjhs.findmemerchant.types.CommonStatus;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
/**
|
||||
* 商户提现银行卡
|
||||
* 对应表:bank_cards
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "bank_cards",
|
||||
indexes = {
|
||||
@Index(name = "idx_bank_cards_merchant_id", columnList = "merchant_id")
|
||||
}
|
||||
)
|
||||
public class BankCard extends AbstractBaseEntity {
|
||||
|
||||
/**
|
||||
* 商户
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "merchant_id", insertable = false, updatable = false)
|
||||
private Merchant merchant;
|
||||
|
||||
/**
|
||||
* 银行名称
|
||||
*/
|
||||
@Column(name = "bank_name", length = 50)
|
||||
@Comment("银行名称")
|
||||
private String bankName;
|
||||
|
||||
/**
|
||||
* 银行代码
|
||||
*/
|
||||
@Column(name = "bank_code", length = 20)
|
||||
@Comment("银行代码")
|
||||
private String bankCode;
|
||||
|
||||
/**
|
||||
* 支行名称
|
||||
*/
|
||||
@Column(name = "branch_name", length = 100)
|
||||
@Comment("支行名称")
|
||||
private String branchName;
|
||||
|
||||
/**
|
||||
* 银行账号(建议加密存储)
|
||||
*/
|
||||
@Column(name = "account_no", length = 30)
|
||||
@Comment("银行卡号(加密存储)")
|
||||
private String accountNo;
|
||||
|
||||
/**
|
||||
* 开户名
|
||||
*/
|
||||
@Column(name = "account_name", length = 50)
|
||||
@Comment("开户名")
|
||||
private String accountName;
|
||||
|
||||
/**
|
||||
* 是否默认卡
|
||||
*/
|
||||
@Column(name = "is_default", nullable = false)
|
||||
@Comment("是否默认卡")
|
||||
private Boolean isDefault = false;
|
||||
|
||||
/**
|
||||
* 状态:0-禁用 1-启用
|
||||
*/
|
||||
@Column(name = "status",columnDefinition = "VARCHAR(15)",length = 15, nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Comment("状态:0禁用 1启用")
|
||||
private CommonStatus status = CommonStatus.ENABLED;
|
||||
|
||||
|
||||
|
||||
// ========== 业务方法 ==========
|
||||
|
||||
/**
|
||||
* 脱敏账号,例如:6222****1234
|
||||
*/
|
||||
public String maskAccountNo() {
|
||||
if (accountNo == null || accountNo.length() <= 8) {
|
||||
return accountNo;
|
||||
}
|
||||
return accountNo.substring(0, 4)
|
||||
+ "****"
|
||||
+ accountNo.substring(accountNo.length() - 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return this.status == CommonStatus.ENABLED;
|
||||
}
|
||||
|
||||
public String getStatusText() {
|
||||
CommonStatus e = this.status;
|
||||
return e != null ? e.getDesc() : "未知";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
|
||||
import com.xjhs.findmemerchant.adapter.json.HashMapJsonConverter;
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import com.xjhs.findmemerchant.types.BusinessLicenseStatus;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* 营业执照资质
|
||||
* 对应表:business_licenses
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "business_licenses",
|
||||
indexes = {
|
||||
@Index(name = "idx_business_licenses_merchant_id", columnList = "merchant_id")
|
||||
}
|
||||
)
|
||||
public class BusinessLicense extends AbstractBaseEntity {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "merchant_id", insertable = false, updatable = false)
|
||||
private Merchant merchant;
|
||||
|
||||
@Column(name = "image_url", nullable = false, length = 500)
|
||||
@Comment("营业执照图片URL")
|
||||
private String imageUrl;
|
||||
|
||||
@Column(name = "company_name", length = 200)
|
||||
@Comment("公司名称(OCR识别)")
|
||||
private String companyName;
|
||||
|
||||
@Column(name = "license_no", length = 50)
|
||||
@Comment("营业执照号(OCR识别)")
|
||||
private String licenseNo;
|
||||
|
||||
@Convert(converter = HashMapJsonConverter.class)
|
||||
@Column(name = "ocr_raw", columnDefinition = "json")
|
||||
@Comment("OCR原始结果")
|
||||
private HashMap<String, Object> ocrRaw;
|
||||
|
||||
@Column(name = "status",length = 15,columnDefinition = "VARCHAR(15)", nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Comment("状态:0待审核 1已通过 2已拒绝")
|
||||
private BusinessLicenseStatus status = BusinessLicenseStatus.PENDING;
|
||||
|
||||
|
||||
// ===== 业务方法 =====
|
||||
|
||||
public boolean isApproved() {
|
||||
return this.status == BusinessLicenseStatus.APPROVED;
|
||||
}
|
||||
|
||||
|
||||
public String getStatusText() {
|
||||
var e = this.status;
|
||||
return e != null ? e.getDesc() : "未知";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
/**
|
||||
* 门店营业时间段
|
||||
* 对应表:business_periods
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "business_periods",
|
||||
indexes = {
|
||||
@Index(name = "idx_business_periods_store_id", columnList = "store_id")
|
||||
}
|
||||
)
|
||||
public class BusinessPeriod extends AbstractBaseEntity {
|
||||
|
||||
/**
|
||||
* 门店
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "store_id", insertable = false, updatable = false)
|
||||
@Comment("门店Id")
|
||||
private Store store;
|
||||
|
||||
/**
|
||||
* 周几:0=周日 ... 6=周六
|
||||
*/
|
||||
@Column(name = "day_of_week", nullable = false)
|
||||
@Comment("周几:0周日 1周一 ... 6周六")
|
||||
private Byte dayOfWeek;
|
||||
|
||||
/**
|
||||
* 开始时间,HH:MM
|
||||
*/
|
||||
@Column(name = "start_time", nullable = false, length = 5)
|
||||
@Comment("开始时间,HH:MM")
|
||||
private String startTime;
|
||||
|
||||
/**
|
||||
* 结束时间,HH:MM
|
||||
*/
|
||||
@Column(name = "end_time", nullable = false, length = 5)
|
||||
@Comment("结束时间,HH:MM")
|
||||
private String endTime;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
@Column(name = "is_enabled", nullable = false)
|
||||
@Comment("是否启用")
|
||||
private Boolean isEnabled = Boolean.TRUE;
|
||||
|
||||
|
||||
// ========== 业务方法 ==========
|
||||
|
||||
/**
|
||||
* 中文周几,比如:周一
|
||||
*/
|
||||
public String getDayName() {
|
||||
if (dayOfWeek == null) {
|
||||
return "未知";
|
||||
}
|
||||
return switch (dayOfWeek) {
|
||||
case 0 -> "周日";
|
||||
case 1 -> "周一";
|
||||
case 2 -> "周二";
|
||||
case 3 -> "周三";
|
||||
case 4 -> "周四";
|
||||
case 5 -> "周五";
|
||||
case 6 -> "周六";
|
||||
default -> "未知";
|
||||
};
|
||||
}
|
||||
}
|
||||
211
src/main/java/com/xjhs/findmemerchant/entity/Coupon.java
Normal file
211
src/main/java/com/xjhs/findmemerchant/entity/Coupon.java
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import com.xjhs.findmemerchant.types.CouponStatus;
|
||||
import com.xjhs.findmemerchant.types.CouponType;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 优惠券模板实体
|
||||
* 对应表:coupons
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "coupons",
|
||||
indexes = {
|
||||
@Index(name = "idx_coupons_merchant_id", columnList = "merchant_id"),
|
||||
@Index(name = "idx_coupons_status", columnList = "status"),
|
||||
@Index(name = "idx_coupons_gift_product_id", columnList = "gift_product_id")
|
||||
}
|
||||
)
|
||||
public class Coupon extends AbstractBaseEntity {
|
||||
|
||||
/**
|
||||
* 优惠券名称
|
||||
*/
|
||||
@Column(name = "name", nullable = false, length = 100)
|
||||
@Comment("优惠券名称")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 优惠券类型:1-折扣券 2-满减券 3-现金券 4-赠品券
|
||||
*/
|
||||
@Column(name = "type",length = 15,columnDefinition = "VARCHAR(15)", nullable = false)
|
||||
@Comment("优惠券类型:1折扣券 2满减券 3现金券 4赠品券")
|
||||
private CouponType type;
|
||||
|
||||
/**
|
||||
* 折扣率 0.01-0.99,decimal(3,2)
|
||||
*/
|
||||
@Column(name = "discount_rate", precision = 3, scale = 2)
|
||||
@Comment("折扣率 0.01-0.99")
|
||||
private BigDecimal discountRate;
|
||||
|
||||
/**
|
||||
* 满减门槛,decimal(10,2)
|
||||
*/
|
||||
@Column(name = "min_amount", precision = 10, scale = 2)
|
||||
@Comment("满减门槛")
|
||||
private BigDecimal minAmount;
|
||||
|
||||
/**
|
||||
* 减免金额,decimal(10,2)
|
||||
*/
|
||||
@Column(name = "reduce_amount", precision = 10, scale = 2)
|
||||
@Comment("减免金额")
|
||||
private BigDecimal reduceAmount;
|
||||
|
||||
/**
|
||||
* 赠品商品ID
|
||||
*/
|
||||
@Column(name = "gift_product_id")
|
||||
@Comment("赠品商品ID")
|
||||
private Long giftProductId;
|
||||
|
||||
/**
|
||||
* 赠品数量,默认 1
|
||||
*/
|
||||
@Column(name = "gift_quantity", nullable = false)
|
||||
@Comment("赠品数量")
|
||||
private Integer giftQuantity = 1;
|
||||
|
||||
/**
|
||||
* 发放总量
|
||||
*/
|
||||
@Column(name = "total_count", nullable = false)
|
||||
@Comment("发放总量")
|
||||
private Integer totalCount;
|
||||
|
||||
/**
|
||||
* 已领取数量
|
||||
*/
|
||||
@Column(name = "claimed_count", nullable = false)
|
||||
@Comment("已领取数量")
|
||||
private Integer claimedCount = 0;
|
||||
|
||||
/**
|
||||
* 每人限领数量,0 表示不限
|
||||
*/
|
||||
@Column(name = "per_user_limit", nullable = false)
|
||||
@Comment("每人限领数量,0表示不限")
|
||||
private Integer perUserLimit = 1;
|
||||
|
||||
/**
|
||||
* 领取后有效天数
|
||||
*/
|
||||
@Column(name = "valid_days", nullable = false)
|
||||
@Comment("领取后有效天数")
|
||||
private Integer validDays;
|
||||
|
||||
/**
|
||||
* 活动开始时间
|
||||
*/
|
||||
@Column(name = "start_time", nullable = false)
|
||||
@Comment("活动开始时间")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
/**
|
||||
* 活动结束时间
|
||||
*/
|
||||
@Column(name = "end_time", nullable = false)
|
||||
@Comment("活动结束时间")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
/**
|
||||
* 使用规则
|
||||
*/
|
||||
@Column(name = "rules", columnDefinition = "text")
|
||||
@Comment("使用规则")
|
||||
private String rules;
|
||||
|
||||
/**
|
||||
* 状态:0-下架 1-进行中 2-已结束
|
||||
*/
|
||||
@Column(name = "status",columnDefinition = "VARCHAR(15)",length = 15, nullable = false)
|
||||
@Comment("状态:0下架 1进行中 2已结束")
|
||||
private CouponStatus status = CouponStatus.ONLINE;
|
||||
|
||||
|
||||
// ================= 关联关系 =================
|
||||
|
||||
/**
|
||||
* 所属商户
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "merchant_id", insertable = false, updatable = false)
|
||||
private Merchant merchant;
|
||||
|
||||
/**
|
||||
* 可用门店关联
|
||||
*/
|
||||
@OneToMany(mappedBy = "coupon", fetch = FetchType.LAZY)
|
||||
private List<CouponStore> stores;
|
||||
|
||||
/**
|
||||
* 所有券码
|
||||
*/
|
||||
@OneToMany(mappedBy = "coupon", fetch = FetchType.LAZY)
|
||||
private List<CouponCode> codes;
|
||||
|
||||
/**
|
||||
* 赠品商品
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "gift_product_id", insertable = false, updatable = false)
|
||||
private Product giftProduct;
|
||||
|
||||
// ================= 业务方法 =================
|
||||
|
||||
/**
|
||||
* 是否当前有效(进行中 + 时间范围内)
|
||||
*/
|
||||
public boolean isActive() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
return this.status == CouponStatus.ONLINE
|
||||
&& now.isAfter(startTime)
|
||||
&& now.isBefore(endTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否还有库存可领取
|
||||
*/
|
||||
public boolean hasStock() {
|
||||
return claimedCount != null && totalCount != null && claimedCount < totalCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 剩余可领取数量
|
||||
*/
|
||||
public int remainingCount() {
|
||||
if (totalCount == null || claimedCount == null) {
|
||||
return 0;
|
||||
}
|
||||
return totalCount - claimedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 优惠券类型文案
|
||||
*/
|
||||
public String getTypeText() {
|
||||
CouponType typeEnum = this.type;
|
||||
return typeEnum != null ? typeEnum.getDesc() : "未知";
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为赠品券
|
||||
*/
|
||||
public boolean isGiftCoupon() {
|
||||
return this.type == CouponType.GIFT;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
172
src/main/java/com/xjhs/findmemerchant/entity/CouponCode.java
Normal file
172
src/main/java/com/xjhs/findmemerchant/entity/CouponCode.java
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import com.xjhs.findmemerchant.types.CouponCodeStatus;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 优惠券码实例
|
||||
* 对应表:coupon_codes
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "coupon_codes",
|
||||
indexes = {
|
||||
@Index(name = "idx_coupon_codes_coupon_id", columnList = "coupon_id"),
|
||||
@Index(name = "idx_coupon_codes_member_id", columnList = "member_id"),
|
||||
@Index(name = "idx_coupon_codes_status", columnList = "status")
|
||||
},
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_coupon_codes_code", columnNames = "code")
|
||||
}
|
||||
)
|
||||
public class CouponCode extends AbstractBaseEntity {
|
||||
|
||||
|
||||
/**
|
||||
* 券码
|
||||
*/
|
||||
@Column(name = "code", nullable = false, length = 32)
|
||||
@Comment("优惠券码")
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 状态:0-未领取 1-已领取 2-已核销 3-已过期
|
||||
*/
|
||||
@Column(name = "status",columnDefinition = "VARCHAR(20)",length = 20, nullable = false)
|
||||
@Comment("状态:0未领取 1已领取 2已核销 3已过期")
|
||||
private CouponCodeStatus status = CouponCodeStatus.UNCLAIMED;
|
||||
|
||||
/**
|
||||
* 领取时间
|
||||
*/
|
||||
@Column(name = "claimed_at")
|
||||
@Comment("领取时间")
|
||||
private LocalDateTime claimedAt;
|
||||
|
||||
/**
|
||||
* 有效期至
|
||||
*/
|
||||
@Column(name = "valid_until")
|
||||
@Comment("有效期至")
|
||||
private LocalDateTime validUntil;
|
||||
|
||||
/**
|
||||
* 核销时间
|
||||
*/
|
||||
@Column(name = "verified_at")
|
||||
@Comment("核销时间")
|
||||
private LocalDateTime verifiedAt;
|
||||
|
||||
/**
|
||||
* 核销人ID(员工)
|
||||
*/
|
||||
@Column(name = "verified_by")
|
||||
@Comment("核销人ID(员工)")
|
||||
private Long verifiedBy;
|
||||
|
||||
/**
|
||||
* 核销门店ID
|
||||
*/
|
||||
@Column(name = "verify_store_id")
|
||||
@Comment("核销门店ID")
|
||||
private Long verifyStoreId;
|
||||
|
||||
|
||||
// =============== 关联关系 ===============
|
||||
|
||||
/**
|
||||
* 所属优惠券模板
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "coupon_id", insertable = false, updatable = false)
|
||||
private Coupon coupon;
|
||||
|
||||
/**
|
||||
* 会员
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "member_id", insertable = false, updatable = false)
|
||||
private Member member;
|
||||
|
||||
/**
|
||||
* 核销员工
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "verified_by", insertable = false, updatable = false)
|
||||
private Employee verifier;
|
||||
|
||||
/**
|
||||
* 核销门店
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "verify_store_id", insertable = false, updatable = false)
|
||||
private Store verifyStore;
|
||||
|
||||
// =============== 业务方法 ===============
|
||||
|
||||
/**
|
||||
* 是否已领取
|
||||
*/
|
||||
public boolean isClaimed() {
|
||||
return this.status != null &&
|
||||
this.status.getCodeValue() >= CouponCodeStatus.CLAIMED.getCodeValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已核销
|
||||
*/
|
||||
public boolean isVerified() {
|
||||
return this.status == CouponCodeStatus.VERIFIED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已过期
|
||||
*/
|
||||
public boolean isExpired() {
|
||||
CouponCodeStatus statusEnum = this.status;
|
||||
if (statusEnum == CouponCodeStatus.EXPIRED) {
|
||||
return true;
|
||||
}
|
||||
if (validUntil != null && LocalDateTime.now().isAfter(validUntil)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可核销
|
||||
*/
|
||||
public boolean canVerify() {
|
||||
return this.status == CouponCodeStatus.CLAIMED && !isExpired();
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态文案
|
||||
*/
|
||||
public String getStatusText() {
|
||||
CouponCodeStatus statusEnum = this.status;
|
||||
return statusEnum != null ? statusEnum.getDesc() : "未知";
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏券码,例如:ABC***XYZ
|
||||
*/
|
||||
public String maskCode() {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
if (code.length() <= 6) {
|
||||
return code;
|
||||
}
|
||||
return code.substring(0, 3) + "***" + code.substring(code.length() - 3);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 优惠券与门店的关联关系
|
||||
* 对应表:coupon_stores
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "coupon_stores",
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(
|
||||
name = "idx_coupon_store",
|
||||
columnNames = {"coupon_id", "store_id"}
|
||||
)
|
||||
}
|
||||
)
|
||||
public class CouponStore extends AbstractBaseEntity {
|
||||
|
||||
|
||||
/**
|
||||
* 优惠券
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "coupon_id", insertable = false, updatable = false)
|
||||
private Coupon coupon;
|
||||
|
||||
/**
|
||||
* 门店
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "store_id", insertable = false, updatable = false)
|
||||
private Store store;
|
||||
}
|
||||
103
src/main/java/com/xjhs/findmemerchant/entity/Employee.java
Normal file
103
src/main/java/com/xjhs/findmemerchant/entity/Employee.java
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import com.xjhs.findmemerchant.types.CommonStatus;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
/**
|
||||
* 员工实体
|
||||
* 对应表:employees
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "employees",
|
||||
indexes = {
|
||||
@Index(name = "idx_employees_merchant_id", columnList = "merchant_id"),
|
||||
@Index(name = "idx_employees_store_id", columnList = "store_id"),
|
||||
@Index(name = "idx_employees_role_id", columnList = "role_id"),
|
||||
@Index(name = "idx_employees_phone", columnList = "phone")
|
||||
}
|
||||
)
|
||||
public class Employee extends AbstractBaseEntity {
|
||||
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@Column(name = "phone", nullable = false, length = 11)
|
||||
@Comment("手机号")
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 员工姓名
|
||||
*/
|
||||
@Column(name = "name", nullable = false, length = 50)
|
||||
@Comment("员工姓名")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 状态:0-禁用 1-启用
|
||||
*/
|
||||
@Column(name = "status",columnDefinition = "VARCHAR(15)",length = 15, nullable = false)
|
||||
@Comment("状态:0禁用 1启用")
|
||||
private CommonStatus status = CommonStatus.ENABLED;
|
||||
|
||||
|
||||
|
||||
// ===== 关联关系 =====
|
||||
|
||||
/**
|
||||
* 商户
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "merchant_id", insertable = false, updatable = false)
|
||||
private Merchant merchant;
|
||||
|
||||
/**
|
||||
* 门店
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "store_id", insertable = false, updatable = false)
|
||||
private Store store;
|
||||
|
||||
/**
|
||||
* 角色
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "role_id", insertable = false, updatable = false)
|
||||
private Role role;
|
||||
|
||||
// ===== 业务方法 =====
|
||||
|
||||
/**
|
||||
* 是否为启用状态
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return this.status == CommonStatus.ENABLED;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 状态文案
|
||||
*/
|
||||
public String getStatusText() {
|
||||
CommonStatus e = this.status;
|
||||
return e != null ? e.getDesc() : "未知";
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回脱敏手机号,例如:138****1234
|
||||
*/
|
||||
public String maskPhone() {
|
||||
if (phone == null || phone.length() != 11) {
|
||||
return phone;
|
||||
}
|
||||
return phone.substring(0, 3) + "****" + phone.substring(7);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.adapter.json.HashMapJsonConverter;
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import com.xjhs.findmemerchant.types.HealthCertificateStatus;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* 健康证资质
|
||||
* 对应表:health_certificates
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "health_certificates",
|
||||
indexes = {
|
||||
@Index(name = "idx_health_certificates_store_id", columnList = "store_id"),
|
||||
@Index(name = "idx_health_certificates_employee_id", columnList = "employee_id")
|
||||
}
|
||||
)
|
||||
public class HealthCertificate extends AbstractBaseEntity {
|
||||
|
||||
@Column(name = "image_url", nullable = false, length = 500)
|
||||
@Comment("健康证图片URL")
|
||||
private String imageUrl;
|
||||
|
||||
@Column(name = "holder_name", length = 50)
|
||||
@Comment("持证人姓名(OCR识别)")
|
||||
private String holderName;
|
||||
|
||||
@Column(name = "valid_until")
|
||||
@Comment("证件有效期至(OCR识别)")
|
||||
private LocalDateTime validUntil;
|
||||
|
||||
@Column(name = "issuer", length = 100)
|
||||
@Comment("签发机构(OCR识别)")
|
||||
private String issuer;
|
||||
|
||||
@Convert(converter = HashMapJsonConverter.class)
|
||||
@Column(name = "ocr_raw", columnDefinition = "json")
|
||||
@Comment("OCR原始结果")
|
||||
private HashMap<String, Object> ocrRaw;
|
||||
|
||||
@Column(name = "status",columnDefinition = "VARCHAR(20)",length = 20, nullable = false)
|
||||
@Comment("状态:0待审核 1有效 2过期")
|
||||
private HealthCertificateStatus status = HealthCertificateStatus.PENDING;
|
||||
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "store_id", insertable = false, updatable = false)
|
||||
private Store store;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "employee_id", insertable = false, updatable = false)
|
||||
private Employee employee;
|
||||
|
||||
// ===== 业务方法 =====
|
||||
|
||||
public boolean isValid() {
|
||||
if (getStatusEnum() != HealthCertificateStatus.VALID) {
|
||||
return false;
|
||||
}
|
||||
if (validUntil != null && LocalDateTime.now().isAfter(validUntil)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
if (validUntil == null) return false;
|
||||
return LocalDateTime.now().isAfter(validUntil);
|
||||
}
|
||||
|
||||
public HealthCertificateStatus getStatusEnum() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
|
||||
public String getStatusText() {
|
||||
var e = getStatusEnum();
|
||||
return e != null ? e.getDesc() : "未知";
|
||||
}
|
||||
}
|
||||
127
src/main/java/com/xjhs/findmemerchant/entity/Member.java
Normal file
127
src/main/java/com/xjhs/findmemerchant/entity/Member.java
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 会员实体(C 端用户)
|
||||
* 对应表:members
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "members",
|
||||
indexes = {
|
||||
@Index(name = "idx_members_phone", columnList = "phone")
|
||||
},
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(
|
||||
name = "idx_merchant_phone",
|
||||
columnNames = {"merchant_id", "phone"}
|
||||
)
|
||||
}
|
||||
)
|
||||
public class Member extends AbstractBaseEntity {
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@Column(name = "phone", nullable = false, length = 11)
|
||||
@Comment("手机号")
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
@Column(name = "nickname", length = 50)
|
||||
@Comment("昵称")
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 头像地址
|
||||
*/
|
||||
@Column(name = "avatar", length = 500)
|
||||
@Comment("头像URL")
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* 累计订单数
|
||||
*/
|
||||
@Column(name = "total_orders", nullable = false)
|
||||
@Comment("累计订单数")
|
||||
private Integer totalOrders = 0;
|
||||
|
||||
/**
|
||||
* 累计消费金额
|
||||
*/
|
||||
@Column(name = "total_amount", nullable = false, precision = 12, scale = 2)
|
||||
@Comment("累计消费金额")
|
||||
private BigDecimal totalAmount = BigDecimal.ZERO;
|
||||
|
||||
/**
|
||||
* 最后一次下单时间
|
||||
*/
|
||||
@Column(name = "last_order_at")
|
||||
@Comment("最后下单时间")
|
||||
private LocalDateTime lastOrderAt;
|
||||
|
||||
// ============ 关联关系 ============
|
||||
/**
|
||||
* 商户
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "merchant_id", insertable = false, updatable = false)
|
||||
private Merchant merchant;
|
||||
|
||||
/**
|
||||
* 拥有的券码
|
||||
*/
|
||||
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
|
||||
private List<CouponCode> couponCodes;
|
||||
|
||||
/**
|
||||
* 订单列表
|
||||
*/
|
||||
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
|
||||
private List<Order> orders;
|
||||
|
||||
// ============ 业务方法 ============
|
||||
|
||||
/**
|
||||
* 返回脱敏手机号,例如:138****1234
|
||||
*/
|
||||
public String maskPhone() {
|
||||
if (phone == null || phone.length() != 11) {
|
||||
return phone;
|
||||
}
|
||||
return phone.substring(0, 3) + "****" + phone.substring(7);
|
||||
}
|
||||
|
||||
/**
|
||||
* 展示用名称:优先昵称,否则使用脱敏手机号
|
||||
*/
|
||||
public String getDisplayName() {
|
||||
if (nickname != null && !nickname.isBlank()) {
|
||||
return nickname;
|
||||
}
|
||||
return maskPhone();
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回头像URL,若为空则返回默认头像
|
||||
*/
|
||||
public String getAvatarOrDefault() {
|
||||
if (avatar != null && !avatar.isBlank()) {
|
||||
return avatar;
|
||||
}
|
||||
return "/static/default-avatar.png";
|
||||
}
|
||||
}
|
||||
145
src/main/java/com/xjhs/findmemerchant/entity/Merchant.java
Normal file
145
src/main/java/com/xjhs/findmemerchant/entity/Merchant.java
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import com.xjhs.findmemerchant.types.AuthStatus;
|
||||
import com.xjhs.findmemerchant.types.CommonStatus;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 商户实体
|
||||
* 对应表:merchants
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "merchants",
|
||||
indexes = {
|
||||
@Index(name = "idx_merchants_auth_status", columnList = "auth_status"),
|
||||
@Index(name = "idx_merchants_status", columnList = "status")
|
||||
},
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_merchant_phone", columnNames = "phone")
|
||||
}
|
||||
)
|
||||
public class Merchant extends AbstractBaseEntity {
|
||||
|
||||
|
||||
/**
|
||||
* 登录手机号
|
||||
*/
|
||||
@Column(name = "phone", nullable = false, length = 11)
|
||||
@Comment("手机号")
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 登录密码Hash
|
||||
*/
|
||||
@Column(name = "password_hash", length = 255)
|
||||
@Comment("密码Hash")
|
||||
private String passwordHash;
|
||||
|
||||
/**
|
||||
* 真实姓名
|
||||
*/
|
||||
@Column(name = "real_name", length = 50)
|
||||
@Comment("真实姓名")
|
||||
private String realName;
|
||||
|
||||
/**
|
||||
* 身份证号(明文)——不入库
|
||||
*/
|
||||
@Transient
|
||||
private String idCardNo;
|
||||
|
||||
/**
|
||||
* 身份证加密存储
|
||||
*/
|
||||
@Column(name = "id_card_encrypted", length = 255)
|
||||
@Comment("身份证加密存储")
|
||||
private String idCardEncrypted;
|
||||
|
||||
/**
|
||||
* 认证状态
|
||||
*/
|
||||
@Column(name = "auth_status",length = 20,columnDefinition = "VARCHAR(20)", nullable = false)
|
||||
@Comment("认证状态:0未认证 1已认证")
|
||||
private AuthStatus authStatus = AuthStatus.NOT_VERIFIED;
|
||||
|
||||
/**
|
||||
* 账户状态
|
||||
*/
|
||||
@Column(name = "status", nullable = false)
|
||||
@Comment("账户状态:0禁用 1启用")
|
||||
private CommonStatus status = CommonStatus.ENABLED;
|
||||
|
||||
// ========== 关联关系 ==========
|
||||
|
||||
/**
|
||||
* 门店列表
|
||||
*/
|
||||
@OneToMany(mappedBy = "merchant", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<Store> stores;
|
||||
|
||||
/**
|
||||
* 添加一个门店
|
||||
* @param store 门店信息
|
||||
*/
|
||||
public void addStore(Store store) {
|
||||
store.setMerchant(this);
|
||||
this.stores.add(store);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 营业执照
|
||||
*/
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "id", referencedColumnName = "merchant_id", insertable = false, updatable = false)
|
||||
private BusinessLicense businessLicense;
|
||||
|
||||
// ========== 业务方法 ==========
|
||||
|
||||
/**
|
||||
* 是否已完成实名认证
|
||||
*/
|
||||
public boolean isAuthenticated() {
|
||||
return getAuthStatusEnum() == AuthStatus.VERIFIED;
|
||||
}
|
||||
|
||||
public AuthStatus getAuthStatusEnum() {
|
||||
return this.authStatus;
|
||||
}
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return getStatusEnum() == CommonStatus.ENABLED;
|
||||
}
|
||||
|
||||
public CommonStatus getStatusEnum() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机脱敏
|
||||
*/
|
||||
public String maskPhone() {
|
||||
if (phone == null || phone.length() != 11) return phone;
|
||||
return phone.substring(0, 3) + "****" + phone.substring(7);
|
||||
}
|
||||
|
||||
/**
|
||||
* 身份证脱敏
|
||||
*/
|
||||
public String maskIdCard() {
|
||||
if (idCardNo == null || idCardNo.length() != 18) return idCardNo;
|
||||
return idCardNo.substring(0, 3) + "***********" + idCardNo.substring(14);
|
||||
}
|
||||
}
|
||||
122
src/main/java/com/xjhs/findmemerchant/entity/Message.java
Normal file
122
src/main/java/com/xjhs/findmemerchant/entity/Message.java
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import com.xjhs.findmemerchant.types.MessageType;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 消息实体
|
||||
* 对应表:messages
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "messages",
|
||||
indexes = {
|
||||
@Index(name = "idx_messages_merchant", columnList = "merchant_id"),
|
||||
@Index(name = "idx_messages_type_read", columnList = "merchant_id,type,is_read")
|
||||
}
|
||||
)
|
||||
public class Message extends AbstractBaseEntity {
|
||||
|
||||
/**
|
||||
* 消息类型:1-系统通知 2-活动提醒 3-私信
|
||||
*/
|
||||
@Column(name = "type",columnDefinition = "VARCHAR(20)",length = 20,nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Comment("消息类型:1系统通知 2活动提醒 3私信")
|
||||
private MessageType type;
|
||||
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
@Column(name = "title", nullable = false, length = 200)
|
||||
@Comment("消息标题")
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 内容
|
||||
*/
|
||||
@Column(name = "content", nullable = false, columnDefinition = "text")
|
||||
@Comment("消息内容")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 是否已读:0-未读 1-已读
|
||||
*/
|
||||
@Column(name = "is_read", nullable = false)
|
||||
@Comment("是否已读:0未读 1已读")
|
||||
private Byte isRead = 0;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Column(name = "created_at", nullable = false)
|
||||
@Comment("创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 阅读时间
|
||||
*/
|
||||
@Column(name = "read_at")
|
||||
@Comment("阅读时间")
|
||||
private LocalDateTime readAt;
|
||||
|
||||
/**
|
||||
* 商户
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "merchant_id", insertable = false, updatable = false)
|
||||
private Merchant merchant;
|
||||
|
||||
// ========== 业务方法 ==========
|
||||
|
||||
/**
|
||||
* 标记为已读
|
||||
*/
|
||||
public void markAsRead() {
|
||||
this.isRead = 1;
|
||||
this.readAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否未读
|
||||
*/
|
||||
public boolean isUnread() {
|
||||
return this.isRead != null && this.isRead == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息类型文案
|
||||
*/
|
||||
public String getTypeText() {
|
||||
MessageType t = getTypeEnum();
|
||||
return t != null ? t.getDesc() : "未知";
|
||||
}
|
||||
|
||||
/**
|
||||
* 内容摘要,最多100字符
|
||||
*/
|
||||
public String getSummary() {
|
||||
if (content == null) {
|
||||
return "";
|
||||
}
|
||||
if (content.length() <= 100) {
|
||||
return content;
|
||||
}
|
||||
return content.substring(0, 100) + "...";
|
||||
}
|
||||
|
||||
// ========== 枚举转换辅助 ==========
|
||||
|
||||
public MessageType getTypeEnum() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
}
|
||||
168
src/main/java/com/xjhs/findmemerchant/entity/Order.java
Normal file
168
src/main/java/com/xjhs/findmemerchant/entity/Order.java
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import com.xjhs.findmemerchant.types.OrderStatus;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 订单实体
|
||||
* 对应表:orders
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "orders",
|
||||
indexes = {
|
||||
@Index(name = "idx_orders_store_id", columnList = "store_id"),
|
||||
@Index(name = "idx_orders_member_id", columnList = "member_id"),
|
||||
@Index(name = "idx_orders_status", columnList = "status")
|
||||
},
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_orders_order_no", columnNames = "order_no")
|
||||
}
|
||||
)
|
||||
public class Order extends AbstractBaseEntity {
|
||||
/**
|
||||
* 订单号
|
||||
*/
|
||||
@Column(name = "order_no", nullable = false, length = 32)
|
||||
@Comment("订单号")
|
||||
private String orderNo;
|
||||
|
||||
|
||||
/**
|
||||
* 订单总金额
|
||||
*/
|
||||
@Column(name = "total_amount", nullable = false, precision = 10, scale = 2)
|
||||
@Comment("订单总金额")
|
||||
private BigDecimal totalAmount;
|
||||
|
||||
/**
|
||||
* 优惠金额
|
||||
*/
|
||||
@Column(name = "discount_amount", nullable = false, precision = 10, scale = 2)
|
||||
@Comment("优惠金额")
|
||||
private BigDecimal discountAmount = BigDecimal.ZERO;
|
||||
|
||||
/**
|
||||
* 实付金额
|
||||
*/
|
||||
@Column(name = "pay_amount", nullable = false, precision = 10, scale = 2)
|
||||
@Comment("实付金额")
|
||||
private BigDecimal payAmount;
|
||||
|
||||
/**
|
||||
* 使用的券码ID(可为空)
|
||||
*/
|
||||
@Column(name = "coupon_code_id")
|
||||
@Comment("优惠券码ID")
|
||||
private Long couponCodeId;
|
||||
|
||||
/**
|
||||
* 订单状态:1-待支付 2-已支付 3-已完成 4-已退款 5-已取消
|
||||
*/
|
||||
@Column(name = "status", nullable = false)
|
||||
@Comment("订单状态:1待支付 2已支付 3已完成 4已退款 5已取消")
|
||||
private OrderStatus status;
|
||||
|
||||
/**
|
||||
* 支付时间
|
||||
*/
|
||||
@Column(name = "paid_at")
|
||||
@Comment("支付时间")
|
||||
private LocalDateTime paidAt;
|
||||
|
||||
|
||||
// ========== 关联关系 ==========
|
||||
|
||||
/**
|
||||
* 门店
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "store_id", insertable = false, updatable = false)
|
||||
private Store store;
|
||||
|
||||
/**
|
||||
* 会员
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "member_id", insertable = false, updatable = false)
|
||||
private Member member;
|
||||
|
||||
/**
|
||||
* 优惠券码
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "coupon_code_id", insertable = false, updatable = false)
|
||||
private CouponCode couponCode;
|
||||
|
||||
/**
|
||||
* 订单明细
|
||||
*/
|
||||
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
|
||||
private List<OrderItem> items;
|
||||
|
||||
// ========== 业务方法 ==========
|
||||
|
||||
/**
|
||||
* 是否已支付(状态>=已支付且非已取消)
|
||||
*/
|
||||
public boolean isPaid() {
|
||||
OrderStatus s = getStatusEnum();
|
||||
if (s == null) return false;
|
||||
return s.getCodeValue() >= OrderStatus.PAID.getCodeValue()
|
||||
&& s != OrderStatus.CANCELLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已完成
|
||||
*/
|
||||
public boolean isCompleted() {
|
||||
return getStatusEnum() == OrderStatus.COMPLETED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可退款(已支付或已完成)
|
||||
*/
|
||||
public boolean canRefund() {
|
||||
OrderStatus s = getStatusEnum();
|
||||
return s == OrderStatus.PAID || s == OrderStatus.COMPLETED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可取消(待支付)
|
||||
*/
|
||||
public boolean canCancel() {
|
||||
return getStatusEnum() == OrderStatus.PENDING;
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态文案
|
||||
*/
|
||||
public String getStatusText() {
|
||||
OrderStatus s = getStatusEnum();
|
||||
return s != null ? s.getDesc() : "未知";
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有优惠
|
||||
*/
|
||||
public boolean hasDiscount() {
|
||||
return discountAmount != null && discountAmount.compareTo(BigDecimal.ZERO) > 0;
|
||||
}
|
||||
|
||||
// ========== 枚举转换辅助 ==========
|
||||
|
||||
public OrderStatus getStatusEnum() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
}
|
||||
124
src/main/java/com/xjhs/findmemerchant/entity/OrderItem.java
Normal file
124
src/main/java/com/xjhs/findmemerchant/entity/OrderItem.java
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.adapter.id.SnowflakeGenerated;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 订单明细
|
||||
* 对应表:order_items
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "order_items",
|
||||
indexes = {
|
||||
@Index(name = "idx_order_items_order_id", columnList = "order_id"),
|
||||
@Index(name = "idx_order_items_product_id", columnList = "product_id"),
|
||||
@Index(name = "idx_order_items_sku_id", columnList = "sku_id")
|
||||
}
|
||||
)
|
||||
public class OrderItem {
|
||||
|
||||
/**
|
||||
* 主键(雪花ID)
|
||||
*/
|
||||
@Id
|
||||
@GeneratedValue
|
||||
@SnowflakeGenerated
|
||||
@Column(nullable = false)
|
||||
@Comment("主键ID")
|
||||
private Long id;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 商品ID(可能被删除所以可空)
|
||||
*/
|
||||
@Column(name = "product_id")
|
||||
@Comment("商品ID(可空)")
|
||||
private Long productId;
|
||||
|
||||
/**
|
||||
* 商品名称(快照)
|
||||
*/
|
||||
@Column(name = "product_name", nullable = false, length = 100)
|
||||
@Comment("商品名称(快照)")
|
||||
private String productName;
|
||||
|
||||
/**
|
||||
* SKU ID(可能被删除所以可空)
|
||||
*/
|
||||
@Column(name = "sku_id")
|
||||
@Comment("SKU ID(可空)")
|
||||
private Long skuId;
|
||||
|
||||
/**
|
||||
* SKU 名称(快照)
|
||||
*/
|
||||
@Column(name = "sku_name", length = 100)
|
||||
@Comment("SKU名称(快照)")
|
||||
private String skuName;
|
||||
|
||||
/**
|
||||
* 商品图片(快照)
|
||||
*/
|
||||
@Column(name = "image", length = 255)
|
||||
@Comment("商品图片(快照)")
|
||||
private String image;
|
||||
|
||||
/**
|
||||
* 商品单价
|
||||
*/
|
||||
@Column(name = "unit_price", nullable = false, precision = 10, scale = 2)
|
||||
@Comment("商品单价")
|
||||
private BigDecimal unitPrice;
|
||||
|
||||
/**
|
||||
* 购买数量
|
||||
*/
|
||||
@Column(name = "quantity", nullable = false)
|
||||
@Comment("购买数量")
|
||||
private Integer quantity = 1;
|
||||
|
||||
/**
|
||||
* 小计金额
|
||||
*/
|
||||
@Column(name = "total_price", nullable = false, precision = 10, scale = 2)
|
||||
@Comment("小计金额")
|
||||
private BigDecimal totalPrice;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Column(name = "created_at", nullable = false)
|
||||
@Comment("创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
// ========== 关联关系 ==========
|
||||
|
||||
/**
|
||||
* 所属订单
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "order_id", insertable = false, updatable = false)
|
||||
private Order order;
|
||||
|
||||
// ========== 业务方法 ==========
|
||||
|
||||
/**
|
||||
* 根据单价 * 数量计算小计
|
||||
*/
|
||||
public BigDecimal getSubtotal() {
|
||||
if (unitPrice == null || quantity == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
return unitPrice.multiply(BigDecimal.valueOf(quantity));
|
||||
}
|
||||
}
|
||||
189
src/main/java/com/xjhs/findmemerchant/entity/Product.java
Normal file
189
src/main/java/com/xjhs/findmemerchant/entity/Product.java
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import com.xjhs.findmemerchant.types.ProductStatus;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 商品实体
|
||||
* 对应表:products
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "products",
|
||||
indexes = {
|
||||
@Index(name = "idx_products_merchant_id", columnList = "merchant_id"),
|
||||
@Index(name = "idx_products_store_id", columnList = "store_id"),
|
||||
@Index(name = "idx_products_category_id", columnList = "category_id"),
|
||||
@Index(name = "idx_products_status", columnList = "status")
|
||||
}
|
||||
)
|
||||
public class Product extends AbstractBaseEntity {
|
||||
|
||||
/**
|
||||
* 商品名称
|
||||
*/
|
||||
@Column(name = "name", nullable = false, length = 100)
|
||||
@Comment("商品名称")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 商品描述
|
||||
*/
|
||||
@Column(name = "description", columnDefinition = "text")
|
||||
@Comment("商品描述")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 商品图片列表(JSON数组字符串)
|
||||
*/
|
||||
@Column(name = "images", columnDefinition = "text")
|
||||
@Comment("商品图片(JSON数组)")
|
||||
private String images;
|
||||
|
||||
/**
|
||||
* 原价
|
||||
*/
|
||||
@Column(name = "original_price", nullable = false, precision = 10, scale = 2)
|
||||
@Comment("原价")
|
||||
private BigDecimal originalPrice;
|
||||
|
||||
/**
|
||||
* 售价
|
||||
*/
|
||||
@Column(name = "sale_price", nullable = false, precision = 10, scale = 2)
|
||||
@Comment("售价")
|
||||
private BigDecimal salePrice;
|
||||
|
||||
/**
|
||||
* 库存,-1 表示无限库存
|
||||
*/
|
||||
@Column(name = "stock", nullable = false)
|
||||
@Comment("库存,-1表示无限库存")
|
||||
private Integer stock = -1;
|
||||
|
||||
/**
|
||||
* 已售数量
|
||||
*/
|
||||
@Column(name = "sold_count", nullable = false)
|
||||
@Comment("已售数量")
|
||||
private Integer soldCount = 0;
|
||||
|
||||
/**
|
||||
* 排序值
|
||||
*/
|
||||
@Column(name = "sort_order", nullable = false)
|
||||
@Comment("排序值")
|
||||
private Integer sortOrder = 0;
|
||||
|
||||
/**
|
||||
* 商品状态:0-下架 1-上架
|
||||
*/
|
||||
@Column(name = "status",columnDefinition = "VARCHAR(20)",length = 20, nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Comment("商品状态:0下架 1上架")
|
||||
private ProductStatus status = ProductStatus.OFF_SALE;
|
||||
|
||||
// ========== 关联关系 ==========
|
||||
|
||||
/**
|
||||
* 商户
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "merchant_id", insertable = false, updatable = false)
|
||||
private Merchant merchant;
|
||||
|
||||
/**
|
||||
* 门店
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "store_id", insertable = false, updatable = false)
|
||||
private Store store;
|
||||
|
||||
/**
|
||||
* 商品分类
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "category_id", insertable = false, updatable = false)
|
||||
private ProductCategory category;
|
||||
|
||||
/**
|
||||
* SKU 列表
|
||||
*/
|
||||
@OneToMany(mappedBy = "product", fetch = FetchType.LAZY)
|
||||
private List<ProductSKU> skus;
|
||||
|
||||
// ========== 业务方法 ==========
|
||||
|
||||
/**
|
||||
* 是否上架中
|
||||
*/
|
||||
public boolean isOnSale() {
|
||||
return getStatusEnum() == ProductStatus.ON_SALE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有库存
|
||||
*/
|
||||
public boolean hasStock() {
|
||||
if (stock == null) return false;
|
||||
return stock == -1 || stock > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有折扣(售价 < 原价)
|
||||
*/
|
||||
public boolean hasDiscount() {
|
||||
if (salePrice == null || originalPrice == null) return false;
|
||||
return salePrice.compareTo(originalPrice) < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 折扣率 (0-100),整数部分
|
||||
*/
|
||||
public int getDiscountRate() {
|
||||
if (originalPrice == null || originalPrice.compareTo(BigDecimal.ZERO) == 0
|
||||
|| salePrice == null) {
|
||||
return 0;
|
||||
}
|
||||
BigDecimal rate = salePrice
|
||||
.divide(originalPrice, 2, BigDecimal.ROUND_HALF_UP)
|
||||
.multiply(BigDecimal.valueOf(100));
|
||||
return rate.intValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态文案
|
||||
*/
|
||||
public String getStatusText() {
|
||||
ProductStatus s = getStatusEnum();
|
||||
return s != null ? s.getDesc() : "未知";
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可扣减库存
|
||||
*/
|
||||
public boolean canDeductStock(int quantity) {
|
||||
if (stock == null || quantity <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (stock == -1) {
|
||||
return true; // 无限库存
|
||||
}
|
||||
return stock >= quantity;
|
||||
}
|
||||
|
||||
// ========== 枚举转换辅助 ==========
|
||||
|
||||
public ProductStatus getStatusEnum() {
|
||||
return this.status;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import com.xjhs.findmemerchant.types.CommonStatus;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 商品分类实体
|
||||
* 对应表:product_categories
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "product_categories",
|
||||
indexes = {
|
||||
@Index(name = "idx_product_categories_merchant_id", columnList = "merchant_id"),
|
||||
@Index(name = "idx_product_categories_parent_id", columnList = "parent_id"),
|
||||
@Index(name = "idx_product_categories_status", columnList = "status")
|
||||
}
|
||||
)
|
||||
public class ProductCategory extends AbstractBaseEntity {
|
||||
|
||||
/**
|
||||
* 分类名称
|
||||
*/
|
||||
@Column(name = "name", nullable = false, length = 50)
|
||||
@Comment("分类名称")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 父分类ID,支持二级分类
|
||||
*/
|
||||
@Column(name = "parent_id")
|
||||
@Comment("父分类ID,支持二级分类")
|
||||
private Long parentId;
|
||||
|
||||
/**
|
||||
* 排序值
|
||||
*/
|
||||
@Column(name = "sort_order", nullable = false)
|
||||
@Comment("排序值")
|
||||
private Integer sortOrder = 0;
|
||||
|
||||
/**
|
||||
* 状态:0-禁用 1-启用
|
||||
*/
|
||||
@Column(name = "status", nullable = false)
|
||||
@Comment("状态:0禁用 1启用")
|
||||
private CommonStatus status = CommonStatus.ENABLED;
|
||||
|
||||
// ========== 关联关系 ==========
|
||||
|
||||
/**
|
||||
* 商户
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "merchant_id", insertable = false, updatable = false)
|
||||
private Merchant merchant;
|
||||
|
||||
/**
|
||||
* 父分类
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "parent_id", insertable = false, updatable = false)
|
||||
private ProductCategory parent;
|
||||
|
||||
/**
|
||||
* 子分类列表
|
||||
*/
|
||||
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
|
||||
private List<ProductCategory> children;
|
||||
|
||||
/**
|
||||
* 分类下商品
|
||||
*/
|
||||
@OneToMany(mappedBy = "category", fetch = FetchType.LAZY)
|
||||
private List<Product> products;
|
||||
|
||||
// ========== 业务方法 ==========
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return getStatusEnum() == CommonStatus.ENABLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为一级分类(无父分类)
|
||||
*/
|
||||
public boolean isTopLevel() {
|
||||
return parentId == null;
|
||||
}
|
||||
|
||||
public CommonStatus getStatusEnum() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public String getStatusText() {
|
||||
CommonStatus e = getStatusEnum();
|
||||
return e != null ? e.getDesc() : "未知";
|
||||
}
|
||||
}
|
||||
127
src/main/java/com/xjhs/findmemerchant/entity/ProductSKU.java
Normal file
127
src/main/java/com/xjhs/findmemerchant/entity/ProductSKU.java
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.adapter.id.SnowflakeGenerated;
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import com.xjhs.findmemerchant.types.CommonStatus;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 商品SKU(规格)实体
|
||||
* 对应表:product_skus
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "product_skus",
|
||||
indexes = {
|
||||
@Index(name = "idx_product_skus_product_id", columnList = "product_id"),
|
||||
@Index(name = "idx_product_skus_sku_code", columnList = "sku_code")
|
||||
}
|
||||
)
|
||||
public class ProductSKU extends AbstractBaseEntity {
|
||||
|
||||
/**
|
||||
* 主键(雪花ID)
|
||||
*/
|
||||
@Id
|
||||
@GeneratedValue
|
||||
@SnowflakeGenerated
|
||||
@Column(nullable = false)
|
||||
@Comment("主键ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 规格名称(如:大杯/加冰)
|
||||
*/
|
||||
@Column(name = "sku_name", nullable = false, length = 100)
|
||||
@Comment("规格名称")
|
||||
private String skuName;
|
||||
|
||||
/**
|
||||
* SKU 编码
|
||||
*/
|
||||
@Column(name = "sku_code", length = 50)
|
||||
@Comment("SKU编码")
|
||||
private String skuCode;
|
||||
|
||||
/**
|
||||
* 售价
|
||||
*/
|
||||
@Column(name = "price", nullable = false, precision = 10, scale = 2)
|
||||
@Comment("规格价格")
|
||||
private BigDecimal price;
|
||||
|
||||
/**
|
||||
* 库存,-1 表示无限库存
|
||||
*/
|
||||
@Column(name = "stock", nullable = false)
|
||||
@Comment("库存,-1表示无限库存")
|
||||
private Integer stock = -1;
|
||||
|
||||
/**
|
||||
* 已售数量
|
||||
*/
|
||||
@Column(name = "sold_count", nullable = false)
|
||||
@Comment("已售数量")
|
||||
private Integer soldCount = 0;
|
||||
|
||||
/**
|
||||
* 状态:0-禁用 1-启用
|
||||
*/
|
||||
@Column(name = "status",columnDefinition = "VARCHAR(20)",length = 20,nullable = false)
|
||||
@Comment("状态:0禁用 1启用")
|
||||
private CommonStatus status = CommonStatus.ENABLED;
|
||||
|
||||
|
||||
/**
|
||||
* 所属商品
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "product_id", insertable = false, updatable = false)
|
||||
private Product product;
|
||||
|
||||
// ===== 业务方法 =====
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return getStatusEnum() == CommonStatus.ENABLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有库存
|
||||
*/
|
||||
public boolean hasStock() {
|
||||
if (stock == null) return false;
|
||||
return stock == -1 || stock > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可扣减库存
|
||||
*/
|
||||
public boolean canDeductStock(int quantity) {
|
||||
if (stock == null || quantity <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (stock == -1) {
|
||||
return true;
|
||||
}
|
||||
return stock >= quantity;
|
||||
}
|
||||
|
||||
public CommonStatus getStatusEnum() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public String getStatusText() {
|
||||
CommonStatus e = getStatusEnum();
|
||||
return e != null ? e.getDesc() : "未知";
|
||||
}
|
||||
}
|
||||
168
src/main/java/com/xjhs/findmemerchant/entity/Review.java
Normal file
168
src/main/java/com/xjhs/findmemerchant/entity/Review.java
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import com.xjhs.findmemerchant.types.ReviewStatus;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
/**
|
||||
* 用户评价(C端共享)
|
||||
* 对应表:reviews
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "reviews",
|
||||
indexes = {
|
||||
@Index(name = "idx_reviews_order_id", columnList = "order_id"),
|
||||
@Index(name = "idx_reviews_merchant_id", columnList = "merchant_id"),
|
||||
@Index(name = "idx_reviews_store_id", columnList = "store_id"),
|
||||
@Index(name = "idx_reviews_user_id", columnList = "user_id"),
|
||||
@Index(name = "idx_reviews_created_at", columnList = "created_at")
|
||||
}
|
||||
)
|
||||
public class Review extends AbstractBaseEntity {
|
||||
|
||||
|
||||
/**
|
||||
* 订单ID
|
||||
*/
|
||||
@Column(name = "order_id", nullable = false)
|
||||
@Comment("订单ID")
|
||||
private Long orderId;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* C端用户ID
|
||||
*/
|
||||
@Column(name = "user_id", nullable = false)
|
||||
@Comment("C端用户ID")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
@Column(name = "user_name", length = 50)
|
||||
@Comment("用户昵称")
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 用户头像
|
||||
*/
|
||||
@Column(name = "user_avatar", length = 255)
|
||||
@Comment("用户头像URL")
|
||||
private String userAvatar;
|
||||
|
||||
/**
|
||||
* 评分 1-5
|
||||
*/
|
||||
@Column(name = "rating", nullable = false)
|
||||
@Comment("评分 1-5")
|
||||
private Integer rating;
|
||||
|
||||
/**
|
||||
* 评价内容
|
||||
*/
|
||||
@Column(name = "content", columnDefinition = "text")
|
||||
@Comment("评价内容")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 图片URL,逗号分隔
|
||||
*/
|
||||
@Column(name = "images", length = 1000)
|
||||
@Comment("评价图片URL,逗号分隔")
|
||||
private String images;
|
||||
|
||||
/**
|
||||
* 是否匿名
|
||||
*/
|
||||
@Column(name = "is_anonymous", nullable = false)
|
||||
@Comment("是否匿名")
|
||||
private Boolean isAnonymous = Boolean.FALSE;
|
||||
|
||||
/**
|
||||
* 状态:1-正常 0-隐藏
|
||||
*/
|
||||
@Column(name = "status",columnDefinition = "VARCHAR(20)",length = 20, nullable = false)
|
||||
@Comment("状态:1正常 0隐藏")
|
||||
private ReviewStatus status = ReviewStatus.NORMAL;
|
||||
|
||||
// ========== 关联关系 ==========
|
||||
|
||||
/**
|
||||
* 回复
|
||||
*/
|
||||
@OneToOne(mappedBy = "review", fetch = FetchType.LAZY)
|
||||
private ReviewReply reply;
|
||||
|
||||
/**
|
||||
* 门店
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "store_id", insertable = false, updatable = false)
|
||||
private Store store;
|
||||
|
||||
/**
|
||||
* 订单
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "order_id", insertable = false, updatable = false)
|
||||
private Order order;
|
||||
|
||||
// ========== 业务方法 ==========
|
||||
|
||||
/**
|
||||
* 评分文案
|
||||
*/
|
||||
public String getRatingText() {
|
||||
if (rating == null) {
|
||||
return "未知";
|
||||
}
|
||||
return switch (rating) {
|
||||
case 5 -> "非常满意";
|
||||
case 4 -> "满意";
|
||||
case 3 -> "一般";
|
||||
case 2 -> "不满意";
|
||||
case 1 -> "非常不满意";
|
||||
default -> "未知";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否好评(4-5星)
|
||||
*/
|
||||
public boolean isPositive() {
|
||||
return rating != null && rating >= 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否差评(1-2星)
|
||||
*/
|
||||
public boolean isNegative() {
|
||||
return rating != null && rating <= 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 展示用昵称(匿名/真实)
|
||||
*/
|
||||
public String getDisplayName() {
|
||||
if (Boolean.TRUE.equals(isAnonymous)) {
|
||||
return "匿名用户";
|
||||
}
|
||||
return userName;
|
||||
}
|
||||
|
||||
public ReviewStatus getStatusEnum() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public String getStatusText() {
|
||||
ReviewStatus e = getStatusEnum();
|
||||
return e != null ? e.getDesc() : "未知";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.adapter.id.SnowflakeGenerated;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
/**
|
||||
* 商家对评价的回复
|
||||
* 对应表:review_replies
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "review_replies",
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_review_replies_review_id", columnNames = "review_id")
|
||||
},
|
||||
indexes = {
|
||||
@Index(name = "idx_review_replies_merchant_id", columnList = "merchant_id")
|
||||
}
|
||||
)
|
||||
public class ReviewReply{
|
||||
|
||||
/**
|
||||
* 主键(雪花ID)
|
||||
*/
|
||||
@Id
|
||||
@GeneratedValue
|
||||
@SnowflakeGenerated
|
||||
@Column(nullable = false)
|
||||
@Comment("主键ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 评价ID(唯一,一个评价一条回复)
|
||||
*/
|
||||
@Column(name = "review_id", nullable = false)
|
||||
@Comment("评价ID")
|
||||
private Long reviewId;
|
||||
|
||||
/**
|
||||
* 商户ID
|
||||
*/
|
||||
@Column(name = "merchant_id", nullable = false)
|
||||
@Comment("商户ID")
|
||||
private Long merchantId;
|
||||
|
||||
/**
|
||||
* 回复内容
|
||||
*/
|
||||
@Column(name = "content", nullable = false, columnDefinition = "text")
|
||||
@Comment("回复内容")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 回复人ID(商家或员工)
|
||||
*/
|
||||
@Column(name = "replier_id", nullable = false)
|
||||
@Comment("回复人ID(商家或员工)")
|
||||
private Long replierId;
|
||||
|
||||
/**
|
||||
* 回复人名称
|
||||
*/
|
||||
@Column(name = "replier_name", length = 50)
|
||||
@Comment("回复人名称")
|
||||
private String replierName;
|
||||
|
||||
/**
|
||||
* 所属评价
|
||||
*/
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "review_id", insertable = false, updatable = false)
|
||||
private Review review;
|
||||
}
|
||||
162
src/main/java/com/xjhs/findmemerchant/entity/Role.java
Normal file
162
src/main/java/com/xjhs/findmemerchant/entity/Role.java
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.adapter.json.StringListJsonConverter;
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.xjhs.findmemerchant.constants.RoleConstants.*;
|
||||
|
||||
|
||||
/**
|
||||
* 角色实体
|
||||
* 对应表:roles
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "roles",
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_roles_code", columnNames = "code")
|
||||
}
|
||||
)
|
||||
public class Role extends AbstractBaseEntity {
|
||||
|
||||
|
||||
/**
|
||||
* 角色名称
|
||||
*/
|
||||
@Column(name = "name", nullable = false, length = 50)
|
||||
@Comment("角色名称")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 角色编码(唯一)
|
||||
*/
|
||||
@Column(name = "code", nullable = false, length = 50)
|
||||
@Comment("角色编码")
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@Column(name = "description", length = 200)
|
||||
@Comment("角色描述")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 权限列表(JSON数组)
|
||||
*/
|
||||
@Convert(converter = StringListJsonConverter.class)
|
||||
@Column(name = "permissions", nullable = false, columnDefinition = "json")
|
||||
@Comment("权限列表(JSON数组)")
|
||||
private List<String> permissions = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 是否系统内置角色:1-是 0-否
|
||||
*/
|
||||
@Column(name = "is_system", nullable = false)
|
||||
@Comment("是否系统内置角色:1是 0否")
|
||||
private Byte isSystem = 0;
|
||||
|
||||
// ========== 业务方法 ==========
|
||||
|
||||
/**
|
||||
* 是否系统预置角色
|
||||
*/
|
||||
public boolean isSystemRole() {
|
||||
return Byte.valueOf((byte) 1).equals(isSystem);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否拥有指定权限(支持 *)
|
||||
*/
|
||||
public boolean hasPermission(String permission) {
|
||||
if (permission == null || permissions == null || permissions.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (String p : permissions) {
|
||||
if (PERMISSION_ALL.equals(p) || permission.equals(p)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回权限列表,避免 null
|
||||
*/
|
||||
public List<String> getPermissionsSafe() {
|
||||
return permissions != null ? permissions : new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统内置默认角色(不带ID、时间,用于初始化)
|
||||
*/
|
||||
public static List<Role> getDefaultRoles() {
|
||||
List<Role> roles = new ArrayList<>();
|
||||
|
||||
// 店长:全部权限
|
||||
Role owner = new Role();
|
||||
owner.setCode(ROLE_CODE_OWNER);
|
||||
owner.setName("店长");
|
||||
owner.setPermissions(List.of(PERMISSION_ALL));
|
||||
owner.setIsSystem((byte) 1);
|
||||
roles.add(owner);
|
||||
|
||||
// 运营
|
||||
Role operator = new Role();
|
||||
operator.setCode(ROLE_CODE_OPERATOR);
|
||||
operator.setName("运营");
|
||||
operator.setPermissions(List.of(
|
||||
PERMISSION_STORE_VIEW,
|
||||
PERMISSION_COUPON_MANAGE,
|
||||
PERMISSION_COUPON_VIEW,
|
||||
PERMISSION_COUPON_VERIFY,
|
||||
PERMISSION_ORDER_VIEW,
|
||||
PERMISSION_ACTIVITY_MANAGE,
|
||||
PERMISSION_ACTIVITY_VIEW,
|
||||
PERMISSION_ANALYTICS_VIEW,
|
||||
PERMISSION_MEMBER_VIEW
|
||||
));
|
||||
operator.setIsSystem((byte) 1);
|
||||
roles.add(operator);
|
||||
|
||||
// 客服
|
||||
Role service = new Role();
|
||||
service.setCode(ROLE_CODE_SERVICE);
|
||||
service.setName("客服");
|
||||
service.setPermissions(List.of(
|
||||
PERMISSION_STORE_VIEW,
|
||||
PERMISSION_COUPON_VIEW,
|
||||
PERMISSION_COUPON_VERIFY,
|
||||
PERMISSION_ORDER_VIEW,
|
||||
PERMISSION_MEMBER_VIEW,
|
||||
PERMISSION_MESSAGE_VIEW
|
||||
));
|
||||
service.setIsSystem((byte) 1);
|
||||
roles.add(service);
|
||||
|
||||
// 营销管理员
|
||||
Role marketing = new Role();
|
||||
marketing.setCode(ROLE_CODE_MARKETING);
|
||||
marketing.setName("营销管理员");
|
||||
marketing.setPermissions(List.of(
|
||||
PERMISSION_COUPON_MANAGE,
|
||||
PERMISSION_COUPON_VIEW,
|
||||
PERMISSION_ACTIVITY_MANAGE,
|
||||
PERMISSION_ACTIVITY_VIEW,
|
||||
PERMISSION_ANALYTICS_VIEW
|
||||
));
|
||||
marketing.setIsSystem((byte) 1);
|
||||
roles.add(marketing);
|
||||
|
||||
return roles;
|
||||
}
|
||||
}
|
||||
167
src/main/java/com/xjhs/findmemerchant/entity/Settlement.java
Normal file
167
src/main/java/com/xjhs/findmemerchant/entity/Settlement.java
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import com.xjhs.findmemerchant.types.SettlementStatus;
|
||||
import com.xjhs.findmemerchant.types.SettlementType;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* 结算记录(按日/按周)
|
||||
* 对应表:settlements
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "settlements",
|
||||
indexes = {
|
||||
@Index(name = "idx_settlements_merchant_id", columnList = "merchant_id"),
|
||||
@Index(name = "idx_settlements_status", columnList = "status")
|
||||
},
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_settlements_no", columnNames = "settlement_no")
|
||||
}
|
||||
)
|
||||
public class Settlement extends AbstractBaseEntity {
|
||||
|
||||
private static final DateTimeFormatter PERIOD_DATE_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
/**
|
||||
* 商户
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "merchant_id", insertable = false, updatable = false)
|
||||
private Merchant merchant;
|
||||
|
||||
|
||||
/**
|
||||
* 结算单号
|
||||
*/
|
||||
@Column(name = "settlement_no", nullable = false, length = 32)
|
||||
@Comment("结算单号")
|
||||
private String settlementNo;
|
||||
|
||||
/**
|
||||
* 结算类型:1-日结 2-周结
|
||||
*/
|
||||
@Column(name = "type", nullable = false)
|
||||
@Comment("结算类型:1日结 2周结")
|
||||
private SettlementType type;
|
||||
|
||||
/**
|
||||
* 结算周期开始时间
|
||||
*/
|
||||
@Column(name = "period_start", nullable = false)
|
||||
@Comment("结算周期开始时间")
|
||||
private LocalDateTime periodStart;
|
||||
|
||||
/**
|
||||
* 结算周期结束时间
|
||||
*/
|
||||
@Column(name = "period_end", nullable = false)
|
||||
@Comment("结算周期结束时间")
|
||||
private LocalDateTime periodEnd;
|
||||
|
||||
/**
|
||||
* 订单数量
|
||||
*/
|
||||
@Column(name = "order_count", nullable = false)
|
||||
@Comment("订单数量")
|
||||
private Integer orderCount = 0;
|
||||
|
||||
/**
|
||||
* 订单总额
|
||||
*/
|
||||
@Column(name = "total_amount", nullable = false, precision = 12, scale = 2)
|
||||
@Comment("订单总额")
|
||||
private BigDecimal totalAmount;
|
||||
|
||||
/**
|
||||
* 退款金额
|
||||
*/
|
||||
@Column(name = "refund_amount", nullable = false, precision = 12, scale = 2)
|
||||
@Comment("退款金额")
|
||||
private BigDecimal refundAmount = BigDecimal.ZERO;
|
||||
|
||||
/**
|
||||
* 平台服务费
|
||||
*/
|
||||
@Column(name = "platform_fee", nullable = false, precision = 10, scale = 2)
|
||||
@Comment("平台服务费")
|
||||
private BigDecimal platformFee = BigDecimal.ZERO;
|
||||
|
||||
/**
|
||||
* 结算金额
|
||||
*/
|
||||
@Column(name = "settlement_amount", nullable = false, precision = 12, scale = 2)
|
||||
@Comment("结算金额")
|
||||
private BigDecimal settlementAmount;
|
||||
|
||||
/**
|
||||
* 结算状态:0-待结算 1-已结算
|
||||
*/
|
||||
@Column(name = "status", nullable = false)
|
||||
@Comment("结算状态:0待结算 1已结算")
|
||||
private SettlementStatus status = SettlementStatus.PENDING;
|
||||
|
||||
/**
|
||||
* 实际结算时间
|
||||
*/
|
||||
@Column(name = "settled_at")
|
||||
@Comment("实际结算时间")
|
||||
private LocalDateTime settledAt;
|
||||
|
||||
|
||||
|
||||
// ========== 业务方法 ==========
|
||||
|
||||
public SettlementType getTypeEnum() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public SettlementStatus getStatusEnum() {
|
||||
return this.status;
|
||||
}
|
||||
/**
|
||||
* 结算类型文案
|
||||
*/
|
||||
public String getTypeText() {
|
||||
SettlementType e = getTypeEnum();
|
||||
return e != null ? e.getDesc() : "未知";
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态文案
|
||||
*/
|
||||
public String getStatusText() {
|
||||
SettlementStatus e = getStatusEnum();
|
||||
return e != null ? e.getDesc() : "未知";
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已结算
|
||||
*/
|
||||
public boolean isSettled() {
|
||||
return getStatusEnum() == SettlementStatus.SETTLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 周期文案,例如:2024-01-01 ~ 2024-01-07
|
||||
*/
|
||||
public String getPeriodText() {
|
||||
if (periodStart == null || periodEnd == null) {
|
||||
return "";
|
||||
}
|
||||
return periodStart.format(PERIOD_DATE_FORMATTER)
|
||||
+ " ~ "
|
||||
+ periodEnd.format(PERIOD_DATE_FORMATTER);
|
||||
}
|
||||
}
|
||||
245
src/main/java/com/xjhs/findmemerchant/entity/Store.java
Normal file
245
src/main/java/com/xjhs/findmemerchant/entity/Store.java
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import com.xjhs.findmemerchant.types.BusinessStatus;
|
||||
import com.xjhs.findmemerchant.types.CommonStatus;
|
||||
import com.xjhs.findmemerchant.types.StoreAuditStatus;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 门店实体
|
||||
* 对应表:stores
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "stores",
|
||||
indexes = {
|
||||
@Index(name = "idx_stores_merchant_id", columnList = "merchant_id"),
|
||||
@Index(name = "idx_stores_audit_status", columnList = "audit_status")
|
||||
}
|
||||
)
|
||||
public class Store extends AbstractBaseEntity {
|
||||
/**
|
||||
* 门店名称
|
||||
*/
|
||||
@Column(name = "name", nullable = false, length = 100)
|
||||
@Comment("门店名称")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 门店Logo
|
||||
*/
|
||||
@Column(name = "logo", length = 500)
|
||||
@Comment("门店Logo")
|
||||
private String logo;
|
||||
|
||||
/**
|
||||
* 联系电话
|
||||
*/
|
||||
@Column(name = "phone", nullable = false, length = 11)
|
||||
@Comment("联系电话")
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 省
|
||||
*/
|
||||
@Column(name = "province", nullable = false, length = 50)
|
||||
@Comment("省")
|
||||
private String province;
|
||||
|
||||
/**
|
||||
* 市
|
||||
*/
|
||||
@Column(name = "city", nullable = false, length = 50)
|
||||
@Comment("市")
|
||||
private String city;
|
||||
|
||||
/**
|
||||
* 区/县
|
||||
*/
|
||||
@Column(name = "district", nullable = false, length = 50)
|
||||
@Comment("区/县")
|
||||
private String district;
|
||||
|
||||
/**
|
||||
* 详细地址
|
||||
*/
|
||||
@Column(name = "address", nullable = false, length = 200)
|
||||
@Comment("详细地址")
|
||||
private String address;
|
||||
|
||||
/**
|
||||
* 经度
|
||||
*/
|
||||
@Column(name = "longitude", precision = 10, scale = 7)
|
||||
@Comment("经度")
|
||||
private Double longitude;
|
||||
|
||||
/**
|
||||
* 纬度
|
||||
*/
|
||||
@Column(name = "latitude", precision = 10, scale = 7)
|
||||
@Comment("纬度")
|
||||
private Double latitude;
|
||||
|
||||
/**
|
||||
* 营业时间描述
|
||||
*/
|
||||
@Column(name = "business_hours", length = 100)
|
||||
@Comment("营业时间描述")
|
||||
private String businessHours;
|
||||
|
||||
/**
|
||||
* 营业状态:0-已打烊 1-营业中 2-临时打烊
|
||||
*/
|
||||
@Column(name = "business_status",columnDefinition = "VARCHAR(20)", length = 20,nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Comment("营业状态:0已打烊 1营业中 2临时打烊")
|
||||
private BusinessStatus businessStatus = BusinessStatus.OPEN;
|
||||
|
||||
/**
|
||||
* 临时打烊原因
|
||||
*/
|
||||
@Column(name = "temp_close_reason", length = 200)
|
||||
@Comment("临时打烊原因")
|
||||
private String tempCloseReason;
|
||||
|
||||
/**
|
||||
* 临时打烊结束时间
|
||||
*/
|
||||
@Column(name = "temp_close_until")
|
||||
@Comment("临时打烊结束时间")
|
||||
private LocalDateTime tempCloseUntil;
|
||||
|
||||
/**
|
||||
* 审核状态
|
||||
*/
|
||||
@Column(name = "audit_status",columnDefinition = "VARCHAR(20)", length = 20, nullable = false)
|
||||
@Comment("审核状态:0待审核 1已通过 2已拒绝")
|
||||
private StoreAuditStatus auditStatus = StoreAuditStatus.PENDING;
|
||||
|
||||
/**
|
||||
* 审核备注
|
||||
*/
|
||||
@Column(name = "audit_remark", length = 500)
|
||||
@Comment("审核备注")
|
||||
private String auditRemark;
|
||||
|
||||
/**
|
||||
* 启用状态:0-禁用 1-启用
|
||||
*/
|
||||
@Column(name = "status",columnDefinition = "VARCHAR(20)", length = 20, nullable = false)
|
||||
@Comment("启用状态:0禁用 1启用")
|
||||
private CommonStatus status = CommonStatus.ENABLED;
|
||||
|
||||
// ========== 关联关系 ==========
|
||||
|
||||
/**
|
||||
* 商户
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "merchant_id", insertable = false, updatable = false)
|
||||
private Merchant merchant;
|
||||
|
||||
/**
|
||||
* 员工列表
|
||||
*/
|
||||
@OneToMany(mappedBy = "store", fetch = FetchType.LAZY)
|
||||
private List<Employee> employees;
|
||||
|
||||
/**
|
||||
* 营业时间段
|
||||
*/
|
||||
@OneToMany(mappedBy = "store", fetch = FetchType.LAZY)
|
||||
private List<BusinessPeriod> businessPeriods;
|
||||
|
||||
// ========== 业务方法 ==========
|
||||
|
||||
/**
|
||||
* 完整地址
|
||||
*/
|
||||
public String getFullAddress() {
|
||||
return (province == null ? "" : province)
|
||||
+ (city == null ? "" : city)
|
||||
+ (district == null ? "" : district)
|
||||
+ (address == null ? "" : address);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否审核通过
|
||||
*/
|
||||
public boolean isApproved() {
|
||||
return getAuditStatusEnum() == StoreAuditStatus.APPROVED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return getStatusEnum() == CommonStatus.ENABLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有地理位置
|
||||
*/
|
||||
public boolean hasLocation() {
|
||||
return longitude != null && latitude != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前是否营业
|
||||
*/
|
||||
public boolean isBusinessOpen() {
|
||||
BusinessStatus bs = getBusinessStatusEnum();
|
||||
if (bs == null) {
|
||||
return false;
|
||||
}
|
||||
if (bs == BusinessStatus.TEMP_CLOSED) {
|
||||
// 如果设置了临时打烊结束时间且已过期,则视作应恢复营业
|
||||
if (tempCloseUntil != null && LocalDateTime.now().isAfter(tempCloseUntil)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return bs == BusinessStatus.OPEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 营业状态文案
|
||||
*/
|
||||
public String getBusinessStatusText() {
|
||||
BusinessStatus bs = getBusinessStatusEnum();
|
||||
return bs != null ? bs.getDesc() : "未知";
|
||||
}
|
||||
|
||||
public BusinessStatus getBusinessStatusEnum() {
|
||||
return this.businessStatus;
|
||||
}
|
||||
|
||||
|
||||
public StoreAuditStatus getAuditStatusEnum() {
|
||||
return this.auditStatus;
|
||||
}
|
||||
|
||||
public CommonStatus getStatusEnum() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public String getAuditStatusText() {
|
||||
StoreAuditStatus e = getAuditStatusEnum();
|
||||
return e != null ? e.getDesc() : "未知";
|
||||
}
|
||||
|
||||
public String getStatusText() {
|
||||
CommonStatus e = getStatusEnum();
|
||||
return e != null ? e.getDesc() : "未知";
|
||||
}
|
||||
}
|
||||
134
src/main/java/com/xjhs/findmemerchant/entity/Transaction.java
Normal file
134
src/main/java/com/xjhs/findmemerchant/entity/Transaction.java
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.adapter.id.SnowflakeGenerated;
|
||||
import com.xjhs.findmemerchant.types.TransactionType;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 钱包交易流水
|
||||
* 对应表:wallet_transactions
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "wallet_transactions",
|
||||
indexes = {
|
||||
@Index(name = "idx_wallet_transactions_wallet_id", columnList = "wallet_id"),
|
||||
@Index(name = "idx_wallet_transactions_merchant_id", columnList = "merchant_id"),
|
||||
@Index(name = "idx_wallet_transactions_type", columnList = "type"),
|
||||
@Index(name = "idx_wallet_transactions_created_at", columnList = "created_at")
|
||||
}
|
||||
)
|
||||
public class Transaction {
|
||||
|
||||
/**
|
||||
* 主键(雪花ID)
|
||||
*/
|
||||
@Id
|
||||
@GeneratedValue
|
||||
@SnowflakeGenerated
|
||||
@Column(nullable = false)
|
||||
@Comment("主键ID")
|
||||
private Long id;
|
||||
|
||||
|
||||
/**
|
||||
* 交易类型:1-收入 2-支出 3-冻结 4-解冻 5-提现
|
||||
*/
|
||||
@Column(name = "type",columnDefinition = "VARCHAR(20)",length = 20, nullable = false)
|
||||
@Comment("交易类型:1收入 2支出 3冻结 4解冻 5提现")
|
||||
private TransactionType type;
|
||||
|
||||
/**
|
||||
* 交易金额
|
||||
*/
|
||||
@Column(name = "amount", nullable = false, precision = 12, scale = 2)
|
||||
@Comment("交易金额")
|
||||
private BigDecimal amount;
|
||||
|
||||
/**
|
||||
* 交易前余额
|
||||
*/
|
||||
@Column(name = "balance_before", nullable = false, precision = 12, scale = 2)
|
||||
@Comment("交易前余额")
|
||||
private BigDecimal balanceBefore;
|
||||
|
||||
/**
|
||||
* 交易后余额
|
||||
*/
|
||||
@Column(name = "balance_after", nullable = false, precision = 12, scale = 2)
|
||||
@Comment("交易后余额")
|
||||
private BigDecimal balanceAfter;
|
||||
|
||||
/**
|
||||
* 关联类型: order, withdrawal, settlement
|
||||
*/
|
||||
@Column(name = "ref_type", length = 32)
|
||||
@Comment("关联类型:order/withdrawal/settlement等")
|
||||
private String refType;
|
||||
|
||||
/**
|
||||
* 关联ID
|
||||
*/
|
||||
@Column(name = "ref_id")
|
||||
@Comment("关联业务ID")
|
||||
private Long refId;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@Column(name = "description", length = 200)
|
||||
@Comment("交易描述")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Column(name = "created_at", nullable = false)
|
||||
@Comment("创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 所属钱包
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "wallet_id", insertable = false, updatable = false)
|
||||
private Wallet wallet;
|
||||
|
||||
|
||||
/**
|
||||
* 商户
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "merchant_id", insertable = false, updatable = false)
|
||||
private Merchant merchant;
|
||||
|
||||
|
||||
// ========== 业务方法 ==========
|
||||
|
||||
public TransactionType getTypeEnum() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 交易类型文案
|
||||
*/
|
||||
public String getTypeText() {
|
||||
TransactionType t = getTypeEnum();
|
||||
return t != null ? t.getDesc() : "未知";
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否收入流水
|
||||
*/
|
||||
public boolean isIncome() {
|
||||
return getTypeEnum() == TransactionType.INCOME;
|
||||
}
|
||||
}
|
||||
112
src/main/java/com/xjhs/findmemerchant/entity/Wallet.java
Normal file
112
src/main/java/com/xjhs/findmemerchant/entity/Wallet.java
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.adapter.id.SnowflakeGenerated;
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 商户钱包
|
||||
* 对应表:wallets
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "wallets",
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_wallets_merchant_id", columnNames = "merchant_id")
|
||||
}
|
||||
)
|
||||
public class Wallet extends AbstractBaseEntity {
|
||||
|
||||
/**
|
||||
* 主键(雪花ID)
|
||||
*/
|
||||
@Id
|
||||
@GeneratedValue
|
||||
@SnowflakeGenerated
|
||||
@Column(nullable = false)
|
||||
@Comment("主键ID")
|
||||
private Long id;
|
||||
|
||||
|
||||
/**
|
||||
* 可用余额
|
||||
*/
|
||||
@Column(name = "balance", nullable = false, precision = 12, scale = 2)
|
||||
@Comment("可用余额")
|
||||
private BigDecimal balance = BigDecimal.ZERO;
|
||||
|
||||
/**
|
||||
* 冻结余额(提现中)
|
||||
*/
|
||||
@Column(name = "frozen_balance", nullable = false, precision = 12, scale = 2)
|
||||
@Comment("冻结余额(提现中)")
|
||||
private BigDecimal frozenBalance = BigDecimal.ZERO;
|
||||
|
||||
/**
|
||||
* 累计收入
|
||||
*/
|
||||
@Column(name = "total_income", nullable = false, precision = 12, scale = 2)
|
||||
@Comment("累计收入")
|
||||
private BigDecimal totalIncome = BigDecimal.ZERO;
|
||||
|
||||
/**
|
||||
* 累计提现
|
||||
*/
|
||||
@Column(name = "total_withdrawn", nullable = false, precision = 12, scale = 2)
|
||||
@Comment("累计提现")
|
||||
private BigDecimal totalWithdrawn = BigDecimal.ZERO;
|
||||
|
||||
/**
|
||||
* 待结算金额
|
||||
*/
|
||||
@Column(name = "pending_settlement", nullable = false, precision = 12, scale = 2)
|
||||
@Comment("待结算金额")
|
||||
private BigDecimal pendingSettlement = BigDecimal.ZERO;
|
||||
|
||||
|
||||
/**
|
||||
* 商户
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "merchant_id", insertable = false, updatable = false)
|
||||
private Merchant merchant;
|
||||
|
||||
// ========== 业务方法 ==========
|
||||
|
||||
/**
|
||||
* 可提现余额
|
||||
*/
|
||||
public BigDecimal getAvailableBalance() {
|
||||
return balance != null ? balance : BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 总余额(含冻结)
|
||||
*/
|
||||
public BigDecimal getTotalBalance() {
|
||||
BigDecimal b = balance != null ? balance : BigDecimal.ZERO;
|
||||
BigDecimal f = frozenBalance != null ? frozenBalance : BigDecimal.ZERO;
|
||||
return b.add(f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可提现指定金额
|
||||
*/
|
||||
public boolean canWithdraw(BigDecimal amount) {
|
||||
if (amount == null) {
|
||||
return false;
|
||||
}
|
||||
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return false;
|
||||
}
|
||||
BigDecimal b = balance != null ? balance : BigDecimal.ZERO;
|
||||
return b.compareTo(amount) >= 0;
|
||||
}
|
||||
}
|
||||
175
src/main/java/com/xjhs/findmemerchant/entity/Withdrawal.java
Normal file
175
src/main/java/com/xjhs/findmemerchant/entity/Withdrawal.java
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
package com.xjhs.findmemerchant.entity;
|
||||
|
||||
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
|
||||
import com.xjhs.findmemerchant.types.WithdrawalStatus;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.Comment;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 提现申请
|
||||
* 对应表:withdrawals
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(
|
||||
name = "withdrawals",
|
||||
indexes = {
|
||||
@Index(name = "idx_withdrawals_merchant_id", columnList = "merchant_id"),
|
||||
@Index(name = "idx_withdrawals_wallet_id", columnList = "wallet_id"),
|
||||
@Index(name = "idx_withdrawals_status", columnList = "status"),
|
||||
@Index(name = "idx_withdrawals_created_at", columnList = "created_at")
|
||||
}
|
||||
)
|
||||
public class Withdrawal extends AbstractBaseEntity {
|
||||
|
||||
|
||||
/**
|
||||
* 提现金额
|
||||
*/
|
||||
@Column(name = "amount", nullable = false, precision = 12, scale = 2)
|
||||
@Comment("提现金额")
|
||||
private BigDecimal amount;
|
||||
|
||||
/**
|
||||
* 手续费
|
||||
*/
|
||||
@Column(name = "fee", nullable = false, precision = 10, scale = 2)
|
||||
@Comment("提现手续费")
|
||||
private BigDecimal fee = BigDecimal.ZERO;
|
||||
|
||||
/**
|
||||
* 实际到账金额
|
||||
*/
|
||||
@Column(name = "actual_amount", nullable = false, precision = 12, scale = 2)
|
||||
@Comment("实际到账金额")
|
||||
private BigDecimal actualAmount;
|
||||
|
||||
/**
|
||||
* 银行名称
|
||||
*/
|
||||
@Column(name = "bank_name", nullable = false, length = 50)
|
||||
@Comment("银行名称")
|
||||
private String bankName;
|
||||
|
||||
/**
|
||||
* 银行账号(加密存储)
|
||||
*/
|
||||
@Column(name = "bank_account", nullable = false, length = 30)
|
||||
@Comment("银行账号(加密)")
|
||||
private String bankAccount;
|
||||
|
||||
/**
|
||||
* 户名
|
||||
*/
|
||||
@Column(name = "account_name", nullable = false, length = 50)
|
||||
@Comment("开户人姓名")
|
||||
private String accountName;
|
||||
|
||||
/**
|
||||
* 状态:0-待审核 1-处理中 2-已完成 3-已拒绝 4-已取消
|
||||
*/
|
||||
@Column(name = "status", nullable = false)
|
||||
@Comment("提现状态")
|
||||
private Byte status = 0;
|
||||
|
||||
/**
|
||||
* 拒绝原因
|
||||
*/
|
||||
@Column(name = "reject_reason", length = 200)
|
||||
@Comment("拒绝原因")
|
||||
private String rejectReason;
|
||||
|
||||
/**
|
||||
* 审核/处理时间
|
||||
*/
|
||||
@Column(name = "processed_at")
|
||||
@Comment("审核/处理时间")
|
||||
private LocalDateTime processedAt;
|
||||
|
||||
/**
|
||||
* 完成时间
|
||||
*/
|
||||
@Column(name = "completed_at")
|
||||
@Comment("完成时间")
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
/**
|
||||
* 银行流水号
|
||||
*/
|
||||
@Column(name = "transaction_no", length = 64)
|
||||
@Comment("银行流水号")
|
||||
private String transactionNo;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
@Column(name = "remark", length = 200)
|
||||
@Comment("备注")
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 商户
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "merchant_id", insertable = false, updatable = false)
|
||||
private Merchant merchant;
|
||||
|
||||
/**
|
||||
* 钱包
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "wallet_id", insertable = false, updatable = false)
|
||||
private Wallet wallet;
|
||||
|
||||
// ========= 业务方法 =========
|
||||
|
||||
public WithdrawalStatus getStatusEnum() {
|
||||
return WithdrawalStatus.fromCode(this.status);
|
||||
}
|
||||
|
||||
public void setStatusEnum(WithdrawalStatus statusEnum) {
|
||||
this.status = statusEnum == null ? null : statusEnum.code();
|
||||
}
|
||||
|
||||
public String getStatusText() {
|
||||
WithdrawalStatus e = getStatusEnum();
|
||||
return e != null ? e.getDesc() : "未知";
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否待审核
|
||||
*/
|
||||
public boolean isPending() {
|
||||
return getStatusEnum() == WithdrawalStatus.PENDING;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已完成
|
||||
*/
|
||||
public boolean isCompleted() {
|
||||
return getStatusEnum() == WithdrawalStatus.COMPLETED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可以取消
|
||||
*/
|
||||
public boolean canCancel() {
|
||||
return getStatusEnum() == WithdrawalStatus.PENDING;
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏银行卡号
|
||||
*/
|
||||
public String getMaskedBankAccount() {
|
||||
if (bankAccount == null || bankAccount.length() <= 8) {
|
||||
return bankAccount;
|
||||
}
|
||||
return bankAccount.substring(0, 4) + "****" + bankAccount.substring(bankAccount.length() - 4);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package com.xjhs.findmemerchant.mapper;
|
||||
|
||||
public class EmployeeMapper {
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.xjhs.findmemerchant.mapper;
|
||||
|
||||
import com.xjhs.findmemerchant.dto.MerchantDto;
|
||||
import com.xjhs.findmemerchant.entity.Merchant;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.ReportingPolicy;
|
||||
|
||||
@Mapper(componentModel = "spring",unmappedTargetPolicy = ReportingPolicy.IGNORE,unmappedSourcePolicy = ReportingPolicy.IGNORE)
|
||||
public interface MerchantMapper {
|
||||
|
||||
MerchantDto toDto(Merchant merchant);
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.xjhs.findmemerchant.mapper;
|
||||
|
||||
import com.xjhs.findmemerchant.dto.store.StoreDto;
|
||||
import com.xjhs.findmemerchant.entity.Store;
|
||||
import com.xjhs.findmemerchant.vo.store.StoreCreateVo;
|
||||
import com.xjhs.findmemerchant.vo.store.StoreUpdateVo;
|
||||
import org.mapstruct.*;
|
||||
|
||||
@Mapper(componentModel = "spring",unmappedTargetPolicy = ReportingPolicy.IGNORE,unmappedSourcePolicy = ReportingPolicy.IGNORE)
|
||||
public interface StoreMapper {
|
||||
StoreDto toDto(Store store);
|
||||
Store toEntity(StoreCreateVo createVo);
|
||||
Store toEntity(StoreUpdateVo updateVo);
|
||||
|
||||
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
|
||||
void updateFromVo(StoreUpdateVo vo, @MappingTarget Store entity);
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.xjhs.findmemerchant.redis;
|
||||
|
||||
import com.xjhs.findmemerchant.security.JwtTokenService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* token 黑名单服务
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TokenBlacklistRedisService {
|
||||
private static final String KEY_TOKEN_BLACKLIST_PREFIX = "token:blacklist:";
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
private final JwtTokenService jwtTokenService;
|
||||
|
||||
/**
|
||||
* 添加访问令牌到黑名单
|
||||
*
|
||||
* @param token 访问令牌
|
||||
* @throws Exception 添加异常
|
||||
*/
|
||||
public void add(String token) throws Exception {
|
||||
var clams = jwtTokenService.parse(token);
|
||||
var mills = clams.getExpirationTime().getValueInMillis();
|
||||
if (mills <= 0) {
|
||||
return;
|
||||
}
|
||||
var blacklistKey = KEY_TOKEN_BLACKLIST_PREFIX + token;
|
||||
this.stringRedisTemplate.opsForValue().setIfAbsent(blacklistKey, "1", Duration.ofMillis(mills));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.Activity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ActivityRepository extends JpaRepository<Activity, Long> {
|
||||
|
||||
List<Activity> findByMerchantId(Long merchantId);
|
||||
|
||||
List<Activity> findByStatus(Byte status);
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.BankCard;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 商户银行卡仓储
|
||||
*/
|
||||
public interface BankCardRepository extends JpaRepository<BankCard, Long>,
|
||||
JpaSpecificationExecutor<BankCard> {
|
||||
|
||||
/**
|
||||
* 按商户查询所有银行卡
|
||||
*/
|
||||
List<BankCard> findByMerchantId(Long merchantId);
|
||||
|
||||
/**
|
||||
* 查询商户的默认卡
|
||||
*/
|
||||
Optional<BankCard> findByMerchantIdAndIsDefaultTrue(Long merchantId);
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.BusinessLicense;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface BusinessLicenseRepository extends JpaRepository<BusinessLicense, Long>,
|
||||
JpaSpecificationExecutor<BusinessLicense> {
|
||||
|
||||
List<BusinessLicense> findByMerchantId(Long merchantId);
|
||||
|
||||
Optional<BusinessLicense> findTopByMerchantIdOrderByCreatedAtDesc(Long merchantId);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.BusinessPeriod;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 门店营业时间段仓储
|
||||
*/
|
||||
public interface BusinessPeriodRepository extends JpaRepository<BusinessPeriod, Long>,
|
||||
JpaSpecificationExecutor<BusinessPeriod> {
|
||||
|
||||
/**
|
||||
* 按门店查询所有营业时间段
|
||||
*/
|
||||
List<BusinessPeriod> findByStoreId(Long storeId);
|
||||
|
||||
/**
|
||||
* 按门店 + 周几查询启用的时间段
|
||||
*/
|
||||
List<BusinessPeriod> findByStoreIdAndDayOfWeekAndIsEnabledTrue(Long storeId, Byte dayOfWeek);
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.CouponCode;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 优惠券码仓储
|
||||
*/
|
||||
public interface CouponCodeRepository extends JpaRepository<CouponCode, Long>,
|
||||
JpaSpecificationExecutor<CouponCode> {
|
||||
|
||||
/**
|
||||
* 按券模板查询券码
|
||||
*/
|
||||
List<CouponCode> findByCouponId(Long couponId);
|
||||
|
||||
/**
|
||||
* 按会员查询券码
|
||||
*/
|
||||
List<CouponCode> findByMemberId(Long memberId);
|
||||
|
||||
/**
|
||||
* 按券码查询
|
||||
*/
|
||||
Optional<CouponCode> findByCode(String code);
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.Coupon;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 优惠券模板仓储
|
||||
*/
|
||||
public interface CouponRepository extends JpaRepository<Coupon, Long> {
|
||||
|
||||
/**
|
||||
* 按商户查询优惠券
|
||||
*/
|
||||
List<Coupon> findByMerchantId(Long merchantId);
|
||||
|
||||
/**
|
||||
* 按状态查询优惠券
|
||||
*/
|
||||
List<Coupon> findByStatus(Byte status);
|
||||
|
||||
/**
|
||||
* 按ID + 商户ID 查询(防越权)
|
||||
*/
|
||||
Optional<Coupon> findByIdAndMerchantId(Long id, Long merchantId);
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.CouponStore;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 优惠券与门店关联仓储
|
||||
*/
|
||||
public interface CouponStoreRepository extends JpaSpecificationExecutor<CouponStore>,JpaRepository<CouponStore, Long> {
|
||||
|
||||
List<CouponStore> findByCouponId(Long couponId);
|
||||
|
||||
List<CouponStore> findByStoreId(Long storeId);
|
||||
|
||||
Optional<CouponStore> findByCouponIdAndStoreId(Long couponId, Long storeId);
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.Employee;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 员工仓储
|
||||
*/
|
||||
public interface EmployeeRepository extends JpaRepository<Employee, Long>,
|
||||
JpaSpecificationExecutor<Employee> {
|
||||
|
||||
/**
|
||||
* 按商户查询员工
|
||||
*/
|
||||
List<Employee> findByMerchantId(Long merchantId);
|
||||
|
||||
/**
|
||||
* 按门店查询员工
|
||||
*/
|
||||
List<Employee> findByStoreId(Long storeId);
|
||||
|
||||
/**
|
||||
* 按手机号与商户查询(防止跨商户重复)
|
||||
*/
|
||||
Optional<Employee> findByMerchantIdAndPhone(Long merchantId, String phone);
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.HealthCertificate;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface HealthCertificateRepository extends JpaRepository<HealthCertificate, Long>,
|
||||
JpaSpecificationExecutor<HealthCertificate> {
|
||||
|
||||
List<HealthCertificate> findByStoreId(Long storeId);
|
||||
|
||||
List<HealthCertificate> findByEmployeeId(Long employeeId);
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.Member;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 会员仓储
|
||||
*/
|
||||
public interface MemberRepository extends JpaRepository<Member, Long>,
|
||||
JpaSpecificationExecutor<Member> {
|
||||
|
||||
/**
|
||||
* 按商户查询会员
|
||||
*/
|
||||
List<Member> findByMerchantId(Long merchantId);
|
||||
|
||||
/**
|
||||
* 按商户 + 手机号查询(唯一)
|
||||
*/
|
||||
Optional<Member> findByPhone(String phone);
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.Merchant;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 商户仓储
|
||||
*/
|
||||
public interface MerchantRepository extends JpaRepository<Merchant, Long>,
|
||||
JpaSpecificationExecutor<Merchant> {
|
||||
|
||||
/**
|
||||
* 根据手机号查询
|
||||
*/
|
||||
Optional<Merchant> findByPhone(String phone);
|
||||
|
||||
Boolean existsByPhone(String phone);
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.Message;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 消息仓储
|
||||
*/
|
||||
public interface MessageRepository extends JpaRepository<Message, Long>,
|
||||
JpaSpecificationExecutor<Message> {
|
||||
|
||||
/**
|
||||
* 按商户查询消息
|
||||
*/
|
||||
List<Message> findByMerchantId(Long merchantId);
|
||||
|
||||
/**
|
||||
* 按商户 + 是否已读查询
|
||||
*/
|
||||
List<Message> findByMerchantIdAndIsRead(Long merchantId, Byte isRead);
|
||||
|
||||
/**
|
||||
* 统计未读消息数量
|
||||
*/
|
||||
long countByMerchantIdAndIsRead(Long merchantId, Byte isRead);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.OrderItem;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 订单明细仓储
|
||||
*/
|
||||
public interface OrderItemRepository extends JpaRepository<OrderItem, Long>,
|
||||
JpaSpecificationExecutor<OrderItem> {
|
||||
|
||||
/**
|
||||
* 按订单查询订单明细
|
||||
*/
|
||||
List<OrderItem> findByOrderId(Long orderId);
|
||||
|
||||
/**
|
||||
* 按商品查询订单明细
|
||||
*/
|
||||
List<OrderItem> findByProductId(Long productId);
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.Order;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 订单仓储
|
||||
*/
|
||||
public interface OrderRepository extends JpaRepository<Order, Long>,
|
||||
JpaSpecificationExecutor<Order> {
|
||||
|
||||
/**
|
||||
* 按订单号查询(唯一)
|
||||
*/
|
||||
Optional<Order> findByOrderNo(String orderNo);
|
||||
|
||||
/**
|
||||
* 按门店查询订单
|
||||
*/
|
||||
List<Order> findByStoreId(Long storeId);
|
||||
|
||||
/**
|
||||
* 按会员查询订单
|
||||
*/
|
||||
List<Order> findByMemberId(Long memberId);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.ProductCategory;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 商品分类仓储
|
||||
*/
|
||||
public interface ProductCategoryRepository extends JpaRepository<ProductCategory, Long>,
|
||||
JpaSpecificationExecutor<ProductCategory> {
|
||||
|
||||
/**
|
||||
* 按商户查询所有分类
|
||||
*/
|
||||
List<ProductCategory> findByMerchantId(Long merchantId);
|
||||
|
||||
/**
|
||||
* 查询某商户下指定父分类的子分类
|
||||
*/
|
||||
List<ProductCategory> findByMerchantIdAndParentId(Long merchantId, Long parentId);
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.Product;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 商品仓储
|
||||
*/
|
||||
public interface ProductRepository extends JpaRepository<Product, Long>,
|
||||
JpaSpecificationExecutor<Product> {
|
||||
|
||||
/**
|
||||
* 按商户查询商品
|
||||
*/
|
||||
List<Product> findByMerchantId(Long merchantId);
|
||||
|
||||
/**
|
||||
* 按门店查询商品
|
||||
*/
|
||||
List<Product> findByStoreId(Long storeId);
|
||||
|
||||
/**
|
||||
* 按分类查询商品
|
||||
*/
|
||||
List<Product> findByCategoryId(Long categoryId);
|
||||
|
||||
/**
|
||||
* 按状态查询商品
|
||||
*/
|
||||
List<Product> findByStatus(Byte status);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.ProductSKU;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 商品SKU仓储
|
||||
*/
|
||||
public interface ProductSKURepository extends JpaRepository<ProductSKU, Long>,
|
||||
JpaSpecificationExecutor<ProductSKU> {
|
||||
|
||||
/**
|
||||
* 查询商品下的所有SKU
|
||||
*/
|
||||
List<ProductSKU> findByProductId(Long productId);
|
||||
|
||||
/**
|
||||
* 按编码查询
|
||||
*/
|
||||
List<ProductSKU> findBySkuCode(String skuCode);
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.ReviewReply;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 评价回复仓储
|
||||
*/
|
||||
public interface ReviewReplyRepository extends JpaRepository<ReviewReply, Long>,
|
||||
JpaSpecificationExecutor<ReviewReply> {
|
||||
|
||||
Optional<ReviewReply> findByReviewId(Long reviewId);
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.Review;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户评价仓储
|
||||
*/
|
||||
public interface ReviewRepository extends JpaRepository<Review, Long>,
|
||||
JpaSpecificationExecutor<Review> {
|
||||
|
||||
List<Review> findByMerchantId(Long merchantId);
|
||||
|
||||
List<Review> findByStoreId(Long storeId);
|
||||
|
||||
List<Review> findByOrderId(Long orderId);
|
||||
|
||||
List<Review> findByUserId(Long userId);
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.Role;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 角色仓储
|
||||
*/
|
||||
public interface RoleRepository extends JpaRepository<Role, Long>,
|
||||
JpaSpecificationExecutor<Role> {
|
||||
|
||||
/**
|
||||
* 按编码查询角色
|
||||
*/
|
||||
Optional<Role> findByCode(String code);
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.Settlement;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 结算记录仓储
|
||||
*/
|
||||
public interface SettlementRepository extends JpaRepository<Settlement, Long>,
|
||||
JpaSpecificationExecutor<Settlement> {
|
||||
|
||||
/**
|
||||
* 按结算单号查询
|
||||
*/
|
||||
Optional<Settlement> findBySettlementNo(String settlementNo);
|
||||
|
||||
/**
|
||||
* 按商户查询结算记录
|
||||
*/
|
||||
List<Settlement> findByMerchantId(Long merchantId);
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.Store;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 门店仓储
|
||||
*/
|
||||
public interface StoreRepository extends JpaRepository<Store, Long>,
|
||||
JpaSpecificationExecutor<Store> {
|
||||
|
||||
/**
|
||||
* 按商户查询门店
|
||||
*/
|
||||
List<Store> findByMerchantId(Long merchantId);
|
||||
|
||||
Optional<Store> findByMerchant_IdAndId(Long merchantId, Long id);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.Transaction;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 钱包交易流水仓储
|
||||
*/
|
||||
public interface TransactionRepository extends JpaRepository<Transaction, Long>,
|
||||
JpaSpecificationExecutor<Transaction> {
|
||||
|
||||
/**
|
||||
* 按钱包查询流水
|
||||
*/
|
||||
List<Transaction> findByWalletId(Long walletId);
|
||||
|
||||
/**
|
||||
* 按商户查询流水
|
||||
*/
|
||||
List<Transaction> findByMerchantId(Long merchantId);
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.Wallet;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 商户钱包仓储
|
||||
*/
|
||||
public interface WalletRepository extends JpaRepository<Wallet, Long>,
|
||||
JpaSpecificationExecutor<Wallet> {
|
||||
|
||||
/**
|
||||
* 根据商户ID查询钱包(唯一)
|
||||
*/
|
||||
Optional<Wallet> findByMerchantId(Long merchantId);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.xjhs.findmemerchant.repository;
|
||||
|
||||
import com.xjhs.findmemerchant.entity.Withdrawal;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 提现仓储
|
||||
*/
|
||||
public interface WithdrawalRepository extends JpaRepository<Withdrawal, Long>,
|
||||
JpaSpecificationExecutor<Withdrawal> {
|
||||
|
||||
/**
|
||||
* 按商户查提现记录
|
||||
*/
|
||||
List<Withdrawal> findByMerchantId(Long merchantId);
|
||||
|
||||
/**
|
||||
* 按钱包查提现记录
|
||||
*/
|
||||
List<Withdrawal> findByWalletId(Long walletId);
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.xjhs.findmemerchant.security;
|
||||
|
||||
|
||||
import com.xjhs.findmemerchant.repository.MemberRepository;
|
||||
import com.xjhs.findmemerchant.repository.MerchantRepository;
|
||||
import com.xjhs.findmemerchant.security.sms.SmsAuthenticationToken;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtTokenService jwtTokenService;
|
||||
private final MerchantRepository merchantRepository;
|
||||
private final MemberRepository memberRepository;
|
||||
|
||||
|
||||
|
||||
private SmsAuthenticationToken getAuthenticationToken(String phone) throws Exception {
|
||||
// 手机号查商户
|
||||
var merchant = merchantRepository.findByPhone(phone).orElse(null);
|
||||
if(merchant != null){
|
||||
var authorities = List.of(new SimpleGrantedAuthority("ROLE_USER"));
|
||||
return new SmsAuthenticationToken(merchant, authorities);
|
||||
}
|
||||
// 手机号查员工
|
||||
var member = memberRepository.findByPhone(phone).orElse(null);
|
||||
if(member != null){
|
||||
var authorities = List.of(new SimpleGrantedAuthority("ROLE_MEMBER"));
|
||||
return new SmsAuthenticationToken(member, authorities);
|
||||
}
|
||||
throw new Exception("用户信息不存在");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||
String token = authHeader.substring(7);
|
||||
try {
|
||||
String phone = jwtTokenService.parseMobile(token);
|
||||
var authToken = getAuthenticationToken(phone);
|
||||
authToken.setAuthenticated(true);
|
||||
SecurityContextHolder.getContext().setAuthentication(authToken);
|
||||
} catch (Exception e) {
|
||||
// token 不合法就当未登录,不中断请求
|
||||
}
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.xjhs.findmemerchant.security;
|
||||
|
||||
import org.jose4j.jws.AlgorithmIdentifiers;
|
||||
import org.jose4j.jws.JsonWebSignature;
|
||||
import org.jose4j.jwt.JwtClaims;
|
||||
import org.jose4j.jwt.NumericDate;
|
||||
import org.jose4j.jwt.consumer.JwtConsumer;
|
||||
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@Component
|
||||
public class JwtTokenService {
|
||||
|
||||
// 建议改成配置:application.yml 里
|
||||
private final String secret = "secret-key-1234567890";
|
||||
private final long expireMillis = 30 * 24 * 60 * 60 * 1000L; // 30 天
|
||||
|
||||
/**
|
||||
* 生成 JWT
|
||||
*/
|
||||
public String generateToken(String mobile) throws Exception {
|
||||
JwtClaims claims = new JwtClaims();
|
||||
claims.setSubject(mobile);
|
||||
claims.setIssuedAt(NumericDate.now());
|
||||
claims.setExpirationTimeMinutesInTheFuture(expireMillis / 1000f / 60f); // 转分钟
|
||||
JsonWebSignature jws = new JsonWebSignature();
|
||||
jws.setPayload(claims.toJson());
|
||||
jws.setKey(new javax.crypto.spec.SecretKeySpec(
|
||||
secret.getBytes(StandardCharsets.UTF_8),
|
||||
"HmacSha256"
|
||||
));
|
||||
|
||||
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.HMAC_SHA256);
|
||||
|
||||
return jws.getCompactSerialization();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并解析手机号
|
||||
*/
|
||||
public String parseMobile(String token) throws Exception {
|
||||
var claims = this.parse(token);
|
||||
return claims.getSubject();
|
||||
}
|
||||
|
||||
|
||||
public JwtClaims parse(String token) throws Exception {
|
||||
JwtConsumer consumer = new JwtConsumerBuilder()
|
||||
.setRequireExpirationTime()
|
||||
.setRequireSubject()
|
||||
.setVerificationKey(
|
||||
new javax.crypto.spec.SecretKeySpec(
|
||||
secret.getBytes(StandardCharsets.UTF_8),
|
||||
"HmacSha256"
|
||||
)
|
||||
)
|
||||
.build();
|
||||
return consumer.processToClaims(token);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.xjhs.findmemerchant.security;
|
||||
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Service
|
||||
public class RefreshTokenService {
|
||||
|
||||
private static class Entry {
|
||||
String mobile;
|
||||
Instant expireAt;
|
||||
|
||||
Entry(String mobile, Instant expireAt) {
|
||||
this.mobile = mobile;
|
||||
this.expireAt = expireAt;
|
||||
}
|
||||
}
|
||||
|
||||
// 生产建议 Redis
|
||||
private final Map<String, Entry> store = new ConcurrentHashMap<>();
|
||||
|
||||
// 30 天
|
||||
private final long expireSeconds = 30L * 24 * 60 * 60;
|
||||
|
||||
public String create(String mobile) {
|
||||
String token = UUID.randomUUID().toString().replace("-", "");
|
||||
store.put(token, new Entry(mobile, Instant.now().plusSeconds(expireSeconds)));
|
||||
return token;
|
||||
}
|
||||
|
||||
public String validate(String refreshToken) {
|
||||
Entry entry = store.get(refreshToken);
|
||||
if (entry == null) return null;
|
||||
|
||||
if (Instant.now().isAfter(entry.expireAt)) {
|
||||
store.remove(refreshToken);
|
||||
return null;
|
||||
}
|
||||
return entry.mobile;
|
||||
}
|
||||
|
||||
public void invalidate(String refreshToken) {
|
||||
store.remove(refreshToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.xjhs.findmemerchant.security.config;
|
||||
|
||||
import com.xjhs.findmemerchant.security.JwtAuthenticationFilter;
|
||||
import com.xjhs.findmemerchant.security.JwtTokenService;
|
||||
import com.xjhs.findmemerchant.security.sms.SmsAuthenticationProvider;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
private final JwtTokenService jwtTokenService;
|
||||
private final SmsAuthenticationProvider smsAuthenticationProvider;
|
||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
|
||||
/**
|
||||
* AuthenticationManager,使用我们的 SmsAuthenticationProvider
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager() {
|
||||
return new ProviderManager(List.of(smsAuthenticationProvider));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.cors(Customizer.withDefaults())
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers(
|
||||
"/auth/sms/send",
|
||||
"/auth/sms/login",
|
||||
"/auth/refresh",
|
||||
"/auth/register"
|
||||
).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.addFilterBefore(
|
||||
jwtAuthenticationFilter,
|
||||
UsernamePasswordAuthenticationFilter.class
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.xjhs.findmemerchant.security.sms;
|
||||
|
||||
import com.xjhs.findmemerchant.repository.MemberRepository;
|
||||
import com.xjhs.findmemerchant.repository.MerchantRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SmsAuthenticationProvider implements AuthenticationProvider {
|
||||
private final SmsCodeService smsCodeService;
|
||||
private final MerchantRepository merchantRepository;
|
||||
private final MemberRepository memberRepository;
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
SmsAuthenticationToken token = (SmsAuthenticationToken) authentication;
|
||||
String phone = (String) token.getPrincipal();
|
||||
String code = (String) token.getCredentials();
|
||||
try {
|
||||
this.smsCodeService.verifyCode(phone,"login" ,code);
|
||||
} catch (Exception e) {
|
||||
throw new UsernameNotFoundException(e.getMessage());
|
||||
}
|
||||
// 手机号查商户
|
||||
var merchant = merchantRepository.findByPhone(phone).orElse(null);
|
||||
if(merchant != null){
|
||||
var authorities = List.of(new SimpleGrantedAuthority("ROLE_USER"));
|
||||
return new SmsAuthenticationToken(merchant, authorities);
|
||||
}
|
||||
// 手机号查员工
|
||||
var member = memberRepository.findByPhone(phone).orElse(null);
|
||||
if(member != null){
|
||||
var authorities = List.of(new SimpleGrantedAuthority("ROLE_MEMBER"));
|
||||
return new SmsAuthenticationToken(member, authorities);
|
||||
}
|
||||
throw new UsernameNotFoundException("用户信息不存在");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.xjhs.findmemerchant.security.sms;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
|
||||
|
||||
private final Object principal; // mobile
|
||||
private Object credentials; // code
|
||||
|
||||
// 未认证(登录请求阶段)
|
||||
public SmsAuthenticationToken(String mobile, String code) {
|
||||
super(null);
|
||||
this.principal = mobile;
|
||||
this.credentials = code;
|
||||
setAuthenticated(false);
|
||||
}
|
||||
|
||||
// 已认证(验证通过后)
|
||||
public SmsAuthenticationToken(Object principal,
|
||||
Collection<? extends GrantedAuthority> authorities) {
|
||||
super(authorities);
|
||||
this.principal = principal;
|
||||
this.credentials = null;
|
||||
super.setAuthenticated(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return credentials;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return principal;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package com.xjhs.findmemerchant.security.sms;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Random;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SmsCodeService {
|
||||
|
||||
/** Redis key 前缀:sms:code:{scene}:{phone} */
|
||||
private static final String SMS_CODE_KEY_PREFIX = "sms:code:";
|
||||
|
||||
/** 验证码有效期:5 分钟 */
|
||||
private static final Duration SMS_CODE_TTL = Duration.ofMinutes(5);
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
private final Random random = new Random();
|
||||
private Object smsSender; // 你自己的短信通道接口
|
||||
|
||||
|
||||
private String buildKey(String phone, String scene) {
|
||||
return SMS_CODE_KEY_PREFIX + scene + ":" + phone;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 6 位数字验证码
|
||||
*/
|
||||
private String generateCode() {
|
||||
return String.format("%06d", random.nextInt(1_000_000));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短信验证码:生成 -> 存 Redis -> 调短信通道发出去
|
||||
*/
|
||||
public void sendVerificationCode(String phone, String scene) {
|
||||
var code = generateCode();
|
||||
var key = buildKey(phone, scene);
|
||||
// 存到 Redis,设置 TTL
|
||||
redisTemplate.opsForValue().set(key, code, SMS_CODE_TTL);
|
||||
|
||||
// TODO:调用你自己的短信通道(阿里云、腾讯云等)
|
||||
// smsSender.sendSmsCode(phone, scene, code);
|
||||
|
||||
// 开发阶段也可以打印一下方便调试
|
||||
log.debug("send sms code, phone={},scene={},code={}", phone,scene, code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验验证码:
|
||||
* - key 不存在 => 过期 / 未发送 => 抛 SmsCodeExpiredException
|
||||
* - code 不匹配 => 抛 SmsCodeInvalidException
|
||||
* - 匹配 => 删除 key(一次性)
|
||||
*/
|
||||
public void verifyCode(String phone, String scene, String inputCode) throws Exception {
|
||||
String key = buildKey(phone, scene);
|
||||
String realCode = redisTemplate.opsForValue().get(key);
|
||||
if (realCode == null) {
|
||||
// 对齐 Go 里的 ErrSMSCodeExpired
|
||||
throw new Exception("验证码已过期或未发送");
|
||||
}
|
||||
|
||||
if (!realCode.equals(inputCode)) {
|
||||
// 不正确,但不删除,让用户继续尝试(错误次数由 AuthService 控制)
|
||||
throw new Exception("验证码错误");
|
||||
}
|
||||
// 验证通过,删除验证码(一次性)
|
||||
redisTemplate.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package com.xjhs.findmemerchant.service;
|
||||
|
||||
import com.xjhs.findmemerchant.dto.MerchantDto;
|
||||
import com.xjhs.findmemerchant.mapper.MerchantMapper;
|
||||
import com.xjhs.findmemerchant.repository.MerchantRepository;
|
||||
import com.xjhs.findmemerchant.types.AuthStatus;
|
||||
import com.xjhs.findmemerchant.vo.merchant.MerchantUpdateVo;
|
||||
import jakarta.transaction.Transactional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MerchantService {
|
||||
public final MerchantRepository merchantRepository;
|
||||
public final MerchantMapper merchantMapper;
|
||||
|
||||
/**
|
||||
* 根据商户id获取商户信息
|
||||
* @param id 商户id
|
||||
* @return 商户信息
|
||||
*/
|
||||
public Optional<MerchantDto> getById(Long id) {
|
||||
return this.merchantRepository.findById(id)
|
||||
.map(this.merchantMapper::toDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新商户信息
|
||||
* @param id 商户id
|
||||
* @param merchantUpdateVo 更新信息
|
||||
* @return 商户信息
|
||||
* @throws Exception 错误信息
|
||||
*/
|
||||
@Transactional(rollbackOn = Exception.class)
|
||||
public MerchantDto updateMerchant(Long id, MerchantUpdateVo merchantUpdateVo) throws Exception {
|
||||
var entity = this.merchantRepository.findById(id)
|
||||
.orElseThrow(() -> new Exception("商户信息不存在"));
|
||||
entity.setRealName(merchantUpdateVo.getRealName());
|
||||
this.merchantRepository.save(entity);
|
||||
return this.merchantMapper.toDto(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 商户身份证信息验证
|
||||
*
|
||||
* @param id 商户id
|
||||
* @param idCardNo 身份证号
|
||||
* @param realName 真实姓名
|
||||
* @throws Exception 验证异常信息
|
||||
*/
|
||||
@Transactional(rollbackOn = Exception.class)
|
||||
public MerchantDto verifyMerchant(Long id, String idCardNo, String realName) throws Exception {
|
||||
var entity = this.merchantRepository.findById(id)
|
||||
.orElseThrow(() -> new Exception("商户信息不存在"));
|
||||
if (entity.getAuthStatus() == AuthStatus.VERIFIED) {
|
||||
throw new Exception("商户信息已认证");
|
||||
}
|
||||
this.idCardVerify(idCardNo, realName);
|
||||
entity.setAuthStatus(AuthStatus.VERIFIED);
|
||||
entity.setIdCardNo(idCardNo);
|
||||
entity.setIdCardEncrypted(idCardNo);
|
||||
entity.setRealName(realName);
|
||||
this.merchantRepository.save(entity);
|
||||
return this.merchantMapper.toDto(entity);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 第三方验证
|
||||
* @param idCardNo 身份证号码
|
||||
* @param realName 真实姓名
|
||||
* @throws Exception 验证失败信息
|
||||
*/
|
||||
public void idCardVerify(String idCardNo, String realName) throws Exception {
|
||||
// TODO: 调用腾讯云身份证二要素核验接口
|
||||
throw new Exception("验证失败,正在开发中");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.xjhs.findmemerchant.service;
|
||||
|
||||
import com.xjhs.findmemerchant.common.jpa.query.JpaSpecs;
|
||||
import com.xjhs.findmemerchant.dto.store.StoreDto;
|
||||
import com.xjhs.findmemerchant.mapper.StoreMapper;
|
||||
import com.xjhs.findmemerchant.repository.StoreRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class StoreService {
|
||||
|
||||
private final StoreRepository storeRepository;
|
||||
private final StoreMapper storeMapper;
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
*
|
||||
* @param pageable 分页参数
|
||||
* @param merchantId 商户id
|
||||
* @return 分页数据
|
||||
*/
|
||||
public Page<StoreDto> findPage(Pageable pageable, Long merchantId) {
|
||||
return this.storeRepository.findAll(Specification.allOf(
|
||||
JpaSpecs.eq("merchant.id", merchantId)
|
||||
), pageable).map(this.storeMapper::toDto);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.xjhs.findmemerchant.types;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 活动状态
|
||||
* 0-未开始 1-进行中 2-已结束 3-已下架
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ActivityStatus {
|
||||
|
||||
NOT_STARTED("未开始"),
|
||||
ONGOING("进行中"),
|
||||
ENDED("已结束"),
|
||||
OFFLINE("已下架");
|
||||
|
||||
private final String desc;
|
||||
|
||||
public static ActivityStatus fromCode(Byte code) {
|
||||
if (code == null) return null;
|
||||
|
||||
return switch (code) {
|
||||
case 0 -> NOT_STARTED;
|
||||
case 1 -> ONGOING;
|
||||
case 2 -> ENDED;
|
||||
case 3 -> OFFLINE;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public byte code() {
|
||||
return switch (this) {
|
||||
case NOT_STARTED -> 0;
|
||||
case ONGOING -> 1;
|
||||
case ENDED -> 2;
|
||||
case OFFLINE -> 3;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.xjhs.findmemerchant.types;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ActivityType {
|
||||
|
||||
GROUP_BUY("团购"),
|
||||
DISCOUNT("折扣"),
|
||||
FLASH_SALE("限时优惠");
|
||||
|
||||
private final String desc;
|
||||
|
||||
// 数据库存储 tinyint -> 转为枚举
|
||||
public static ActivityType fromCode(Byte code) {
|
||||
if (code == null) return null;
|
||||
return switch (code) {
|
||||
case 1 -> GROUP_BUY;
|
||||
case 2 -> DISCOUNT;
|
||||
case 3 -> FLASH_SALE;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
// 枚举 -> 数据库存储值
|
||||
public byte code() {
|
||||
return switch (this) {
|
||||
case GROUP_BUY -> 1;
|
||||
case DISCOUNT -> 2;
|
||||
case FLASH_SALE -> 3;
|
||||
};
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue