Compare commits

..

7 commits
master ... dev

Author SHA1 Message Date
guotao
c150c74206 修复文件上传根目录配置读取错误 2026-01-12 19:23:53 +08:00
guotao
78d47cf363 添加开发配置 2026-01-12 19:18:18 +08:00
guotao
cf2295b85f 完成以下第三方接口对接:
1.地图功能-地理/逆地理编码
2.发送短信验证码
3.COS云对象存储对接
其他:
完成文件上传/预览公共接口
2026-01-12 18:50:25 +08:00
guotao
39953cca84 完成以下第三方接口对接:
1.地图功能-地理/逆地理编码
2.发送短信验证码
3.COS云对象存储对接
其他:
完成文件上传/预览公共接口
2026-01-12 18:50:22 +08:00
guotao
0edffa145b 修复用户登录 2026-01-12 15:42:01 +08:00
guotao
f14f45faf5 清理代码 2026-01-12 14:54:31 +08:00
guotao
b6a678b874 处理类型转换 2026-01-09 12:59:02 +08:00
119 changed files with 1002 additions and 6059 deletions

34
pom.xml
View file

@ -17,7 +17,25 @@
<properties>
<java.version>25</java.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2025.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
@ -76,6 +94,22 @@
<artifactId>commons-lang3</artifactId>
<version>3.18.0</version>
</dependency>
<!--腾讯云SDK-->
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java</artifactId>
<version>3.1.1396</version>
</dependency>
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
<version>5.6.246</version>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>3.2.3</version>
</dependency>
</dependencies>
<build>

View file

