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