Merge branch 'master' into cc_20250905_franchiseAgreement

# Conflicts:
#	coolstore-partner-common/src/main/java/com/cool/store/enums/ErrorCodeEnum.java
#	coolstore-partner-model/src/main/java/com/cool/store/response/AddSignFranchiseResponse.java
#	coolstore-partner-service/src/main/java/com/cool/store/service/impl/PushServiceImpl.java
#	coolstore-partner-service/src/main/java/com/cool/store/service/impl/SignFranchiseServiceImpl.java
#	coolstore-partner-web/src/main/java/com/cool/store/controller/webb/PCTestController.java
This commit is contained in:
苏竹红
2025-12-01 10:50:32 +08:00
348 changed files with 15065 additions and 431 deletions

View File

@@ -0,0 +1,14 @@
package com.cool.store.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 平台库数据源
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PlatformDB {
}

View File

@@ -26,6 +26,11 @@ public class CommonConstants {
public static final int NORMAL_LOCK_TIMES = 60 * 1000;
/**
* 短期token过期时间单位秒
*/
public static final int SHORT_TERM_TOKEN_EXPIRE = 60 * 5;
public static final int AN_HOUR_SECONDS = 3600;
//十秒
public static final int TEN_SECONDS = 10000;
@@ -55,6 +60,11 @@ public class CommonConstants {
public static final String ZXJP_MINI_PROGRAM_LOGIN_FLAG = "zxjp_mini_program_login_flag:{0}";
/**
* 小程序短期token key
*/
public static final String ZXJP_MIN_PROGRAM_SHORT_TERM_LOGIN_FLAG = "zxjp_mini_program_short_term_login_flag:{0}";
public static final String ROOT_DEPT_ID_STR = "1";
public static final Integer DEAL_RECORD_MAX_SIZE = 1000;
@@ -201,5 +211,29 @@ public class CommonConstants {
public static final String WX_SELF_AUTH_URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_base&state=1#wechat_redirect";
/**
* 密码最大错误次数
*/
public static final int MAX_ERROR_PASSWORD_COUNT = 5;
/**
* 用户密码
*/
public static final String USER_AUTH_KEY = "user_auth_key";
/**
* accessToken有效期单位秒
*/
public static final int ACTION_TOKEN_EXPIRE = 24 * 60 * 60;
/**
* refreshToken有效期单位秒
*/
public static final int REFRESH_TOKEN_EXPIRE = 30 * 24 * 60 * 60;
public static final int BATCH_SIZE = 200;
public static final Integer INDEX_ZERO = 0;
public static final Integer INDEX_ONE = 1;
public static final Integer INDEX_TWO = 2;
}

View File

@@ -283,4 +283,25 @@ public class RedisConstant {
public static final String SUBMIT_BUILD_KEY = "submit_build_key_";
public static final String GET_AI_MODULE = "get_ai_module_";
public static final String HUOMA_STORE_DEVICE_RESOURCE_KEY = "huoma_store_device_resource";
public static final String HUO_MA_STORE_ID = "huo_ma_store_id";
public static final String HUO_MA_TOKEN= "huo_ma_token:{0}";
/**
* 钱包开通失败/打标失败原因 wallet_open_fail:storeId:1/2 1平安/2网商
*/
public static final String WALLET_OPEN_FAIL = "wallet_open_fail:{0}:{1}";
/**
* 打标接口创建网商账户失败标识 wallet_online_bank_tag_fail:storeId
*/
public static final String WALLET_ONLINE_BANK_TAG_FAIL = "wallet_online_bank_tag_fail:{0}";
/**
* 网商账户是否已激活
*/
public static final String WALLET_ONLINE_BANK_ACTIVATED = "wallet_online_bank_activated:{0}";
}

View File

@@ -0,0 +1,25 @@
package com.cool.store.datasource;
/**
* <p>
* 数据源上下文
* </p>
*
* @author wangff
* @since 2025/9/4
*/
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
public static String getDataSourceType() {
return contextHolder.get();
}
public static void clearDataSourceType() {
contextHolder.remove();
}
}

View File

@@ -0,0 +1,53 @@
package com.cool.store.datasource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.AbstractDataSource;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* <p>
* 动态数据源
* </p>
*
* @author wangff
* @since 2025/9/4
*/
@Component
@Primary
public class DynamicDataSource extends AbstractDataSource {
@Autowired
private DataSource defaultDataSource;
@Autowired
private DataSource platformDataSource;
@Override
public Connection getConnection() throws SQLException {
DataSource currentDB = getCurrentDB();
return currentDB.getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
DataSource currentDB = getCurrentDB();
Connection connection = currentDB.getConnection(username, password);
connection.setCatalog(DataSourceContextHolder.getDataSourceType());
return connection;
}
protected DataSource getCurrentDB() {
String dbName = DataSourceContextHolder.getDataSourceType();
if (StringUtils.isBlank(dbName)) {
return defaultDataSource;
}
return platformDataSource;
}
}

View File

@@ -9,7 +9,8 @@ package com.cool.store.enums;
public enum BusinessModelEnum {
NULL(0, ""),
DIRECT_SALES(1, "直营"),
JOIN_SALES(2, "加盟");
JOIN_SALES(2, "加盟"),
JOINT_STORE(3, "联营");
private Integer code;
private String desc;

View File

@@ -0,0 +1,37 @@
package com.cool.store.enums.Decoration;
/**
* @Author suzhuhong
* @Date 2025/11/3 9:40
* @Version 1.0
*/
public enum DecorationDescStatus {
TO_BE_ASSIGNED(0, "待分配"),
ASSIGNED(1, "已分配"),
;
private Integer code;
private String descStatus;
DecorationDescStatus(Integer code, String descStatus) {
this.code = code;
this.descStatus = descStatus;
}
public Integer getCode() {
return code;
}
public String getDescStatus() {
return descStatus;
}
}

View File

@@ -0,0 +1,32 @@
package com.cool.store.enums.Decoration;
/**
* @Author suzhuhong
* @Date 2025/10/30 14:35
* @Version 1.0
*/
public enum DecorationUseSystemEnum {
CRM(1,"CRM"),
HQT(2,"红圈通");
private Integer code;
private String userSystemName;
DecorationUseSystemEnum(Integer code, String userSystemName) {
this.code = code;
this.userSystemName = userSystemName;
}
public Integer getCode() {
return code;
}
public String getUserSystemName() {
return userSystemName;
}
}

View File

@@ -40,6 +40,7 @@ public enum ErrorCodeEnum {
LOGIN_ERROR(400004, "登录失败", null),
ENTERPRISE_INIT(400006, "企业正在初始化,请稍后访问!",null),
NOT_AUTH(400007, "暂无权限,请联系管理员!", null),
REFRESH_TOKEN_INVALID(400008, "refresh token invalid", null),
USER_FREEZE(1021019,"账号被冻结,请联系管理员",null),
ENTERPRISE_NOT_EXIST(1021020,"企业不存在",null),
USER_NOT_EXIST(1021021,"用户不存在",null),
@@ -62,6 +63,11 @@ public enum ErrorCodeEnum {
DATA_CONVERT_ERROR(400002, "日期转换异常!", null),
PARENT_NODE_NOT_EXIST(400002, "父节点不存在", null),
LOGIN_ERROR_MOBILE_ERROR(418, "登录失败 获取手机号失败!!", null),
PASSWORD_ERROR_MAX_COUNT(1021084, "密码错误{0}次,今日账号已锁定",null),
PASSWORD_MISSING(1021085, "密码不能为空!",null),
IMPROVE_USER_INFO(1021086,"请联系管理员,完善用户信息!",null),
PASSWORD_ERROR(1021087, "密码输入错误",null),
PASSWORD_ERROR_MULTI(1021088, "密码错误{0}次,请使用验证码登录",null),
//红圈通
HQT_SHOP_DECORATION_ATTRIBUTES(1022000, "获取红圈通装修属性错误", null),
HQT_PARAMS_ERROR(1022001, "构建红圈通请求参数错误", null),
@@ -206,7 +212,9 @@ public enum ErrorCodeEnum {
INVOICING_EXIST(109016, "当前门店发票信息已存在!", null),
SHOP_STATUS_NOT_SUPPORT_HANDLER(109016, "当前门店状态为:{0},不能进行结束跟进操作", null),
SHOP_STATUS_NOT_SUPPORT_HANDLER(109017, "当前门店状态为:{0},不能进行结束跟进操作", null),
SYSTEM_NAME_NOT__SUPPORT(109018, "请不要使用系统默认店名!", null),
INSERT_OPENING_OPERATION_PLAN_AUDIT_FALSE(103001,"插入运营方案审核信息失败",null),
@@ -299,10 +307,24 @@ public enum ErrorCodeEnum {
MESSAGE_TEMPLATE_NOT_SUPPORT_DELETED(1610002,"只有未发布的消息能删除,请确认!",null),
STORE_MESSAGE_REVOKE(1610003,"当前门店消息已撤销,请务重复操作",null),
STORE_MESSAGE_HANDLED(1610004,"当前门店消息已处理,无法撤销!",null),
MATTER_STORE_OR_USER_IS_NULL(1610005,"当前事项门店或者人员为空,发布失败,请确认!",null),
CONFIG_NOT_EXIST(1610006,"配置不存在或被禁用,请确认!",null),
MESSAGE_NOT_EXIST(1610007,"消息模板不存在或已被删除",null),
MESSAGE_NOT_HANDLED(1610008,"当前消息无需处理,请确认消息处理类型!",null),
MESSAGE_PUBLISH(1610009,"您选择通知任务正在发布中,请稍后重试!",null),
NOT_FLAGSHIP_STORE(1610010,"非直营店,无法跳过缴费阶段!",null),
NOT_FLAGSHIP_STORE_NOT_EXIST(1610011,"当前阶段加盟类型不能变更!",null),
JOIN_MODE_NOT_ALLOW_OPERATE(1610012,"加盟部人员只能新建加盟店或联营店,请确认!",null),
STORE_NOT_FIND(1610013,"门店不存在",null),
SHOP_NAME_INVALID(1610014, "门店名称包含禁止关键词",null),
//装修
TEAM_USED(1612001,"该装修团队有门店使用,无法删除,请确认!",null),
NOT_FLAGSHIP_STORE(16100005,"非直营店,无法跳过缴费阶段!",null),
NOT_FLAGSHIP_STORE_NOT_EXIST(16100006,"当前阶段加盟类型不能变更!",null),
WALLET_OPEN_ACCOUNT_FAIL(1620001,"钱包开通失败",null),
WALLET_WITH_DRAWER_FAIL(1620002,"提现失败",null),
WALLET_API_ERROR(1620003,"{0}",null),
;
CURRENT_BRAND_SORT_NUMBER_EXIST(16100007,"当前品牌已存在该排序数字!",null),
CONTRACT_CONFIG_NOT_EXIST(16100008,"合同配置不存在!",null),;

View File

@@ -38,4 +38,19 @@ public enum JoinModeEnum {
}
return null;
}
/**
* 是否是加盟部加盟店或者联营店 如果不是 返回false
*/
public static boolean isFranchise(Integer code) {
return code == FRANCHISE_DEPARTMENT.code || code == AFFILIATES.code;
}
public static JoinModeEnum getModelByCode(Integer code) {
for (JoinModeEnum e : JoinModeEnum.values()) {
if (e.getCode() == code) {
return e;
}
}
return null;
}
}

View File

@@ -0,0 +1,25 @@
package com.cool.store.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* <p>
* 登录类型 枚举类
* </p>
*
* @author wangff
* @since 2025/9/4
*/
@Getter
@AllArgsConstructor
public enum LoginTypeEnum {
PASSWORD("账号密码", "passwordLoginServiceImpl"),
;
private final String message;
private final String clazzName;
}

View File

@@ -10,7 +10,8 @@ public enum ResponseCodeEnum {
/**
* 成功返回
*/
SUCCESS(200000, "SUCCESS");
SUCCESS(200000, "SUCCESS"),
SUCCESS_WALLET(200, "SUCCESS");
/**
* 返回码

View File

@@ -32,6 +32,10 @@ public enum RocketMqGroupEnum {
*/
FEI_SHU_EVENT_LISTENER("fei_shu_event_listener", new ArrayList<>(Arrays.asList(RocketMqTagEnum.USER_EVENT, RocketMqTagEnum.AUTH_SCOPE_CHANGE, RocketMqTagEnum.DEPT_EVENT))),
STORE_USER_UPDATE("store_user_update", new ArrayList<>(Arrays.asList(RocketMqTagEnum.STORE_USER_UPDATE))),
SHOP_DECORATION_ASSIGN("shop_decoration_assign", new ArrayList<>(Arrays.asList(RocketMqTagEnum.DELAY_SHOP_DECORATION_ASSIGN)))
;
private final String group;

View File

@@ -18,6 +18,8 @@ public enum RocketMqTagEnum {
ZXJP_CREATE_STORE("zxjp_create_store", "正新鸡排招商创建门店"),
PARTNER_LICENSE_SYNC_QUEUE("partner_license_sync_queue", "招商证照信息同步"),
BUSINESS_SYNC("business_sync", "工商食安信息同步"),
STORE_USER_UPDATE("store_user_update", "门店信息人员变更同步菜品"),
DELAY_SHOP_DECORATION_ASSIGN("shop_decoration_assign","门店装修分配");
;

View File

@@ -7,11 +7,11 @@ package com.cool.store.enums;
*/
public enum SMSMsgEnum {
PAY_FRANCHISE_FEES("缴纳加盟费/保证金", "", "SMS_474655067"),
SIGN_CONTRACT("合同签署", "", "SMS_474450102"),
DESIGN_STAGE("设计阶段", "", "SMS_474490087"),
CONSTRUCTION_STAGE("施工阶段", "", "SMS_474525082"),
PLATFORM_BUILD_STORE("平台建店", "", "SMS_474645064"),
PAY_FRANCHISE_FEES("缴纳加盟费/保证金", "", "SMS_498895215"),
SIGN_CONTRACT("合同签署", "", "SMS_498840165"),
DESIGN_STAGE("设计阶段", "", "SMS_498750214"),
CONSTRUCTION_STAGE("施工阶段", "", "SMS_498870170"),
PLATFORM_BUILD_STORE("平台建店", "", "SMS_498730163"),
;
private String title;

View File

@@ -0,0 +1,58 @@
package com.cool.store.enums;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Author suzhuhong
* @Date 2025/11/5 16:03
* @Version 1.0
*/
public enum SpecialTagEnum {
ELECTRONIC_PRICE_LIST("电子价目牌"),
ACTIVITY_CAROUSEL("活动轮播"),
ACTIVITY_PACKAGE("活动套餐"),
PROMOTIONAL_VIDEO("宣传视频");
private final String tagName;
SpecialTagEnum(String tagName) {
this.tagName = tagName;
}
public String getTagName() {
return tagName;
}
/**
* 根据标签名称获取枚举值
*/
public static SpecialTagEnum fromTagName(String tagName) {
for (SpecialTagEnum tag : values()) {
if (tag.getTagName().equals(tagName)) {
return tag;
}
}
return null;
}
/**
* 获取所有标签名称列表
*/
public static List<String> getAllTagNames() {
return Arrays.stream(values())
.map(SpecialTagEnum::getTagName)
.collect(Collectors.toList());
}
/**
* 获取电子价目牌标签名称
* @return
*/
public static List<String> getElectronicPriceTagName() {
return Arrays.asList(ELECTRONIC_PRICE_LIST.getTagName());
}
}

View File

@@ -0,0 +1,77 @@
package com.cool.store.enums;
/**
* Created by Administrator on 2020/1/20.
*/
public enum StoreStatusEnum {
//营业
OPEN("open","在营"),
//闭店
CLOSED("closed","闭店解约"),
//未开业
NOT_OPEN("not_open","未开业"),
//迁址
CHANGE_ADDRESS("change_address","迁址"),
//退单
CHARGEBACK("chargeback","退单"),
//暂停营业
CLOSE_UP("close_up","暂停营业");
;
private final String value;
private final String name;
StoreStatusEnum(String value, String name) {
this.value = value;
this.name = name;
}
public String getValue() {
return value;
}
public String getName() {
return name;
}
public static StoreStatusEnum parse(String value) {
for (StoreStatusEnum storeStatusEnum : StoreStatusEnum.values()) {
if (storeStatusEnum.getValue().equals(value)) {
return storeStatusEnum;
}
}
return null;
}
public static String getName(String value) {
for (StoreStatusEnum storeStatusEnum : StoreStatusEnum.values()) {
if (storeStatusEnum.getValue().equals(value)) {
return storeStatusEnum.name;
}
}
return null;
}
public static String getCode(String flag) {
switch (flag) {
case "营业":
case "在营":
return "open";
case "闭店":
case "闭店解约":
return "closed";
case "未开业":
return "not_open";
case "迁址":
return "change_address";
case "退单":
return "chargeback";
case "暂停营业":
return "close_up";
}
return "open";
}
}

View File

@@ -69,7 +69,8 @@ public enum UserRoleEnum {
JING_DONG_OPERATIONS_CUSTOMER(500000000L,"京东运营大区客服"),
JING_DONG_HEADQUARTERS_BUILD_CUSTOMER(510000000L,"京东总部建店客服"),
FRANCHISEES(530000000L,"加盟商")
FRANCHISEES(530000000L,"加盟商"),
SERVICE_PACKAGE_DEDICATED(1762761165005L,"服务包专用"),
;
private Long code;

View File

@@ -10,6 +10,11 @@ public enum MatterTypeEnum {
QUESTION(0,"门店违规工单"),
LICENSE(1,"证照过期提醒"),
NOTICE(2,"通知消息"),
LOGISTICS(3,"物流"),
SERVICE_PACKAGE(4,"服务包"),
RESTOCK(5,"补货"),
INVENTORY(6,"盘点"),
REALTIME(7, "即时消息"),
;
MatterTypeEnum(Integer code, String message) {

View File

@@ -10,10 +10,13 @@ import java.util.List;
*/
public enum ModuleCodeEnum {
STORE_WORK(0,"店务", Arrays.asList(MatterTypeEnum.QUESTION,MatterTypeEnum.LICENSE,MatterTypeEnum.NOTICE)),
PRODUCT_UPDATE(1,"营销政策/产品上新",Arrays.asList(MatterTypeEnum.NOTICE)),
INVENTORY_MODULE(2,"库存模块",Arrays.asList(MatterTypeEnum.NOTICE)),
STORE_WORK(0,"店务/培训", Arrays.asList(MatterTypeEnum.QUESTION,MatterTypeEnum.LICENSE)),
PRODUCT_UPDATE(1,"营销/上新",Arrays.asList(MatterTypeEnum.SERVICE_PACKAGE)),
INVENTORY_MODULE(2,"订货/库存",Arrays.asList(MatterTypeEnum.RESTOCK,MatterTypeEnum.INVENTORY,MatterTypeEnum.LOGISTICS)),
DISH(3,"菜品",Arrays.asList(MatterTypeEnum.NOTICE)),
FRANCHISE(4,"加盟",Arrays.asList(MatterTypeEnum.NOTICE)),
//其他(投诉与客户服务、临时通知)
OTHER(5,"其他",Arrays.asList(MatterTypeEnum.NOTICE, MatterTypeEnum.REALTIME)),
;
ModuleCodeEnum(Integer code, String message,List<MatterTypeEnum> matterTypeEnums) {

View File

@@ -7,8 +7,8 @@ package com.cool.store.enums.notice;
*/
public enum RemindTypeEnum {
CONTINUOUS_REMINDER(0,"持续提醒"),
STAGE_REMINDER(1,"阶段提醒"),
CONTINUOUS_REMINDER(1,"持续提醒"),
STAGE_REMINDER(2,"阶段提醒"),
;
RemindTypeEnum(Integer code, String message) {

View File

@@ -0,0 +1,86 @@
package com.cool.store.enums.notice;
/**
* @Author suzhuhong
* @Date 2025/8/26 15:43
* @Version 1.0
*/
public enum SceneEnum {
SUBMIT_ORDER(0, "提交订单", "https://oss-cool.coolstore.cn/eid/214ac5a3a517472a87268e02a2e6410a/2508/j_l6em.jpg", MatterTypeEnum.LOGISTICS),
ASSIGNED_PICKING(5, "已分配拣货", "https://oss-cool.coolstore.cn/eid/214ac5a3a517472a87268e02a2e6410a/2508/YLjBkv.jpg", MatterTypeEnum.LOGISTICS),
PICKING_COMPLETED(10, "拣货完成", "https://oss-cool.coolstore.cn/eid/214ac5a3a517472a87268e02a2e6410a/2508/gG9Y-h.jpg", MatterTypeEnum.LOGISTICS),
SHIPPED(15, "已出库", "https://oss-cool.coolstore.cn/eid/214ac5a3a517472a87268e02a2e6410a/2508/Ehz8_n.jpg", MatterTypeEnum.LOGISTICS),
DELIVERY_IN_PROGRESS(20, "配送中", "https://oss-cool.coolstore.cn/eid/214ac5a3a517472a87268e02a2e6410a/2508/I6bAmA.jpg", MatterTypeEnum.LOGISTICS),
ORDER_SIGNING_AND_ACCEPTANCE(25, "订单签收", "https://oss-cool.coolstore.cn/eid/214ac5a3a517472a87268e02a2e6410a/2508/0z7jkU.jpg", MatterTypeEnum.LOGISTICS),
SERVICE_PACKAGE(30, "服务包", "", MatterTypeEnum.LOGISTICS),
RESTOCK(35, "补货", "", MatterTypeEnum.LOGISTICS),
INVENTORY(40, "盘点", "", MatterTypeEnum.LOGISTICS),
REALTIME(45, "即时消息", "", MatterTypeEnum.REALTIME),
;
private Integer sceneCode;
private String sceneName;
private String scenePicture;
private MatterTypeEnum matterTypeEnum;
public Integer getSceneCode() {
return sceneCode;
}
public void setSceneCode(Integer sceneCode) {
this.sceneCode = sceneCode;
}
public String getSceneName() {
return sceneName;
}
public void setSceneName(String sceneName) {
this.sceneName = sceneName;
}
public String getScenePicture() {
return scenePicture;
}
public void setScenePicture(String scenePicture) {
this.scenePicture = scenePicture;
}
public MatterTypeEnum getMatterTypeEnum() {
return matterTypeEnum;
}
public void setMatterTypeEnum(MatterTypeEnum matterTypeEnum) {
this.matterTypeEnum = matterTypeEnum;
}
SceneEnum(Integer sceneCode, String sceneName, String scenePicture, MatterTypeEnum matterTypeEnum) {
this.sceneCode = sceneCode;
this.sceneName = sceneName;
this.scenePicture = scenePicture;
this.matterTypeEnum = matterTypeEnum;
}
/**
* 通过code获取枚举
* @param code
* @return
*/
public static SceneEnum getByCode(Integer code) {
for (SceneEnum value : values()) {
if (value.sceneCode.equals(code)) {
return value;
}
}
return null;
}
}

View File

@@ -18,6 +18,7 @@ public enum ShopSubStageEnum {
SHOP_STAGE_3(ShopStageEnum.SHOP_STAGE_2, 30, "营业执照办理", 23),
SHOP_STAGE_4(ShopStageEnum.SHOP_STAGE_2, 40, "食安许可证", 55),
SHOP_STAGE_5(ShopStageEnum.SHOP_STAGE_2, 50, "员工招聘", 23),
SHOP_STAGE_6(ShopStageEnum.SHOP_STAGE_2, 260, "开通门店平安钱包", 1),
SHOP_STAGE_7(ShopStageEnum.SHOP_STAGE_2, 70, "缴纳加盟费/保证金", 1),
SHOP_STAGE_8(ShopStageEnum.SHOP_STAGE_2, 80, "加盟合同签约", 4),
SHOP_STAGE_8_5(ShopStageEnum.SHOP_STAGE_2, 85, "发票回传", 5),
@@ -110,6 +111,8 @@ public enum ShopSubStageEnum {
return ShopSubStageStatusEnum.SHOP_SUB_STAGE_STATUS_10;
case SHOP_STAGE_5:
return ShopSubStageStatusEnum.SHOP_SUB_STAGE_STATUS_50;
// case SHOP_STAGE_6:
// return ShopSubStageStatusEnum.SHOP_SUB_STAGE_STATUS_60;
case SHOP_STAGE_7:
return ShopSubStageStatusEnum.SHOP_SUB_STAGE_STATUS_70;
case SHOP_STAGE_2:

View File

@@ -36,6 +36,10 @@ public enum ShopSubStageStatusEnum {
SHOP_SUB_STAGE_STATUS_50(ShopSubStageEnum.SHOP_STAGE_5, 500, "登记中", Boolean.FALSE),
SHOP_SUB_STAGE_STATUS_51(ShopSubStageEnum.SHOP_STAGE_5, 510, "已完成", Boolean.TRUE),
//开通门店平安钱包
SHOP_SUB_STAGE_STATUS_60(ShopSubStageEnum.SHOP_STAGE_6, 2600, "未开通", Boolean.FALSE),
SHOP_SUB_STAGE_STATUS_61(ShopSubStageEnum.SHOP_STAGE_6, 2610, "已完成", Boolean.TRUE),
//缴纳加盟费/保证金
SHOP_SUB_STAGE_STATUS_70(ShopSubStageEnum.SHOP_STAGE_7, 700, "待内勤发布账单", Boolean.FALSE),
SHOP_SUB_STAGE_STATUS_71(ShopSubStageEnum.SHOP_STAGE_7, 710, "待加盟商缴费", Boolean.FALSE),

View File

@@ -0,0 +1,26 @@
package com.cool.store.enums.wallet;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* <p>
* 银行卡业务类型 枚举类
* </p>
*
* @author wangff
* @since 2025/11/14
*/
@Getter
@AllArgsConstructor
public enum BankAccountTypeEnum {
PUBLIC(1, "对公"),
PRIVATE(2, "对私"),
;
private final Integer type;
private final String desc;
}

View File

@@ -0,0 +1,28 @@
package com.cool.store.enums.wallet;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* <p>
* 银行开户类型 枚举类
* </p>
*
* @author wangff
* @since 2025/11/14
*/
@Getter
@AllArgsConstructor
public enum BankBusinessTypeEnum {
ENTERPRISE(1, "企业"),
INDIVIDUAL(2, "个体工商户"),
PERSONAL(3, "个人(小微商户)"),
;
private final Integer type;
private final String desc;
}

View File

@@ -0,0 +1,27 @@
package com.cool.store.enums.wallet;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* <p>
* 平安钱包账户开通状态
* </p>
*
* @author wangff
* @since 2025/11/14
*/
@Getter
@AllArgsConstructor
public enum PingAnAccountStatusEnum {
UNCOMMITTED(1, "待提交"),
UNAUTHORIZED(2, "待鉴权"),
AUTHENTICATING(3, "鉴权中"),
OPEN(4, "开通"),
;
private final Integer status;
private final String desc;
}

View File

@@ -0,0 +1,47 @@
package com.cool.store.enums.wallet;
import com.cool.store.enums.JoinModeEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* <p>
* 营帐通门店模式
* </p>
*
* @author wangff
* @since 2025/11/19
*/
@Getter
@AllArgsConstructor
public enum YztStoreModel {
// 1.社会加盟 2.强管 3.强加盟 (门店模式)
SOCIAL_JOIN(1, "社会加盟", Arrays.asList(JoinModeEnum.FRANCHISE_DEPARTMENT, JoinModeEnum.FRANCHISE_COMPANIES)),
STRONG_MANAGEMENT(2, "强管", Collections.singletonList(JoinModeEnum.FLAGSHIP_STORE)),
STRONG_JOIN(3, "强加盟", Collections.singletonList(JoinModeEnum.AFFILIATES)),
;
private final Integer model;
private final String desc;
/**
* 对应crm加盟模式枚举类
*/
private final List<JoinModeEnum> joinModelList;
public static Integer getYztStoreModel(Integer joinModel) {
JoinModeEnum joinModelEnum = JoinModeEnum.getModelByCode(joinModel);
for (YztStoreModel e : YztStoreModel.values()) {
if (e.joinModelList.contains(joinModelEnum)) {
return e.model;
}
}
return null;
}
}

View File

@@ -0,0 +1,25 @@
package com.cool.store.enums.wechat;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* <p>
* 钱包类型
* </p>
*
* @author wangff
* @since 2025/11/20
*/
@Getter
@AllArgsConstructor
public enum WalletTypeEnum {
PING_AN(1, "平安银行"),
ONLINE_BANK(2, "网商银行"),
;
private final Integer type;
private final String desc;
}

View File

@@ -0,0 +1,53 @@
package com.cool.store.enums.wechat;
/**
* @Author suzhuhong
* @Date 2025/10/16 15:19
* @Version 1.0
*/
public enum WechatTemplateDetailEnum {
CHARACTER_STRING2("编号","character_string2"),
THING10("项目名称","thing10"),
TIME14("完成时间","time14"),
THING25("客户名称","thing25"),
THING60("位置","thing60"),
THING6("工单标题","thing6"),
TIME33("创建时间","time33"),
CHARACTER_STRING14("工单编号","character_string14"),
;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
private String name;
private String code;
WechatTemplateDetailEnum(String name, String code) {
this.name = name;
this.code = code;
}
}

View File

@@ -0,0 +1,79 @@
package com.cool.store.enums.wechat;
import com.fasterxml.jackson.annotation.JsonValue;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.List;
import static com.cool.store.enums.wechat.WechatTemplateDetailEnum.*;
/**
* @Author suzhuhong
* @Date 2025/10/10 14:39
* @Version 1.0
*/
public enum WechatTemplateEnum {
QUESTION_NOTICE("QUESTION_NOTICE", "T3sp5gBItHKD8oCeEiQMjn7JXpngFiz3dDcaArk84xY", "收到工单通知",
Arrays.asList(CHARACTER_STRING2,THING10,TIME14,THING25,THING60)),
NEW_QUESTION_NOTICE("new_question_notice", "35rmoQ5wEHvUgCgSIJ2CpOb1DOJgWOiy8J7DXT_e4_Y", "新工单提醒",
Arrays.asList(THING6,TIME33,CHARACTER_STRING14)),
;
private final String code;
private final String templateId;
private final String title;
private final List<WechatTemplateDetailEnum> contentList;
WechatTemplateEnum(String code, String templateId, String title, List<WechatTemplateDetailEnum> contentList) {
this.code = code;
this.templateId = templateId;
this.title = title;
this.contentList = contentList;
}
@JsonValue
public String getCode() {
return code;
}
public String getTemplateId() {
return templateId;
}
public String getTitle() {
return title;
}
public List<WechatTemplateDetailEnum> getContentList() {
return contentList;
}
/**
* 根据code获取枚举
*/
public static WechatTemplateEnum getByCode(String code) {
for (WechatTemplateEnum template : values()) {
if (template.getCode().equals(code)) {
return template;
}
}
return null;
}
/**
* 根据模板ID获取枚举
*/
public static WechatTemplateEnum getByTemplateId(String templateId) {
for (WechatTemplateEnum template : values()) {
if (template.getTemplateId().equals(templateId)) {
return template;
}
}
return null;
}
}

View File

@@ -0,0 +1,80 @@
package com.cool.store.executor;
import com.cool.store.utils.ThreadMdcUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
/**
* @author zhangchenbiao
* @FileName: MdcTaskExecutor
* @Description:
* @date 2021-11-02 21:00
*/
public class MdcTaskExecutor extends ThreadPoolTaskExecutor {
private Logger log = LoggerFactory.getLogger(MdcTaskExecutor.class);
@Override
public <T> Future<T> submit(Callable<T> task) {
Map<String, String> context = MDC.getCopyOfContextMap();
return super.submit(() -> {
T result;
if (context != null) {
//将父线程的MDC内容传给子线程
MDC.setContextMap(context);
}
//直接给子线程设置MDC
ThreadMdcUtil.setTraceIdIfAbsent();
try {
//执行任务
result = task.call();
} finally {
log.info("ThreadMonitor:{}info:ExecutedTasks->{},totalTask->{}, RunningTasks->{}, PendingTasks->{},corePoolSize-{},currentPoolSize->{},LargestPoolSize->{}",
this.getThreadNamePrefix(),this.getThreadPoolExecutor().getCompletedTaskCount(),this.getThreadPoolExecutor().getTaskCount(),
this.getActiveCount(),this.getThreadPoolExecutor().getQueue().size(),this.getCorePoolSize(),
this.getPoolSize(),this.getThreadPoolExecutor().getLargestPoolSize());
try {
MDC.clear();
} catch (Exception e) {
log.warn("MDC clear exception", e);
}
}
return result;
});
}
@Override
public void execute(Runnable task) {
log.info("mdc thread pool task executor execute");
Map<String, String> context = MDC.getCopyOfContextMap();
super.execute(() -> {
if (context != null) {
//将父线程的MDC内容传给子线程
MDC.setContextMap(context);
}
//直接给子线程设置MDC
ThreadMdcUtil.setTraceIdIfAbsent();
try {
//执行任务
task.run();
} finally {
log.info("ThreadMonitor:{}info:ExecutedTasks->{},totalTask->{}, RunningTasks->{}, PendingTasks->{},corePoolSize-{},currentPoolSize->{},LargestPoolSize->{}",
this.getThreadNamePrefix(),this.getThreadPoolExecutor().getCompletedTaskCount(),this.getThreadPoolExecutor().getTaskCount(),
this.getActiveCount(),this.getThreadPoolExecutor().getQueue().size(),this.getCorePoolSize(),
this.getPoolSize(),this.getThreadPoolExecutor().getLargestPoolSize());
try {
MDC.clear();
} catch (Exception e) {
log.warn("MDC clear exception", e);
}
}
});
}
}

View File

@@ -0,0 +1,42 @@
package com.cool.store.executor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @Author suzhuhong
* @Date 2025/10/15 17:29
* @Version 1.0
*/
@Configuration
public class ThreadPoolTask {
@Bean
public TaskExecutor noticeThreadPool() {
int cores = Runtime.getRuntime().availableProcessors();
ThreadPoolTaskExecutor executor = new MdcTaskExecutor();
// 核心线程数目
executor.setCorePoolSize(cores*2);
// 指定最大线程数
executor.setMaxPoolSize(100);
// 队列中最大的数目
executor.setQueueCapacity(10000);
// 线程名称前缀
executor.setThreadNamePrefix("noticeThreadPool_");
// 对拒绝task的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 线程空闲后的最大存活时间
executor.setKeepAliveSeconds(60);
// 加载
executor.initialize();
return executor;
}
}

View File

@@ -0,0 +1,45 @@
package com.cool.store.executor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* <p>
* 线程池配置类
* </p>
*
* @author wangff
* @since 2025/9/5
*/
@Configuration
public class ThreadPoolTaskConfig {
/**
* 通用线程池
*/
@Bean
public TaskExecutor generalThreadPool() {
int cores = Runtime.getRuntime().availableProcessors();
ThreadPoolTaskExecutor executor = new MdcTaskExecutor();
// 核心线程数目
executor.setCorePoolSize(cores*2);
// 指定最大线程数
executor.setMaxPoolSize(200);
// 队列中最大的数目
executor.setQueueCapacity(5000);
// 线程名称前缀
executor.setThreadNamePrefix("generalThreadPool_");
// 对拒绝task的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 线程空闲后的最大存活时间
executor.setKeepAliveSeconds(60);
// 加载
executor.initialize();
return executor;
}
}

View File

@@ -0,0 +1,54 @@
package com.cool.store.utils;
import cn.hutool.core.bean.copier.CopyOptions;
import com.github.pagehelper.PageInfo;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* <p>
* bean转换工具
* </p>
*
* @author wangff
* @since 2025/3/6
*/
public class BeanUtil extends cn.hutool.core.bean.BeanUtil {
public static <T, R> List<R> toList(List<T> list, Class<R> clazz) {
if (list == null || list.isEmpty()) {
return Collections.emptyList();
}
List<R> result = new ArrayList<>(list.size());
for (T t : list) {
R r = toBean(t, clazz);
result.add(r);
}
return result;
}
public static <T, R> List<R> toList(List<T> list, Class<R> clazz, CopyOptions copyOptions) {
if (list == null || list.isEmpty()) {
return Collections.emptyList();
}
List<R> result = new ArrayList<>(list.size());
for (T t : list) {
R r = toBean(t, clazz, copyOptions);
result.add(r);
}
return result;
}
public static <T, R> PageInfo<R> toPage(PageInfo<T> page, Class<R> clazz) {
PageInfo<R> newPage = new PageInfo<>();
newPage.setPages(page.getPages());
newPage.setTotal(page.getTotal());
newPage.setPageNum(page.getPageNum());
newPage.setPageSize(page.getPageSize());
List<R> list = toList(page.getList(), clazz);
newPage.setList(list);
return newPage;
}
}

View File

@@ -0,0 +1,62 @@
package com.cool.store.utils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @Author suzhuhong
* @Date 2025/11/4 17:34
* @Version 1.0
*/
public class BrowserVersionUtils {
/**
* 检测是否为旧版Chrome浏览器版本小于60
* @param userAgent 浏览器User-Agent字符串
* @return true-是旧版Chromefalse-不是旧版Chrome
*/
public static boolean isOldChromeBrowser(String userAgent) {
if (userAgent == null || userAgent.isEmpty()) {
return false;
}
// 检查是否是Chrome浏览器
if (!userAgent.contains("Chrome")) {
return false; // 不是Chrome浏览器
}
// 提取Chrome版本号
Integer chromeVersion = extractChromeVersion(userAgent);
if (chromeVersion == null) {
return false; // 无法提取版本号
}
// 判断版本是否小于60
return chromeVersion < 60;
}
/**
* 从User-Agent中提取Chrome主版本号
* @param userAgent 浏览器User-Agent字符串
* @return Chrome主版本号如果无法提取返回null
*/
public static Integer extractChromeVersion(String userAgent) {
// 正则表达式匹配 Chrome/版本号 模式
Pattern pattern = Pattern.compile("Chrome/(\\d+)");
Matcher matcher = pattern.matcher(userAgent);
if (matcher.find()) {
try {
return Integer.parseInt(matcher.group(1));
} catch (NumberFormatException e) {
System.err.println("版本号格式错误: " + matcher.group(1));
return null;
}
}
return null;
}
}

View File

@@ -0,0 +1,25 @@
package com.cool.store.utils;
import org.apache.commons.lang3.StringUtils;
import java.math.BigDecimal;
/**
* <p>
* 公共工具
* </p>
*
* @author wangff
* @since 2025/10/29
*/
public class CommonUtil {
public static BigDecimal convertToBig(String value) {
if (StringUtils.isBlank(value)) {
return BigDecimal.ZERO;
}
try {
return new BigDecimal(value);
} catch (Exception ignored) {}
return BigDecimal.ZERO;
}
}

View File

@@ -35,6 +35,7 @@ public class CoolDateUtils {
public static final String DATE_FORMAT_SEC_6 = "yyyy.MM.dd";
public static final String DATE_FORMAT_SEC_7 = "yyyy/MM/dd HH:mm";
public static final String DATE_FORMAT_SEC_8 = "yyyyMMdd";
public static final String DATE_FORMAT_SEC_9 = "yyyyMMddHHmmssSSS";
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@@ -70,7 +71,7 @@ public class CoolDateUtils {
}
public static void main(String[] args) {
// 创建一个Date对象
//创建一个Date对象
Date date = new Date();
// 将Date对象转换为LocalDate对象
@@ -146,6 +147,13 @@ public class CoolDateUtils {
return LocalDate.now().format(DATE_FORMATTER);
}
public static final String getTodayMillis(){
DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_FORMAT_SEC_9);
return LocalDateTime.now().format(DATE_FORMATTER);
}
/**
* 获取当前日期字符串 (yyyy-MM-dd)
*/

View File

@@ -0,0 +1,82 @@
package com.cool.store.utils;
/**
* @Author suzhuhong
* @Date 2025/11/13 12:15
* @Version 1.0
*/
public class KeyFormatUtil {
/**
* 将单行密钥转换为标准PEM多行格式
*/
public static String convertToPEMFormat(String singleLineKey, String keyType) {
if (singleLineKey == null || singleLineKey.trim().isEmpty()) {
throw new IllegalArgumentException("密钥不能为空");
}
// 清理密钥,移除可能存在的头尾标记和空白
String cleanedKey = cleanKey(singleLineKey);
// 定义头尾标记
String header = getKeyHeader(keyType);
String footer = getKeyFooter(keyType);
// 构建多行格式
StringBuilder pemKey = new StringBuilder();
pemKey.append(header).append("\n");
// 每64个字符换行
for (int i = 0; i < cleanedKey.length(); i += 64) {
int end = Math.min(i + 64, cleanedKey.length());
pemKey.append(cleanedKey.substring(i, end)).append("\n");
}
pemKey.append(footer);
return pemKey.toString();
}
/**
* 清理密钥字符串
*/
public static String cleanKey(String key) {
return key.replaceAll("-----BEGIN.*-----", "")
.replaceAll("-----END.*-----", "")
.replaceAll("\\s", "") // 移除所有空白字符
.trim();
}
private static String getKeyHeader(String keyType) {
switch (keyType.toUpperCase()) {
case "PRIVATE":
case "PRIVATE_KEY":
return "-----BEGIN PRIVATE KEY-----";
case "PUBLIC":
case "PUBLIC_KEY":
return "-----BEGIN PUBLIC KEY-----";
case "RSA_PRIVATE":
return "-----BEGIN RSA PRIVATE KEY-----";
case "RSA_PUBLIC":
return "-----BEGIN RSA PUBLIC KEY-----";
default:
throw new IllegalArgumentException("不支持的密钥类型: " + keyType);
}
}
private static String getKeyFooter(String keyType) {
switch (keyType.toUpperCase()) {
case "PRIVATE":
case "PRIVATE_KEY":
return "-----END PRIVATE KEY-----";
case "PUBLIC":
case "PUBLIC_KEY":
return "-----END PUBLIC KEY-----";
case "RSA_PRIVATE":
return "-----END RSA PRIVATE KEY-----";
case "RSA_PUBLIC":
return "-----END RSA PUBLIC KEY-----";
default:
throw new IllegalArgumentException("不支持的密钥类型: " + keyType);
}
}
}

View File

@@ -0,0 +1,37 @@
package com.cool.store.utils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import java.util.UUID;
/**
* @Author suzhuhong
* @Date 2025/10/15 17:32
* @Version 1.0
*/
public class MDCUtils {
public MDCUtils() {
}
public static void putIfAbsent(String key, String value) {
String k = MDC.get(key);
if (StringUtils.isBlank(value)) {
value = UUID.randomUUID().toString().replace("-", "");
}
if (StringUtils.isBlank(k)) {
MDC.put(key, value);
}
}
public static void put(String key, String value) {
MDC.put(key, value);
}
public static void put(String key) {
MDC.put(key, UUID.randomUUID().toString().replace("-", ""));
}
}

View File

@@ -0,0 +1,197 @@
package com.cool.store.utils;
import com.cool.store.enums.ErrorCodeEnum;
import com.cool.store.exception.ServiceException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
/**
* @Author suzhuhong
* @Date 2025/10/10 14:21
* @Version 1.0
* OkHttp工具类
*/
@Slf4j
@Component
public class OkHttpUtil {
@Autowired
private OkHttpClient okHttpClient;
@Autowired
private ObjectMapper objectMapper;
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
/**
* GET请求
*/
public String doGet(String url) throws IOException {
return doGet(url, null);
}
/**
* GET请求 - 带请求头
*/
public String doGet(String url, Map<String, String> headers) throws IOException {
Request.Builder builder = new Request.Builder().url(url);
// 添加请求头
if (headers != null && !headers.isEmpty()) {
headers.forEach(builder::addHeader);
}
Request request = builder.build();
try (Response response = okHttpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code: " + response);
}
ResponseBody body = response.body();
return body != null ? body.string() : null;
}
}
/**
* POST请求 - JSON数据
*/
public String doPostJson(String url, Object data) throws IOException {
return doPostJson(url, data, null);
}
/**
* POST请求 - JSON数据带请求头
*/
public String doPostJson(String url, Object data, Map<String, String> headers) throws IOException {
//打印日志
logRequest(url, data);
String json = objectMapper.writeValueAsString(data);
RequestBody requestBody = RequestBody.create( JSON,json);
Request.Builder builder = new Request.Builder()
.url(url)
.post(requestBody);
// 添加请求头
if (headers != null && !headers.isEmpty()) {
headers.forEach(builder::addHeader);
}
Request request = builder.build();
try (Response response = okHttpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new ServiceException(ErrorCodeEnum.THIRD_API_ERROR,
"HTTP请求失败状态码: " + response.code());
}
ResponseBody body = response.body();
String responseBody = body != null ? body.string() : null;
logResponse(url, response.code(), responseBody);
return responseBody;
}
}
/**
* 异步GET请求
*/
public void doGetAsync(String url, Callback callback) {
doGetAsync(url, null, callback);
}
/**
* 异步GET请求 - 带请求头
*/
public void doGetAsync(String url, Map<String, String> headers, Callback callback) {
Request.Builder builder = new Request.Builder().url(url);
if (headers != null && !headers.isEmpty()) {
headers.forEach(builder::addHeader);
}
Request request = builder.build();
okHttpClient.newCall(request).enqueue(callback);
}
/**
* 异步POST请求
*/
public void doPostAsync(String url, Object data, Callback callback) {
doPostAsync(url, data, null, callback);
}
/**
* 异步POST请求 - 带请求头
*/
public void doPostAsync(String url, Object data, Map<String, String> headers, Callback callback) {
try {
String json = objectMapper.writeValueAsString(data);
RequestBody requestBody = RequestBody.create( JSON,json);
Request.Builder builder = new Request.Builder()
.url(url)
.post(requestBody);
if (headers != null && !headers.isEmpty()) {
headers.forEach(builder::addHeader);
}
Request request = builder.build();
okHttpClient.newCall(request).enqueue(callback);
} catch (IOException e) {
callback.onFailure(null, e);
}
}
private void logRequest(String url, Object requestBody) {
if (log.isInfoEnabled()) {
try {
log.info("\n======= 请求开始 =======\n" +
"API地址: {}\n" +
"请求参数: {}\n" +
"======= 请求结束 =======",
url,
objectMapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(requestBody));
} catch (JsonProcessingException e) {
log.warn("日志JSON序列化失败", e);
}
}
}
private void logResponse(String url, int statusCode, String responseBody) {
if (log.isInfoEnabled()) {
try {
// 尝试美化JSON输出
Object json = objectMapper.readValue(responseBody, Object.class);
String prettyResponse = objectMapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(json);
log.info("\n======= 响应开始 =======\n" +
"API地址: {}\n" +
"HTTP状态码: {}\n" +
"响应内容: {}\n" +
"======= 响应结束 =======",
url,
statusCode,
prettyResponse);
} catch (Exception e) {
// 非JSON响应或解析失败时直接输出原始内容
log.info("\n======= 响应开始 =======\n" +
"API地址: {}\n" +
"HTTP状态码: {}\n" +
"原始响应: {}\n" +
"======= 响应结束 =======",
url,
statusCode,
responseBody);
}
}
}
}

View File

@@ -5,9 +5,10 @@ import lombok.extern.slf4j.Slf4j;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.*;
import java.util.stream.Collectors;
@@ -110,4 +111,5 @@ public class OpenSignatureUtil {
}
return result.toString();
}
}

View File

@@ -0,0 +1,348 @@
package com.cool.store.utils;
import java.net.URLEncoder;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.asn1.pkcs.RSAPrivateKey;
import sun.security.util.DerInputStream;
import sun.security.util.DerValue;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.*;
import java.util.*;
/**
* RSA签名工具类
* @Author suzhuhong
* @Date 2025/11/12 22:57
* @Version 1.0
*/
@Slf4j
public class RsaSignUtil {
/**
* 加签方法
*/
public static String generateSign(Map<String, Object> params, String privateKeyPEM) {
// 移除可能存在的sign字段
params.remove("sign");
String afterSort = objectToSortedString(params);
log.info("排序后的参数:{}", afterSort);
String formattedPrivateKey = KeyFormatUtil.convertToPEMFormat(privateKeyPEM, "RSA_PRIVATE");
return rsaPrivateKeySign(afterSort, privateKeyPEM, "SHA256withRSA");
}
/**
* 验签方法
*/
public static boolean verifySign(Map<String, Object> params, String publicKeyPEM) {
String sign = String.valueOf(params.get("sign"));
params.remove("sign");
String afterSort = objectToSortedString(params);
log.info("排序后的参数:{}", afterSort);
return rsaPublicKeyVerify(afterSort, sign, publicKeyPEM, "SHA256withRSA");
}
/**
* 查询签名验证 - 主入口方法
*/
public static boolean querySignCrm(String publicKey, Map<String, Object> params) {
return verifySign(params, publicKey);
}
/**
* 将对象转换为排序后的字符串
*/
public static String objectToSortedString(Map<String, Object> obj) {
return convertToSortedString(obj, "");
}
/**
* 递归将参数转换为排序字符串
*/
private static String convertToSortedString(Map<String, Object> params, String prefix) {
// 按键排序
List<String> keys = new ArrayList<>(params.keySet());
Collections.sort(keys);
List<String> parts = new ArrayList<>();
for (String key : keys) {
Object value = params.get(key);
if (value == null || "".equals(value) ||
(value instanceof Number && ((Number) value).doubleValue() == 0)) {
continue;
}
String currentKey = key;
if (!prefix.isEmpty()) {
currentKey = prefix + "[" + key + "]";
}
if (value instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> nestedMap = (Map<String, Object>) value;
String nestedString = convertToSortedString(nestedMap, currentKey);
if (!nestedString.isEmpty()) {
parts.add(nestedString);
}
} else if (value instanceof List) {
List<?> list = (List<?>) value;
List<String> elemStrs = new ArrayList<>();
for (Object element : list) {
String elementStr;
if (element instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> elementMap = (Map<String, Object>) element;
String inner = convertToSortedString(elementMap, "");
elementStr = "{" + inner + "}";
} else if (element instanceof Double) {
elementStr = String.format("%.0f", (Double) element);
} else if (element instanceof Float) {
elementStr = String.format("%.0f", (Float) element);
} else {
elementStr = String.valueOf(element);
}
elemStrs.add(elementStr);
}
// 对元素字符串排序
Collections.sort(elemStrs);
// URL编码并添加到parts
for (String es : elemStrs) {
String encodedValue = urlEncode(es);
parts.add(currentKey + "[]=" + encodedValue);
}
} else if (value instanceof Double) {
String strValue = String.format("%.0f", (Double) value);
parts.add(currentKey + "=" + urlEncode(strValue));
} else if (value instanceof Float) {
String strValue = String.format("%.0f", (Float) value);
parts.add(currentKey + "=" + urlEncode(strValue));
} else {
String strValue = String.valueOf(value);
parts.add(currentKey + "=" + urlEncode(strValue));
}
}
return String.join("&", parts);
}
/**
* URL编码替换+为%20
*/
private static String urlEncode(String value) {
try {
return URLEncoder.encode(value, StandardCharsets.UTF_8.name())
.replace("+", "%20");
} catch (Exception e) {
return value;
}
}
/**
* RSA私钥加签
*/
public static String rsaPrivateKeySign(String text, String privateKeyPEM, String algorithm) {
try {
// 1. 解析私钥
log.debug("Received private key PEM: {}", privateKeyPEM);
PrivateKey privateKey = parsePrivateKey(privateKeyPEM);
if (privateKey == null) {
throw new RuntimeException("解析私钥失败");
}
// 2. 创建签名实例
Signature signature = Signature.getInstance(algorithm);
signature.initSign(privateKey);
signature.update(text.getBytes(StandardCharsets.UTF_8));
// 3. 生成签名并Base64编码
byte[] digitalSignature = signature.sign();
return Base64.getEncoder().encodeToString(digitalSignature);
} catch (Exception e) {
throw new RuntimeException("加签失败: " + e.getMessage(), e);
}
}
/**
* RSA公钥验签
*/
public static boolean rsaPublicKeyVerify(String text, String signBase64,
String publicKeyPEM, String algorithm) {
try {
// 1. 解码Base64签名
byte[] signature = Base64.getDecoder().decode(signBase64);
// 2. 解析公钥
PublicKey publicKey = parsePublicKey(publicKeyPEM);
if (publicKey == null) {
return false;
}
// 3. 验证签名
Signature sig = Signature.getInstance(algorithm);
sig.initVerify(publicKey);
sig.update(text.getBytes(StandardCharsets.UTF_8));
return sig.verify(signature);
} catch (Exception e) {
return false;
}
}
/**
* 解析PEM格式私钥兼容PKCS#1和PKCS#8格式
*/
private static PrivateKey parsePrivateKey(String privateKeyPEM) {
try {
String pemContent = privateKeyPEM
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replace("-----BEGIN RSA PRIVATE KEY-----", "")
.replace("-----END RSA PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] keyBytes = Base64.getDecoder().decode(pemContent);
// 尝试PKCS#8格式
try {
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
} catch (Exception e) {
// 使用Bouncy Castle处理PKCS#1
RSAPrivateKey rsaPrivKey = RSAPrivateKey.getInstance(keyBytes);
return KeyFactory.getInstance("RSA").generatePrivate(
new RSAPrivateKeySpec(rsaPrivKey.getModulus(), rsaPrivKey.getPrivateExponent()));
}
} catch (Exception e) {
throw new RuntimeException("解析私钥失败: " + e.getMessage(), e);
}
}
/**
* 解析PKCS#1格式私钥
*/
private static PrivateKey parsePKCS1PrivateKey(byte[] keyBytes) {
try {
int length = keyBytes.length;
byte[] pkcs8Header = new byte[] {
0x30, (byte) 0x82,
(byte) ((length + 26) >> 8), (byte) (length + 26), // 总长度
0x02, 0x01, 0x00,
0x30, 0x0D, 0x06, 0x09, 0x2A, (byte) 0x86, 0x48, (byte) 0x86, (byte) 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00,
0x04, (byte) 0x82,
(byte) (length >> 8), (byte) length // BIT STRING 长度
};
byte[] pkcs8Key = new byte[pkcs8Header.length + keyBytes.length];
System.arraycopy(pkcs8Header, 0, pkcs8Key, 0, pkcs8Header.length);
System.arraycopy(keyBytes, 0, pkcs8Key, pkcs8Header.length, keyBytes.length);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(pkcs8Key);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
} catch (Exception e) {
throw new RuntimeException("解析PKCS#1私钥失败: " + e.getMessage(), e);
}
}
/**
* 解析PEM格式公钥兼容PKCS#1和PKCS#8格式
*/
private static PublicKey parsePublicKey(String publicKeyPEM) {
try {
// 移除PEM头尾标记和空白字符
String pemContent = publicKeyPEM
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replace("-----BEGIN RSA PUBLIC KEY-----", "")
.replace("-----END RSA PUBLIC KEY-----", "")
.replaceAll("\\s", "");
byte[] keyBytes = Base64.getDecoder().decode(pemContent);
// 先尝试PKCS#8格式
try {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(keySpec);
} catch (Exception e) {
// 如果PKCS#8失败尝试PKCS#1格式
return parsePKCS1PublicKey(keyBytes);
}
} catch (Exception e) {
return null;
}
}
/**
* 解析PKCS#1格式公钥
*/
private static PublicKey parsePKCS1PublicKey(byte[] keyBytes) {
try {
// PKCS#1 RSA公钥转换为PKCS#8格式
byte[] pkcs8Prefix = new byte[] {
0x30, (byte) 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, (byte) 0x86,
0x48, (byte) 0x86, (byte) 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03,
(byte) 0x82, 0x01, 0x0f, 0x00
};
byte[] pkcs8Key = new byte[pkcs8Prefix.length + keyBytes.length];
System.arraycopy(pkcs8Prefix, 0, pkcs8Key, 0, pkcs8Prefix.length);
System.arraycopy(keyBytes, 0, pkcs8Key, pkcs8Prefix.length, keyBytes.length);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(pkcs8Key);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(keySpec);
} catch (Exception e) {
return null;
}
}
/**
* 针对 /zxjp/open/v1/wallet/** 接口的专用验签方法
* @param params 包含签名参数的Map
* @param privateKeyPEM 用于生成签名的私钥
* @return 验签是否通过
*/
public static boolean verifyWalletSign(Map<String, Object> params, String privateKeyPEM) {
try {
// 1. 获取请求中的签名
String requestSign = String.valueOf(params.get("sign"));
if (requestSign == null || requestSign.isEmpty()) {
log.warn("请求中缺少签名参数");
return false;
}
// 2. 移除签名参数,生成待签名字符串
Map<String, Object> paramsToSign = new HashMap<>(params);
paramsToSign.remove("sign");
// 3. 使用相同的私钥生成签名
String generatedSign = generateSign(paramsToSign, privateKeyPEM);
// 4. 对比签名
boolean isVerified = Objects.equals(requestSign, generatedSign);
if (!isVerified) {
log.warn("签名验证失败,请求签名: {}, 生成签名: {}", requestSign, generatedSign);
}
return isVerified;
} catch (Exception e) {
log.error("验签过程中发生异常", e);
return false;
}
}
}

View File

@@ -62,6 +62,8 @@ public class SignatureUtils {
.add(timeStamp)
.add(random)
.toString();
log.info("plainText:{}",plainText);
// 签名算法
Signature signature = Signature.getInstance("SHA256WithRSA");

View File

@@ -0,0 +1,34 @@
package com.cool.store.utils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* <p>
* Spring上下文工具
* </p>
*
* @author wangff
* @since 2025/9/4
*/
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtil.applicationContext = applicationContext;
}
/**
* 获取bean
* @param name beanName
* @param clazz bean类型
* @return bean
*/
public static <T> T getBean(String name, Class<T> clazz) {
return applicationContext.getBean(name, clazz);
}
}

View File

@@ -0,0 +1,55 @@
package com.cool.store.utils;
import com.cool.store.constants.CommonConstants;
import org.slf4j.MDC;
import java.util.Map;
import java.util.concurrent.Callable;
/**
* 线程MDC包装类
*
* @author hetiantian
* @version 1.0
* @Date 2020/03/18 15:18
*/
public class ThreadMdcUtil {
public static void setTraceIdIfAbsent() {
MDCUtils.putIfAbsent(CommonConstants.REQUEST_ID, UUIDUtils.get32UUID());
}
public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
return () -> {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try {
return callable.call();
} finally {
MDC.clear();
}
};
}
public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
return new Runnable() {
@Override
public void run() {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try {
runnable.run();
} finally {
MDC.clear();
}
}
};
}
}