@ -34,7 +34,7 @@ public class AbstractBaseEntity {
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
@Comment("创建时间")
private LocalDateTime createdAt;
private LocalDateTime createTime;
/**
* 创建人
*/
@ -49,7 +49,7 @@ public class AbstractBaseEntity {
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
@Comment("更新时间")
private LocalDateTime updatedAt;
private LocalDateTime updateTime;
/**
* 更新人
*/
@ -62,20 +62,20 @@ public class AbstractBaseEntity {
*/
@Column(name = "deleted_at")
@Comment("软删除时间")
private LocalDateTime deletedAt;
private LocalDateTime deleteTime;
/**
* 是否已删除
*/
public boolean isDeleted() {
return deletedAt != null;
return deleteTime != null;
}
/**
* 标记删除
*/
public void markDeleted() {
this.deletedAt = LocalDateTime.now();
this.deleteTime = LocalDateTime.now();
}
}

View file

@ -0,0 +1,75 @@
package com.xjhs.findmemerchant.common.openapi;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.exception.CosClientException;
import com.qcloud.cos.model.GetObjectRequest;
import com.qcloud.cos.model.PutObjectRequest;
import com.xjhs.findmemerchant.common.openapi.dto.CosPutObjectResult;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.InputStream;
import java.util.UUID;
@Slf4j
@Service
@RequiredArgsConstructor
public class TencentCOSService {
@Value("${appconfig.tencentCos.bucketName}")
private String bucketName;
private final COSClient tencentCosClient;
/**
* 文件上传到Cos存储桶
*
* @param file 文件
* @return 存储对象信息
*/
public CosPutObjectResult putObject(File file) throws Exception {
try {
var key = UUID.randomUUID().toString();
var req = new PutObjectRequest(this.bucketName, key, file);
var result = tencentCosClient.putObject(req);
return new CosPutObjectResult(key, result.getETag());
} catch (CosClientException e) {
log.error("文件上传到对象存储失败", e);
throw new Exception("文件上传到对象存储失败");
}
}
/**
* 获取文件输入流(下载文件)
* @param key 对象key
* @return 输入流
* @throws Exception 下载失败
*/
public InputStream getObject(String key) throws Exception{
try {
var req = new GetObjectRequest(this.bucketName, key);
var cosObject = this.tencentCosClient.getObject(req);
return cosObject.getObjectContent();
} catch (CosClientException e) {
log.error("从对象存储下载文件失败",e);
throw new Exception("从对象存储下载文件失败");
}
}
/**
* 删除存储对象
*
* @param key 对象key
* @throws Exception 删除失败
*/
public void deleteObject(String key) throws Exception {
try {
this.tencentCosClient.deleteObject(this.bucketName, key);
} catch (CosClientException e) {
log.error("文件上传到对象存储失败", e);
throw new Exception("文件上传到对象存储失败");
}
}
}

View file

@ -0,0 +1,57 @@
package com.xjhs.findmemerchant.common.openapi;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.sms.v20190711.models.SendSmsRequest;
import com.tencentcloudapi.sms.v20190711.SmsClient;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor()
public class TencentCloudSMSService {
@Value("${appconfig.tencentSms.sdkAppId}")
private String sdkAppid;
@Value("${appconfig.tencentSms.templateId}")
private String templateId;
@Value("${appconfig.tencentSms.signName}")
private String signName;
private final SmsClient tencentSmsClient;
/**
* 发送短信验证码
*
* @param phone 11位国内手机号码
* @param code 验证码
* @return 验证码值
* @throws Exception 发送失败
*/
public String sendVerifyCode(String phone, String code) throws Exception {
try {
if (!phone.startsWith("+86")) {
phone = "+86" + phone;
}
var req = new SendSmsRequest();
req.setSmsSdkAppid(this.sdkAppid);
req.setTemplateID(this.templateId);
req.setSign(this.signName);
req.setPhoneNumberSet(new String[]{phone});
req.setTemplateParamSet(new String[]{code});
var resp = this.tencentSmsClient.SendSms(req);
if (resp.getSendStatusSet().length == 0){
throw new Exception("取回短信发送结果失败");
}
if (!"ok".equalsIgnoreCase(resp.getSendStatusSet()[0].getCode())){
throw new Exception(resp.getSendStatusSet()[0].getMessage());
}
return code;
} catch (TencentCloudSDKException e) {
log.error("验证码发送失败", e);
throw new Exception("系统错误,验证码发送失败");
}
}
}

View file

@ -0,0 +1,35 @@
package com.xjhs.findmemerchant.common.openapi.amap;
import com.xjhs.findmemerchant.common.openapi.amap.response.AmapGeocodeResponse;
import com.xjhs.findmemerchant.common.openapi.amap.response.AmapReGeocodeResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 高德地图服务接口
*/
@FeignClient(name = "amapFeignClient", url = "https://restapi.amap.com")
public interface AmapFeignClient {
/**
* 地理编码 API 服务
* @param key 高德Key
* @param address 结构化地址信息,规则遵循国家省份城市区县城镇乡村街道门牌号码屋邨大厦北京市朝阳区阜通东大街6号
* @return 响应信息
*/
@GetMapping("/v3/geocode/geo")
AmapGeocodeResponse getGeo(@RequestParam(name = "key") String key,
@RequestParam(name = "address") String address);
/**
* 逆地理编码 API 服务地址
* @param key 高德Key
* @param location 经纬度坐标 传入内容规则经度在前纬度在后经纬度间以,分割经纬度小数点后不要超过 6
*/
@GetMapping("/v3/geocode/regeo")
AmapReGeocodeResponse getReGeo(@RequestParam(name = "key") String key,
@RequestParam(name = "location") String location);
}

View file

@ -0,0 +1,39 @@
package com.xjhs.findmemerchant.common.openapi.amap;
import com.xjhs.findmemerchant.common.openapi.amap.response.AmapGeocodeResponse;
import com.xjhs.findmemerchant.common.openapi.amap.response.AmapReGeocodeResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class AmapService {
@Value("${appconfig.amapKey}")
private String amapKey = "c618de6e686c43095a8593db836c7de2";
private final AmapFeignClient amapFeignClient;
/**
* 地址转经纬度
*
* @param address 地址信息
* @return 经纬度信息结果
*/
public AmapGeocodeResponse getGeo(String address) {
return this.amapFeignClient.getGeo(this.amapKey, address);
}
/**
* 经纬度转地理位置信息
*
* @param lng 经度
* @param lat 纬度
* @return 位置信息
*/
public AmapReGeocodeResponse getRegeo(double lng, double lat) {
return this.amapFeignClient.getReGeo(this.amapKey, lng + "," + lat);
}
}

View file

@ -0,0 +1,95 @@
package com.xjhs.findmemerchant.common.openapi.amap.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 地理编码 API 服务响应内容
*/
@Data
public class AmapGeocodeResponse {
/**
* 请求状态值0 表示请求失败1 表示请求成功
*/
private int status;
/**
* 返回结果的数量
*/
private int count;
/**
* 返回结果的详细说明 status 0 info 会包含错误原因否则返回 "OK"
*/
private String info;
/**
* 地理编码信息列表包含多个地址组件
*/
private List<GeocodeDetails> geocodes = new ArrayList<>();
/**
* 地理编码信息的详细字段
*/
@Data
public static class GeocodeDetails {
@JsonProperty("formatted_address")
private String address;
/**
* 国家默认返回中国
*/
private String country;
/**
* 省份名称例如北京市
*/
private String province;
/**
* 城市名称例如北京市
*/
private String city;
/**
* 城市编码例如010
*/
private String citycode;
/**
* 区域名称例如朝阳区
*/
private String district;
/**
* 街道名称例如单通东大街
*/
private String street;
/**
* 门牌号例如6号
*/
private String number;
/**
* 区域编码例如110101
*/
private String adcode;
/**
* 经纬度坐标,逗号分隔
*/
private String location;
/**
* 匹配级别用于地理匹配的精确度
*/
private String level;
}
}

View file

@ -0,0 +1,207 @@
package com.xjhs.findmemerchant.common.openapi.amap.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 逆地理位置服务响应内容
*/
@Data
public class AmapReGeocodeResponse {
/**
* 请求状态1 表示请求成功
*/
private String status;
/**
* 地址组件信息
*/
private Regeocode regeocode;
/**
* 返回的状态信息
*/
private String info;
/**
* 状态码10000 表示成功
*/
private String infocode;
/**
* 地址组件类包含具体的地理信息
*/
@Data
public static class Regeocode {
/**
* 地址组件包含城市街道等信息
*/
private AddressComponent addressComponent;
/**
* 格式化后的地址整合了详细信息
*/
@JsonProperty("formatted_address")
private String address;
}
/**
* 地址组件包含详细的地理信息
*/
@Data
public static class AddressComponent {
/**
* 城市名称可能为空
*/
private List<String> city = new ArrayList<>();
/**
* 省份名称
*/
private String province;
/**
* 区域编码
*/
private String adcode;
/**
* 区域名称
*/
private String district;
/**
* 城镇编码
*/
private String towncode;
/**
* 街道信息
*/
private StreetNumber streetNumber;
/**
* 国家名称
*/
private String country;
/**
* 街道所属乡镇名称
*/
private String township;
/**
* 商圈信息包含多个商圈
*/
private List<BusinessArea> businessAreas;
/**
* 建筑信息
*/
private Building building;
/**
* 邻里信息
*/
private Neighborhood neighborhood;
/**
* 城市编码
*/
private String citycode;
}
/**
* 街道信息包括街道名称位置方向等
*/
@Data
public static class StreetNumber {
/**
* 门牌号
*/
private String number;
/**
* 经纬度位置
*/
private String location;
/**
* 方向
*/
private String direction;
/**
* 距离
*/
private String distance;
/**
* 街道名称
*/
private String street;
}
/**
* 商圈信息
*/
@Data
public static class BusinessArea {
/**
* 商圈位置经纬度
*/
private String location;
/**
* 商圈名称
*/
private String name;
/**
* 商圈ID
*/
private String id;
}
/**
* 建筑信息包括建筑名称和类型
*/
@Data
public static class Building {
/**
* 建筑名称
*/
private String name;
/**
* 建筑类型
*/
private String type;
}
/**
* 邻里信息
*/
@Data
public static class Neighborhood {
/**
* 邻里名称
*/
private String name;
/**
* 邻里类型
*/
private String type;
}
}

View file

@ -0,0 +1,13 @@
package com.xjhs.findmemerchant.common.openapi.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CosPutObjectResult {
private String key;
private String eTag;
}

View file

@ -0,0 +1,13 @@
package com.xjhs.findmemerchant.config;
import com.xjhs.findmemerchant.common.openapi.amap.AmapFeignClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableFeignClients(basePackageClasses = {
AmapFeignClient.class,
})
public class FeignConfig {
}

View file

@ -1,6 +1,6 @@
package com.xjhs.findmemerchant.config;
import com.xjhs.findmemerchant.entity.Merchant;
import com.xjhs.findmemerchant.security.LoginUser;
import com.xjhs.findmemerchant.security.sms.SmsAuthenticationToken;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -20,8 +20,8 @@ public class JpaConfig {
var auth = SecurityContextHolder.getContext().getAuthentication();
if (auth instanceof SmsAuthenticationToken){
var principal = auth.getPrincipal();
if (principal instanceof Merchant merchant){
return Optional.of(merchant.getId());
if (principal instanceof LoginUser loginUser){
return Optional.of(loginUser.getUserId());
}
}
return Optional.of(0L);

View file

@ -0,0 +1,36 @@
package com.xjhs.findmemerchant.config;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.auth.BasicCOSCredentials;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.common.profile.Region;
import com.tencentcloudapi.sms.v20190711.SmsClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ThirdOpenApiConfig {
@Bean
public SmsClient tencentSmsClient(@Value("${appconfig.tencentSms.secretId}") String secretId,
@Value("${appconfig.tencentSms.secretKey}") String secretKey) {
var cred = new Credential(secretId, secretKey);
var httpProfile = new HttpProfile();
httpProfile.setEndpoint("sms.tencentcloudapi.com");
var clientProfile = new ClientProfile();
clientProfile.setHttpProfile(httpProfile);
return new SmsClient(cred, "ap-guangzhou", clientProfile);
}
@Bean
public COSClient tencentCosClient(@Value("${appconfig.tencentCos.secretId}") String secretId,
@Value("${appconfig.tencentCos.secretKey}") String secretKey){
var cred = new BasicCOSCredentials(secretId,secretKey);
ClientConfig clientConfig = new ClientConfig(new com.qcloud.cos.region.Region("ap-guangzhou"));
return new COSClient(cred, clientConfig);
}
}

View file

@ -1,20 +1,22 @@
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.dto.merchant.MerchantDto;
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.LoginUser;
import com.xjhs.findmemerchant.security.RefreshTokenService;
import com.xjhs.findmemerchant.security.sms.SmsAuthenticationToken;
import com.xjhs.findmemerchant.security.sms.SmsCodeService;
import com.xjhs.findmemerchant.system.SystemUserService;
import com.xjhs.findmemerchant.vo.auth.SmsLoginVo;
import com.xjhs.findmemerchant.vo.auth.SmsSendVo;
import com.xjhs.findmemerchant.service.MerchantService;
import com.xjhs.findmemerchant.vo.merchant.MerchantUpdateVo;
import com.xjhs.findmemerchant.vo.auth.RegisterVo;
import com.xjhs.findmemerchant.vo.merchant.MerchantUpdateVo;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -22,6 +24,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@ -40,7 +43,8 @@ public class AuthController {
private final RefreshTokenService refreshTokenService;
private final MerchantRepository merchantRepository;
private final TokenBlacklistRedisService tokenBlacklistRedisService;
private final MerchantService merchantService;
private final SystemUserService systemUserService;
private final MerchantMapper merchantMapper;
/**
* 发送短信验证码
@ -122,41 +126,46 @@ public class AuthController {
* @return 注册成功返回登录令牌信息
*/
@PostMapping("/register")
public ApiResult<RegisterDto> register(@Valid @RequestBody RegisterVo registerVo) {
@Transactional
public ApiResult<RegisterDto> register(@Valid @RequestBody RegisterVo registerVo) throws Exception {
try {
this.smsCodeService.verifyCode(registerVo.getPhone(), "register", registerVo.getCode());
var exists = merchantRepository.existsByPhone(registerVo.getPhone());
if (exists) {
return ApiResult.fail("手机号已被注册");
var systemUser = this.systemUserService.getAndRegisterByPhone(registerVo.getPhone());
if (systemUser.getMerchant() != null){
throw new Exception("手机号已被注册");
}
var merchant = new Merchant();
merchant.setPhone(registerVo.getPhone());
merchant.setSystemUser(systemUser);
systemUser.setMerchant(merchant);
this.merchantRepository.save(merchant);
return ApiResult.data(
new RegisterDto(
merchant.getId(),
merchant.getId().toString(),
this.jwtTokenService.generateToken(registerVo.getPhone()),
this.refreshTokenService.create(registerVo.getPhone())
)
);
} catch (Exception e) {
log.error("注册失败", e);
return ApiResult.fail("注册失败:" + e.getMessage());
throw e;
}
}
/**
* 获取当前登录的商家基本信息
*
* @param merchant @ignore 当前商家
* @param loginUser @ignore 当前登录用户
* @return 商家信息
*/
@GetMapping("/profile")
public ApiResult<MerchantDto> getProfile(@AuthenticationPrincipal Merchant merchant) {
if (merchant == null) {
@Transactional(readOnly = true)
public ApiResult<MerchantDto> getProfile(@AuthenticationPrincipal LoginUser loginUser) {
if (loginUser.getMerchantId() == null) {
return ApiResult.fail("商家信息不存在");
}
return this.merchantService.getById(merchant.getId())
return this.merchantRepository.findById(loginUser.getMerchantId())
.map(this.merchantMapper::toDto)
.map(ApiResult::data)
.orElse(ApiResult.fail("商家信息不存在"));
}
@ -172,8 +181,8 @@ public class AuthController {
public ApiResult<MerchantDto> updateProfile(@AuthenticationPrincipal Merchant merchant,
@Valid @RequestBody MerchantUpdateVo merchantUpdateVo) {
try {
var result = this.merchantService.updateMerchant(merchant.getId(), merchantUpdateVo);
return ApiResult.data(result);
// TODO : 待完成
return ApiResult.data(null);
} catch (Exception e) {
return ApiResult.fail(e.getMessage());
}

View file

@ -1,75 +0,0 @@
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.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.security.Principal;
/**
* 商家管理接口
*/
@Slf4j
@RestController
@RequestMapping("/merchant")
@RequiredArgsConstructor
public class MerchantController {
private final MerchantService merchantService;
/**
* 查询当前登录商家详情
*
* @param merchant @ignore 当前商家
* @return 商家详情信息
*/
@GetMapping
public ApiResult<MerchantDto> getMerchant(@AuthenticationPrincipal Merchant merchant) {
return this.merchantService.getById(merchant.getId())
.map(ApiResult::data)
.orElse(ApiResult.fail("商家信息不存在"));
}
/**
* 更新当前登录商家信息
*
* @param merchant @ignore 当前商家
* @param merchantUpdateVo 更新参数
* @return 商家信息
*/
@PutMapping
public ApiResult<MerchantDto> updateMerchant(@AuthenticationPrincipal Merchant merchant,
@Valid @RequestBody MerchantUpdateVo merchantUpdateVo) {
try {
var dto = this.merchantService.updateMerchant(merchant.getId(), merchantUpdateVo);
return ApiResult.data(dto);
} catch (Exception e) {
return ApiResult.fail(e.getMessage());
}
}
/**
* 当前商家验证
*
* @param merchant @ignore 当前商家
* @param merchantVerifyVo 验证参数
* @return 商家信息
*/
@PostMapping("/verify")
public ApiResult<MerchantDto> verifyMerchant(@AuthenticationPrincipal Merchant merchant,
@Valid @RequestBody MerchantVerifyVo merchantVerifyVo) {
try {
var dto = this.merchantService.verifyMerchant(merchant.getId(), merchantVerifyVo.getIdCardNo(), merchantVerifyVo.getRealName());
return ApiResult.data(dto);
} catch (Exception e) {
return ApiResult.fail(e.getMessage());
}
}
}

View file

@ -1,237 +0,0 @@
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.common.mvc.PageVo;
import com.xjhs.findmemerchant.dto.store.BusinessPeriodDto;
import com.xjhs.findmemerchant.dto.store.StoreBusinessStatusDto;
import com.xjhs.findmemerchant.dto.store.StoreDto;
import com.xjhs.findmemerchant.entity.BusinessPeriod;
import com.xjhs.findmemerchant.entity.Merchant;
import com.xjhs.findmemerchant.entity.Store;
import com.xjhs.findmemerchant.mapper.StoreMapper;
import com.xjhs.findmemerchant.repository.BusinessPeriodRepository;
import com.xjhs.findmemerchant.repository.MerchantRepository;
import com.xjhs.findmemerchant.repository.StoreRepository;
import com.xjhs.findmemerchant.vo.store.BusinessPeriodVo;
import com.xjhs.findmemerchant.vo.store.StoreBusinessStatusUpdateVo;
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.jpa.domain.Specification;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.security.Principal;
import java.util.List;
import java.util.Objects;
/**
* 门店管理接口
*/
@Slf4j
@RestController
@RequestMapping("/stores")
@RequiredArgsConstructor
public class StoreController {
private final MerchantRepository merchantRepository;
private final StoreRepository storeRepository;
private final StoreMapper storeMapper;
private final BusinessPeriodRepository businessPeriodRepository;
/**
* 门店信息(分页查询)
*
* @param pageVo 分页参数
* @param merchant @ignore 当前登录商家
* @return 分页数据信息
*/
@GetMapping
@Transactional(readOnly = true)
public ApiResult<PageData<StoreDto>> findPage(@AuthenticationPrincipal Merchant merchant, PageVo pageVo) {
var pageData = this.storeRepository.findAll(Specification.allOf(
JpaSpecs.eq("merchant.id", merchant.getId())
), pageVo.getPageable()).map(this.storeMapper::toDto);
return ApiResult.page(pageData.getTotalElements(), pageData.getContent());
}
/**
* 创建商家的门店
*
* @param vo 门店信息
* @param merchant @ignore 当前登录的商家
* @return 门店信息
*/
@PostMapping
@Transactional(rollbackFor = Exception.class)
public ApiResult<StoreDto> create(@AuthenticationPrincipal Merchant merchant,
@Valid @RequestBody StoreCreateVo vo) {
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 merchant @ignore 当前登录的商家
* @return 门店详情
*/
@GetMapping("/{storeId}")
@Transactional(readOnly = true)
public ApiResult<StoreDto> findById(@AuthenticationPrincipal Merchant merchant,
@PathVariable("storeId") String storeId) {
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 merchant @ignore 当前登录的商家
* @param vo 更新对象
* @return 门店信息
*/
@PutMapping("/{storeId}")
@Transactional(rollbackFor = Exception.class)
public ApiResult<StoreDto> updateById(@AuthenticationPrincipal Merchant merchant,
@PathVariable("storeId") String storeId,
@Valid @RequestBody StoreUpdateVo vo) {
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 merchant @ignore 当前登录的商家
*/
@DeleteMapping("/{storeId}")
@Transactional(rollbackFor = Exception.class)
public ApiResult<Void> delteById(@AuthenticationPrincipal Merchant merchant,
@PathVariable("storeId") String storeId) {
var storeIdLong = Long.parseLong(storeId);
merchant.getStores().removeIf(x -> Objects.equals(storeIdLong, x.getId()));
this.merchantRepository.save(merchant);
return ApiResult.success("删除成功");
}
/**
* 获取门店营业状态
*
* @param storeId 门店id
* @param merchant @ignore 当前商户
* @return 门店营业状态信息
*/
@GetMapping("/{storeId}/business-status")
@Transactional(readOnly = true)
public ApiResult<StoreBusinessStatusDto> getStoreBusinessStatus(@AuthenticationPrincipal Merchant merchant,
@PathVariable("storeId") String storeId) {
var storeIdLong = Long.parseLong(storeId);
return merchant.getStores().stream()
.filter(x -> Objects.equals(x.getId(), storeIdLong))
.map(this.storeMapper::toBusinessStatusDto)
.map(ApiResult::data)
.findFirst()
.orElse(ApiResult.fail("门店信息不存在"));
}
/**
* 设置门店营业状态
*
* @param storeId 门店id
* @param merchant @ignore 当前商家
* @param updateVo 更新参数
* @return 门店营业状态信息
*/
@PutMapping("/{storeId}/business-status")
@Transactional(rollbackFor = Exception.class)
public ApiResult<StoreBusinessStatusDto> putStoreBusinessStatus(@PathVariable("storeId") String storeId,
@AuthenticationPrincipal Merchant merchant,
@Validated @RequestBody StoreBusinessStatusUpdateVo updateVo) {
var storeIdLong = Long.parseLong(storeId);
return merchant.getStores().stream()
.filter(x -> Objects.equals(x.getId(), storeIdLong))
.findFirst()
.map(x -> {
this.storeMapper.updateFromBusinessUpdateVo(updateVo, x);
this.storeRepository.save(x);
return this.storeMapper.toBusinessStatusDto(x);
})
.map(ApiResult::data)
.orElse(ApiResult.fail("门店信息不存在"));
}
/**
* 获取门店营业时间段
*
* @param storeId 门店id
* @param merchant @ignore 当前商家
* @return 门店营业时间段 列表
*/
@GetMapping("/{storeId}/business-periods")
public ApiResult<List<BusinessPeriodDto>> getStoreBusinessPeriodList(@AuthenticationPrincipal Merchant merchant,
@PathVariable("storeId") String storeId) {
var storeIdLong = Long.parseLong(storeId);
return merchant.getStores().stream()
.filter(x -> Objects.equals(x.getId(), storeIdLong))
.findFirst()
.map(x -> this.storeMapper.toDtoList(x.getBusinessPeriods()))
.map(ApiResult::data)
.orElse(ApiResult.fail("门店信息不存在"));
}
/**
* 设置门店营业时间段
*
* @param storeId 门店id
* @param merchant @ignore 当前商家
* @param updateVoList 门店营业时间段更新参数列表
* @return 门店营业时间段 列表
*/
@PutMapping("/{storeId}/business-periods")
public ApiResult<List<BusinessPeriodDto>> putStoreBusinessPeriodList(@PathVariable("storeId") String storeId,
@AuthenticationPrincipal Merchant merchant,
@Validated @RequestBody List<BusinessPeriodVo> updateVoList) {
var storeIdLong = Long.parseLong(storeId);
return merchant.getStores().stream()
.filter(x -> Objects.equals(x.getId(), storeIdLong))
.findFirst()
.map(store -> {
store.getBusinessPeriods().clear();
for (BusinessPeriodVo businessPeriodVo : updateVoList) {
var entity = this.storeMapper.toEntity(businessPeriodVo);
store.addBusinessPeriods(entity);
this.businessPeriodRepository.save(entity);
}
return this.storeMapper.toDtoList(store.getBusinessPeriods());
})
.map(ApiResult::data)
.orElse(ApiResult.fail("门店信息不存在"));
}
}

View file

@ -1,157 +0,0 @@
package com.xjhs.findmemerchant.controller;
import com.xjhs.findmemerchant.common.ApiResult;
import com.xjhs.findmemerchant.dto.member.EmployeeDto;
import com.xjhs.findmemerchant.entity.Merchant;
import com.xjhs.findmemerchant.mapper.EmployeeMapper;
import com.xjhs.findmemerchant.repository.EmployeeRepository;
import com.xjhs.findmemerchant.vo.member.EmployeeCreateVo;
import com.xjhs.findmemerchant.vo.member.EmployeeUpdateVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Objects;
/**
* 门店员工管理接口
*/
@Slf4j
@RestController
@RequestMapping("/stores/{storeId}/employees")
@RequiredArgsConstructor
public class StoreEmployeeController {
private final EmployeeRepository employeeRepository;
private final EmployeeMapper employeeMapper;
/**
* 查询门店员工列表
* @param merchant @ignore 当前商户
* @param storeId 门店id
* @return 员工列表信息
*/
@GetMapping
@Transactional(readOnly = true)
public ApiResult<List<EmployeeDto>> getList(@AuthenticationPrincipal Merchant merchant,
@PathVariable("storeId") Long storeId) {
var list = this.employeeRepository.findByStoreId(storeId)
.stream().map(this.employeeMapper::toDto)
.toList();
return ApiResult.data(list);
}
/**
* 新建门店员工
* @param merchant @ignore 当前商户
* @param storeId 门店id
* @param createVo 员工信息参数
* @return 门店员工信息
*/
@PostMapping
@Transactional
public ApiResult<EmployeeDto> create(@AuthenticationPrincipal Merchant merchant,
@PathVariable("storeId") Long storeId,
@Validated @RequestBody EmployeeCreateVo createVo) throws Exception {
try {
var store = merchant.getStores().stream()
.filter(x -> Objects.equals(storeId, x.getId()))
.findFirst()
.orElseThrow(() -> new Exception("门店信息不存在"));
var phoneExists = store.getEmployees().stream()
.anyMatch(x -> Objects.equals(x.getPhone(), createVo.getPhone()));
if (phoneExists) {
throw new Exception("手机号码已被使用");
}
var employee = this.employeeMapper.toEntity(createVo);
store.addEmployee(employee);
this.employeeRepository.save(employee);
var dto = this.employeeMapper.toDto(employee);
return ApiResult.data(dto);
} catch (Exception e) {
log.error("员工信息注册失败", e);
throw e;
}
}
/**
* 查询门店员工详情
* @param merchant @ignore 当前商户
* @param storeId 门店id
* @param employeeId 门店员工id
* @return 员工详情
*/
@GetMapping("/{employeeId}")
@Transactional(readOnly = true)
public ApiResult<EmployeeDto> findById(@AuthenticationPrincipal Merchant merchant,
@PathVariable("storeId") Long storeId,
@PathVariable("employeeId") Long employeeId) {
return merchant.getStores().stream()
.filter(x -> Objects.equals(storeId, x.getId()))
.flatMap(x -> x.getEmployees().stream())
.filter(x -> Objects.equals(employeeId, x.getId()))
.findFirst()
.map(this.employeeMapper::toDto)
.map(ApiResult::data)
.orElse(ApiResult.fail("员工信息不存在"));
}
/**
* 根据员工id更新员工信息
* @param merchant @ignore 当前商户
* @param storeId 门店id
* @param employeeId 员工id
* @param updateVo 更新参数
* @return 员工详情
*/
@PutMapping("/{employeeId}")
@Transactional
public ApiResult<EmployeeDto> updateById(@AuthenticationPrincipal Merchant merchant,
@PathVariable("storeId") Long storeId,
@PathVariable("employeeId") Long employeeId,
@Validated @RequestBody EmployeeUpdateVo updateVo) {
return merchant.getStores().stream()
.filter(x -> Objects.equals(storeId, x.getId()))
.flatMap(x -> x.getEmployees().stream())
.filter(x -> Objects.equals(employeeId, x.getId()))
.findFirst()
.map(employee -> {
this.employeeMapper.updateEntityFormUpdateVo(updateVo, employee);
this.employeeRepository.save(employee);
return ApiResult.data(
this.employeeMapper.toDto(employee)
);
})
.orElse(ApiResult.fail("员工信息不存在"));
}
/**
* 根据id删除员工信息
* @param merchant @ignore 当前商户
* @param storeId 门店id
* @param employeeId 员工id
* @return 删除结果
*/
@DeleteMapping("/{employeeId}")
@Transactional
public ApiResult<Void> deleteById(@AuthenticationPrincipal Merchant merchant,
@PathVariable("storeId") Long storeId,
@PathVariable("employeeId") Long employeeId) throws Exception {
try {
var store = merchant.getStores().stream()
.filter(x -> Objects.equals(storeId, x.getId()))
.findFirst()
.orElseThrow(()->new Exception("门店信息不存在"));
store.getEmployees().removeIf(x->Objects.equals(employeeId,x.getId()));
return ApiResult.success();
} catch (Exception e) {
log.error("删除员工失败",e);
throw e;
}
}
}

View file

@ -1,45 +0,0 @@
package com.xjhs.findmemerchant.dto;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.xjhs.findmemerchant.types.AuthStatus;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 商家信息
*/
@Data
public class MerchantDto {
/**
* 商家id
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/**
* 手机号码
* TODO: 手机号码需要脱敏
*/
private String phone;
/**
* 真实姓名
*/
private String realName;
/**
* 身份证号明文
*/
private String idCardNo;
/**
* 认证状态
*/
private AuthStatus authStatus;
/**
* 认证状态(说明)
*/
private String authStatusDesc;
/**
* 创建时间
*/
private LocalDateTime createdAt;
}

View file

@ -16,8 +16,7 @@ public class RegisterDto {
/**
* 商家id
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long merchantId;
private String merchantId;
/**
* 访问令牌
*/

View file

@ -1,40 +0,0 @@
package com.xjhs.findmemerchant.dto.member;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class EmployeeDto {
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/**
* 员工姓名
*/
private String name;
/**
* 手机号码
*/
private String phone;
/**
* 关联角色id
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long roleId;
/**
* 关联门店id
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long storeId;
/**
* 员工状态
*/
private Integer status;
/**
* 创建时间
*/
private LocalDateTime createdAt;
}

View file

@ -0,0 +1,9 @@
package com.xjhs.findmemerchant.dto.merchant;
import lombok.Data;
@Data
public class MerchantDto {
private String id;
private String merchantName;
}

View file

@ -1,30 +0,0 @@
package com.xjhs.findmemerchant.dto.store;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
@Data
public class BusinessPeriodDto {
/**
* 门店id
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long storeId;
/**
* 周几周天0,...
*/
private Integer dayOfWeek;
/**
* 开始营业时间 HH:mm 格式
*/
private String startTime;
/**
* 结束营业时间 HH:mm 格式
*/
private String endTime;
/**
* 是否启用
*/
private Boolean enabled;
}

View file

@ -1,36 +0,0 @@
package com.xjhs.findmemerchant.dto.store;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 门店营业状态
*/
@Data
public class StoreBusinessStatusDto {
/**
* 业务状态值
*/
private Integer status;
/**
* 业务状态文案
*/
private String statusText;
/**
* 临时关闭原因可为空
*/
private String tempCloseReason;
/**
* 临时关闭截止时间可为空
*/
private LocalDateTime tempCloseUntil;
/**
* 是否营业
*/
private Boolean isOpen;
}

View file

@ -1,110 +0,0 @@
package com.xjhs.findmemerchant.dto.store;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
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
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/**
* 商家id
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long 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

@ -1,137 +0,0 @@
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

@ -1,109 +0,0 @@
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")
@Comment("是否默认卡")
private Boolean isDefault = false;
/**
* 状态0-禁用 1-启用
*/
@Column(name = "status",columnDefinition = "VARCHAR(15)",length = 15)
@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

@ -1,67 +0,0 @@
package com.xjhs.findmemerchant.entity;
import com.xjhs.findmemerchant.common.jpa.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", 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)")
@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

@ -1,81 +0,0 @@
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")
@Comment("周几0周日 1周一 ... 6周六")
private Integer dayOfWeek;
/**
* 开始时间HH:MM
*/
@Column(name = "start_time", length = 5)
@Comment("开始时间HH:MM")
private String startTime;
/**
* 结束时间HH:MM
*/
@Column(name = "end_time", length = 5)
@Comment("结束时间HH:MM")
private String endTime;
/**
* 是否启用
*/
@Column(name = "is_enabled")
@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

@ -1,211 +0,0 @@
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", length = 100)
@Comment("优惠券名称")
private String name;
/**
* 优惠券类型1-折扣券 2-满减券 3-现金券 4-赠品券
*/
@Column(name = "type",length = 15,columnDefinition = "VARCHAR(15)")
@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")
@Comment("赠品数量")
private Integer giftQuantity = 1;
/**
* 发放总量
*/
@Column(name = "total_count")
@Comment("发放总量")
private Integer totalCount;
/**
* 已领取数量
*/
@Column(name = "claimed_count")
@Comment("已领取数量")
private Integer claimedCount = 0;
/**
* 每人限领数量0 表示不限
*/
@Column(name = "per_user_limit")
@Comment("每人限领数量0表示不限")
private Integer perUserLimit = 1;
/**
* 领取后有效天数
*/
@Column(name = "valid_days")
@Comment("领取后有效天数")
private Integer validDays;
/**
* 活动开始时间
*/
@Column(name = "start_time")
@Comment("活动开始时间")
private LocalDateTime startTime;
/**
* 活动结束时间
*/
@Column(name = "end_time")
@Comment("活动结束时间")
private LocalDateTime endTime;
/**
* 使用规则
*/
@Column(name = "rules", columnDefinition = "text")
@Comment("使用规则")
private String rules;
/**
* 状态0-下架 1-进行中 2-已结束
*/
@Column(name = "status",columnDefinition = "VARCHAR(15)",length = 15)
@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

@ -1,172 +0,0 @@
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", length = 32)
@Comment("优惠券码")
private String code;
/**
* 状态0-未领取 1-已领取 2-已核销 3-已过期
*/
@Column(name = "status",columnDefinition = "VARCHAR(20)",length = 20)
@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.ordinal() >= CouponCodeStatus.CLAIMED.ordinal();
}
/**
* 是否已核销
*/
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

@ -1,40 +0,0 @@
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

@ -1,95 +0,0 @@
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", length = 11)
@Comment("手机号")
private String phone;
/**
* 员工姓名
*/
@Column(name = "name", length = 50)
@Comment("员工姓名")
private String name;
/**
* 状态0-禁用 1-启用
*/
@Column(name = "status",columnDefinition = "VARCHAR(15)",length = 15)
@Comment("状态")
private CommonStatus status = CommonStatus.ENABLED;
// ===== 关联关系 =====
/**
* 门店
*/
@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

@ -1,90 +0,0 @@
package com.xjhs.findmemerchant.entity;
import com.xjhs.findmemerchant.common.jpa.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", 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)
@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

@ -1,127 +0,0 @@
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", 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")
@Comment("累计订单数")
private Integer totalOrders = 0;
/**
* 累计消费金额
*/
@Column(name = "total_amount", 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

@ -1,147 +1,27 @@
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 com.xjhs.findmemerchant.system.entity.SystemUser;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Comment;
import java.util.List;
/**
* 商家实体
* 对应表merchants
*/
@Getter
@Setter
@EqualsAndHashCode(callSuper = true)
@Data
@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")
}
)
@Table
@Comment("商户信息表")
public class Merchant extends AbstractBaseEntity {
/**
* 登录手机号
* 用户信息
*/
@Column(name = "phone", length = 11)
@Comment("手机号")
private String phone;
@OneToOne
@JoinColumn(name = "user_id")
@Comment("关联的用户信息")
private SystemUser systemUser;
/**
* 登录密码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)")
@Comment("认证状态")
@Enumerated(EnumType.STRING)
private AuthStatus authStatus = AuthStatus.NOT_VERIFIED;
/**
* 账户状态
*/
@Column(name = "status",length = 20,columnDefinition = "VARCHAR(20)")
@Comment("账户状态")
@Enumerated(EnumType.STRING)
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

@ -1,122 +0,0 @@
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)
@Enumerated(EnumType.STRING)
@Comment("消息类型1系统通知 2活动提醒 3私信")
private MessageType type;
/**
* 标题
*/
@Column(name = "title", length = 200)
@Comment("消息标题")
private String title;
/**
* 内容
*/
@Column(name = "content", columnDefinition = "text")
@Comment("消息内容")
private String content;
/**
* 是否已读0-未读 1-已读
*/
@Column(name = "is_read")
@Comment("是否已读0未读 1已读")
private Boolean isRead = false;
/**
* 创建时间
*/
@Column(name = "created_at")
@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 = true;
this.readAt = LocalDateTime.now();
}
/**
* 是否未读
*/
public boolean isUnread() {
return this.isRead != null && !this.isRead;
}
/**
* 消息类型文案
*/
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

@ -1,168 +0,0 @@
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", length = 32)
@Comment("订单号")
private String orderNo;
/**
* 订单总金额
*/
@Column(name = "total_amount", precision = 10, scale = 2)
@Comment("订单总金额")
private BigDecimal totalAmount;
/**
* 优惠金额
*/
@Column(name = "discount_amount", precision = 10, scale = 2)
@Comment("优惠金额")
private BigDecimal discountAmount = BigDecimal.ZERO;
/**
* 实付金额
*/
@Column(name = "pay_amount", 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")
@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

@ -1,123 +0,0 @@
package com.xjhs.findmemerchant.entity;
import com.xjhs.findmemerchant.common.jpa.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
@Comment("主键ID")
private Long id;
/**
* 商品ID可能被删除所以可空
*/
@Column(name = "product_id")
@Comment("商品ID可空")
private Long productId;
/**
* 商品名称快照
*/
@Column(name = "product_name", 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", precision = 10, scale = 2)
@Comment("商品单价")
private BigDecimal unitPrice;
/**
* 购买数量
*/
@Column(name = "quantity")
@Comment("购买数量")
private Integer quantity = 1;
/**
* 小计金额
*/
@Column(name = "total_price", precision = 10, scale = 2)
@Comment("小计金额")
private BigDecimal totalPrice;
/**
* 创建时间
*/
@Column(name = "created_at")
@Comment("创建时间")
private LocalDateTime createdAt;
// ========== 关联关系 ==========
/**
* 所属订单
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
// ========== 业务方法 ==========
/**
* 根据单价 * 数量计算小计
*/
public BigDecimal getSubtotal() {
if (unitPrice == null || quantity == null) {
return BigDecimal.ZERO;
}
return unitPrice.multiply(BigDecimal.valueOf(quantity));
}
}

View file

@ -1,190 +0,0 @@
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.math.RoundingMode;
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", 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", precision = 10, scale = 2)
@Comment("原价")
private BigDecimal originalPrice;
/**
* 售价
*/
@Column(name = "sale_price", precision = 10, scale = 2)
@Comment("售价")
private BigDecimal salePrice;
/**
* 库存-1 表示无限库存
*/
@Column(name = "stock")
@Comment("库存,-1表示无限库存")
private Integer stock = -1;
/**
* 已售数量
*/
@Column(name = "sold_count")
@Comment("已售数量")
private Integer soldCount = 0;
/**
* 排序值
*/
@Column(name = "sort_order")
@Comment("排序值")
private Integer sortOrder = 0;
/**
* 商品状态0-下架 1-上架
*/
@Column(name = "status",columnDefinition = "VARCHAR(20)",length = 20)
@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, RoundingMode.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

@ -1,109 +0,0 @@
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", length = 50)
@Comment("分类名称")
private String name;
/**
* 父分类ID支持二级分类
*/
@Column(name = "parent_id")
@Comment("父分类ID支持二级分类")
private Long parentId;
/**
* 排序值
*/
@Column(name = "sort_order")
@Comment("排序值")
private Integer sortOrder = 0;
/**
* 状态0-禁用 1-启用
*/
@Column(name = "status")
@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

@ -1,126 +0,0 @@
package com.xjhs.findmemerchant.entity;
import com.xjhs.findmemerchant.common.jpa.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
@Comment("主键ID")
private Long id;
/**
* 规格名称大杯/加冰
*/
@Column(name = "sku_name", length = 100)
@Comment("规格名称")
private String skuName;
/**
* SKU 编码
*/
@Column(name = "sku_code", length = 50)
@Comment("SKU编码")
private String skuCode;
/**
* 售价
*/
@Column(name = "price", precision = 10, scale = 2)
@Comment("规格价格")
private BigDecimal price;
/**
* 库存-1 表示无限库存
*/
@Column(name = "stock")
@Comment("库存,-1表示无限库存")
private Integer stock = -1;
/**
* 已售数量
*/
@Column(name = "sold_count")
@Comment("已售数量")
private Integer soldCount = 0;
/**
* 状态0-禁用 1-启用
*/
@Column(name = "status",columnDefinition = "VARCHAR(20)",length = 20)
@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

@ -1,170 +0,0 @@
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 {
/**
* C端用户ID
*/
@Column(name = "user_id")
@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")
@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")
@Comment("是否匿名")
private Boolean isAnonymous = Boolean.FALSE;
/**
* 状态1-正常 0-隐藏
*/
@Column(name = "status",columnDefinition = "VARCHAR(20)",length = 20)
@Comment("状态1正常 0隐藏")
private ReviewStatus status = ReviewStatus.NORMAL;
// ========== 关联关系 ==========
/**
* 回复
*/
@OneToOne(mappedBy = "review", fetch = FetchType.LAZY)
private ReviewReply reply;
/**
* 门店
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "merchant_id")
private Merchant merchant;
/**
* 门店
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "store_id")
private Store store;
/**
* 订单
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
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

@ -1,77 +0,0 @@
package com.xjhs.findmemerchant.entity;
import com.xjhs.findmemerchant.common.jpa.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
@Comment("主键ID")
private Long id;
/**
* 评价ID唯一一个评价一条回复
*/
@Column(name = "review_id")
@Comment("评价ID")
private Long reviewId;
/**
* 商家ID
*/
@Column(name = "merchant_id")
@Comment("商家ID")
private Long merchantId;
/**
* 回复内容
*/
@Column(name = "content", columnDefinition = "text")
@Comment("回复内容")
private String content;
/**
* 回复人ID商家或员工
*/
@Column(name = "replier_id")
@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

@ -1,162 +0,0 @@
package com.xjhs.findmemerchant.entity;
import com.xjhs.findmemerchant.common.jpa.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", length = 50)
@Comment("角色名称")
private String name;
/**
* 角色编码唯一
*/
@Column(name = "code", length = 50)
@Comment("角色编码")
private String code;
/**
* 描述
*/
@Column(name = "description", length = 200)
@Comment("角色描述")
private String description;
/**
* 权限列表JSON数组
*/
@Convert(converter = StringListJsonConverter.class)
@Column(name = "permissions", columnDefinition = "json")
@Comment("权限列表JSON数组")
private List<String> permissions = new ArrayList<>();
/**
* 是否系统内置角色1- 0-
*/
@Column(name = "is_system")
@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

@ -1,167 +0,0 @@
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", length = 32)
@Comment("结算单号")
private String settlementNo;
/**
* 结算类型1-日结 2-周结
*/
@Column(name = "type")
@Comment("结算类型1日结 2周结")
private SettlementType type;
/**
* 结算周期开始时间
*/
@Column(name = "period_start")
@Comment("结算周期开始时间")
private LocalDateTime periodStart;
/**
* 结算周期结束时间
*/
@Column(name = "period_end")
@Comment("结算周期结束时间")
private LocalDateTime periodEnd;
/**
* 订单数量
*/
@Column(name = "order_count")
@Comment("订单数量")
private Integer orderCount = 0;
/**
* 订单总额
*/
@Column(name = "total_amount", precision = 12, scale = 2)
@Comment("订单总额")
private BigDecimal totalAmount;
/**
* 退款金额
*/
@Column(name = "refund_amount", precision = 12, scale = 2)
@Comment("退款金额")
private BigDecimal refundAmount = BigDecimal.ZERO;
/**
* 平台服务费
*/
@Column(name = "platform_fee", precision = 10, scale = 2)
@Comment("平台服务费")
private BigDecimal platformFee = BigDecimal.ZERO;
/**
* 结算金额
*/
@Column(name = "settlement_amount", precision = 12, scale = 2)
@Comment("结算金额")
private BigDecimal settlementAmount;
/**
* 结算状态0-待结算 1-已结算
*/
@Column(name = "status")
@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

@ -1,258 +0,0 @@
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.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
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", length = 100)
@Comment("门店名称")
private String name;
/**
* 门店Logo
*/
@Column(name = "logo", length = 500)
@Comment("门店Logo")
private String logo;
/**
* 联系电话
*/
@Column(name = "phone", length = 11)
@Comment("联系电话")
private String phone;
/**
*
*/
@Column(name = "province", length = 50)
@Comment("")
private String province;
/**
*
*/
@Column(name = "city", length = 50)
@Comment("")
private String city;
/**
* /
*/
@Column(name = "district", length = 50)
@Comment("区/县")
private String district;
/**
* 详细地址
*/
@Column(name = "address", length = 200)
@Comment("详细地址")
private String address;
/**
* 经度
*/
@Column(name = "longitude", precision = 10, scale = 7)
@Comment("经度")
private BigDecimal longitude;
/**
* 纬度
*/
@Column(name = "latitude", precision = 10, scale = 7)
@Comment("纬度")
private BigDecimal latitude;
/**
* 营业时间描述
*/
@Column(name = "business_hours", length = 100)
@Comment("营业时间描述")
private String businessHours;
/**
* 营业状态0-已打烊 1-营业中 2-临时打烊
*/
@Column(name = "business_status",columnDefinition = "VARCHAR(20)", length = 20)
@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)
@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)
@Comment("启用状态0禁用 1启用")
private CommonStatus status = CommonStatus.ENABLED;
// ========== 关联关系 ==========
/**
* 商家
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "merchant_id")
private Merchant merchant;
/**
* 员工列表
*/
@OneToMany(mappedBy = "store", fetch = FetchType.LAZY,cascade = CascadeType.ALL,orphanRemoval = true)
private List<Employee> employees = new ArrayList<>();
public void addEmployee(Employee employee){
employee.setStore(this);
this.employees.add(employee);
}
/**
* 营业时间段
*/
@OneToMany(mappedBy = "store", fetch = FetchType.LAZY,cascade = CascadeType.ALL,orphanRemoval = true)
private List<BusinessPeriod> businessPeriods = new ArrayList<>();
public void addBusinessPeriods(BusinessPeriod businessPeriod){
businessPeriod.setStore(this);
this.businessPeriods.add(businessPeriod);
}
// ========== 业务方法 ==========
/**
* 完整地址
*/
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

@ -1,133 +0,0 @@
package com.xjhs.findmemerchant.entity;
import com.xjhs.findmemerchant.common.jpa.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
@Comment("主键ID")
private Long id;
/**
* 交易类型1-收入 2-支出 3-冻结 4-解冻 5-提现
*/
@Column(name = "type",columnDefinition = "VARCHAR(20)",length = 20)
@Comment("交易类型1收入 2支出 3冻结 4解冻 5提现")
private TransactionType type;
/**
* 交易金额
*/
@Column(name = "amount", precision = 12, scale = 2)
@Comment("交易金额")
private BigDecimal amount;
/**
* 交易前余额
*/
@Column(name = "balance_before", precision = 12, scale = 2)
@Comment("交易前余额")
private BigDecimal balanceBefore;
/**
* 交易后余额
*/
@Column(name = "balance_after", 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")
@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

@ -1,100 +0,0 @@
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;
/**
* 商家钱包
* 对应表wallets
*/
@Getter
@Setter
@Entity
@Table(
name = "wallets",
uniqueConstraints = {
@UniqueConstraint(name = "uk_wallets_merchant_id", columnNames = "merchant_id")
}
)
public class Wallet extends AbstractBaseEntity {
/**
* 可用余额
*/
@Column(name = "balance", precision = 12, scale = 2)
@Comment("可用余额")
private BigDecimal balance = BigDecimal.ZERO;
/**
* 冻结余额提现中
*/
@Column(name = "frozen_balance", precision = 12, scale = 2)
@Comment("冻结余额(提现中)")
private BigDecimal frozenBalance = BigDecimal.ZERO;
/**
* 累计收入
*/
@Column(name = "total_income", precision = 12, scale = 2)
@Comment("累计收入")
private BigDecimal totalIncome = BigDecimal.ZERO;
/**
* 累计提现
*/
@Column(name = "total_withdrawn", precision = 12, scale = 2)
@Comment("累计提现")
private BigDecimal totalWithdrawn = BigDecimal.ZERO;
/**
* 待结算金额
*/
@Column(name = "pending_settlement", precision = 12, scale = 2)
@Comment("待结算金额")
private BigDecimal pendingSettlement = BigDecimal.ZERO;
/**
* 商家
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "merchant_id")
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

@ -1,175 +0,0 @@
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", precision = 12, scale = 2)
@Comment("提现金额")
private BigDecimal amount;
/**
* 手续费
*/
@Column(name = "fee", precision = 10, scale = 2)
@Comment("提现手续费")
private BigDecimal fee = BigDecimal.ZERO;
/**
* 实际到账金额
*/
@Column(name = "actual_amount", precision = 12, scale = 2)
@Comment("实际到账金额")
private BigDecimal actualAmount;
/**
* 银行名称
*/
@Column(name = "bank_name", length = 50)
@Comment("银行名称")
private String bankName;
/**
* 银行账号加密存储
*/
@Column(name = "bank_account", length = 30)
@Comment("银行账号(加密)")
private String bankAccount;
/**
* 户名
*/
@Column(name = "account_name", length = 50)
@Comment("开户人姓名")
private String accountName;
/**
* 状态0-待审核 1-处理中 2-已完成 3-已拒绝 4-已取消
*/
@Column(name = "status")
@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,108 @@
package com.xjhs.findmemerchant.file;
import com.xjhs.findmemerchant.common.ApiResult;
import com.xjhs.findmemerchant.file.dao.FileMetaInfo;
import com.xjhs.findmemerchant.file.dao.FileMetaInfoRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.tika.mime.MimeTypes;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
/**
* 统一文件管理控制器
*/
@Slf4j
@RestController
@RequiredArgsConstructor
public class FileController {
@Value("${appconfig.saveFileRoot}")
private String fileSaveRoot="./file-data";
private final FileMetaInfoRepository fileMetaInfoRepository;
/**
* 获取并检查创建文件存储目录
* @return 文件存储目录带日期
*/
public Path getFileSavePath() throws IOException {
var datePath = DateTimeFormatter.ofPattern("yyyy/MM-dd").format(LocalDate.now());
var path = Path.of(this.fileSaveRoot, datePath);
if (!Files.exists(path)){
Files.createDirectories(path);
}
return path;
}
/**
* 上传文件
*
* @param file 文件数据内容
* @return 文件描信息
*/
@PostMapping("/platform/file/upload")
public ApiResult<FileMetaInfo> uploadFile(@RequestPart(name = "file") MultipartFile file) {
try {
var ext = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));
if (StringUtils.isEmpty(ext)) {
try {
ext = MimeTypes.getDefaultMimeTypes().forName(file.getContentType()).getExtension();
} catch (Exception e) {
ext = "";
}
}
var filePath = Path.of(this.fileSaveRoot, UUID.randomUUID() + ext);
file.transferTo(filePath);
var fileMetaInfo = new FileMetaInfo();
fileMetaInfo.setName(file.getOriginalFilename());
fileMetaInfo.setContentType(file.getContentType());
fileMetaInfo.setExtension(ext);
fileMetaInfo.setSavePath(filePath.toFile().getAbsolutePath());
fileMetaInfoRepository.save(fileMetaInfo);
return ApiResult.data(fileMetaInfo);
} catch (Exception e) {
log.error(e.getMessage(),e);
return ApiResult.fail(e.getMessage());
}
}
/**
* 预览文件
* @param fileId 文件id
* @return 文件内容
*/
@GetMapping("/platform/file/view/{fileId}")
public ResponseEntity<?> uploadFile(@PathVariable("fileId") String fileId) {
return this.fileMetaInfoRepository.findById(fileId).map(item->{
try {
var bytes = Files.readAllBytes(Paths.get(item.getSavePath()));
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(item.getContentType()))
.body(bytes);
} catch (IOException e) {
return ResponseEntity.<byte[]>internalServerError().build();
}
}).orElse(ResponseEntity.<byte[]>internalServerError().build());
}
}

View file

@ -0,0 +1,41 @@
package com.xjhs.findmemerchant.file.dao;
import com.xjhs.findmemerchant.common.jpa.id.SnowflakeGenerated;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.Data;
import org.hibernate.annotations.Comment;
import java.time.LocalDateTime;
/**
* 文件信息表
*/
@Data
@Entity
@Comment("文件信息表")
public class FileMetaInfo {
@Id
@SnowflakeGenerated
@Comment("主键")
private Long id;
@Comment("文件原始名称")
private String name;
@Comment("文件媒体类型")
private String contentType;
@Comment("文件扩展名称")
@Column(length = 10)
private String extension;
@Comment("本地存储路径")
private String savePath;
@Comment("是否已迁移到云服务")
private boolean moveToCloud = false;
@Comment("云对象id")
@Column(length = 50)
private String cloudObjectId;
@Comment("创建时间")
private LocalDateTime createTime = LocalDateTime.now();
}

View file

@ -0,0 +1,10 @@
package com.xjhs.findmemerchant.file.dao;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
@Repository
public interface FileMetaInfoRepository extends JpaRepository<FileMetaInfo,String>, JpaSpecificationExecutor<FileMetaInfo> {
}

View file

@ -1,20 +0,0 @@
package com.xjhs.findmemerchant.mapper;
import com.xjhs.findmemerchant.dto.member.EmployeeDto;
import com.xjhs.findmemerchant.entity.Employee;
import com.xjhs.findmemerchant.vo.member.EmployeeCreateVo;
import com.xjhs.findmemerchant.vo.member.EmployeeUpdateVo;
import org.mapstruct.*;
@Mapper(componentModel = "spring",unmappedTargetPolicy = ReportingPolicy.IGNORE,unmappedSourcePolicy = ReportingPolicy.IGNORE)
public interface EmployeeMapper {
@Mapping(target = "storeId",source = "store.id")
@Mapping(target = "roleId",source = "role.id")
EmployeeDto toDto(Employee employee);
Employee toEntity(EmployeeCreateVo createVo);
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateEntityFormUpdateVo(EmployeeUpdateVo updateVo, @MappingTarget Employee employee);
}

View file

@ -1,15 +1,12 @@
package com.xjhs.findmemerchant.mapper;
import com.xjhs.findmemerchant.dto.MerchantDto;
import com.xjhs.findmemerchant.dto.merchant.MerchantDto;
import com.xjhs.findmemerchant.entity.Merchant;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ReportingPolicy;
@Mapper(componentModel = "spring",unmappedTargetPolicy = ReportingPolicy.IGNORE,unmappedSourcePolicy = ReportingPolicy.IGNORE)
public interface MerchantMapper {
@Mapping(target = "authStatusDesc",source = "authStatus.desc")
MerchantDto toDto(Merchant merchant);
}

View file

@ -1,32 +0,0 @@
package com.xjhs.findmemerchant.mapper;
import com.xjhs.findmemerchant.dto.store.BusinessPeriodDto;
import com.xjhs.findmemerchant.dto.store.StoreBusinessStatusDto;
import com.xjhs.findmemerchant.dto.store.StoreDto;
import com.xjhs.findmemerchant.entity.BusinessPeriod;
import com.xjhs.findmemerchant.entity.Store;
import com.xjhs.findmemerchant.vo.store.BusinessPeriodVo;
import com.xjhs.findmemerchant.vo.store.StoreBusinessStatusUpdateVo;
import com.xjhs.findmemerchant.vo.store.StoreCreateVo;
import com.xjhs.findmemerchant.vo.store.StoreUpdateVo;
import org.mapstruct.*;
import java.util.List;
@Mapper(componentModel = "spring",unmappedTargetPolicy = ReportingPolicy.IGNORE,unmappedSourcePolicy = ReportingPolicy.IGNORE)
public interface StoreMapper {
StoreDto toDto(Store store);
StoreBusinessStatusDto toBusinessStatusDto(Store store);
Store toEntity(StoreCreateVo createVo);
Store toEntity(StoreUpdateVo updateVo);
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateFromVo(StoreUpdateVo vo, @MappingTarget Store entity);
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateFromBusinessUpdateVo(StoreBusinessStatusUpdateVo updateVo,@MappingTarget Store store);
BusinessPeriod toEntity(BusinessPeriodVo vo);
@Mapping(source = "store.id",target = "storeId")
BusinessPeriodDto toDto(BusinessPeriod entity);
List<BusinessPeriodDto> toDtoList(List<BusinessPeriod> entityList);
}

View file

@ -0,0 +1,15 @@
package com.xjhs.findmemerchant.mapper;
import com.xjhs.findmemerchant.security.LoginUser;
import com.xjhs.findmemerchant.system.entity.SystemUser;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ReportingPolicy;
@Mapper(componentModel = "spring",unmappedTargetPolicy = ReportingPolicy.IGNORE,unmappedSourcePolicy = ReportingPolicy.IGNORE)
public interface SystemUserMapper {
@Mapping(target = "merchantId",source = "systemUser.merchant.id")
LoginUser toLoginUserInfo(SystemUser systemUser);
}

View file

@ -1,14 +0,0 @@
package com.xjhs.findmemerchant.repository;
import com.xjhs.findmemerchant.entity.Activity;
import com.xjhs.findmemerchant.types.ActivityStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ActivityRepository extends JpaRepository<Activity, Long> {
List<Activity> findByMerchant_Id(Long merchantId);
List<Activity> findByStatus(ActivityStatus status);
}

View file

@ -1,25 +0,0 @@
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> findByMerchant_Id(Long merchantId);
/**
* 查询商家的默认卡
*/
Optional<BankCard> findByMerchant_IdAndIsDefaultTrue(Long merchantId);
}

View file

@ -1,16 +0,0 @@
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> findByMerchant_Id(Long merchantId);
Optional<BusinessLicense> findTopByMerchant_IdOrderByCreatedAtDesc(Long merchantId);
}

View file

@ -1,24 +0,0 @@
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> findByStore_Id(Long storeId);
/**
* 按门店 + 周几查询启用的时间段
*/
List<BusinessPeriod> findByStore_IdAndDayOfWeekAndIsEnabledTrue(Long storeId, Byte dayOfWeek);
}

View file

@ -1,30 +0,0 @@
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> findByCoupon_Id(Long couponId);
/**
* 按会员查询券码
*/
List<CouponCode> findByMember_Id(Long memberId);
/**
* 按券码查询
*/
Optional<CouponCode> findByCode(String code);
}

View file

@ -1,29 +0,0 @@
package com.xjhs.findmemerchant.repository;
import com.xjhs.findmemerchant.entity.Coupon;
import com.xjhs.findmemerchant.types.CouponStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
/**
* 优惠券模板仓储
*/
public interface CouponRepository extends JpaRepository<Coupon, Long> {
/**
* 按商家查询优惠券
*/
List<Coupon> findByMerchant_Id(Long merchantId);
/**
* 按状态查询优惠券
*/
List<Coupon> findByStatus(CouponStatus status);
/**
* 按ID + 商家ID 查询防越权
*/
Optional<Coupon> findByIdAndMerchant_Id(Long id, Long merchantId);
}

View file

@ -1,20 +0,0 @@
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> findByCoupon_Id(Long couponId);
List<CouponStore> findByStore_Id(Long storeId);
Optional<CouponStore> findByCoupon_IdAndStore_Id(Long couponId, Long storeId);
}

View file

@ -1,17 +0,0 @@
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> findByStoreId(Long storeId);
}

View file

@ -1,15 +0,0 @@
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> findByStore_Id(Long storeId);
List<HealthCertificate> findByEmployee_Id(Long employeeId);
}

View file

@ -1,27 +0,0 @@
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> findByMerchant_Id(Long merchantId);
/**
* 按商家 + 手机号查询唯一
*/
Optional<Member> findByPhone(String phone);
}

View file

@ -6,16 +6,6 @@ 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);
public interface MerchantRepository extends JpaSpecificationExecutor<Merchant>, JpaRepository<Merchant,Long> {
Optional<Merchant> findBySystemUserPhone(String phone);
}

View file

@ -1,26 +0,0 @@
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> findByMerchant_Id(Long merchantId);
List<Message> findByMerchant_IdAndIsRead(Long merchantId, Boolean isRead);
/**
* 统计未读消息数量
*/
long countByMerchant_IdAndIsRead(Long merchantId, Boolean isRead);
}

View file

@ -1,21 +0,0 @@
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> findByOrder_Id(Long orderId);
List<OrderItem> findByProductId(Long productId);
}

View file

@ -1,27 +0,0 @@
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> findByStore_Id(Long storeId);
List<Order> findByMember_Id(Long memberId);
}

View file

@ -1,24 +0,0 @@
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> findByMerchant_Id(Long merchantId);
/**
* 查询某商家下指定父分类的子分类
*/
List<ProductCategory> findByMerchantIdAndParentId(Long merchantId, Long parentId);
}

View file

@ -1,32 +0,0 @@
package com.xjhs.findmemerchant.repository;
import com.xjhs.findmemerchant.entity.Product;
import com.xjhs.findmemerchant.types.ProductStatus;
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> findByMerchant_Id(Long merchantId);
/**
* 按门店查询商品
*/
List<Product> findByStoreId(Long storeId);
/**
* 按分类查询商品
*/
List<Product> findByCategoryId(Long categoryId);
/**
* 按状态查询商品
*/
List<Product> findByStatus(ProductStatus status);
}

View file

@ -1,24 +0,0 @@
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

@ -1,16 +0,0 @@
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

@ -1,22 +0,0 @@
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> findByMerchant_Id(Long merchantId);
List<Review> findByStoreId(Long storeId);
List<Review> findByOrderId(Long orderId);
List<Review> findByUserId(Long userId);
}

View file

@ -1,19 +0,0 @@
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

@ -1,25 +0,0 @@
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

@ -1,22 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,19 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,7 +1,7 @@
package com.xjhs.findmemerchant.security;
import com.xjhs.findmemerchant.repository.MemberRepository;
import com.xjhs.findmemerchant.mapper.SystemUserMapper;
import com.xjhs.findmemerchant.repository.MerchantRepository;
import com.xjhs.findmemerchant.security.sms.SmsAuthenticationToken;
import jakarta.servlet.FilterChain;
@ -24,22 +24,16 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenService jwtTokenService;
private final MerchantRepository merchantRepository;
private final MemberRepository memberRepository;
private final SystemUserMapper systemUserMapper;
private SmsAuthenticationToken getAuthenticationToken(String phone) throws Exception {
// 手机号查商家
var merchant = merchantRepository.findByPhone(phone).orElse(null);
if(merchant != null){
var merchant = merchantRepository.findBySystemUserPhone(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);
var loginUser = systemUserMapper.toLoginUserInfo(merchant.getSystemUser());
return new SmsAuthenticationToken(loginUser, authorities);
}
throw new Exception("用户信息不存在");
}

