Commit b1b0453a by yxdubhe_bot

merge:feature-sharer-20250306:operator:wb.xiaoke@mesg.corp.netease.com:auto_bran…

merge:feature-sharer-20250306:operator:wb.xiaoke@mesg.corp.netease.com:auto_branch_merge_by_branchService_end
parents 91bbc8d2 d8d6c246
Pipeline #71932 passed with stages
in 1 minute 30 seconds
include:
- project: tech/ci-operator
file: /yanxuan-wx-store-sharer/gitlab-ci.yml
ref: master
\ No newline at end of file
variables:
SERVICE_CODE: 'yanxuan-wx-store-sharer'
ARTIFACT_NAME: '$SERVICE_CODE.zip'
stages:
# - inspection
# - unitTest
- package
- upload
- upload-bee
# # 前置检查
# include:
# - project: tech/ci-operator
# file: /submodels/precheck.gitlab-ci.yml
# ref: master
# # 代码审查
# inspection-job:
# stage: inspection
# script:
# - ci_tools inspection $SERVICE_CODE
# tags:
# - ci-backend
# # 单元测试
# unitTest-job:
# stage: unitTest
# script:
# - mvn clean org.jacoco:jacoco-maven-plugin:0.8.2:prepare-agent test org.jacoco:jacoco-maven-plugin:0.8.2:report -Dmaven.test.failure.ignore=true
# - ci_tools report_unit_test_result $TARGET_FILE_PATH/site/jacoco/jacoco.xml $SERVICE_CODE $CI_COMMIT_REF_NAME $CI_PIPELINE_ID
# tags:
# - ci-backend
# 编译打包
common_package:
stage: package
script:
- mvn clean package -Dmaven.test.skip=true -Dencoding=UTF-8
- mv build/${SERVICE_CODE}.zip $ARTIFACT_NAME
artifacts:
name: $SERVICE_CODE
paths:
- $ARTIFACT_NAME
- swagger/
expire_in: 1h
tags:
- ci-backend
only:
- master
- dev
- /^release.*$/
- /^feature.*$/
- /^hotfix.*$/
# 普通制品上传(制品标识为全环境通用)
test_upload:
stage: upload
script:
- version_tools time && CURRENT_TIMESTAMP=$(version_tools result)
- version_tools version && PROJECT_VERSION=$(version_tools result)
- ARTIFACT_VERSION="${PROJECT_VERSION}-${CI_COMMIT_REF_NAME##*/}-${CURRENT_TIMESTAMP}-${CI_PIPELINE_ID}"
- eval opera truck $OPERA_ARGS --env=test --artifactPath=$ARTIFACT_NAME --artifactVersion=$ARTIFACT_VERSION
tags:
- ci-backend
only:
- dev
- /^release.*$/
- /^feature.*$/
- /^hotfix.*$/
dependencies:
- common_package
online_upload:
stage: upload
script:
- version_tools time && CURRENT_TIMESTAMP=$(version_tools result)
- version_tools version && PROJECT_VERSION=$(version_tools result)
- ARTIFACT_VERSION="${PROJECT_VERSION}-${CI_COMMIT_REF_NAME##*/}-${CURRENT_TIMESTAMP}-${CI_PIPELINE_ID}"
- eval opera truck $OPERA_ARGS --env=online --artifactPath=$ARTIFACT_NAME --artifactVersion=$ARTIFACT_VERSION
tags:
- ci-backend
only:
- /^release.*$/
- /^hotfix.*$/
dependencies:
- common_package
common_upload:
stage: upload
script:
- version_tools time && CURRENT_TIMESTAMP=$(version_tools result)
- version_tools version && PROJECT_VERSION=$(version_tools result)
- ARTIFACT_VERSION="${PROJECT_VERSION}-${CI_COMMIT_REF_NAME##*/}-${CURRENT_TIMESTAMP}-${CI_PIPELINE_ID}"
- eval opera truck $OPERA_ARGS --env=all --artifactPath=$ARTIFACT_NAME --artifactVersion=$ARTIFACT_VERSION
tags:
- ci-backend
only:
- master
- /^release.*$/
- /^hotfix.*$/
dependencies:
- common_package
# 接口文件上传
common_upload_bee:
stage: upload-bee
script:
- cd swagger
- beeUpload $SERVICE_CODE $CI_BUILD_REF_NAME $GITLAB_USER_NAME $GITLAB_USER_EMAIL
tags:
- ci-backend
only:
- master
- dev
- /^release.*$/
- /^feature.*$/
dependencies:
- common_package
-- 推客信息表
CREATE TABLE `SHARER_INFO`
(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`open_id` varchar(64) NOT NULL DEFAULT '' COMMENT '用户开放ID',
`sharer_appid` varchar(64) NOT NULL DEFAULT '' COMMENT '推客应用ID',
`bind_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '绑定时间',
`commission_ratio` varchar(32) NOT NULL DEFAULT '' COMMENT '佣金比例',
`commission_type` varchar(32) NOT NULL DEFAULT '' COMMENT '佣金类型',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_open_id` (`open_id`),
KEY `idx_sharer_appid` (`sharer_appid`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COMMENT='推客信息表';
-- 推客商品分佣表
CREATE TABLE `SHARER_PRODUCT_COMMISSION`
(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`open_id` varchar(64) NOT NULL DEFAULT '' COMMENT '用户开放ID',
`sharer_appid` varchar(64) NOT NULL DEFAULT '' COMMENT '推客应用ID',
`product_id` varchar(64) NOT NULL DEFAULT '' COMMENT '商品ID',
`commission_ratio` varchar(32) NOT NULL DEFAULT '' COMMENT '佣金比例',
`commission_type` varchar(32) NOT NULL DEFAULT '' COMMENT '佣金类型',
`unset` int(11) NOT NULL DEFAULT '1' COMMENT '是否未配置,1:未配置,取默认佣金比例,0:已配置',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_open_id` (`open_id`),
KEY `idx_sharer_appid` (`sharer_appid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推客商品分佣表';
-- 推客商品分佣配置操作记录表
CREATE TABLE `SHARER_PRODUCT_COMMISSION_RECORD`
(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`open_id` varchar(64) NOT NULL DEFAULT '' COMMENT '用户开放ID',
`sharer_appid` varchar(64) NOT NULL DEFAULT '' COMMENT '推客应用ID',
`product_id` varchar(64) NOT NULL DEFAULT '' COMMENT '商品ID',
`opt_type` varchar(32) NOT NULL DEFAULT '' COMMENT '操作类型',
`opt_info` varchar(256) NOT NULL DEFAULT '' COMMENT '操作信息,记录操作前比例 & 操作后比例',
`opt_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
`opt_status` varchar(1) NOT NULL DEFAULT '1' COMMENT '状态,1 操作成功 2 操作失败',
`ext_info` varchar(1024) NOT NULL DEFAULT '' COMMENT '拓展信息',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_open_id` (`open_id`),
KEY `idx_sharer_appid` (`sharer_appid`),
KEY `idx_product_id` (`product_id`)
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8mb4 COMMENT='推客商品分佣配置操作记录表';
\ No newline at end of file
......@@ -2,17 +2,21 @@ package com.netease.yanxuan.wx.store.sharer.assembly;
import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
import com.netease.yanxuan.missa.client.annotation.EnableMissaClients;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableAsync;
/**
* 项目启动类
*/
@EnableAsync
@ComponentScan(basePackages = "com.netease.yanxuan.wx.store.sharer")
@EnableApolloConfig
@EnableMissaClients(basePackages = "com.netease.yanxuan.wx.store.sharer")
@MapperScan(basePackages = "com.netease.yanxuan.wx.store.sharer.dal")
@SpringBootApplication
public class Application extends SpringBootServletInitializer {
......
package com.netease.yanxuan.wx.store.sharer.biz.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 线程池配置类
*
* @author fanjiaxin
* @date 2024/6/12 15:55
*/
@Slf4j
@Configuration
@EnableAsync
public class ExecutorConfig {
/**
* 异步线程池
*/
@Bean(name = "asyncProductListServiceExecutor")
public Executor asyncProductListServiceExecutor() {
log.info("start asyncProductListServiceExecutor...");
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//配置核心线程数
executor.setCorePoolSize(10);
//配置最大线程数
executor.setMaxPoolSize(20);
//配置队列大小
executor.setQueueCapacity(500);
//线程空闲时间
executor.setKeepAliveSeconds(30);
//配置线程池中的线程的名称前缀
executor.setThreadNamePrefix("async-product-list-service-executor-");
/**
* rejection-policy:当pool已经达到max size的时候,如何处理新任务
* CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
*/
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//等待所有任务完成再关闭
executor.setWaitForTasksToCompleteOnShutdown(true);
//最长等待时间,秒
executor.setAwaitTerminationSeconds(60);
//初始化执行器
executor.initialize();
return executor;
}
}
package com.netease.yanxuan.wx.store.sharer.biz.config;
import com.netease.yanxuan.wx.store.sharer.biz.interceptor.AuthInterceptor;
import com.netease.yanxuan.wx.store.sharer.biz.interceptor.UserInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.Collections;
/**
* @Description 配置
* @Author fanjiaxin
* @Date 2025/3/10 10:54
*/
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final AuthInterceptor authInterceptor;
private final UserInterceptor userInterceptor;
private static final String[] EXCLUDE_URLS = {"/i/health",
"/sharer/open/**", "/sharer/test/**",
"/sharer/user/login", "/sharer/product/page/list"};
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(getExcludePathPatterns());
registry.addInterceptor(userInterceptor)
.addPathPatterns("/**");
}
/**
* 白名单
*/
private ArrayList<String> getExcludePathPatterns() {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, EXCLUDE_URLS);
return list;
}
}
package com.netease.yanxuan.wx.store.sharer.biz.core;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
/**
* @Description 授权上下文
* @Author fanjiaxin
* @Date 2025/3/10 11:10
*/
public class LoginUserContextHolder {
public static ThreadLocal<LoginUserInfo> HOLDER = new ThreadLocal<>();
public static void set(LoginUserInfo loginUserInfo) {
HOLDER.set(loginUserInfo);
}
public static LoginUserInfo get() {
return HOLDER.get();
}
public static void remove() {
HOLDER.remove();
}
/**
* 生成用户Token
*/
public static String generateToken(String code, String openId) {
return Jwts.builder()
.claim("code", code)
.signWith(SignatureAlgorithm.HS256, openId)
.compact();
}
}
package com.netease.yanxuan.wx.store.sharer.biz.core;
import com.alibaba.fastjson.JSON;
import com.netease.yanxuan.wx.store.sharer.common.constant.CoreConstant;
import com.netease.yanxuan.wx.store.sharer.common.handler.RedisClient;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
/**
* @Description 微信认证信息上下文持有器
* @Author fanjiaxin
* @Date 2025/2/24 11:53
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class LoginUserHelper {
private final RedisClient redisClient;
/**
* 获取用户认证信息
*/
public LoginUserInfo getLoginUserInfo(String token) {
String loginUserInfoJson = redisClient.getStr(CoreConstant.REDIS_TOKEN_USER_KEY + token);
if (StringUtils.isNotBlank(loginUserInfoJson)) {
return JSON.parseObject(loginUserInfoJson, LoginUserInfo.class);
}
return null;
}
/**
* 保存用户认证信息
*/
public void setLoginUserInfo(String token, String sharerAppId) {
LoginUserInfo loginUserInfo = getLoginUserInfo(token);
loginUserInfo.setSharerAppId(sharerAppId);
setLoginUserInfo(token, loginUserInfo);
}
/**
* 保存用户认证信息
*/
public void setLoginUserInfo(String token, String openId, String sharerAppId) {
LoginUserInfo loginUserInfo = new LoginUserInfo(token, openId, sharerAppId);
setLoginUserInfo(token, loginUserInfo);
}
/**
* 保存用户认证信息
*/
public void setLoginUserInfo(String token, LoginUserInfo loginUserInfo) {
redisClient.setStr(CoreConstant.REDIS_TOKEN_USER_KEY + token,
JSON.toJSONString(loginUserInfo), CoreConstant.REDIS_TOKEN_EXPIRE_SECONDS);
}
}
package com.netease.yanxuan.wx.store.sharer.biz.core;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.io.Serializable;
/**
* @Description 认证信息
* @Author fanjiaxin
* @Date 2025/3/10 11:04
*/
@Data
@AllArgsConstructor
public class LoginUserInfo implements Serializable {
private static final long serialVersionUID = 3375070827863778256L;
/**
* 推客认证token
*/
private String accessToken;
/**
* 推客在小程序中的 openid
*/
private String openId;
/**
* 推客在微信电商平台注册的身份标识
*/
private String sharerAppId;
}
package com.netease.yanxuan.wx.store.sharer.biz.interceptor;
import com.netease.yanxuan.wx.store.sharer.biz.core.LoginUserContextHolder;
import com.netease.yanxuan.wx.store.sharer.biz.core.LoginUserHelper;
import com.netease.yanxuan.wx.store.sharer.biz.core.LoginUserInfo;
import com.netease.yanxuan.wx.store.sharer.common.constant.CoreConstant;
import com.netease.yanxuan.wx.store.sharer.common.exception.NoAuthException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @Description 鉴权拦截器
* @Author fanjiaxin
* @Date 2025/3/10 10:20
*/
@Slf4j
@RequiredArgsConstructor
@Component
public class AuthInterceptor implements HandlerInterceptor {
private final LoginUserHelper loginUserHelper;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader(CoreConstant.AUTH_HEADER_TOKEN_KEY);
if (StringUtils.isBlank(token)) {
throw new NoAuthException("用户认证失败,TOKEN为空");
}
LoginUserInfo loginUserInfo = loginUserHelper.getLoginUserInfo(token);
if (null == loginUserInfo) {
throw new NoAuthException("用户登录状态过期");
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
LoginUserContextHolder.remove();
}
}
package com.netease.yanxuan.wx.store.sharer.biz.interceptor;
import com.netease.yanxuan.wx.store.sharer.biz.core.LoginUserContextHolder;
import com.netease.yanxuan.wx.store.sharer.biz.core.LoginUserHelper;
import com.netease.yanxuan.wx.store.sharer.biz.core.LoginUserInfo;
import com.netease.yanxuan.wx.store.sharer.common.constant.CoreConstant;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @Description 用户信息拦截器
* @Author fanjiaxin
* @Date 2025/3/10 10:20
*/
@Slf4j
@RequiredArgsConstructor
@Component
public class UserInterceptor implements HandlerInterceptor {
private final LoginUserHelper loginUserHelper;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader(CoreConstant.AUTH_HEADER_TOKEN_KEY);
LoginUserInfo loginUserInfo = loginUserHelper.getLoginUserInfo(token);
if (null != loginUserInfo) {
// 刷新缓存
loginUserHelper.setLoginUserInfo(token, loginUserInfo);
LoginUserContextHolder.set(loginUserInfo);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
LoginUserContextHolder.remove();
}
}
package com.netease.yanxuan.wx.store.sharer.biz.meta.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* @Description 操作记录类型
* @Author fanjiaxin
* @Date 2025/3/3 13:58
*/
@Getter
@RequiredArgsConstructor
public enum CommissionChangeOptTypeEnum {
SET("1", "设置分佣比例"),
UNSET("2", "取消设置分佣比例"),
DEFAULT_CHANGE("3", "默认分佣比例变更");
private final String code;
private final String desc;
}
\ No newline at end of file
/**
* 枚举对象
*
* @author 莫闲.
* @date 2021/1/29.
*/
package com.netease.yanxuan.wx.store.sharer.biz.meta.enums;
package com.netease.yanxuan.wx.store.sharer.biz.meta.model.bo;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* @Description 用户分佣比例-业务对象
* @Author fanjiaxin
* @Date 2025/3/9 13:44
*/
@Data
public class CommissionSharerBO implements Serializable {
private static final long serialVersionUID = -1482990976577986435L;
/**
* 用户ID
*/
private String openId;
/**
* 分佣比例
*/
private BigDecimal commissionRatio;
}
package com.netease.yanxuan.wx.store.sharer.biz.meta.model.bo;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* @Description 用户商品分佣比例-业务对象
* @Author fanjiaxin
* @Date 2025/3/9 13:44
*/
@Data
public class CommissionSharerProductBO implements Serializable {
private static final long serialVersionUID = -1482990976577986435L;
/**
* 用户ID
*/
private String openId;
/**
* 商品ID
*/
private String productId;
/**
* 分佣比例
*/
private BigDecimal commissionRatio;
/**
* 启用状态,1启用,0禁用
*/
private Boolean enable;
}
package com.netease.yanxuan.wx.store.sharer.biz.meta.model.bo;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* @Description 用户商品分佣比例-业务对象
* @Author fanjiaxin
* @Date 2025/3/9 13:44
*/
@Data
public class CommissionSharerProductListBO implements Serializable {
private static final long serialVersionUID = -6353220469962589464L;
/**
* 用户ID
*/
private String openId;
/**
* 商品ID
*/
private String productId;
/**
* 分佣比例
*/
private BigDecimal commissionRatio;
/**
* 启用状态,1启用,0禁用
*/
private Boolean enable;
}
package com.netease.yanxuan.wx.store.sharer.biz.meta.model.bo;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* @Description 用户授权-业务对象
* @Author fanjiaxin
* @Date 2025/3/9 13:44
*/
@Data
public class LoginBO {
/**
* 登录时获取的code
*/
@NotBlank(message = "授权码不能为空")
private String code;
}
/**
* bo对象
*
* @author 莫闲.
* @date 2021/1/29.
*/
package com.netease.yanxuan.wx.store.sharer.biz.meta.model.bo;
package com.netease.yanxuan.wx.store.sharer.biz.meta.model.vo;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* @Description 商品列表
* @Author fanjiaxin
* @Date 2025/3/11 11:05
*/
@Data
public class ProductListVO implements Serializable {
private static final long serialVersionUID = -8340371734171952574L;
/**
* 商品标题
*/
private String title;
/**
* 商品主图
*/
private String headImg;
/**
* 商品价格
*/
private BigDecimal price;
/**
* 商品佣金
*/
private BigDecimal commission;
/**
* 商品店铺ID
*/
private String shopAppid;
/**
* 商品ID
*/
private String productId;
/**
* 商品卡片透传参数【注意:内嵌商品卡片时一定要传,不然会归因失败】
*/
private String productPromotionLink;
}
package com.netease.yanxuan.wx.store.sharer.biz.meta.model.vo;
import lombok.Data;
import java.io.Serializable;
/**
* @Description 商品短链
* @Author fanjiaxin
* @Date 2025/3/11 11:05
*/
@Data
public class ProductPromotionLinkVO implements Serializable {
private static final long serialVersionUID = -9193922166179487836L;
/**
* 商品短链
*/
private String promotionLink;
}
package com.netease.yanxuan.wx.store.sharer.biz.meta.model.vo;
import lombok.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* @Description 用户信息
* @Author fanjiaxin
* @Date 2025/3/11 10:59
*/
@Data
public class SharerInfoVO {
/**
* 推客在小程序中的 openid
*/
private String openId;
/**
* 推客在微信电商平台注册的身份标识
*/
private String sharerAppId;
/**
* 和机构的绑定状态
* 0:未绑定 1:已绑定
*/
private Integer bindStatus;
/**
* 当前推客的注册状态
* 0:未注册
* 1:注册中,还未完成
* 2:已完成注册
* 3:用户未支付实名,需要把微信先支付实名才能继续注册
*/
private Integer registerStatus;
/**
* register_status等于 0 或者 1时
* 调用注册流程时,openBusinessView需要的businessType
*/
private String registerBusinessType;
/**
* 注册时需要的queryString参数
*/
private String registerQueryString;
/**
* bind_status等于0时,调用绑定流程时,openBusinessView需要的businessType
*/
private String bindBusinessType;
/**
* 绑定时需要的queryString参数
*/
private String bindQueryString;
@Getter
@RequiredArgsConstructor
public enum bindStatus{
UNBIND(0),
BIND(1);
private final Integer code;
}
}
package com.netease.yanxuan.wx.store.sharer.biz.meta.model.vo;
import lombok.Builder;
import lombok.Data;
import java.math.BigDecimal;
/**
* @Description 用户分佣比例信息
* @Author fanjiaxin
* @Date 2025/3/11 10:59
*/
@Data
@Builder
public class UserCommissionRatioVO {
/**
* 默认分佣比例
*/
private BigDecimal commissionRatio;
}
package com.netease.yanxuan.wx.store.sharer.biz.meta.model.vo;
import lombok.Builder;
import lombok.Data;
/**
* @Description 用户token信息
* @Author fanjiaxin
* @Date 2025/3/11 10:59
*/
@Data
@Builder
public class UserTokenVO {
/**
* token
*/
private String token;
}
package com.netease.yanxuan.wx.store.sharer.biz.meta.page;
import lombok.Data;
import java.io.Serializable;
/**
* 分页查询实体类
*/
@Data
public class PageQuery implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 页数
*/
private Integer pageSize;
/**
* 翻页的上下文,用于请求下一页
*/
private String nextKey;
public Integer getPageSize() {
return null == pageSize || pageSize <= 0 ? 10 : pageSize;
}
}
package com.netease.yanxuan.wx.store.sharer.biz.meta.page;
import lombok.Data;
import org.springframework.util.CollectionUtils;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
/**
* 分页数据对象
*/
@Data
public class PageVO<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 是否有下一页
*/
private Boolean hasMore;
/**
* 翻页的上下文,用于请求下一页
*/
private String nextKey;
/**
* 列表数据
*/
private List<T> list;
public List<T> getList() {
return CollectionUtils.isEmpty(list) ? Collections.emptyList() : list;
}
}
/**
* <p>
* 核心业务逻辑,接口定义以Service结尾,例如:AbcService。
* 接口定义可以根据业务场景,定义业务分包。对应实现放到[impl]子包中,
* 接口实现以ServiceImpl结尾,例如:AbcServiceImpl
* </p>
*
* @author hzwangliyuan.
* @date 2019/11/28.
*/
package com.netease.yanxuan.wx.store.sharer.biz;
package com.netease.yanxuan.wx.store.sharer.biz.service;
import com.netease.yanxuan.wx.store.sharer.biz.meta.model.vo.ProductListVO;
import com.netease.yanxuan.wx.store.sharer.biz.meta.model.vo.ProductPromotionLinkVO;
import com.netease.yanxuan.wx.store.sharer.biz.meta.page.PageQuery;
import com.netease.yanxuan.wx.store.sharer.biz.meta.page.PageVO;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.vo.WeChatGetSharerProductCommissionVO;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.vo.WeChatSharerListVO;
/**
* @Description 商品-业务层
* @Author fanjiaxin
* @Date 2025/3/10 12:28
*/
public interface IProductService {
/**
* 获取商品分页列表
*/
PageVO<ProductListVO> getProductPageList(PageQuery pageQuery, String keyword);
/**
* 商品分享短链
*/
ProductPromotionLinkVO getProductPromotionLink(String sharerAppid, String productId);
/**
* 获取获取机构绑定的推客信息
*/
WeChatSharerListVO getBindSharerList(String sharerOpenid);
/**
* 获取推客的某个商品的推广分佣比例
*/
WeChatGetSharerProductCommissionVO getSharerProductCommissionInfo(String sharerAppid, String productId);
}
package com.netease.yanxuan.wx.store.sharer.biz.service;
import com.netease.yanxuan.wx.store.sharer.biz.meta.model.bo.LoginBO;
import com.netease.yanxuan.wx.store.sharer.biz.meta.model.vo.UserCommissionRatioVO;
import com.netease.yanxuan.wx.store.sharer.biz.meta.model.vo.UserTokenVO;
import com.netease.yanxuan.wx.store.sharer.biz.meta.model.vo.SharerInfoVO;
/**
* @Description 用户-业务层
* @Author fanjiaxin
* @Date 2025/3/10 12:28
*/
public interface IUserService {
/**
* 登录
*/
UserTokenVO login(LoginBO bo);
/**
* 刷新用户信息
*/
SharerInfoVO refreshUserInfo();
/**
* 获取用户信息
*/
SharerInfoVO getUserInfo();
/**
* 查询平台默认的分佣比例
*/
UserCommissionRatioVO getCommissionRatioDefault();
/**
* 绑定平台默认的分佣比例
*/
void bindCommissionRatioDefault();
}
/**
* 服务实现
*
* @author 莫闲.
* @date 2021/1/29.
*/
package com.netease.yanxuan.wx.store.sharer.biz.service.impl;
/**
* 服务接口
*
* @author 莫闲.
* @date 2021/1/29.
*/
package com.netease.yanxuan.wx.store.sharer.biz.service;
dev.meta=http://dev.yx.localhost:8550/proxy/dev.apolloy-configservice.service.mailsaas
test.meta=http://127.0.0.1:8550/proxy/test.apolloy-configservice.service.mailsaas/
release.meta=http://127.0.0.1:8550/proxy/release.apolloy-configservice.service.mailsaas/
pressure.meta=http://10.200.168.231:8080
regression.meta=http://127.0.0.1:8550/proxy/regression.apolloy-configservice.service.mailsaas/
online.meta=http://127.0.0.1:8550/proxy/online.apolloy-configservice.service.mailsaas/
\ No newline at end of file
/**
* dto对象
*
* @author 莫闲.
* @date 2021/1/29.
*/
package com.netease.yanxuan.wx.store.sharer.client.meta.model.dto;
/**
* 本层的定义意在实现本系统的能力暴露,以便于依赖方可以通过这个jar包直接访问我们web层的接口
*
* @author hzwangliyuan.
* @date 2019/11/28.
*/
package com.netease.yanxuan.wx.store.sharer.client;
package com.netease.yanxuan.wx.store.sharer.common.config;
import lombok.Data;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplateProperties getRestTemplateProperties() {
return new RestTemplateProperties();
}
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(httpRequestFactory());
return restTemplate;
}
/**
* httpclient 实现的ClientHttpRequestFactory
*/
@Bean
public ClientHttpRequestFactory httpRequestFactory() {
return new HttpComponentsClientHttpRequestFactory(httpClient(getRestTemplateProperties()));
}
/**
* 使用连接池的 httpclient
*/
@Bean
public HttpClient httpClient(RestTemplateProperties restTemplateProperties) {
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", SSLConnectionSocketFactory.getSocketFactory())
.build();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
// 最大链接数
connectionManager.setMaxTotal(restTemplateProperties.getMaxTotal());
// 同路由并发数20
connectionManager.setDefaultMaxPerRoute(restTemplateProperties.getMaxPerRoute());
// 增加空闲连接校验间隔‌
connectionManager.setValidateAfterInactivity(restTemplateProperties.getValidateAfterInactivity());
RequestConfig requestConfig = RequestConfig.custom()
// 读超时
.setSocketTimeout(restTemplateProperties.getReadTimeout())
// 链接超时
.setConnectTimeout(restTemplateProperties.getConnectTimeout())
// 链接不够用的等待时间
.setConnectionRequestTimeout(restTemplateProperties.getReadTimeout())
.build();
return HttpClientBuilder.create()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(connectionManager)
.setRetryHandler(new DefaultHttpRequestRetryHandler(0, true))
.build();
}
@Data
@Component
@ConfigurationProperties(prefix = "httpclient")
public class RestTemplateProperties {
/**
* 最大链接数
*/
private int maxTotal = 2000;
/**
* 同路由最大并发数
*/
private int maxPerRoute = 100;
/**
* 读取超时时间 ms
*/
private int readTimeout = 35000;
/**
* 链接超时时间 ms
*/
private int connectTimeout = 10000;
/**
* 空闲连接校验间隔‌
*/
private int validateAfterInactivity = 2000;
}
}
package com.netease.yanxuan.wx.store.sharer.common.constant;
/**
* @Description 通用常量信息
* @Author fanjiaxin
* @Date 2025/3/9 13:15
*/
public interface CoreConstant {
/**
* 请求头认证Token
*/
String AUTH_HEADER_TOKEN_KEY = "Authorization";
/**
* Redis用户认证Token
*/
String REDIS_TOKEN_USER_KEY = "TOKEN:USER:";
/**
* Redis认证Token失效时间,秒
*/
int REDIS_TOKEN_EXPIRE_SECONDS = 30 * 24 * 60 * 60;
/**
* Redis微信认证Token
*/
String REDIS_TOKEN_WECHAT_KEY = "TOKEN:WECHAT";
/**
* Redis微信认证Token锁
*/
String REDIS_TOKEN_WECHAT_LOCK_KEY = "TOKEN:WECHAT:LOCK";
/**
* Redis微信认证Token锁时间,秒
*/
String REDIS_TOKEN_WECHAT_LOCK_VALUE = "wechat:lock";
/**
* Redis微信认证Token锁时间,秒
*/
int REDIS_TOKEN_WECHAT_LOCK_TIME = 5;
/**
* Redis微信小店认证Token
*/
String REDIS_TOKEN_WECHAT_SHOP_KEY = "TOKEN:WECHAT:SHOP";
/**
* Redis微信小店认证Token锁
*/
String REDIS_TOKEN_WECHAT_SHOP_LOCK_KEY = "TOKEN:WECHAT:SHOP:LOCK";
/**
* Redis微信小店认证Token锁时间,秒
*/
String REDIS_TOKEN_WECHAT_SHOP_LOCK_VALUE = "wechat:shop:lock";
/**
* Redis微信小店认证Token锁时间,秒
*/
int REDIS_TOKEN_WECHAT_SHOP_LOCK_TIME = 5;
/**
* Redis商品详情缓存KEY
*/
String REDIS_PRODUCT_DETAIL_KEY = "PRODUCT:DETAIL:";
/**
* Redis商品详情过期时间,秒
*/
int REDIS_PRODUCT_DETAIL_EXPIRE_SECONDS = 6 * 60 * 60;
}
package com.netease.yanxuan.wx.store.sharer.common.constant;
/**
* 返回状态码
*/
public interface HttpStatusConstant {
/**
* 成功标记
*/
int SUCCESS = 200;
/**
* 鉴权失败
*/
int UNAUTHORIZED = 401;
/**
* 未绑定推客
*/
int UNAUTHSHARER = 40101;
/**
* 失败标记
*/
int FAIL = 500;
}
package com.netease.yanxuan.wx.store.sharer.common.core;
import com.netease.yanxuan.wx.store.sharer.common.constant.HttpStatusConstant;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang.StringUtils;
import java.io.Serializable;
/**
* @Description 响应信息主体
* @Author fanjiaxin
* @Date 2025/3/9 13:13
*/
@Data
@NoArgsConstructor
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 成功
*/
public static final int SUCCESS = HttpStatusConstant.SUCCESS;
/**
* 失败
*/
public static final int FAIL = HttpStatusConstant.FAIL;
/**
* 消息状态码
*/
private Integer code;
/**
* 消息内容
*/
private String msg;
/**
* 数据对象
*/
private T data;
public static <T> Result<T> ok() {
return restResult(null, SUCCESS, "操作成功");
}
public static <T> Result<T> ok(T data) {
return restResult(data, SUCCESS, "操作成功");
}
public static <T> Result<T> ok(String msg) {
return restResult(null, SUCCESS, msg);
}
public static <T> Result<T> ok(String msg, T data) {
return restResult(data, SUCCESS, msg);
}
public static <T> Result<T> fail() {
return restResult(null, FAIL, "操作失败");
}
public static <T> Result<T> fail(String msg) {
return restResult(null, FAIL, msg);
}
public static <T> Result<T> fail(T data) {
return restResult(data, FAIL, "操作失败");
}
public static <T> Result<T> fail(String msg, T data) {
return restResult(data, FAIL, msg);
}
public static <T> Result<T> fail(int code, String msg) {
return restResult(null, code, msg);
}
public static <T> Result<T> fail(Throwable throwable) {
String msg = StringUtils.substring(throwable.getMessage(), 0, 2000);
return restResult(null, FAIL, msg);
}
private static <T> Result<T> restResult(T data, int code, String msg) {
Result<T> r = new Result<>();
r.setCode(code);
r.setData(data);
r.setMsg(msg);
return r;
}
public static <T> Boolean isError(Result<T> ret) {
return !isSuccess(ret);
}
public static <T> Boolean isSuccess(Result<T> ret) {
return Result.SUCCESS == ret.getCode();
}
}
package com.netease.yanxuan.wx.store.sharer.common.exception;
/**
* 业务-异常类
*/
public class BizException extends RuntimeException {
private static final long serialVersionUID = -7960867947542194198L;
public BizException() {
super();
}
public BizException(String message) {
super(message);
}
public BizException(String message, Throwable cause) {
super(message, cause);
}
public BizException(Throwable cause) {
super(cause);
}
public BizException(Object obj) {
super();
}
public BizException(String message, Object obj) {
super(message);
}
public BizException(String message, Throwable cause, Object obj) {
super(message, cause);
}
public BizException(Throwable cause, Object obj) {
super(cause);
}
}
package com.netease.yanxuan.wx.store.sharer.common.exception;
/**
* 授权未通过-异常类
*/
public class NoAuthException extends BizException {
private static final long serialVersionUID = 6300030459749920168L;
public NoAuthException() {
super();
}
public NoAuthException(String message) {
super(message);
}
public NoAuthException(String message, Throwable cause) {
super(message, cause);
}
public NoAuthException(Throwable cause) {
super(cause);
}
public NoAuthException(Object obj) {
super();
}
public NoAuthException(String message, Object obj) {
super(message);
}
public NoAuthException(String message, Throwable cause, Object obj) {
super(message, cause);
}
public NoAuthException(Throwable cause, Object obj) {
super(cause);
}
}
package com.netease.yanxuan.wx.store.sharer.common.exception;
/**
* 未绑定推客-异常类
*/
public class NoBindSharerException extends BizException {
private static final long serialVersionUID = 6300030459749920168L;
public NoBindSharerException() {
super();
}
public NoBindSharerException(String message) {
super(message);
}
public NoBindSharerException(String message, Throwable cause) {
super(message, cause);
}
public NoBindSharerException(Throwable cause) {
super(cause);
}
public NoBindSharerException(Object obj) {
super();
}
public NoBindSharerException(String message, Object obj) {
super(message);
}
public NoBindSharerException(String message, Throwable cause, Object obj) {
super(message, cause);
}
public NoBindSharerException(Throwable cause, Object obj) {
super(cause);
}
}
package com.netease.yanxuan.wx.store.sharer.common.exception;
/**
* 微信业务-异常类
*/
public class WeChatException extends RuntimeException {
private static final long serialVersionUID = -7960867947542194198L;
public WeChatException() {
super();
}
public WeChatException(String message) {
super(message);
}
public WeChatException(String message, Throwable cause) {
super(message, cause);
}
public WeChatException(Throwable cause) {
super(cause);
}
public WeChatException(Object obj) {
super();
}
public WeChatException(String message, Object obj) {
super(message);
}
public WeChatException(String message, Throwable cause, Object obj) {
super(message, cause);
}
public WeChatException(Throwable cause, Object obj) {
super(cause);
}
}
package com.netease.yanxuan.wx.store.sharer.common.handler;
import com.netease.yanxuan.wx.store.sharer.common.constant.HttpStatusConstant;
import com.netease.yanxuan.wx.store.sharer.common.core.Result;
import com.netease.yanxuan.wx.store.sharer.common.exception.BizException;
import com.netease.yanxuan.wx.store.sharer.common.exception.NoAuthException;
import com.netease.yanxuan.wx.store.sharer.common.exception.NoBindSharerException;
import com.netease.yanxuan.wx.store.sharer.common.exception.WeChatException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
/**
* 全局异常处理器
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 认证失败
*/
@ExceptionHandler(NoAuthException.class)
public Result<Void> handleNoAuthException(NoAuthException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, e.getMessage());
return Result.fail(HttpStatusConstant.UNAUTHORIZED, "认证失败,无法访问系统资源");
}
/**
* 未绑定推客异常
*/
@ExceptionHandler(NoBindSharerException.class)
public Result<Void> handleNoBindSharerException(NoBindSharerException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, e.getMessage());
return Result.fail(HttpStatusConstant.UNAUTHSHARER, "未授权推客");
}
/**
* 业务异常-微信
*/
@ExceptionHandler(WeChatException.class)
public Result<Void> handleWeChatException(WeChatException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',微信业务异常.", requestURI, e);
return Result.fail(e);
}
/**
* 业务异常
*/
@ExceptionHandler(BizException.class)
public Result<Void> handleBizException(Exception e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生业务异常.", requestURI, e);
return Result.fail(e);
}
/**
* 系统异常
*/
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生系统异常.", requestURI, e);
return Result.fail(e);
}
}
package com.netease.yanxuan.wx.store.sharer.common.handler;
import org.springframework.http.HttpMethod;
/**
* @Description HTTP请求
* @Author fanjiaxin
* @Date 2025/3/11 17:27
*/
public interface HttpRequestClient {
/**
* 执行 HTTP 请求
*
* @param url 请求地址
* @param method 请求类型 (GET/POST)
* @param params 请求参数 (GET 对应查询参数,POST 对应表单/JSON 参数)
* @return 响应结果
*/
<T> String execute(String url, HttpMethod method, T params);
}
package com.netease.yanxuan.wx.store.sharer.common.handler;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* @Description redis客户端
* @Author fanjiaxin
* @Date 2025/3/10 10:32
*/
@Service
@RequiredArgsConstructor
public class RedisClient {
private final RedisTemplate<String, Object> redisTemplate;
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
public void set(String key, Object value, int expireSeconds) {
redisTemplate.opsForValue().set(key, value, expireSeconds, TimeUnit.SECONDS);
}
public String getStr(String key) {
Object value = redisTemplate.opsForValue().get(key);
return null != value ? value.toString() : null;
}
public void setStr(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
public void setStr(String key, String value, int expireSeconds) {
redisTemplate.opsForValue().set(key, value, expireSeconds, TimeUnit.SECONDS);
}
public Boolean setIfAbsent(String key, Object value, int expireSeconds) {
return redisTemplate.opsForValue().setIfAbsent(key, value, expireSeconds, TimeUnit.SECONDS);
}
public Boolean delete(String key) {
return redisTemplate.delete(key);
}
}
\ No newline at end of file
package com.netease.yanxuan.wx.store.sharer.common.handler;
import com.alibaba.fastjson.JSON;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.util.Map;
import java.util.Objects;
/**
* @Description RestTemplateClient
* @Author fanjiaxin
* @Date 2025/3/11 17:27
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class RestTemplateClient implements HttpRequestClient {
private final RestTemplate restTemplate;
@Override
public <T> String execute(String url, HttpMethod method, T params) {
if (method == HttpMethod.GET) {
return handleGetRequest(url, params);
} else {
return handlePostRequest(url, params);
}
}
private <T> String handleGetRequest(String url, T params) {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
if (Objects.nonNull(params)) {
Map<String, Object> paramsMap = JSON.parseObject(JSON.toJSONString(params), Map.class);
paramsMap.forEach(builder::queryParam);
}
URI uri = builder.build().toUri();
return restTemplate.getForObject(uri, String.class);
}
private <T> String handlePostRequest(String url, T params) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<T> requestEntity = new HttpEntity<>(params, headers);
return restTemplate.postForObject(url, requestEntity, String.class);
}
}
/**
* @(#)FastJsonRedisSerializer.java, 2022/7/8.
* <p/>
* Copyright 2022 Netease, Inc. All rights reserved.
* NETEASE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package com.netease.yanxuan.wx.store.sharer.common.json;
import com.alibaba.fastjson.parser.ParserConfig;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* @author 柳敦盛 (liudunsheng@corp.netease.com)
*/
public class FastJsonRedisSerializer implements RedisSerializer {
static {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
ParserConfig.getGlobalInstance().addAccept("com.netease.");
}
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
@Override
public byte[] serialize(Object t) throws SerializationException {
if (t == null) {
return new byte[0];
}
return FastJsonSerializer.serialize(t).getBytes(DEFAULT_CHARSET);
}
@Override
public Object deserialize(byte[] bytes) throws SerializationException {
if (bytes == null || bytes.length <= 0) {
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return FastJsonSerializer.deserialize(str);
}
}
package com.netease.yanxuan.wx.store.sharer.common.util;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Function;
/**
* @Description 异步处理工具类
* @Author fanjiaxin
* @Date 2024/6/13 09:31
*/
public class AsyncUtils {
/**
* 异步处理列表中的每个元素,并收集所有结果。
*
* @param <T> 列表中的元素类型
* @param <R> 异步处理结果类型
* @param itemList 要处理的列表
* @param asyncTask 定义如何异步处理每个元素的函数
* @return 异步处理后所有结果的列表
*/
public static <T, R> List<R> processListAsync(List<T> itemList, Function<T, R> asyncTask) {
CompletableFuture<R>[] futures = new CompletableFuture[itemList.size()];
for (int i = 0; i < itemList.size(); i++) {
T item = itemList.get(i);
CompletableFuture<R> future = CompletableFuture.supplyAsync(() -> asyncTask.apply(item));
futures[i] = future;
}
CompletableFuture.allOf(futures).join();
List<R> resultList = new ArrayList<>(itemList.size());
for (CompletableFuture<R> future : futures) {
resultList.add(future.join());
}
return resultList;
}
/**
* 异步处理列表中的每个元素,并收集所有结果。
*
* @param <T> 列表中的元素类型
* @param <R> 异步处理结果类型
* @param itemList 要处理的列表
* @param asyncTask 定义如何异步处理每个元素的函数
* @param executor 异步执行器
* @return 异步处理后所有结果的列表
*/
public static <T, R> List<R> processListAsync(List<T> itemList, Function<T, R> asyncTask, Executor executor) {
CompletableFuture<R>[] futures = new CompletableFuture[itemList.size()];
for (int i = 0; i < itemList.size(); i++) {
T item = itemList.get(i);
CompletableFuture<R> future = CompletableFuture.supplyAsync(() -> asyncTask.apply(item), executor);
futures[i] = future;
}
CompletableFuture.allOf(futures).join();
List<R> resultList = new ArrayList<>(itemList.size());
for (CompletableFuture<R> future : futures) {
resultList.add(future.join());
}
return resultList;
}
}
package com.netease.yanxuan.wx.store.sharer.common.util;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
@Component
public class SpringContextUtils implements ApplicationContextAware {
/**
* 上下文对象实例
*/
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtils.applicationContext = applicationContext;
}
/**
* 获取applicationContext
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
/**
* 获取HttpServletRequest
*/
public static HttpServletRequest getHttpServletRequest() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
public static String getDomain() {
HttpServletRequest request = getHttpServletRequest();
StringBuffer url = request.getRequestURL();
return url.delete(url.length() - request.getRequestURI().length(), url.length()).toString();
}
public static String getOrigin() {
HttpServletRequest request = getHttpServletRequest();
return request.getHeader("Origin");
}
/**
* 通过name获取 Bean.
*/
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}
/**
* 通过class获取Bean.
*/
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
/**
* 通过name,以及Clazz返回指定的Bean
*/
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
/**
* 通过name,以及Clazz返回指定的Bean,为空不报错
*/
public static <T> T getBeanIfExists(String name, Class<T> clazz) {
try {
return getApplicationContext().getBean(name, clazz);
} catch (NoSuchBeanDefinitionException e) {
return null;
}
}
}
......@@ -16,32 +16,5 @@
<groupId>com.netease.yanxuan</groupId>
<artifactId>yanxuan-wx-store-sharer-common</artifactId>
</dependency>
<!-- store-db
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>netease.ddb</groupId>
<artifactId>db</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
-->
<!-- store-redis
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
-->
</dependencies>
</project>
\ No newline at end of file
package com.netease.yanxuan.wx.store.sharer.dal.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.netease.yanxuan.shadow.common.IDGenerator;
import com.netease.yanxuan.shadow.conf.DrmShadowConfig;
import com.netease.yanxuan.shadow.conf.ShadowConfig;
import com.netease.yanxuan.shadow.ddb.IDGeneratorDipperDDB;
import com.netease.yanxuan.shadow.druid.ShadowTableFilter;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.util.Collections;
@Slf4j
@Configuration
@EnableTransactionManagement
public class DataSourceConfiguration {
@Autowired
private DrmDBConfig drmDBConfig;
@Bean("mysqlDataSource")
public DataSource genDataSource(@Qualifier("shadowTableFilter") ShadowTableFilter shadowTableFilter) {
DruidDataSource dataSource = new DruidDataSource();
//url
dataSource.setUrl(drmDBConfig.getMysqlUrl());
dataSource.setUsername(drmDBConfig.getMysqlUserName());
dataSource.setPassword(drmDBConfig.getMysqlPassWord());
dataSource.setDriverClassName(drmDBConfig.getMysqlDriverClassName());
//pool
dataSource.setDbType(drmDBConfig.getMysqlDbType());
dataSource.setInitialSize(drmDBConfig.getMysqlInitialSize());
dataSource.setMinIdle(drmDBConfig.getMysqlMinIdle());
dataSource.setMaxActive(drmDBConfig.getMysqlMaxActive());
dataSource.setMaxWait(drmDBConfig.getMysqlMaxWait());
dataSource.setMinEvictableIdleTimeMillis(drmDBConfig.getMysqlMinEvictableTimeMillis());
dataSource.setRemoveAbandoned(drmDBConfig.isMysqlRemoveAbandoned());
dataSource.setRemoveAbandonedTimeoutMillis(drmDBConfig.getMysqlRemoveAbandonedTimeMills());
dataSource.setUseUnfairLock(drmDBConfig.isMysqlUseUnfairLock());
dataSource.setTestWhileIdle(drmDBConfig.isMysqlTestWhileIdle());
dataSource.setValidationQuery(drmDBConfig.getMysqlValidationQuery());
dataSource.setValidationQueryTimeout(drmDBConfig.getMysqlValidationQueryTimeout());
dataSource.setTimeBetweenEvictionRunsMillis(drmDBConfig.getMysqlTimeBetweenEvictionRunsMillis());
dataSource.setTestOnBorrow(drmDBConfig.isMysqlTestOnBorrow());
dataSource.setTestOnReturn(drmDBConfig.isMysqlTestOnReturn());
dataSource.setPoolPreparedStatements(drmDBConfig.isMysqlPoolPreparedStatements());
dataSource.setMaxPoolPreparedStatementPerConnectionSize(
drmDBConfig.getMysqlMaxPoolPreparedStatementPerConnectionSize());
dataSource.setProxyFilters(Collections.singletonList(shadowTableFilter));
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setDbType("mysql");
return dataSource;
}
/**
* session factory
*/
@Primary
@Bean(name = "sqlSessionFactoryBean")
public MybatisSqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
// 配置类型别名
sqlSessionFactoryBean.setTypeAliasesPackage("com.netease.yanxuan.marketing.mall.reward.dal.meta.model.po");
// 配置mapper的扫描,找到所有的mapper.xml映射文件
PathMatchingResourcePatternResolver patternResolver = new PathMatchingResourcePatternResolver();
Resource[] resources = patternResolver.getResources("classpath:mapper/*.xml");
sqlSessionFactoryBean.setMapperLocations(resources);
// 加载全局的配置文件
DefaultResourceLoader defaultResourceLoader = new DefaultResourceLoader();
Resource resource = defaultResourceLoader.getResource("classpath:mybatis/mybatis-config.xml");
sqlSessionFactoryBean.setConfigLocation(resource);
sqlSessionFactoryBean.setPlugins(mybatisPlusInterceptor());
return sqlSessionFactoryBean;
}
/**
* sqlSessionTemplate
*/
@Primary
@Bean(name = "sqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
/**
* 用于事务管理
*/
@Bean(name = "transactionManager")
@Primary
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
@Bean
public ShadowConfig shadowConfig() {
return new DrmShadowConfig();
}
@Bean
public IDGenerator ddbIDGenerator(ShadowConfig shadowConfig) {
return new IDGeneratorDipperDDB(shadowConfig);
}
@Bean
public ShadowTableFilter shadowTableFilter(@Qualifier("shadowConfig") ShadowConfig shadowConfig) {
ShadowTableFilter shadowTableFilter = new ShadowTableFilter();
shadowTableFilter.setShadowConfig(shadowConfig);
return shadowTableFilter;
}
}
package com.netease.yanxuan.wx.store.sharer.dal.config;
import com.ctrip.framework.apollo.spring.annotation.EnableAutoUpdateApolloConfig;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 数据库配置 不会动态生效,需重启生效
*/
@Getter
@Component
@EnableAutoUpdateApolloConfig("db.config")
public class DrmDBConfig {
@Value("${mysql.url:}")
private String mysqlUrl;
@Value("${mysql.username:}")
private String mysqlUserName;
@Value("${mysql.password:}")
private String mysqlPassWord;
@Value("${mysql.driverClassName:com.mysql.jdbc.Driver}")
private String mysqlDriverClassName;
@Value("${mysql.type:mysql}")
private String mysqlDbType;
@Value("${mysql.initial.size:5}")
private int mysqlInitialSize;
@Value("${mysql.minIdle:5}")
private int mysqlMinIdle;
@Value("${mysql.maxActive:50}")
private int mysqlMaxActive;
@Value("${mysql.maxWait:1000}")
private long mysqlMaxWait;
@Value("${mysql.minEvictableIdleTimeMillis:35000}")
private long mysqlMinEvictableTimeMillis;
@Value("${mysql.removeAbandoned:false}")
private boolean mysqlRemoveAbandoned;
@Value("${mysql.removeAbandonedTimeoutMillis:180000}")
private long mysqlRemoveAbandonedTimeMills;
@Value("${mysql.connectProperties:socketTimeout=3000;connectTimeout=1200}")
private String mysqlConnectionProperties;
@Value("${mysql.useUnfairLock:false}")
private boolean mysqlUseUnfairLock;
@Value("${mysql.testWhileIdle:true}")
private boolean mysqlTestWhileIdle;
@Value("${mysql.validationQuery:SELECT 1}")
private String mysqlValidationQuery;
@Value("${mysql.validationQueryTimeout:1}")
private int mysqlValidationQueryTimeout;
@Value("${mysql.timeBetweenEvictionRunsMillis:30000}")
private long mysqlTimeBetweenEvictionRunsMillis;
@Value("${mysql.testOnBorrow:false}")
private boolean mysqlTestOnBorrow;
@Value("${mysql.testOnReturn:false}")
private boolean mysqlTestOnReturn;
@Value("${mysql.poolPreparedStatements:false}")
private boolean mysqlPoolPreparedStatements;
@Value("${mysql.maxPoolPreparedStatementPerConnectionSize:0}")
private int mysqlMaxPoolPreparedStatementPerConnectionSize;
/**
* redis
*/
private List<String> nodes;
@Value("${redis.cluster.nodes:10.104.0.129:16380,10.104.0.129:16381,10.104.0.129:16382}")
private String clusterNodes;
@Value("${redis.cluster.password:}")
private String password;
@Value("${redis.cluster.max-redirects:3}")
private Integer maxRedirects;
@Value("${redis.maxTotal:100}")
private Integer maxTotal;
@Value("${redis.maxIdle:100}")
private Integer maxIdle;
@Value("${redis.minIdle:20}")
private Integer minIdle;
@Value("${redis.numTestsPerEvictionRun:1024}")
private Integer numTestsPerEvictionRun;
@Value("${redis.timeBetweenEvictionRunsMillis:30000}")
private Integer timeBetweenEvictionRunsMillis;
@Value("${redis.minEvictableIdleTimeMillis:1800000}")
private Integer minEvictableIdleTimeMillis;
@Value("${redis.softMinEvictableIdleTimeMillis:10000}")
private Integer softMinEvictableIdleTimeMillis;
@Value("${redis.maxWaitMillis:1500}")
private Integer maxWaitMillis;
@Value("${redis.testOnBorrow:false}")
private Boolean testOnBorrow;
@Value("${redis.testWhileIdle:true}")
private Boolean testWhileIdle;
@Value("${redis.blockWhenExhausted:false}")
private Boolean blockWhenExhausted;
public List<String> getClusterNodes() {
String[] split = this.clusterNodes.split(",");
nodes = new ArrayList<>(Arrays.asList(split));
return nodes;
}
}
/**
* @(#)GalaxyMetaObjectHandler.java, 2024/9/2.
* <p/>
* Copyright 2024 Netease, Inc. All rights reserved. NETEASE PROPRIETARY/CONFIDENTIAL. Use is subject to license
* terms.
*/
package com.netease.yanxuan.wx.store.sharer.dal.config;
import java.util.Date;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
/**
* 配置自动填充插件
* @author 刘运星 (liuyunxing01@corp.netease.com)
*/
@Component
public class GalaxyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
}
}
/**
* @(#)RedisConfiguration.java, 2022/7/8.
* <p/>
* Copyright 2022 Netease, Inc. All rights reserved.
* NETEASE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package com.netease.yanxuan.wx.store.sharer.dal.config;
import com.netease.yanxuan.wx.store.sharer.common.json.FastJsonRedisSerializer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.JedisPoolConfig;
import java.time.Duration;
/**
* @author 柳敦盛 (liudunsheng@corp.netease.com)
*/
@Configuration
public class RedisConfiguration {
private static final long CONNECT_TIMEOUT = 500L;
private static final long READ_TIMEOUT = 500L;
@Autowired
private DrmDBConfig drmDBConfig;
@Bean
public RedisClusterConfiguration redisClusterConfiguration() {
RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(
drmDBConfig.getClusterNodes());
redisClusterConfiguration.setMaxRedirects(drmDBConfig.getMaxRedirects());
redisClusterConfiguration.setPassword(RedisPassword.of(drmDBConfig.getPassword()));
return redisClusterConfiguration;
}
@Bean
public JedisPoolConfig jedisPoolConfig() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 最大连接数
jedisPoolConfig.setMaxTotal(drmDBConfig.getMaxTotal());
// 最大空闲连接数
jedisPoolConfig.setMaxIdle(drmDBConfig.getMaxIdle());
jedisPoolConfig.setMinIdle(drmDBConfig.getMinIdle());
// 每次释放连接的最大数目
jedisPoolConfig.setNumTestsPerEvictionRun(drmDBConfig.getNumTestsPerEvictionRun());
// 释放连接的扫描间隔(毫秒)
jedisPoolConfig.setTimeBetweenEvictionRunsMillis(drmDBConfig.getTimeBetweenEvictionRunsMillis());
// 连接最小空闲时间
jedisPoolConfig.setMinEvictableIdleTimeMillis(drmDBConfig.getMinEvictableIdleTimeMillis());
// 连接空闲多久后释放, 当空闲时间>该值 且 空闲连接>最大空闲连接数 时直接释放
jedisPoolConfig.setSoftMinEvictableIdleTimeMillis(drmDBConfig.getSoftMinEvictableIdleTimeMillis());
// 获取连接时的最大等待毫秒数,小于零:阻塞不确定的时间,默认-1
jedisPoolConfig.setMaxWaitMillis(drmDBConfig.getMaxWaitMillis());
// 在获取连接的时候检查有效性, 默认false
jedisPoolConfig.setTestOnBorrow(drmDBConfig.getTestOnBorrow());
// 在空闲时检查有效性, 默认false
jedisPoolConfig.setTestWhileIdle(drmDBConfig.getTestWhileIdle());
// 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true
jedisPoolConfig.setBlockWhenExhausted(drmDBConfig.getBlockWhenExhausted());
return jedisPoolConfig;
}
@Bean
public JedisClientConfiguration jedisClientConfiguration() {
JedisClientConfiguration.JedisClientConfigurationBuilder builder = JedisClientConfiguration.builder();
return builder.connectTimeout(Duration.ofMillis(CONNECT_TIMEOUT)).readTimeout(Duration.ofMillis(READ_TIMEOUT))
.usePooling().poolConfig(jedisPoolConfig()).build();
}
@Bean(name = "connectionFactory")
public JedisConnectionFactory jedisConnectionFactory() {
return new JedisConnectionFactory(redisClusterConfiguration(), jedisClientConfiguration());
}
@Bean("redisTemplate")
@Primary
public RedisTemplate<String, Object> getRedisTemplate(
@Qualifier("connectionFactory") JedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setDefaultSerializer(fastJsonRedisSerializer);
redisTemplate.setValueSerializer(fastJsonRedisSerializer);
redisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
return redisTemplate;
}
@Bean("redisStringTemplate")
public RedisTemplate<Object, Object> getRedisStringTemplate(
@Qualifier("connectionFactory") JedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer();
redisTemplate.setDefaultSerializer(fastJsonRedisSerializer);
redisTemplate.setValueSerializer(redisSerializer);
redisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
return redisTemplate;
}
@Bean("openapiCacheManager")
public CacheManager cacheManager(@Qualifier("connectionFactory") JedisConnectionFactory connectionFactory) {
return RedisCacheManager.create(connectionFactory);
}
}
package com.netease.yanxuan.wx.store.sharer.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.netease.yanxuan.wx.store.sharer.dal.meta.model.po.SharerInfo;
/**
* @Description 推客信息表
* @Author fanjiaxin
* @Date 2025/3/11 22:08
*/
public interface SharerInfoMapper extends BaseMapper<SharerInfo> {
}
package com.netease.yanxuan.wx.store.sharer.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.netease.yanxuan.wx.store.sharer.dal.meta.model.po.SharerProductCommission;
/**
* @Description 推客商品分佣表
* @Author fanjiaxin
* @Date 2025/3/11 21:54
*/
public interface SharerProductCommissionMapper extends BaseMapper<SharerProductCommission> {
}
package com.netease.yanxuan.wx.store.sharer.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.netease.yanxuan.wx.store.sharer.dal.meta.model.po.SharerProductCommissionRecord;
/**
* @Description 推客商品分佣配置操作记录表
* @Author fanjiaxin
* @Date 2025/3/11 21:54
*/
public interface SharerProductCommissionRecordMapper extends BaseMapper<SharerProductCommissionRecord> {
}
/**
* mapper对象
*
* @author 莫闲.
* @date 2021/1/29.
*/
package com.netease.yanxuan.wx.store.sharer.dal.mapper;
package com.netease.yanxuan.wx.store.sharer.dal.meta.model.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Date;
/**
* @Description 推客信息表
* @Author fanjiaxin
* @Date 2025/3/11 21:54
*/
@Data
@TableName("SHARER_INFO")
public class SharerInfo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID,自动生成。
*/
@TableId(type = IdType.AUTO)
private BigInteger id;
/**
* 用户开放ID
*/
private String openId;
/**
* 推客应用ID
*/
private String sharerAppid;
/**
* 绑定时间
*/
private Date bindTime;
/**
* 佣金比例
*/
private String commissionRatio;
/**
* 分佣类型【0:平台分佣 1:机构分佣】
*/
private Integer commissionType;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}
package com.netease.yanxuan.wx.store.sharer.dal.meta.model.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Date;
/**
* @Description 推客商品分佣表
* @Author fanjiaxin
* @Date 2025/3/11 21:54
*/
@Data
@TableName("SHARER_PRODUCT_COMMISSION")
public class SharerProductCommission implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID,自动生成。
*/
@TableId(type = IdType.AUTO)
private BigInteger id;
/**
* 用户开放ID
*/
private String openId;
/**
* 推客应用ID
*/
private String sharerAppid;
/**
* 商品ID
*/
private String productId;
/**
* 佣金比例
*/
private String commissionRatio;
/**
* 佣金类型
*/
private String commissionType;
/**
* 是否未配置,1:未配置,取默认佣金比例,0:已配置
*/
private String unset;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
@Getter
@RequiredArgsConstructor
public enum UnsetEnum {
UNSET("1", "未配置,取默认佣金比例"),
SET("0", "已配置");
private final String code;
private final String desc;
}
}
package com.netease.yanxuan.wx.store.sharer.dal.meta.model.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Date;
/**
* @Description 推客商品分佣配置操作记录表
* @Author fanjiaxin
* @Date 2025/3/11 21:54
*/
@Data
@TableName("SHARER_PRODUCT_COMMISSION_RECORD")
public class SharerProductCommissionRecord implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID,自动生成。
*/
@TableId(type = IdType.AUTO)
private BigInteger id;
/**
* 用户开放ID
*/
private String openId;
/**
* 推客应用ID
*/
private String sharerAppid;
/**
* 商品ID
*/
private String productId;
/**
* 操作类型
* 1- 设置商品分佣比例
* 2- 取消设置商品分佣比例
* 3- 默认分佣比例变更
*/
private String optType;
/**
* 操作信息,记录操作前比例 & 操作后比例
*/
private String optInfo;
/**
* 操作时间
*/
private Date optTime;
/**
* 状态,1 操作成功 2 操作失败
*/
private String optStatus;
/**
* 拓展信息
*/
private String extInfo;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}
/**
* po对象
*
* @author 莫闲.
* @date 2021/1/29.
*/
package com.netease.yanxuan.wx.store.sharer.dal.meta.model.po;
/**
* dao包作为可选包,用于融合值得复用的mapper层的统一处理,例如参数的调整、适配,
* 多个mapper关联的情况等,当然这些也可以考虑在service实现。
* dao=data access object,所以这里把NoSQL,例如mongo的访问封装也放到这里。
*
* @author hzwangliyuan.
* @date 2019/11/28.
*/
package com.netease.yanxuan.wx.store.sharer.dal;
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 这个配置使全局的映射器启用或禁用缓存 -->
<setting name="cacheEnabled" value="false"/>
<!-- 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载 -->
<setting name="lazyLoadingEnabled" value="false"/>
<!-- 当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载 -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 允许JDBC支持生成的键。需要适合的驱动。如果设置为true则这个设置强制生成的键被使用,尽管一些驱动拒绝兼容但仍然有效(比如Derby) -->
<setting name="useGeneratedKeys" value="true"/>
<!-- 允许或不允许多种结果集从一个单独的语句中返回(需要适合的驱动) -->
<setting name="multipleResultSetsEnabled" value="true"/>
<!-- 使用列标签代替列名。不同的驱动在这方便表现不同。参考驱动文档或充分测试两种方法来决定所使用的驱动 -->
<setting name="useColumnLabel" value="true"/>
<!-- 指定MyBatis如何自动映射列到字段/属性。PARTIAL只会自动映射简单,没有嵌套的结果。FULL会自动映射任意复杂的结果(嵌套的或其他情况) -->
<setting name="autoMappingBehavior" value="PARTIAL"/>
<!-- 配置默认的执行器。SIMPLE执行器没有什么特别之处。REUSE执行器重用预处理语句。BATCH执行器重用语句和批量更新 -->
<setting name="defaultExecutorType" value="SIMPLE"/>
<!-- 设置超时时间,它决定驱动等待一个数据库响应的时间 -->
<setting name="defaultStatementTimeout" value="25000"/>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="logImpl" value="SLF4J"/>
</settings>
<typeHandlers>
</typeHandlers>
</configuration>
package com.netease.yanxuan.wx.store.sharer.integration.config;
import com.ctrip.framework.apollo.spring.annotation.EnableAutoUpdateApolloConfig;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @Description 应用配置
* @Author fanjiaxin
* @Date 2025/3/10 17:26
*/
@Data
@Component
@EnableAutoUpdateApolloConfig
public class WeChatConfig {
/**
* 微信接口基地址
*/
@Value("${wechat.url:https://api.weixin.qq.com}")
private String url;
/**
* 微信开放接口appId
*/
@Value("${wechat.appid:wx57ee288d2823d5e3}")
private String appid;
/**
* 微信开放接口appSecret
*/
@Value("${wechat.appsecret:0bf536fe0b0a20542dd97a490f8e89b7}")
private String appsecret;
/**
* 微信开放接口小店appId
*/
@Value("${wechat.shop.appid:wxc956822fcdb4a43c}")
private String shopAppid;
/**
* 微信开放接口小店appSecret
*/
@Value("${wechat.shop.appsecret:67176561fb2bd4ebf0bb535e6ca10051}")
private String shopAppsecret;
}
\ No newline at end of file
package com.netease.yanxuan.wx.store.sharer.integration.constant;
/**
* @Description 微信API
* @Author fanjiaxin
* @Date 2025/3/11 11:59
*/
public class WeChatApi {
public static final String WECHAT_REQUEST_SUCCESS_CODE = "0";
/**
* 微信分佣倍数
*/
public static final int WECHAT_COMMISSION_RATIO_MULTIPLIER = 1000000;
/**
* 微信请求Token字段名
*/
public static final String WECHAT_REQUEST_TOKEN_FIELD_NAME = "access_token";
/**
* 获取用户认证信息
*/
public static final String GET_TOKEN = "/cgi-bin/token";
// -------------------------------------- 用户 --------------------------------------
/**
* 获取小程序用户信息
*/
public static final String GET_USER_INFO = "/sns/jscode2session";
// -------------------------------------- 推客 --------------------------------------
/**
* 获取推客的注册状态,以及和机构的绑定状态
*/
public static final String GET_PROMOTER_REGISTER_AND_BIND_STATUS = "/channels/ec/promoter/get_promoter_register_and_bind_status";
/**
* 获取机构绑定的推客信息
*/
public static final String GET_BIND_SHARER_LIST = "/channels/ec/promoter/get_bind_sharer_list";
/**
* 设置推客的的分佣类型和比例信息
*/
public static final String SET_SHARER_COMMISSION_INFO = "/channels/ec/promoter/set_sharer_commission_info";
// -------------------------------------- 商品 --------------------------------------
/**
* 获取可推广的商品id列表
*/
public static final String GET_PROMOTE_PRODUCT_LIST = "/channels/ec/promoter/get_promote_product_list";
/**
* 获取可以推广的商品详情
*/
public static final String GET_PROMOTE_PRODUCT_DETAIL = "/channels/ec/promoter/get_promote_product_detail";
/**
* 获取推客对某个商品的推广短链
*/
public static final String GET_PRODUCT_PROMOTION_LINK_INFO = "/channels/ec/promoter/get_product_promotion_link_info";
/**
* 获取推客的某个商品的推广分佣比例
*/
public static final String GET_SHARER_PRODUCT_COMMISSION_INFO = "/channels/ec/promoter/get_sharer_product_commission_info";
/**
* 设置推客的单个商品的分佣比例信息
*/
public static final String SET_SHARER_PRODUCT_COMMISSION_INFO = "/channels/ec/promoter/set_sharer_product_commission_info";
}
package com.netease.yanxuan.wx.store.sharer.integration.core;
import com.netease.yanxuan.wx.store.sharer.common.constant.CoreConstant;
import com.netease.yanxuan.wx.store.sharer.common.exception.WeChatException;
import com.netease.yanxuan.wx.store.sharer.common.handler.RedisClient;
import com.netease.yanxuan.wx.store.sharer.integration.handler.impl.WeChatShopAccessTokenRequest;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.vo.WeChatAccessTokenVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* @Description 微信认证信息上下文持有器
* @Author fanjiaxin
* @Date 2025/2/24 11:53
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WeChatContextHolder {
private final RedisClient redisClient;
private final WeChatShopAccessTokenRequest weChatShopAccessTokenRequest;
private static final long RETRY_DELAY_MS = 200;
private static final int EXPIRE_SECONDS = 30 * 60;
/**
* 获取微信小店认证信息
*/
public String getShopAccessToken() {
// 检查锁定
String accessToken = redisClient.getStr(CoreConstant.REDIS_TOKEN_WECHAT_SHOP_KEY);
if (StringUtils.isNotBlank(accessToken)) {
return accessToken;
}
return createShopAccessToken();
}
/**
* 创建微信小店认证信息
*/
public String createShopAccessToken() {
try {
// 设置10秒的锁
boolean locked = redisClient.setIfAbsent(CoreConstant.REDIS_TOKEN_WECHAT_SHOP_LOCK_KEY,
CoreConstant.REDIS_TOKEN_WECHAT_SHOP_LOCK_VALUE, CoreConstant.REDIS_TOKEN_WECHAT_SHOP_LOCK_TIME);
if (!locked) {
// 未获取到锁,等待一段时间后重试
TimeUnit.MILLISECONDS.sleep(RETRY_DELAY_MS);
return getShopAccessToken();
}
WeChatAccessTokenVO handle = weChatShopAccessTokenRequest.handle();
redisClient.setStr(CoreConstant.REDIS_TOKEN_WECHAT_SHOP_KEY, handle.getAccess_token(),
handle.getExpires_in() - EXPIRE_SECONDS);
return handle.getAccess_token();
} catch (InterruptedException e) {
throw new WeChatException("微信小店认证AccessToken生成失败", e);
}
}
}
package com.netease.yanxuan.wx.store.sharer.integration.handler;
import org.springframework.http.HttpMethod;
/**
* @Description IWeChatRequest
* @Author fanjiaxin
* @Date 2025/3/11 17:12
*/
public interface IWeChatRequest {
/**
* 请求类型
*/
HttpMethod getRequestMethod();
/**
* 请求地址
*/
String getRequestUrl();
}
package com.netease.yanxuan.wx.store.sharer.integration.handler;
import com.alibaba.fastjson.JSON;
import com.netease.yanxuan.wx.store.sharer.common.constant.CoreConstant;
import com.netease.yanxuan.wx.store.sharer.common.exception.WeChatException;
import com.netease.yanxuan.wx.store.sharer.common.handler.RedisClient;
import com.netease.yanxuan.wx.store.sharer.common.handler.RestTemplateClient;
import com.netease.yanxuan.wx.store.sharer.integration.constant.WeChatApi;
import com.netease.yanxuan.wx.store.sharer.integration.meta.enums.WeChatErrorCodeEnum;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.vo.WeChatCoreVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
/**
* @Description RestTemplateClient
* @Author fanjiaxin
* @Date 2025/3/11 17:27
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WeChatRestTemplateHandler {
private final RestTemplateClient restTemplateClient;
private final RedisClient redisClient;
public <T, R extends WeChatCoreVO> R execute(String url, HttpMethod method, T params, Class<R> resType) {
R result;
try {
String resultJson = restTemplateClient.execute(url, method, params);
result = JSON.parseObject(resultJson, resType);
} catch (Exception e) {
throw new WeChatException(e);
}
if (StringUtils.isNotBlank(result.getErrcode())
&& !WeChatApi.WECHAT_REQUEST_SUCCESS_CODE.equals(result.getErrcode())) {
if(WeChatErrorCodeEnum.INVALID_CREDENTIAL.getCode().equals(result.getErrcode())){
// token过期,清除微信小店认证信息
redisClient.delete(CoreConstant.REDIS_TOKEN_WECHAT_SHOP_KEY);
}
throw new WeChatException(String.join(",","调用微信接口失败" + result.getErrmsg()));
}
return result;
}
}
package com.netease.yanxuan.wx.store.sharer.integration.handler.impl;
import com.netease.yanxuan.wx.store.sharer.integration.config.WeChatConfig;
import com.netease.yanxuan.wx.store.sharer.integration.constant.WeChatApi;
import com.netease.yanxuan.wx.store.sharer.integration.handler.IWeChatRequest;
import com.netease.yanxuan.wx.store.sharer.integration.handler.WeChatRestTemplateHandler;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo.WeChatAccessTokenBO;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.vo.WeChatAccessTokenVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
/**
* @Description 微信凭证请求
* @Author fanjiaxin
* @Date 2025/3/11 17:33
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WeChatAccessTokenRequest implements IWeChatRequest {
private final WeChatRestTemplateHandler weChatRestTemplateHandler;
private final WeChatConfig weChatConfig;
private static final String GRANT_TYPE = "client_credential";
@Override
public HttpMethod getRequestMethod() {
return HttpMethod.GET;
}
@Override
public String getRequestUrl() {
return weChatConfig.getUrl() + WeChatApi.GET_TOKEN;
}
/**
* 处理
*/
public WeChatAccessTokenVO handle() {
WeChatAccessTokenBO params = WeChatAccessTokenBO.builder()
.grant_type(GRANT_TYPE)
.appid(weChatConfig.getAppid())
.secret(weChatConfig.getAppsecret())
.build();
return weChatRestTemplateHandler.execute(getRequestUrl(), getRequestMethod(), params, WeChatAccessTokenVO.class);
}
}
package com.netease.yanxuan.wx.store.sharer.integration.handler.impl;
import com.netease.yanxuan.wx.store.sharer.integration.config.WeChatConfig;
import com.netease.yanxuan.wx.store.sharer.integration.constant.WeChatApi;
import com.netease.yanxuan.wx.store.sharer.integration.handler.IWeChatRequest;
import com.netease.yanxuan.wx.store.sharer.integration.handler.WeChatRestTemplateHandler;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo.WeChatAccessTokenBO;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.vo.WeChatAccessTokenVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
/**
* @Description 微信小店凭证请求
* @Author fanjiaxin
* @Date 2025/3/11 17:33
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WeChatShopAccessTokenRequest implements IWeChatRequest {
private final WeChatRestTemplateHandler weChatRestTemplateHandler;
private final WeChatConfig weChatConfig;
private static final String GRANT_TYPE = "client_credential";
@Override
public HttpMethod getRequestMethod() {
return HttpMethod.GET;
}
@Override
public String getRequestUrl() {
return weChatConfig.getUrl() + WeChatApi.GET_TOKEN;
}
/**
* 处理
*/
public WeChatAccessTokenVO handle() {
WeChatAccessTokenBO params = WeChatAccessTokenBO.builder()
.grant_type(GRANT_TYPE)
.appid(weChatConfig.getShopAppid())
.secret(weChatConfig.getShopAppsecret())
.build();
return weChatRestTemplateHandler.execute(getRequestUrl(), getRequestMethod(), params, WeChatAccessTokenVO.class);
}
}
package com.netease.yanxuan.wx.store.sharer.integration.handler.impl;
import com.netease.yanxuan.wx.store.sharer.integration.config.WeChatConfig;
import com.netease.yanxuan.wx.store.sharer.integration.constant.WeChatApi;
import com.netease.yanxuan.wx.store.sharer.integration.core.WeChatContextHolder;
import com.netease.yanxuan.wx.store.sharer.integration.handler.IWeChatRequest;
import com.netease.yanxuan.wx.store.sharer.integration.handler.WeChatRestTemplateHandler;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo.WeChatGetSharerProductCommissionBO;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.vo.WeChatGetSharerProductCommissionVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
/**
* @Description 设置推客的单个商品的分佣比例信息
* @Author fanjiaxin
* @Date 2025/3/11 17:33
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WeChatShopGetSharerProductCommissionRequest implements IWeChatRequest {
private final WeChatContextHolder weChatContextHolder;
private final WeChatRestTemplateHandler weChatRestTemplateHandler;
private final WeChatConfig weChatConfig;
@Override
public HttpMethod getRequestMethod() {
return HttpMethod.POST;
}
@Override
public String getRequestUrl() {
return weChatConfig.getUrl() + WeChatApi.GET_SHARER_PRODUCT_COMMISSION_INFO
+ "?" + WeChatApi.WECHAT_REQUEST_TOKEN_FIELD_NAME +"="
+ weChatContextHolder.getShopAccessToken();
}
/**
* 处理
*/
public WeChatGetSharerProductCommissionVO handle(String sharerAppid, Long productId) {
WeChatGetSharerProductCommissionBO params = WeChatGetSharerProductCommissionBO.builder()
.sharer_appid(sharerAppid)
.product_id(productId)
.build();
return weChatRestTemplateHandler.execute(getRequestUrl(), getRequestMethod(), params, WeChatGetSharerProductCommissionVO.class);
}
}
package com.netease.yanxuan.wx.store.sharer.integration.handler.impl;
import com.netease.yanxuan.wx.store.sharer.integration.config.WeChatConfig;
import com.netease.yanxuan.wx.store.sharer.integration.constant.WeChatApi;
import com.netease.yanxuan.wx.store.sharer.integration.core.WeChatContextHolder;
import com.netease.yanxuan.wx.store.sharer.integration.handler.IWeChatRequest;
import com.netease.yanxuan.wx.store.sharer.integration.handler.WeChatRestTemplateHandler;
import com.netease.yanxuan.wx.store.sharer.integration.meta.enums.ProductPlanTypeEnum;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo.WeChatPromoteProductDetailBO;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.vo.WeChatPromoteProductDetailVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
/**
* @Description 商品详情
* @Author fanjiaxin
* @Date 2025/3/11 17:33
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WeChatShopPromoteProductDetailRequest implements IWeChatRequest {
private final WeChatContextHolder weChatContextHolder;
private final WeChatRestTemplateHandler weChatRestTemplateHandler;
private final WeChatConfig weChatConfig;
@Override
public HttpMethod getRequestMethod() {
return HttpMethod.POST;
}
@Override
public String getRequestUrl() {
return weChatConfig.getUrl() + WeChatApi.GET_PROMOTE_PRODUCT_DETAIL
+ "?" + WeChatApi.WECHAT_REQUEST_TOKEN_FIELD_NAME + "="
+ weChatContextHolder.getShopAccessToken();
}
/**
* 处理
*/
public WeChatPromoteProductDetailVO handle(String shopAppid, Long productId) {
WeChatPromoteProductDetailBO params = WeChatPromoteProductDetailBO.builder()
.plan_type(ProductPlanTypeEnum.TARGET.getCode())
.shop_appid(shopAppid)
.product_id(productId)
.build();
return weChatRestTemplateHandler.execute(getRequestUrl(), getRequestMethod(), params, WeChatPromoteProductDetailVO.class);
}
}
package com.netease.yanxuan.wx.store.sharer.integration.handler.impl;
import com.netease.yanxuan.wx.store.sharer.integration.config.WeChatConfig;
import com.netease.yanxuan.wx.store.sharer.integration.constant.WeChatApi;
import com.netease.yanxuan.wx.store.sharer.integration.core.WeChatContextHolder;
import com.netease.yanxuan.wx.store.sharer.integration.handler.IWeChatRequest;
import com.netease.yanxuan.wx.store.sharer.integration.handler.WeChatRestTemplateHandler;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo.WeChatPromoteProductLinkBO;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.vo.WeChatPromoteProductLinkVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
/**
* @Description 商品推广链接
* @Author fanjiaxin
* @Date 2025/3/11 17:33
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WeChatShopPromoteProductLinkRequest implements IWeChatRequest {
private final WeChatContextHolder weChatContextHolder;
private final WeChatRestTemplateHandler weChatRestTemplateHandler;
private final WeChatConfig weChatConfig;
@Override
public HttpMethod getRequestMethod() {
return HttpMethod.POST;
}
@Override
public String getRequestUrl() {
return weChatConfig.getUrl() + WeChatApi.GET_PRODUCT_PROMOTION_LINK_INFO
+ "?" + WeChatApi.WECHAT_REQUEST_TOKEN_FIELD_NAME + "="
+ weChatContextHolder.getShopAccessToken();
}
/**
* 处理
*/
public WeChatPromoteProductLinkVO handle(String sharerAppId, Long productId, String shopAppid) {
WeChatPromoteProductLinkBO params = WeChatPromoteProductLinkBO.builder()
.sharer_appid(sharerAppId)
.product_id(productId)
.shop_appid(shopAppid)
.build();
return weChatRestTemplateHandler.execute(getRequestUrl(), getRequestMethod(), params, WeChatPromoteProductLinkVO.class);
}
}
package com.netease.yanxuan.wx.store.sharer.integration.handler.impl;
import com.netease.yanxuan.wx.store.sharer.integration.config.WeChatConfig;
import com.netease.yanxuan.wx.store.sharer.integration.constant.WeChatApi;
import com.netease.yanxuan.wx.store.sharer.integration.core.WeChatContextHolder;
import com.netease.yanxuan.wx.store.sharer.integration.handler.IWeChatRequest;
import com.netease.yanxuan.wx.store.sharer.integration.handler.WeChatRestTemplateHandler;
import com.netease.yanxuan.wx.store.sharer.integration.meta.enums.ProductPlanTypeEnum;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo.WeChatPromoteProductListBO;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.vo.WeChatPromoteProductListVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
/**
* @Description 商品列表
* @Author fanjiaxin
* @Date 2025/3/11 17:33
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WeChatShopPromoteProductListRequest implements IWeChatRequest {
private final WeChatContextHolder weChatContextHolder;
private final WeChatRestTemplateHandler weChatRestTemplateHandler;
private final WeChatConfig weChatConfig;
@Override
public HttpMethod getRequestMethod() {
return HttpMethod.POST;
}
@Override
public String getRequestUrl() {
return weChatConfig.getUrl() + WeChatApi.GET_PROMOTE_PRODUCT_LIST
+ "?" + WeChatApi.WECHAT_REQUEST_TOKEN_FIELD_NAME + "="
+ weChatContextHolder.getShopAccessToken();
}
/**
* 处理
*/
public WeChatPromoteProductListVO handle(String keyword, String nextKey, Integer pageSize) {
WeChatPromoteProductListBO params = WeChatPromoteProductListBO.builder()
.plan_type(ProductPlanTypeEnum.TARGET.getCode())
.keyword(keyword)
.next_key(nextKey)
.page_size(pageSize)
.build();
return weChatRestTemplateHandler.execute(getRequestUrl(), getRequestMethod(), params, WeChatPromoteProductListVO.class);
}
}
package com.netease.yanxuan.wx.store.sharer.integration.handler.impl;
import com.netease.yanxuan.wx.store.sharer.integration.config.WeChatConfig;
import com.netease.yanxuan.wx.store.sharer.integration.constant.WeChatApi;
import com.netease.yanxuan.wx.store.sharer.integration.core.WeChatContextHolder;
import com.netease.yanxuan.wx.store.sharer.integration.handler.IWeChatRequest;
import com.netease.yanxuan.wx.store.sharer.integration.handler.WeChatRestTemplateHandler;
import com.netease.yanxuan.wx.store.sharer.integration.meta.enums.CommissionTypeEnum;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo.WeChatSetSharerCommissionBO;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.vo.WeChatCoreVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
/**
* @Description 设置推客的分佣比例信息
* @Author fanjiaxin
* @Date 2025/3/11 17:33
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WeChatShopSetSharerCommissionRequest implements IWeChatRequest {
private final WeChatContextHolder weChatContextHolder;
private final WeChatRestTemplateHandler weChatRestTemplateHandler;
private final WeChatConfig weChatConfig;
@Override
public HttpMethod getRequestMethod() {
return HttpMethod.POST;
}
@Override
public String getRequestUrl() {
return weChatConfig.getUrl() + WeChatApi.SET_SHARER_COMMISSION_INFO
+ "?" + WeChatApi.WECHAT_REQUEST_TOKEN_FIELD_NAME + "="
+ weChatContextHolder.getShopAccessToken();
}
/**
* 处理
*/
public WeChatCoreVO handle(String sharerAppid, Long commissionRatio) {
WeChatSetSharerCommissionBO params = WeChatSetSharerCommissionBO.builder()
.sharer_appid(sharerAppid)
.commission_type(CommissionTypeEnum.PLATFORM.getCode())
.commission_ratio(commissionRatio)
.build();
return weChatRestTemplateHandler.execute(getRequestUrl(), getRequestMethod(), params, WeChatCoreVO.class);
}
}
package com.netease.yanxuan.wx.store.sharer.integration.handler.impl;
import com.netease.yanxuan.wx.store.sharer.integration.config.WeChatConfig;
import com.netease.yanxuan.wx.store.sharer.integration.constant.WeChatApi;
import com.netease.yanxuan.wx.store.sharer.integration.core.WeChatContextHolder;
import com.netease.yanxuan.wx.store.sharer.integration.handler.IWeChatRequest;
import com.netease.yanxuan.wx.store.sharer.integration.handler.WeChatRestTemplateHandler;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo.WeChatSetSharerProductCommissionBO;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.vo.WeChatCoreVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
/**
* @Description 设置推客的单个商品的分佣比例信息
* @Author fanjiaxin
* @Date 2025/3/11 17:33
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WeChatShopSetSharerProductCommissionRequest implements IWeChatRequest {
private final WeChatContextHolder weChatContextHolder;
private final WeChatRestTemplateHandler weChatRestTemplateHandler;
private final WeChatConfig weChatConfig;
@Override
public HttpMethod getRequestMethod() {
return HttpMethod.POST;
}
@Override
public String getRequestUrl() {
return weChatConfig.getUrl() + WeChatApi.SET_SHARER_PRODUCT_COMMISSION_INFO
+ "?" + WeChatApi.WECHAT_REQUEST_TOKEN_FIELD_NAME + "="
+ weChatContextHolder.getShopAccessToken();
}
/**
* 处理
*/
public WeChatCoreVO handle(String sharerAppid, Long productId, Long commissionRatio) {
WeChatSetSharerProductCommissionBO params = WeChatSetSharerProductCommissionBO.builder()
.sharer_appid(sharerAppid)
.product_id(productId)
.commission_ratio(commissionRatio)
.build();
return weChatRestTemplateHandler.execute(getRequestUrl(), getRequestMethod(), params, WeChatCoreVO.class);
}
}
package com.netease.yanxuan.wx.store.sharer.integration.handler.impl;
import com.netease.yanxuan.wx.store.sharer.integration.config.WeChatConfig;
import com.netease.yanxuan.wx.store.sharer.integration.constant.WeChatApi;
import com.netease.yanxuan.wx.store.sharer.integration.core.WeChatContextHolder;
import com.netease.yanxuan.wx.store.sharer.integration.handler.IWeChatRequest;
import com.netease.yanxuan.wx.store.sharer.integration.handler.WeChatRestTemplateHandler;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo.WeChatSharerListBO;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.vo.WeChatSharerListVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
/**
* @Description 获取机构绑定的推客信息
* @Author fanjiaxin
* @Date 2025/3/11 17:33
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WeChatShopSharerListRequest implements IWeChatRequest {
private final WeChatContextHolder weChatContextHolder;
private final WeChatRestTemplateHandler weChatRestTemplateHandler;
private final WeChatConfig weChatConfig;
@Override
public HttpMethod getRequestMethod() {
return HttpMethod.POST;
}
@Override
public String getRequestUrl() {
return weChatConfig.getUrl() + WeChatApi.GET_BIND_SHARER_LIST
+ "?" + WeChatApi.WECHAT_REQUEST_TOKEN_FIELD_NAME + "="
+ weChatContextHolder.getShopAccessToken();
}
/**
* 处理
*/
public WeChatSharerListVO handle(String sharerOpenid, String nextKey, Integer pageSize) {
WeChatSharerListBO params = WeChatSharerListBO.builder()
.sharer_openid(sharerOpenid)
.next_key(nextKey)
.page_size(pageSize)
.build();
return weChatRestTemplateHandler.execute(getRequestUrl(), getRequestMethod(), params, WeChatSharerListVO.class);
}
}
package com.netease.yanxuan.wx.store.sharer.integration.handler.impl;
import com.netease.yanxuan.wx.store.sharer.integration.config.WeChatConfig;
import com.netease.yanxuan.wx.store.sharer.integration.constant.WeChatApi;
import com.netease.yanxuan.wx.store.sharer.integration.core.WeChatContextHolder;
import com.netease.yanxuan.wx.store.sharer.integration.handler.IWeChatRequest;
import com.netease.yanxuan.wx.store.sharer.integration.handler.WeChatRestTemplateHandler;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo.WeChatSharerRegisterBindBO;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.vo.WeChatSharerRegisterBindVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
/**
* @Description 微信凭证请求
* @Author fanjiaxin
* @Date 2025/3/11 17:33
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WeChatShopSharerRegisterBindRequest implements IWeChatRequest {
private final WeChatContextHolder weChatContextHolder;
private final WeChatRestTemplateHandler weChatRestTemplateHandler;
private final WeChatConfig weChatConfig;
@Override
public HttpMethod getRequestMethod() {
return HttpMethod.POST;
}
@Override
public String getRequestUrl() {
return weChatConfig.getUrl() + WeChatApi.GET_PROMOTER_REGISTER_AND_BIND_STATUS
+ "?" + WeChatApi.WECHAT_REQUEST_TOKEN_FIELD_NAME + "="
+ weChatContextHolder.getShopAccessToken();
}
/**
* 处理
*/
public WeChatSharerRegisterBindVO handle(String sharerOpenid) {
WeChatSharerRegisterBindBO params = WeChatSharerRegisterBindBO.builder()
.is_simple_register(true)
.sharer_openid(sharerOpenid)
.build();
return weChatRestTemplateHandler.execute(getRequestUrl(), getRequestMethod(), params, WeChatSharerRegisterBindVO.class);
}
}
package com.netease.yanxuan.wx.store.sharer.integration.handler.impl;
import com.netease.yanxuan.wx.store.sharer.integration.config.WeChatConfig;
import com.netease.yanxuan.wx.store.sharer.integration.constant.WeChatApi;
import com.netease.yanxuan.wx.store.sharer.integration.handler.IWeChatRequest;
import com.netease.yanxuan.wx.store.sharer.integration.handler.WeChatRestTemplateHandler;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo.WeChatUserInfoBO;
import com.netease.yanxuan.wx.store.sharer.integration.meta.model.vo.WeChatUserInfoVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
/**
* @Description 微信凭证请求
* @Author fanjiaxin
* @Date 2025/3/11 17:33
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WeChatUserInfoRequest implements IWeChatRequest {
private final WeChatRestTemplateHandler weChatRestTemplateHandler;
private final WeChatConfig weChatConfig;
private static final String GRANT_TYPE = "authorization_code";
@Override
public HttpMethod getRequestMethod() {
return HttpMethod.GET;
}
@Override
public String getRequestUrl() {
return weChatConfig.getUrl() + WeChatApi.GET_USER_INFO;
}
/**
* 处理
*/
public WeChatUserInfoVO handle(String code) {
WeChatUserInfoBO params = WeChatUserInfoBO.builder()
.appid(weChatConfig.getAppid())
.secret(weChatConfig.getAppsecret())
.js_code(code)
.grant_type(GRANT_TYPE)
.build();
return weChatRestTemplateHandler.execute(getRequestUrl(), getRequestMethod(), params, WeChatUserInfoVO.class);
}
}
package com.netease.yanxuan.wx.store.sharer.integration.meta.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* @Description 分佣类型
* @Author fanjiaxin
* @Date 2025/3/3 13:58
*/
@Getter
@RequiredArgsConstructor
public enum CommissionTypeEnum {
PLATFORM(0, "平台分佣"),
AGENCY(1, "机构分佣");
private final Integer code;
private final String desc;
}
\ No newline at end of file
package com.netease.yanxuan.wx.store.sharer.integration.meta.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* @Description 商品的计划类型
* @Author fanjiaxin
* @Date 2025/3/3 13:58
*/
@Getter
@RequiredArgsConstructor
public enum ProductPlanTypeEnum {
TARGET(1, "定向计划"),
OPEN(2, "公开计划");
private final Integer code;
private final String desc;
}
\ No newline at end of file
package com.netease.yanxuan.wx.store.sharer.integration.meta.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* @Description 微信错误码
* @Author fanjiaxin
* @Date 2025/3/3 13:58
*/
@Getter
@RequiredArgsConstructor
public enum WeChatErrorCodeEnum {
INVALID_CREDENTIAL("40001", "获取 access_token 时 AppSecret 错误,或者 access_token 无效。");
private final String code;
private final String desc;
}
\ No newline at end of file
package com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
/**
* @Description 用户认证-业务对象
* @Author fanjiaxin
* @Date 2025/3/11 18:43
*/
@Data
@Builder
public class WeChatAccessTokenBO implements Serializable {
private static final long serialVersionUID = 1996751915518651231L;
/**
* 小店唯一凭证
*/
private String appid;
/**
* 小店唯一凭证密钥
*/
private String secret;
/**
* 授权类型
*/
private String grant_type;
}
package com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
/**
* @Description 获取推客的单个商品的分佣比例信息-视图对象
* @Author fanjiaxin
* @Date 2025/3/11 19:05
*/
@Data
@Builder
public class WeChatGetSharerProductCommissionBO implements Serializable {
private static final long serialVersionUID = 8529644543147459802L;
/**
* 推客在微信电商平台注册的身份标识
*/
private String sharer_appid;
/**
* 商品列表
*/
private Long product_id;
}
package com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
/**
* @Description 微信分页参数-业务对象
* @Author fanjiaxin
* @Date 2025/3/11 18:43
*/
@Data
@Builder
public class WeChatPageBO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 分页参数,第一页为空,后面返回前面一页返回的数据
*/
private String next_key;
/**
* 一页获取多少个推客,最大 20
*/
private Integer page_size;
}
package com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
/**
* @Description 商品详情-业务对象
* @Author fanjiaxin
* @Date 2025/3/11 18:43
*/
@Data
@Builder
public class WeChatPromoteProductDetailBO implements Serializable {
private static final long serialVersionUID = 1996751915518651231L;
/**
* 所属小店appid
*/
private String shop_appid;
/**
* 商品id
*/
private Long product_id;
/**
* 商品的计划类型 1:定向计划 2:公开计划
*/
private Integer plan_type;
/**
* 填true返回商品可用的机构券
*/
private Boolean get_available_coupon;
}
package com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
/**
* @Description 商品推广链接-业务对象
* @Author fanjiaxin
* @Date 2025/3/11 18:43
*/
@Data
@Builder
public class WeChatPromoteProductLinkBO implements Serializable {
private static final long serialVersionUID = 1996751915518651231L;
/**
* 推客 appid,和sharer_openid二选一
*/
private String sharer_appid;
/**
* 推客在小程序中的openid,和sharer_appid二选一
*/
private String sharer_openid;
/**
* 商品 id,如果使用该参数,需要传入shop_appid
*/
private Long product_id;
/**
* 商品所属店铺 appid
*/
private String shop_appid;
/**
* 商品短链,和 product_id 二选一
*/
private String product_short_link;
}
package com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
/**
* @Description 商品列表-业务对象
* @Author fanjiaxin
* @Date 2025/3/11 18:43
*/
@Data
@Builder
public class WeChatPromoteProductListBO implements Serializable {
private static final long serialVersionUID = 1996751915518651231L;
/**
* 商品的计划类型 1:定向计划 2:公开计划
*/
private Integer plan_type;
/**
* 搜索关键词
*/
private String keyword;
/**
* 分页参数,第一页为空,后面返回前面一页返回的数据
*/
private String next_key;
/**
* 一页获取多少个推客,最大 20
*/
private Integer page_size;
}
package com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
/**
* @Description 设置推客的的分佣类型和比例信息-业务对象
* @Author fanjiaxin
* @Date 2025/3/11 19:05
*/
@Data
@Builder
public class WeChatSetSharerCommissionBO implements Serializable {
private static final long serialVersionUID = 8529644543147459802L;
/**
* 推客在微信电商平台注册的身份标识
*/
private String sharer_appid;
/**
* 分佣类型【 0:平台分佣, 1:机构自己分佣】
*/
private Integer commission_type;
/**
* 平台分佣时的分佣比例,范围为【100000 - 900000】,代表【10%-90%】
*/
private Long commission_ratio;
}
package com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
/**
* @Description 设置推客的单个商品的分佣比例信息-业务对象
* @Author fanjiaxin
* @Date 2025/3/11 19:05
*/
@Data
@Builder
public class WeChatSetSharerProductCommissionBO implements Serializable {
private static final long serialVersionUID = 8529644543147459802L;
/**
* 推客在微信电商平台注册的身份标识
*/
private String sharer_appid;
/**
* 商品列表
*/
private Long product_id;
/**
* 平台分佣时的分佣比例,范围为【100000 - 900000】,代表【10%-90%】
*/
private Long commission_ratio;
}
package com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
/**
* @Description 推客列表-业务对象
* @Author fanjiaxin
* @Date 2025/3/11 18:43
*/
@Data
@Builder
public class WeChatSharerListBO implements Serializable {
private static final long serialVersionUID = 1996751915518651231L;
/**
* 查询某个推客,传入推客的小程序openid
*/
private String sharer_openid;
/**
* 查询某个推客,传入推客的appid
*/
private String sharer_appid;
/**
* 分页参数,第一页为空,后面返回前面一页返回的数据
*/
private String next_key;
/**
* 一页获取多少个推客,最大 20
*/
private Integer page_size;
}
package com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
/**
* @Description 推客注册绑定信息-业务对象
* @Author fanjiaxin
* @Date 2025/3/11 18:43
*/
@Data
@Builder
public class WeChatSharerRegisterBindBO implements Serializable {
private static final long serialVersionUID = 1996751915518651231L;
/**
* 推客在小程序中的 openid,和 sharer_appid 二选一
*/
private String sharer_openid;
/**
* 推客在微信电商平台注册的身份标识,和 sharer_openid 二选一
*/
private String sharer_appid;
/**
* 是否走简易版本注册【当走简易版本注册时,可以不要求推客开通商户号,但分佣只能走机构自己分佣;如果不走简易注册流程时,要求推客开通商户号作为收款账户,可以平台分佣】
*/
private Boolean is_simple_register;
}
package com.netease.yanxuan.wx.store.sharer.integration.meta.model.bo;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
/**
* @Description 微信用户信息-业务对象
* @Author fanjiaxin
* @Date 2025/3/11 18:43
*/
@Data
@Builder
public class WeChatUserInfoBO implements Serializable {
private static final long serialVersionUID = 1996751915518651231L;
/**
* 小店唯一凭证
*/
private String appid;
/**
* 小店唯一凭证密钥
*/
private String secret;
/**
* 登录时获取的 code
*/
private String js_code;
/**
* 授权类型
*/
private String grant_type;
}
package com.netease.yanxuan.wx.store.sharer.integration.meta.model.vo;
import lombok.Data;
/**
* @Description 微信凭证-视图对象
* @Author fanjiaxin
* @Date 2025/3/11 19:05
*/
@Data
public class WeChatAccessTokenVO extends WeChatCoreVO {
private static final long serialVersionUID = 8529644543147459802L;
/**
* 获取到的凭证
*/
private String access_token;
/**
* 凭证有效时间,单位:秒。目前是7200秒之内的值
*/
private Integer expires_in;
}
package com.netease.yanxuan.wx.store.sharer.integration.meta.model.vo;
import lombok.Data;
import java.io.Serializable;
/**
* @Description 微信凭证-核心对象
* @Author fanjiaxin
* @Date 2025/3/11 19:05
*/
@Data
public class WeChatCoreVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 错误编码
*/
private String errcode;
/**
* 错误信息
*/
private String errmsg;
}
package com.netease.yanxuan.wx.store.sharer.integration.meta.model.vo;
import lombok.Data;
/**
* @Description 设置推客的单个商品的分佣比例信息-视图对象
* @Author fanjiaxin
* @Date 2025/3/11 19:05
*/
@Data
public class WeChatGetSharerProductCommissionVO extends WeChatCoreVO {
private static final long serialVersionUID = 8529644543147459802L;
/**
* 平台分佣时的分佣比例,范围为【100000 - 900000】,代表【10%-90%】
*/
private String commission_ratio;
/**
* 是否设置了这个商品的佣金率
*/
private Boolean is_set;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment