首次提交

This commit is contained in:
guotao 2026-01-09 12:20:24 +08:00
commit 1101681331
131 changed files with 7017 additions and 0 deletions

View file

@ -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);
}
}

View 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;
}
}

View 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;
}

View 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;
}

View file

@ -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 {
}

View file

@ -0,0 +1,4 @@
package com.xjhs.findmemerchant.common.jackson;
public class SafeLongDeserializer {
}

View file

@ -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();
}
}

View file

@ -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 {
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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<>();
}
}
}

View file

@ -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<>();
}
}
}

View file

@ -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();
};
}
}

View file

@ -0,0 +1,4 @@
package com.xjhs.findmemerchant.common.mvc;
public class GlobalResponseHandler {
}

View file

@ -0,0 +1,4 @@
package com.xjhs.findmemerchant.common.mvc;
public class PageVo {
}

View 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;
}
}

View file

@ -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));
};
}
}

View file

@ -0,0 +1,4 @@
package com.xjhs.findmemerchant.config;
public class JpaConfig {
}

View file

@ -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;
}
}

View 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 {
}

View file

@ -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";
}

View file

@ -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("商户信息不存在");
}
}

View file

@ -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("商户信息不存在");
}
}

View file

@ -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("删除成功");
}
}

View file

@ -0,0 +1,4 @@
package com.xjhs.findmemerchant.controller;
public class StoreEmployeeController {
}

View 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;
}

View file

@ -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
) {
}

View file

@ -0,0 +1,4 @@
package com.xjhs.findmemerchant.dto.member;
public class EmployeeDto {
}

View file

@ -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;
}

View file

@ -0,0 +1,4 @@
package com.xjhs.findmemerchant.dto.store;
public class StoreBusinessStatusDto {
}

View 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;
}

View 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;
}
}

View 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() : "未知";
}
}

View file

@ -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() : "未知";
}
}

View file

@ -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 -> "未知";
};
}
}

View 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.99decimal(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;
}
}

View 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);
}
}

View file

@ -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;
}

View 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);
}
}

View file

@ -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() : "未知";
}
}

View 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";
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View 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));
}
}

View 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;
}
}

View 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;
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() : "未知";
}
}

View 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() : "未知";
}
}

View 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() : "未知";
}
}

View file

@ -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;
}

View 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;
}
}

View 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);
}
}

View 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() : "未知";
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View file

@ -0,0 +1,4 @@
package com.xjhs.findmemerchant.mapper;
public class EmployeeMapper {
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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));
}
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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("验证失败,正在开发中");
}
}

View file

@ -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);
}
}

View file

@ -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;
};
}
}

View file

@ -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;
};
}
}

View file

@ -0,0 +1,34 @@
package com.xjhs.findmemerchant.types;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 商户实名认证状态
* 0-未认证 1-已认证
*/
@Getter
@AllArgsConstructor
public enum AuthStatus {
NOT_VERIFIED("未认证"),
VERIFIED("已认证");
private final String desc;
public static AuthStatus fromCode(Byte code) {
if (code == null) return null;
return switch (code) {
case 0 -> NOT_VERIFIED;
case 1 -> VERIFIED;
default -> null;
};
}
public byte code() {
return switch (this) {
case NOT_VERIFIED -> 0;
case VERIFIED -> 1;
};
}
}

View file

@ -0,0 +1,38 @@
package com.xjhs.findmemerchant.types;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 营业执照审核状态
* 0-待审核 1-已通过 2-已拒绝
*/
@Getter
@AllArgsConstructor
public enum BusinessLicenseStatus {
PENDING("待审核"),
APPROVED("已通过"),
REJECTED("已拒绝");
private final String desc;
public static BusinessLicenseStatus fromCode(Byte code) {
if (code == null) return null;
return switch (code) {
case 0 -> PENDING;
case 1 -> APPROVED;
case 2 -> REJECTED;
default -> null;
};
}
public byte code() {
return switch (this) {
case PENDING -> 0;
case APPROVED -> 1;
case REJECTED -> 2;
};
}
}

View file

@ -0,0 +1,37 @@
package com.xjhs.findmemerchant.types;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 门店营业状态
* 1-营业中 0-已打烊 2-临时打烊
*/
@Getter
@AllArgsConstructor
public enum BusinessStatus {
CLOSED("已打烊"), // 0
OPEN("营业中"), // 1
TEMP_CLOSED("临时打烊"); // 2
private final String desc;
public static BusinessStatus fromCode(Byte code) {
if (code == null) return null;
return switch (code) {
case 0 -> CLOSED;
case 1 -> OPEN;
case 2 -> TEMP_CLOSED;
default -> null;
};
}
public byte code() {
return switch (this) {
case CLOSED -> 0;
case OPEN -> 1;
case TEMP_CLOSED -> 2;
};
}
}

View file

@ -0,0 +1,34 @@
package com.xjhs.findmemerchant.types;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 通用启用/禁用状态
* 0-禁用 1-启用
*/
@Getter
@AllArgsConstructor
public enum CommonStatus {
DISABLED("禁用"),
ENABLED("启用");
private final String desc;
public static CommonStatus fromCode(Byte code) {
if (code == null) return null;
return switch (code) {
case 0 -> DISABLED;
case 1 -> ENABLED;
default -> null;
};
}
public byte code() {
return switch (this) {
case DISABLED -> 0;
case ENABLED -> 1;
};
}
}

Some files were not shown because too many files have changed in this diff Show more