View file

@ -0,0 +1,23 @@
package com.xjhs.findmemerchant.security;
import lombok.Data;
/**
* 登录用户信息
*/
@Data
public class LoginUser {
/**
* 用户id
*/
private Long userId;
/**
* 手机号
*/
private String phone;
/**
* 关联商户id,不存在则为null
*/
private Long merchantId;
}

View file

@ -1,25 +1,31 @@
package com.xjhs.findmemerchant.security.sms;
import com.xjhs.findmemerchant.repository.MemberRepository;
import com.xjhs.findmemerchant.repository.MerchantRepository;
import com.xjhs.findmemerchant.mapper.SystemUserMapper;
import com.xjhs.findmemerchant.system.SystemUserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.CredentialsExpiredException;
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 org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Objects;
@Slf4j
@Component
@RequiredArgsConstructor
public class SmsAuthenticationProvider implements AuthenticationProvider {
private final SmsCodeService smsCodeService;
private final MerchantRepository merchantRepository;
private final MemberRepository memberRepository;
private final SystemUserService systemUserService;
private final SystemUserMapper systemUserMapper;
@Override
@Transactional(readOnly = true)
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsAuthenticationToken token = (SmsAuthenticationToken) authentication;
String phone = (String) token.getPrincipal();
@ -27,21 +33,17 @@ public class SmsAuthenticationProvider implements AuthenticationProvider {
try {
this.smsCodeService.verifyCode(phone,"login" ,code);
} catch (Exception e) {
throw new UsernameNotFoundException(e.getMessage());
throw new CredentialsExpiredException(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 systemUser = systemUserService.getAndRegisterByPhone(phone);
if (Objects.isNull(systemUser.getMerchant())){
throw new UsernameNotFoundException("手机号码未注册");
}
// 手机号查员工
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("用户信息不存在");
var loginUser = this.systemUserMapper.toLoginUserInfo(systemUser);
var authorities = List.of(new SimpleGrantedAuthority("ADMIN"));
return new SmsAuthenticationToken(loginUser, authorities);
}
@Override

View file

@ -17,7 +17,7 @@ public class SmsCodeService {
private static final String SMS_CODE_KEY_PREFIX = "sms:code:";
/** 验证码有效期5 分钟 */
private static final Duration SMS_CODE_TTL = Duration.ofMinutes(5);
private static final Duration SMS_CODE_TTL = Duration.ofMinutes(1);
private final StringRedisTemplate redisTemplate;
private final Random random = new Random();
@ -29,10 +29,10 @@ public class SmsCodeService {
}
/**
* 生成 6 位数字验证码
* 生成 4 位数字验证码
*/
private String generateCode() {
return String.format("%06d", random.nextInt(1_000_000));
return String.format("%04d", random.nextInt(10000));
}
/**
@ -64,7 +64,6 @@ public class SmsCodeService {
// 对齐 Go 里的 ErrSMSCodeExpired
throw new Exception("验证码已过期或未发送");
}
if (!realCode.equals(inputCode)) {
// 不正确但不删除让用户继续尝试错误次数由 AuthService 控制
throw new Exception("验证码错误");

View file

@ -1,85 +0,0 @@
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

@ -1,34 +0,0 @@
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,36 @@
package com.xjhs.findmemerchant.system;
import com.xjhs.findmemerchant.system.entity.SystemUser;
import com.xjhs.findmemerchant.system.repository.SystemUserRepository;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Slf4j
@Service
@AllArgsConstructor
public class SystemUserService {
private final SystemUserRepository systemUserRepository;
/**
* 通过手机号获取用户信息不存在则进行注册
*
* @param phone 手机号码
* @return 用户信息
*/
public SystemUser getAndRegisterByPhone(String phone) {
return this.systemUserRepository.findByPhoneAndDeleteTimeIsNull(phone)
.orElseGet(() -> {
var systemUser = new SystemUser();
systemUser.setPhone(phone);
return this.systemUserRepository.save(systemUser);
});
}
public Optional<SystemUser> findByPhone(String phone){
return this.systemUserRepository.findByPhoneAndDeleteTimeIsNull(phone);
}
}

View file

@ -0,0 +1,31 @@
package com.xjhs.findmemerchant.system.entity;
import com.xjhs.findmemerchant.common.jpa.AbstractBaseEntity;
import com.xjhs.findmemerchant.entity.Merchant;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Comment;
/**
* 用户信息表
*/
@Getter
@Setter
@Entity
@Table
public class SystemUser extends AbstractBaseEntity {
@Column(columnDefinition = "CHAR(11)",length = 11)
@Comment("用户手机号")
private String phone;
@Column(length = 20)
@Comment("用户真实姓名")
private String realName;
@Column(length = 18)
@Comment("身份证号")
private String idCardNo;
@OneToOne(mappedBy = "systemUser",cascade = CascadeType.ALL)
private Merchant merchant;
}

View file

@ -0,0 +1,11 @@
package com.xjhs.findmemerchant.system.repository;
import com.xjhs.findmemerchant.system.entity.SystemUser;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import java.util.Optional;
public interface SystemUserRepository extends JpaRepository<SystemUser,Long> , JpaSpecificationExecutor<SystemUser> {
Optional<SystemUser> findByPhoneAndDeleteTimeIsNull(String phone);
}

View file

@ -1,52 +0,0 @@
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

@ -1,44 +0,0 @@
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

@ -1,40 +0,0 @@
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

@ -1,47 +0,0 @@
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

@ -1,46 +0,0 @@
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

@ -1,40 +0,0 @@
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;
};
}
}

