feat:外部接口接入

This commit is contained in:
苏竹红
2025-04-01 21:36:00 +08:00
parent f6fc1c133a
commit a54644b8bd
37 changed files with 1669 additions and 2 deletions

View File

@@ -236,7 +236,9 @@ public enum ErrorCodeEnum {
UPDATE_INVESTMENT_MANAGER_FAIL(131005,"当前用户已经为该门店招商经理",null), UPDATE_INVESTMENT_MANAGER_FAIL(131005,"当前用户已经为该门店招商经理",null),
CONFIRM_THE_APPROVER(131006,"您提交的铺位暂时找不到选址审批人,请联系系统管理员配置选址审批权限后再提交铺位审批",null), CONFIRM_THE_APPROVER(131006,"您提交的铺位暂时找不到选址审批人,请联系系统管理员配置选址审批权限后再提交铺位审批",null),
TALLY_BOOK_NOT_EXIST(180001, "记账本数据不存在", null) TALLY_BOOK_NOT_EXIST(180001, "记账本数据不存在", null),
THIRD_API_ERROR(151001,"第三方服务异常->{0}",null),
; ;

View File

@@ -9,6 +9,8 @@ import java.text.DateFormat;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.*; import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjusters;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
@@ -32,6 +34,13 @@ public class CoolDateUtils {
public static final String DATE_FORMAT_SEC_5 = "yyyy.MM.dd HH:mm"; public static final String DATE_FORMAT_SEC_5 = "yyyy.MM.dd HH:mm";
public static final String DATE_FORMAT_SEC_6 = "yyyy.MM.dd"; 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_7 = "yyyy/MM/dd HH:mm";
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter getDateFormatter(String format){
return DateTimeFormatter.ofPattern(format);
}
/** /**
* 几天后的当前 * 几天后的当前
* @param d * @param d
@@ -128,4 +137,26 @@ public class CoolDateUtils {
} }
/**
* 获取当前日期字符串 (yyyy-MM-dd)
*/
public static String getCurrentDate() {
return LocalDate.now().format(getDateFormatter(DATE_FORMAT_DAY));
}
/**
* 获取上月同一天的日期字符串 (yyyy-MM-dd)
* 如果上个月没有同一天(如当前是3月31日但2月没有31日),则返回上个月的最后一天
*/
public static String getSameDayLastMonth() {
LocalDate today = LocalDate.now();
LocalDate lastMonthSameDay = today.minusMonths(1);
// 处理跨月情况如3月31日2月没有31日
if (lastMonthSameDay.getDayOfMonth() != today.getDayOfMonth()) {
lastMonthSameDay = lastMonthSameDay.with(TemporalAdjusters.lastDayOfMonth());
}
return lastMonthSameDay.format(getDateFormatter(DATE_FORMAT_DAY));
}
} }

View File

@@ -0,0 +1,44 @@
package com.cool.store.utils;
import java.util.BitSet;
/**
* @Author suzhuhong
* @Date 2025/4/1 20:39
* @Version 1.0
*/
public class GeoHashUtils {
private static final String BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz";
private static final int[] BITS = {16, 8, 4, 2, 1};
public static String encode(double lat, double lon, int precision) {
BitSet latBits = getBits(lat, -90, 90);
BitSet lonBits = getBits(lon, -180, 180);
StringBuilder hash = new StringBuilder();
for (int i = 0; i < precision; i++) {
int index = 0;
for (int j = 0; j < 5; j++) {
boolean lonBit = lonBits.get(i * 5 + j);
boolean latBit = latBits.get(i * 5 + j);
index = (index << 1) | (lonBit ? 1 : 0);
index = (index << 1) | (latBit ? 1 : 0);
}
hash.append(BASE32.charAt(index));
}
return hash.toString();
}
private static BitSet getBits(double val, double min, double max) {
BitSet bits = new BitSet();
for (int i = 0; i < 30; i++) {
double mid = (min + max) / 2;
if (val >= mid) {
bits.set(i);
min = mid;
} else {
max = mid;
}
}
return bits;
}
}

View File

@@ -0,0 +1,24 @@
package com.cool.store.utils;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.util.Map;
/**
* @Author suzhuhong
* @Date 2025/3/26 19:39
* @Version 1.0
*/
public class JsonUtils {
private static final Gson gson = new Gson();
public static String toJson(Object obj) {
return gson.toJson(obj);
}
public static Map<String, Object> parseJsonToMap(String json) {
return gson.fromJson(json, new TypeToken<Map<String, Object>>() {}.getType());
}
}

View File

@@ -0,0 +1,73 @@
package com.cool.store.utils;
import lombok.extern.slf4j.Slf4j;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.*;
/**
* @Author suzhuhong
* @Date 2025/3/26 19:32
* @Version 1.0
*/
@Slf4j
public class SignatureUtils {
/**
* 生成 HmacSHA256 签名
* @param data 待签名字符串
* @param secret 密钥
* @return 签名Hex小写
*/
public static String hmacSha256(String data, String secret) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKey);
byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hash).toLowerCase();
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("HmacSHA256签名失败", e);
}
}
/**
* 生成待签名字符串(参数按字母排序 + appkey + timestamp
* @param params 请求参数Map需提前过滤空值
* @param appKey 应用Key
* @param timestamp 时间戳(毫秒)
*/
public static String buildSignString(Map<String, Object> params, String appKey, long timestamp) {
TreeMap<String, Object> sortedParams = new TreeMap<>(params);
StringBuilder sb = new StringBuilder();
// 拼接排序后的参数
sortedParams.forEach((key, value) ->{
if (Objects.isNull(value)||(value instanceof Double &&((Double) value).intValue()==0)){
log.info("0或者空值不参与签名");
}else {
if (value instanceof Double){
sb.append(key).append("=").append(((Double) value).intValue()).append("&");
}else {
sb.append(key).append("=").append(value).append("&");
}
}
});
sb.append("appkey=").append(appKey).append("&timestamp=").append(timestamp);
return sb.toString();
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}

View File

@@ -0,0 +1,16 @@
package com.cool.store.request.bigdata;
import lombok.Data;
/**
* @Author suzhuhong
* @Date 2025/3/27 17:49
* @Version 1.0
*/
@Data
public class ProfitDataRequest {
private String store_code;
private String bill_date;
}

View File

@@ -0,0 +1,31 @@
package com.cool.store.request.oppty;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @Author suzhuhong
* @Date 2025/4/1 20:30
* @Version 1.0
*/
@Data
public class BerthOperationRequest {
@ApiModelProperty(" 操作类型: 1(新增), 2(更新), 3(删除)")
private String opType;
@ApiModelProperty("机会点编号")
private String code;
@ApiModelProperty("关注人用户编号")
private String userId;
@ApiModelProperty("关注用户手机号")
private String mobile;
@ApiModelProperty("关注人用户名")
private String userName;
@ApiModelProperty("铺位ID")
private Integer berthId;
@ApiModelProperty("铺位名称")
private String name;
@ApiModelProperty("铺位地址")
private String address;
@ApiModelProperty("铺位经纬度")
private String location;
}

View File

@@ -0,0 +1,19 @@
package com.cool.store.request.oppty;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @Author suzhuhong
* @Date 2025/4/1 16:15
* @Version 1.0
*/
@Data
public class CityRequest {
@ApiModelProperty("父ID")
private Integer pid;
}

View File

@@ -0,0 +1,23 @@
package com.cool.store.request.oppty;
import lombok.Data;
/**
* @Author suzhuhong
* @Date 2025/3/26 18:50
* @Version 1.0
*/
@Data
public class FollowRequest {
private Integer opType;
private String code;
private String userId;
private String mobile;
private String userName;
}

View File

@@ -0,0 +1,20 @@
package com.cool.store.request.oppty;
import lombok.Data;
/**
* @Author suzhuhong
* @Date 2025/4/1 20:05
* @Version 1.0
*/
@Data
public class InspectionRequest {
private String code;
private String userId;
private String mobile;
private String userName;
}

View File

@@ -0,0 +1,27 @@
package com.cool.store.request.oppty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* @Author suzhuhong
* @Date 2025/3/26 18:47
* @Version 1.0
*/
@Data
public class OpportunityBaseRequest {
@NotNull(message = "notice不能为空")
private Integer notice;
@NotNull(message = "timestamp不能为空")
private Long timestamp;
@NotBlank(message = "key不能为空")
private String key;
private String sign;
}

View File

@@ -0,0 +1,15 @@
package com.cool.store.request.oppty;
import lombok.Data;
/**
* @Author suzhuhong
* @Date 2025/3/26 17:23
* @Version 1.0
*/
@Data
public class OpportunityDetailRequest{
private String code;
}

View File

@@ -0,0 +1,108 @@
package com.cool.store.request.oppty;
import lombok.Data;
/**
* @Author suzhuhong
* @Date 2025/3/26 14:38
* @Version 1.0
*/
@Data
public class OpportunityListRequest{
private String latLng;
private String code;
private Integer province;
private Integer city;
private Integer district;
private String userId;
private Integer orderType = 1;
private Integer inspectionStatus ;
private Integer pageNum = 1;
private Integer pageSize = 20;
public String getLatLng() {
return latLng;
}
public void setLatLng(String latLng) {
this.latLng = latLng;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public Integer getProvince() {
return province;
}
public void setProvince(Integer province) {
this.province = province;
}
public Integer getCity() {
return city;
}
public void setCity(Integer city) {
this.city = city;
}
public Integer getDistrict() {
return district;
}
public void setDistrict(Integer district) {
this.district = district;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public Integer getOrderType() {
return orderType;
}
public void setOrderType(Integer orderType) {
this.orderType = orderType;
}
public Integer getInspectionStatus() {
return inspectionStatus;
}
public void setInspectionStatus(Integer inspectionStatus) {
this.inspectionStatus = inspectionStatus;
}
public Integer getPageNum() {
return pageNum;
}
public void setPageNum(Integer pageNum) {
this.pageNum = pageNum;
}
public Integer getPageSize() {
return pageSize;
}
public void setPageSize(Integer pageSize) {
this.pageSize = pageSize;
}
}

View File

@@ -0,0 +1,57 @@
package com.cool.store.request.oppty;
import lombok.Data;
/**
* @Author suzhuhong
* @Date 2025/4/1 20:31
* @Version 1.0
*/
@Data
public class StoreOperationRequest {
/**
* 操作类型1-新开门店2-更新门店3-闭店
*/
private String opType;
/**
* 机会点编号
*/
private String code;
/**
* 门店ID
*/
private Integer storeId;
/**
* 门店编号
*/
private String storeSn;
/**
* 门店模式1-加盟2-强管
*/
private Integer storeModel;
/**
* 门店品牌(默认值:正新鸡排)
*/
private String storeBrand = "正新鸡排";
/**
* 门店名称
*/
private String storeName;
/**
* 门店地址
*/
private String address;
/**
* 门店经纬度geohash
*/
private String location;
}

View File

@@ -0,0 +1,22 @@
package com.cool.store.request.oppty;
import lombok.Data;
/**
* @Author suzhuhong
* @Date 2025/3/26 18:53
* @Version 1.0
*/
@Data
public class UserRequest {
private String userId;
private String mobile;
private String userName;
}

View File

@@ -0,0 +1,19 @@
package com.cool.store.response.bigdata;
import lombok.Data;
/**
* @Author suzhuhong
* @Date 2025/4/1 13:42
* @Version 1.0
*/
@Data
public class ActDataResponse {
private String act_total_amt;
private String Store_code;
private String act_amt;
}

View File

@@ -0,0 +1,15 @@
package com.cool.store.response.bigdata;
import lombok.Data;
/**
* @Author suzhuhong
* @Date 2025/4/1 10:49
* @Version 1.0
*/
@Data
public class ApiResponse<T> {
private String msg;
private int code;
private T data;
}

View File

@@ -0,0 +1,16 @@
package com.cool.store.response.bigdata;
import lombok.Data;
/**
* @Author suzhuhong
* @Date 2025/3/27 17:46
* @Version 1.0
*/
@Data
public class ProfitDataResponse {
private String store_code;
private Double profit_total_amt;
private Double profit_amt;
private Double profit_rate;
}

View File

@@ -0,0 +1,16 @@
package com.cool.store.response.bigdata;
import lombok.Data;
/**
* @Author suzhuhong
* @Date 2025/4/1 13:25
* @Version 1.0
*/
@Data
public class ProfitRateResponse {
private String profit_amt ;
private String store_code ;
private String profit_rate;
private String bill_date;
}

View File

@@ -0,0 +1,31 @@
package com.cool.store.response.oppty;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
/**
* @Author suzhuhong
* @Date 2025/3/26 14:33
* @Version 1.0
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class BerthInfoResponse {
/**
* 铺位ID
*/
private Integer berthId;
/**
* 铺位名称
*/
private String name;
/**
* 铺位地址
*/
private String address;
/**
* 铺位经纬度
*/
private String geohash;
}

View File

@@ -0,0 +1,34 @@
package com.cool.store.response.oppty;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
/**
* @Author suzhuhong
* @Date 2025/3/26 14:29
* @Version 1.0
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class BrandResponse {
/**
* 品牌编号
*/
private String brandCode;
/**
* 品牌名称
*/
private String brandName;
/**
* 品牌Logo base64编码
*/
private String brandLogo;
/**
* 该品牌门店所在位置
*/
private String geohash;
/**
* 品牌数量
*/
private Integer count;
}

View File

@@ -0,0 +1,20 @@
package com.cool.store.response.oppty;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
/**
* @Author suzhuhong
* @Date 2025/4/1 16:16
* @Version 1.0
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class CityResponse {
private Integer code;
private Integer name;
private Integer pid;
private String location;
}

View File

@@ -0,0 +1,18 @@
package com.cool.store.response.oppty;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
/**
* @Author suzhuhong
* @Date 2025/4/1 10:49
* @Version 1.0
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class OpportunityApiResponse<T> {
private String msg;
private int code;
private Boolean success;
private T data;
}

View File

@@ -0,0 +1,33 @@
package com.cool.store.response.oppty;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import org.apache.catalina.User;
import java.util.List;
/**
* @Author suzhuhong
* @Date 2025/3/26 17:24
* @Version 1.0
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class OpportunityDetailResponse {
private String code; // 机会点编号
private Integer level; // 等级
private String name; // 名称
private String address; // 地址
private String geohash; // GeoHash
private Integer province; // 省份编码
private Integer city; // 城市编码
private Integer district; // 区县编码
private UserResponse bdUser; // 拓展人信息
private List<UserResponse> atUsers; // 关注人列表
private List<BrandResponse> brandStats; // 周边品牌
private StoreInfoResponse storeInfo; // 门店信息
private List<BerthInfoResponse> berthInfo;
}

View File

@@ -0,0 +1,24 @@
package com.cool.store.response.oppty;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.util.List;
/**
* @Author suzhuhong
* @Date 2025/4/1 18:40
* @Version 1.0
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class OpportunityInfoPageResponse {
private PageResponse page;
private List<OpportunityInfoResponse> pageData;
}

View File

@@ -0,0 +1,26 @@
package com.cool.store.response.oppty;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.util.List;
/**
* @Author suzhuhong
* @Date 2025/3/26 14:40
* @Version 1.0
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class OpportunityInfoResponse {
private String code;
private Integer level;
private String name;
private String address;
private String geohash;
private Integer province;
private Integer city;
private Integer district;
private Integer berthNum;
private List<UserResponse> atUser;
}

View File

@@ -0,0 +1,22 @@
package com.cool.store.response.oppty;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
/**
* @Author suzhuhong
* @Date 2025/3/26 14:27
* @Version 1.0
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class PageResponse {
private Integer currentPage;
private Integer pageSize;
private Integer total;
private Integer count;
private Boolean first;
private Boolean last;
}

View File

@@ -0,0 +1,42 @@
package com.cool.store.response.oppty;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
/**
* @Author suzhuhong
* @Date 2025/3/26 14:31
* @Version 1.0
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class StoreInfoResponse {
/**
* 门店ID
*/
private Integer storeId;
/**
* 门店编号
*/
private String storeSn;
/**
* 门店模式:1:加盟 2强管
*/
private Integer storeModel;
/**
* 门店品牌 预留,不传默认正新鸡排
*/
private String storeBrand;
/**
* 门店名称
*/
private String storeName;
/**
* 门店地址
*/
private String address;
/**
* 门店经纬度
*/
private String geohash;
}

View File

@@ -0,0 +1,22 @@
package com.cool.store.response.oppty;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
/**
* @Author suzhuhong
* @Date 2025/3/26 14:28
* @Version 1.0
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class UserResponse {
private String userId;
private String mobile;
private String userName;
private String atTime;
private String inspectionStatus;
private String inspectionTime;
}

View File

@@ -0,0 +1,20 @@
package com.cool.store.config.rest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author suzhuhong
* @Date 2025/4/1 10:54
* @Version 1.0
*/
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
return mapper;
}
}

View File

@@ -0,0 +1,110 @@
package com.cool.store.config.rest;
import okhttp3.OkHttpClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.net.ssl.*;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @Author suzhuhong
* @Date 2025/4/1 9:56
* @Version 1.0
*/
@Configuration
public class OkHttpConfig {
private static final String TRUSTED_DOMAIN = "zhengxinfood.com";
private static final int CONNECT_TIMEOUT = 30;
private static final int READ_TIMEOUT = 30;
private static final int WRITE_TIMEOUT = 30;
@Bean
public OkHttpClient okHttpClient() {
return createCustomTrustOkHttpClient(TRUSTED_DOMAIN);
}
private OkHttpClient createCustomTrustOkHttpClient(String trustedDomain) {
try {
// 获取系统默认TrustManager
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
TrustManager[] defaultTrustManagers = trustManagerFactory.getTrustManagers();
X509TrustManager defaultTrustManager = (X509TrustManager) defaultTrustManagers[0];
// 创建自定义TrustManager
X509TrustManager customTrustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
defaultTrustManager.checkClientTrusted(chain, authType);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
try {
defaultTrustManager.checkServerTrusted(chain, authType);
} catch (CertificateException e) {
if (!isCertificateForDomain(chain[0], trustedDomain)) {
throw new CertificateException("证书不匹配目标域名: " + trustedDomain);
}
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return defaultTrustManager.getAcceptedIssuers();
}
};
// 配置SSLContext
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{customTrustManager}, new SecureRandom());
return new OkHttpClient.Builder()
.sslSocketFactory(sslContext.getSocketFactory(), customTrustManager)
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
.build();
} catch (Exception e) {
throw new RuntimeException("创建OkHttpClient失败", e);
}
}
private boolean isCertificateForDomain(X509Certificate certificate, String domain) {
try {
// 检查CN (Common Name)
String principal = certificate.getSubjectX500Principal().getName();
if (principal.contains("CN=" + domain) || principal.contains("CN=*." + domain)) {
return true;
}
// 检查SAN (Subject Alternative Names)
Collection<List<?>> sans = certificate.getSubjectAlternativeNames();
if (sans != null) {
for (List<?> san : sans) {
// 类型2是DNS名称
if (san.get(0).equals(2)) {
String sanValue = (String) san.get(1);
if (sanValue.equals(domain) || sanValue.endsWith("." + domain)) {
return true;
}
}
}
}
return false;
} catch (Exception e) {
return false;
}
}
}

View File

@@ -0,0 +1,57 @@
package com.cool.store.service;
import com.cool.store.request.bigdata.ProfitDataRequest;
import com.cool.store.request.oppty.CityRequest;
import com.cool.store.response.bigdata.ActDataResponse;
import com.cool.store.response.bigdata.ProfitDataResponse;
import com.cool.store.response.bigdata.ProfitRateResponse;
import com.cool.store.response.oppty.CityResponse;
import java.util.List;
/**
* @Author suzhuhong
* @Date 2025/3/27 17:37
* @Version 1.0
*/
public interface ThirdBigDataService {
/**
* 查询昨日毛利
* @param request
* @return
*/
ProfitDataResponse getProfitData(ProfitDataRequest request);
/**
* 查询当月毛利
* @param requestBody
* @return
*/
ProfitDataResponse getCurMonthProfitData(ProfitDataRequest requestBody);
/**
* 上月毛利
* @param requestBody
* @return
*/
ProfitDataResponse getPreMonthProfitData(ProfitDataRequest requestBody);
/**
*getMonthProfitTrend
* @param requestBody
* @return
*/
List<ProfitRateResponse> getMonthProfitTrend(ProfitDataRequest requestBody);
/**
* 实收数据
* @param requestBody
* @return
*/
ActDataResponse getActData(ProfitDataRequest requestBody);
}

View File

@@ -0,0 +1,70 @@
package com.cool.store.service;
import com.cool.store.request.oppty.*;
import com.cool.store.response.oppty.CityResponse;
import com.cool.store.response.oppty.OpportunityDetailResponse;
import com.cool.store.response.oppty.OpportunityInfoPageResponse;
import java.util.List;
/**
* @Author suzhuhong
* @Date 2025/3/26 14:50
* @Version 1.0
* 正新 第三方机会点服务
*/
public interface ThirdOpportunityService {
/**
* 机会点列表 我的机会点列表
* @param request
* @return
*/
OpportunityInfoPageResponse listOpportunities(OpportunityListRequest request);
/**
* 查询机会点详情
* @param opportunityDetailRequest
* @return
*/
OpportunityDetailResponse getOpportunitiesDetail(OpportunityDetailRequest opportunityDetailRequest);
/**
* 关注机会点
* @param request
* @return
*/
String followOpportunity(FollowRequest request);
/**
* 考察机会点
* @param request
* @return
*/
String inspectionOpportunity(InspectionRequest request);
/**
* 铺位关注机会点
* @param requestBody
* @return
*/
String berthOperation(BerthOperationRequest requestBody);
/**
* 门店关注机会点
* @param requestBody
* @return
*/
String storeOperation(StoreOperationRequest requestBody);
/**
* 省市区列表
* @param request
* @return
*/
List<CityResponse> cityList(CityRequest request);
}

View File

@@ -0,0 +1,209 @@
package com.cool.store.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.cool.store.enums.ErrorCodeEnum;
import com.cool.store.exception.ServiceException;
import com.cool.store.request.bigdata.ProfitDataRequest;
import com.cool.store.request.oppty.CityRequest;
import com.cool.store.response.bigdata.ActDataResponse;
import com.cool.store.response.bigdata.ApiResponse;
import com.cool.store.response.bigdata.ProfitDataResponse;
import com.cool.store.response.bigdata.ProfitRateResponse;
import com.cool.store.response.oppty.CityResponse;
import com.cool.store.response.oppty.OpportunityApiResponse;
import com.cool.store.response.oppty.OpportunityDetailResponse;
import com.cool.store.service.ThirdBigDataService;
import com.cool.store.utils.JsonUtils;
import com.cool.store.utils.SignatureUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* @Author suzhuhong
* @Date 2025/3/27 17:37
* @Version 1.0
*/
@Service
@Slf4j
public class ThirdBigDataServiceImpl implements ThirdBigDataService {
@Value("${zx.big.data.url}")
private String apiUrl;
@Value("${zx.big.data.appKey}")
private String appKey;
@Value("${zx.big.data.appSecret}")
private String appSecret;
@Resource
ObjectMapper objectMapper;
@Resource
OkHttpClient okHttpClient;
@Override
public ProfitDataResponse getProfitData(ProfitDataRequest requestBody) {
// 1. 发送POST请求
String url = apiUrl + "api/web/v1/act/yesd_profit";
return executeApiCall(url, requestBody, ProfitDataResponse.class);
}
@Override
public ProfitDataResponse getCurMonthProfitData(ProfitDataRequest requestBody) {
// 1. 发送POST请求
String url = apiUrl + "api/web/v1/act/curr_month_profit";
return executeApiCall(url, requestBody, ProfitDataResponse.class);
}
@Override
public ProfitDataResponse getPreMonthProfitData(ProfitDataRequest requestBody) {
// 1. 发送POST请求
String url = apiUrl + "api/web/v1/act/prev_month_profit";
return executeApiCall(url, requestBody, ProfitDataResponse.class);
}
@Override
public List<ProfitRateResponse> getMonthProfitTrend(ProfitDataRequest requestBody) {
// 1. 发送POST请求
String url = apiUrl + "api/web/v1/act/month_profit_trend";
return executeApiCall(url, requestBody, List.class);
}
@Override
public ActDataResponse getActData(ProfitDataRequest requestBody) {
// 1. 发送POST请求
String url = apiUrl + "api/web/v1/income/store_act";
return executeApiCall(url, requestBody, ActDataResponse.class);
}
private <T> T executeApiCall(String url, Object requestBody, Class<T> responseType) {
// 1. 打印请求前日志
logRequest(url, requestBody);
try {
Request request = buildRequest(requestBody, url);
try (Response response = okHttpClient.newCall(request).execute()) {
// 2. 获取原始响应内容
String responseBody = response.body().string();
// 3. 打印响应日志
logResponse(url, response.code(), responseBody);
if (!response.isSuccessful()) {
throw new ServiceException(ErrorCodeEnum.THIRD_API_ERROR,
"HTTP请求失败状态码: " + response.code());
}
// 4. 解析响应
JavaType javaType = objectMapper.getTypeFactory()
.constructParametricType(OpportunityApiResponse.class, responseType);
OpportunityApiResponse<T> apiResponse = objectMapper.readValue(responseBody, javaType);
if (apiResponse.getCode() != 0) {
throw new ServiceException(ErrorCodeEnum.THIRD_API_ERROR,
"业务逻辑错误: " + apiResponse.getMsg());
}
return apiResponse.getData();
}
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
log.error("API调用异常 - URL: {}, 错误: {}", url, e.getMessage(), e);
throw new ServiceException(ErrorCodeEnum.THIRD_API_ERROR, "接口调用异常: " + e.getMessage());
}
}
private Request buildRequest(Object requestBody, String url) {
Map<String, Object> params = JsonUtils.parseJsonToMap(JSONObject.toJSONString(requestBody));
long timestamp = System.currentTimeMillis();
String signString = SignatureUtils.buildSignString(params, appKey, timestamp);
String sign = SignatureUtils.hmacSha256(signString, appSecret);
log.debug("签名生成 - 原始字符串: {}, 签名结果: {}", signString, sign);
RequestBody body = RequestBody.create(okhttp3.MediaType.parse("application/json"),
JSONObject.toJSONString(requestBody)
);
return new Request.Builder()
.url(url)
.post(body)
.addHeader("Content-Type", "application/json")
.addHeader("Accept", "application/json")
.addHeader("sign", sign)
.addHeader("timestamp", String.valueOf(timestamp))
.addHeader("appkey", appKey)
.build();
}
/**
* 记录请求日志
*/
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

@@ -0,0 +1,217 @@
package com.cool.store.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.cool.store.enums.ErrorCodeEnum;
import com.cool.store.exception.ServiceException;
import com.cool.store.request.oppty.*;
import com.cool.store.response.oppty.*;
import com.cool.store.service.ThirdOpportunityService;
import com.cool.store.utils.JsonUtils;
import com.cool.store.utils.SignatureUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.util.List;
import java.util.Map;
/**
* @Author suzhuhong
* @Date 2025/3/26 15:20
* @Version 1.0
*/
@Service
@Slf4j
public class ThirdOpportunityServiceImpl implements ThirdOpportunityService {
@Value("${zx.opportunity.url}")
private String apiUrl;
@Value("${third.party.appKey}")
private String appKey;
@Value("${third.party.appSecret}")
private String appSecret;
@Resource
OkHttpClient okHttpClient;
@Resource
ObjectMapper objectMapper;
@Override
public OpportunityInfoPageResponse listOpportunities(OpportunityListRequest requestBody) {
// 1. 发送POST请求
String url = apiUrl+"open/oppty/v1/listOppty";
return executeApiCall(url, requestBody, OpportunityInfoPageResponse.class);
}
@Override
public OpportunityDetailResponse getOpportunitiesDetail(OpportunityDetailRequest requestBody) {
String url = apiUrl + "open/oppty/v1/opptyDetail";
return executeApiCall(url, requestBody, OpportunityDetailResponse.class);
}
@Override
public String followOpportunity(FollowRequest requestBody) {
String url = apiUrl+"open/oppty/v1/saveAtUser";
return executeApiCall(url, requestBody, String.class);
}
@Override
public String inspectionOpportunity(InspectionRequest requestBody) {
String url = apiUrl+"open/oppty/v1/inspection";
return executeApiCall(url, requestBody, String.class);
}
@Override
public String berthOperation(BerthOperationRequest requestBody) {
String url = apiUrl+"open/oppty/v1/addBerthInfo";
return executeApiCall(url, requestBody, String.class);
}
@Override
public String storeOperation(StoreOperationRequest requestBody) {
String url = apiUrl+"open/oppty/v1/addStore";
return executeApiCall(url, requestBody, String.class);
}
@Override
public List<CityResponse> cityList(CityRequest requestBody) {
// 1. 发送POST请求
String url = apiUrl+"open/oppty/v1/listCity";
return executeApiCall(url, requestBody, List.class);
}
private <T> T executeApiCall(String url, Object requestBody, Class<T> responseType) {
// 1. 打印请求前日志
logRequest(url, requestBody);
try {
Request request = buildRequest(requestBody, url);
try (Response response = okHttpClient.newCall(request).execute()) {
// 2. 获取原始响应内容
String responseBody = response.body().string();
// 3. 打印响应日志
logResponse(url, response.code(), responseBody);
if (!response.isSuccessful()) {
throw new ServiceException(ErrorCodeEnum.THIRD_API_ERROR,
"HTTP请求失败状态码: " + response.code());
}
// 4. 解析响应
JavaType javaType = objectMapper.getTypeFactory()
.constructParametricType(OpportunityApiResponse.class, responseType);
OpportunityApiResponse<T> apiResponse = objectMapper.readValue(responseBody, javaType);
if (apiResponse.getCode() != 200) {
throw new ServiceException(ErrorCodeEnum.THIRD_API_ERROR,
"业务逻辑错误: " + apiResponse.getMsg());
}
return apiResponse.getData();
}
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
log.error("API调用异常 - URL: {}, 错误: {}", url, e.getMessage(), e);
throw new ServiceException(ErrorCodeEnum.THIRD_API_ERROR, "接口调用异常: " + e.getMessage());
}
}
private Request buildRequest(Object requestBody, String url) {
Map<String, Object> params = JsonUtils.parseJsonToMap(JSONObject.toJSONString(requestBody));
long timestamp = System.currentTimeMillis() / 1000;
String signString = SignatureUtils.buildSignString(params, appKey, timestamp);
String sign = SignatureUtils.hmacSha256(signString, appSecret);
log.debug("签名生成 - 原始字符串: {}, 签名结果: {}", signString, sign);
RequestBody body = RequestBody.create(okhttp3.MediaType.parse("application/json"),
JSONObject.toJSONString(requestBody)
);
return new Request.Builder()
.url(url)
.post(body)
.addHeader("Content-Type", "application/json")
.addHeader("Accept", "application/json")
.addHeader("sign", sign)
.addHeader("timestamp", String.valueOf(timestamp))
.addHeader("appkey", appKey)
.build();
}
/**
* 记录请求日志
*/
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

@@ -0,0 +1,124 @@
package com.cool.store.controller.webc;
import com.cool.store.request.bigdata.ProfitDataRequest;
import com.cool.store.request.oppty.*;
import com.cool.store.response.ResponseResult;
import com.cool.store.response.bigdata.ActDataResponse;
import com.cool.store.response.bigdata.ProfitDataResponse;
import com.cool.store.response.bigdata.ProfitRateResponse;
import com.cool.store.response.oppty.CityResponse;
import com.cool.store.response.oppty.OpportunityDetailResponse;
import com.cool.store.response.oppty.OpportunityInfoPageResponse;
import com.cool.store.service.ThirdBigDataService;
import com.cool.store.service.ThirdOpportunityService;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.List;
/**
* @Author suzhuhong
* @Date 2025/4/1 20:49
* @Version 1.0
*/
@Slf4j
@RestController
@RequestMapping("/mini/third/api")
public class ThirdApiController {
@Resource
ThirdOpportunityService thirdOpportunityService;
@Resource
ThirdBigDataService thirdBigDataService;
@PostMapping("/list")
@ApiOperation("机会点列表/我的机会点")
public ResponseResult<OpportunityInfoPageResponse> listOpportunities(@Valid @RequestBody OpportunityListRequest request) {
return ResponseResult.success(thirdOpportunityService.listOpportunities(request));
}
@PostMapping("/getOpportunitiesDetail")
@ApiOperation("机会点详情")
public ResponseResult<OpportunityDetailResponse> getOpportunitiesDetail(@Valid @RequestBody OpportunityDetailRequest request) {
return ResponseResult.success(thirdOpportunityService.getOpportunitiesDetail(request));
}
@PostMapping("/followOpportunity")
@ApiOperation("关注/取消关注 机会点")
public ResponseResult<String> followOpportunity(@Valid @RequestBody FollowRequest request) {
return ResponseResult.success(thirdOpportunityService.followOpportunity(request));
}
@PostMapping("/inspectionOpportunity")
@ApiOperation("考察机会点")
public ResponseResult<String> inspectionOpportunity(@Valid @RequestBody InspectionRequest request) {
return ResponseResult.success(thirdOpportunityService.inspectionOpportunity(request));
}
@PostMapping("/berthOperation")
@ApiOperation("铺位关注机会点")
public ResponseResult<String> berthOperation(@Valid @RequestBody BerthOperationRequest request) {
return ResponseResult.success(thirdOpportunityService.berthOperation(request));
}
@PostMapping("/storeOperation")
@ApiOperation("门店关注机会点")
public ResponseResult<String> storeOperation(@Valid @RequestBody StoreOperationRequest request) {
return ResponseResult.success(thirdOpportunityService.storeOperation(request));
}
@PostMapping("/cityList")
@ApiOperation("省市区列表 查询省传0")
public ResponseResult<List<CityResponse>> listOpportunities(@Valid @RequestBody CityRequest request) {
// 调用第三方接口
return ResponseResult.success(thirdOpportunityService.cityList(request));
}
@PostMapping("/getProfitData")
@ApiOperation("昨天毛利")
public ResponseResult<ProfitDataResponse> listOpportunities(@Valid @RequestBody ProfitDataRequest request) {
// 调用第三方接口
return ResponseResult.success(thirdBigDataService.getProfitData(request));
}
@PostMapping("/getCurMonthProfitData")
@ApiOperation("本月毛利")
public ResponseResult<ProfitDataResponse> getCurMonthProfitData(@Valid @RequestBody ProfitDataRequest request) {
// 调用第三方接口
return ResponseResult.success(thirdBigDataService.getCurMonthProfitData(request));
}
@PostMapping("/getPreMonthProfitData")
@ApiOperation("上月毛利")
public ResponseResult<ProfitDataResponse> getPreMonthProfitData(@Valid @RequestBody ProfitDataRequest request) {
// 调用第三方接口
return ResponseResult.success(thirdBigDataService.getPreMonthProfitData(request));
}
@PostMapping("/getMonthProfitTrend")
@ApiOperation("毛利趋势")
public ResponseResult<List<ProfitRateResponse>> getMonthProfitTrend(@Valid @RequestBody ProfitDataRequest request) {
// 调用第三方接口
return ResponseResult.success(thirdBigDataService.getMonthProfitTrend(request));
}
@PostMapping("/getActData")
@ApiOperation("门店实收")
public ResponseResult<ActDataResponse> getActData(@Valid @RequestBody ProfitDataRequest request) {
// 调用第三方接口
return ResponseResult.success(thirdBigDataService.getActData(request));
}
}

View File

@@ -28,7 +28,7 @@ isv.domain=https://abstore-isv.coolstore.cn/isv
#rocketmq \u914D\u7F6E #rocketmq \u914D\u7F6E
rocketmq.accessKey=LTAI5tJbgtyoHUvofTaeP1RP rocketmq.accessKey=LTAI5tJbgtyoHUvofTaeP1RP
rocketmq.secretKey=myg755iCx0j4PyQkHMaUVeOr0bw1tA rocketmq.secretKey=myg755iCx0j4PyQkHMaUVeOr0bw1tA
rocketmq.nameSrvAdder=http://MQ_INST_1947409023213164_BX3sLZnA.cn-hangzhou.mq-internal.aliyuncs.com:8080 rocketmq.nameSrvAdder=http://MQ_INST_1947409023213164_BX3sLZnA.cn-hangzhou.mq.aliyuncs.com:80
rocketmq.topic=simple_message rocketmq.topic=simple_message
#oss配置 #oss配置
@@ -82,3 +82,13 @@ enterprise.dingCorpId=wpayJeDAAAhGIFgUJpJN-zg39JuNbYhg
##qywx.task.notice.url1=https://tstore-api.coolstore.cn/notice?corpId=%s&appType=%s&target=%s ##qywx.task.notice.url1=https://tstore-api.coolstore.cn/notice?corpId=%s&appType=%s&target=%s
qywx.task.notice.url2=https://tstore-h5.coolstore.cn/?corpId=%s&appType=%s#/notice?target=%s&noticeType=zx&corpId=%s&appType=%s&eid=%s qywx.task.notice.url2=https://tstore-h5.coolstore.cn/?corpId=%s&appType=%s#/notice?target=%s&noticeType=zx&corpId=%s&appType=%s&eid=%s
third.party.appKey=IGSAEQoakR2HEaYx
third.party.appSecret=aPsA99K1obFeFm3m
zx.opportunity.url=https://snp.wenmatech.com/
#大数据地址
zx.big.data.url=https://ds.zhengxinfood.com/
zx.big.data.appKey=ff203b5567744feaaae49fb86f58c5bf
zx.big.data.appSecret=35b8b9a400b4430fa022190be0913cd6