View file

@ -1,46 +0,0 @@
package com.xjhs.findmemerchant.types;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 券码状态
* 0-未领取 1-已领取 2-已核销 3-已过期
*/
@Getter
@AllArgsConstructor
public enum CouponCodeStatus {
/**
* 未领取
*/
UNCLAIMED("未领取"),
/**
* 已领取
*/
CLAIMED("已领取"),
/**
* 已核销
*/
VERIFIED("已核销"),
/**
* 已过期
*/
EXPIRED("已过期");
private final String desc;
public static CouponCodeStatus fromCode(Byte code) {
if (code == null) return null;
return switch (code) {
case 0 -> UNCLAIMED;
case 1 -> CLAIMED;
case 2 -> VERIFIED;
case 3 -> EXPIRED;
default -> null;
};
}
}

View file

@ -1,46 +0,0 @@
package com.xjhs.findmemerchant.types;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 优惠券状态
* 0-下架 1-进行中 2-已结束
*/
@Getter
@AllArgsConstructor
public enum CouponStatus {
/**
* 下架
*/
OFFLINE("下架"),
/**
* 进行中
*/
ONLINE("进行中"),
/**
* 已结束
*/
ENDED("已结束");
private final String desc;
public static CouponStatus fromCode(Byte code) {
if (code == null) return null;
return switch (code) {
case 0 -> OFFLINE;
case 1 -> ONLINE;
case 2 -> ENDED;
default -> null;
};
}
public byte code() {
return switch (this) {
case OFFLINE -> 0;
case ONLINE -> 1;
case ENDED -> 2;
};
}
}

View file

@ -1,58 +0,0 @@
package com.xjhs.findmemerchant.types;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 优惠券类型
* 1-折扣券 2-满减券 3-现金券 4-赠品券
*/
@Getter
@AllArgsConstructor
public enum CouponType {
/**
* 折扣券
*/
DISCOUNT("折扣券"),
/**
* 满减券
*/
REDUCE("满减券"),
/**
* 现金券
*/
CASH("现金券"),
/**
* 赠品券
*/
GIFT("赠品券");
private final String desc;
/**
* 数据库 tinyint -> 枚举
*/
public static CouponType fromCode(Byte code) {
if (code == null) return null;
return switch (code) {
case 1 -> DISCOUNT;
case 2 -> REDUCE;
case 3 -> CASH;
case 4 -> GIFT;
default -> null;
};
}
/**
* 枚举 -> 数据库 tinyint
*/
public byte code() {
return switch (this) {
case DISCOUNT -> 1;
case REDUCE -> 2;
case CASH -> 3;
case GIFT -> 4;
};
}
}

View file

@ -1,47 +0,0 @@
package com.xjhs.findmemerchant.types;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 健康证状态
* 0-待审核 1-有效 2-过期
*/
@Getter
@AllArgsConstructor
public enum HealthCertificateStatus {
/**
* 待审核
*/
PENDING("待审核"),
/**
* 有效
*/
VALID("有效"),
/**
* 已过期
*/
EXPIRED("已过期");
private final String desc;
public static HealthCertificateStatus fromCode(Byte code) {
if (code == null) return null;
return switch (code) {
case 0 -> PENDING;
case 1 -> VALID;
case 2 -> EXPIRED;
default -> null;
};
}
public byte code() {
return switch (this) {
case PENDING -> 0;
case VALID -> 1;
case EXPIRED -> 2;
};
}
}

View file

@ -1,46 +0,0 @@
package com.xjhs.findmemerchant.types;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 消息类型
* 1-系统通知 2-活动提醒 3-私信
*/
@Getter
@AllArgsConstructor
public enum MessageType {
/**
* 系统通知
*/
SYSTEM("系统通知"),
/**
* 活动提醒
*/
ACTIVITY("活动提醒"),
/**
* 私信
*/
PRIVATE("私信");
private final String desc;
public static MessageType fromCode(Byte code) {
if (code == null) return null;
return switch (code) {
case 1 -> SYSTEM;
case 2 -> ACTIVITY;
case 3 -> PRIVATE;
default -> null;
};
}
public byte code() {
return switch (this) {
case SYSTEM -> 1;
case ACTIVITY -> 2;
case PRIVATE -> 3;
};
}
}

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