Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;

/**
* 异步任务配置类
*
* <p>通过 {@code @EnableAsync} 开启 Spring 对方法级异步调用的支持,
* 使发短信、发邮件、推送模板消息等非核心耗时操作可以从主请求线程中剥离、
* 在独立线程池中后台执行,避免阻塞小程序接口的响应。</p>
*
* @author litemall
* @since 1.0
*/
@Configuration
@EnableAsync
public class AsyncConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,47 @@
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
* 跨域资源共享(CORS)配置类
*
* <p>浏览器中的 H5 或管理端与后端接口属于不同源(协议、域名、端口任一不同即跨域),
* 浏览器会拦截跨域请求。这里通过注册一个全局 CorsFilter,对 {@code /**} 所有接口
* 放行跨域预检与实际请求,使前端能够正常调用后端 API。</p>
*
* <p>注意:当前为方便演示,放行源、请求头、请求方法均为 "*",生产环境应收紧为具体域名。</p>
*
* @author litemall
* @since 1.0
*/
@Configuration
public class CorsConfig {
// 当前跨域请求最大有效时长。这里默认30天
/** 跨域预检请求(OPTIONS)的缓存时长,单位秒;设为 30 天,避免每次请求都发预检 */
private long maxAge = 30 * 24 * 60 * 60;

/**
* 构造一条 CORS 规则:允许任意来源、任意请求头、任意 HTTP 方法,并携带凭证
*
* @return 配置好的 CorsConfiguration
*/
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*"); // 1 设置访问源地址
corsConfiguration.addAllowedHeader("*"); // 2 设置访问源请求头
corsConfiguration.addAllowedMethod("*"); // 3 设置访问源请求方法
corsConfiguration.addAllowedOrigin("*"); // 允许任意访问源(生产环境建议改为具体域名)
corsConfiguration.addAllowedHeader("*"); // 允许请求携带任意请求头
corsConfiguration.addAllowedMethod("*"); // 允许 GET/POST/PUT/DELETE 等任意方法
corsConfiguration.setMaxAge(maxAge);
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setAllowCredentials(true); // 允许携带 Cookie 等凭证信息
return corsConfiguration;
}

/**
* 将跨域规则注册到所有接口路径上,交由 Spring 过滤器链处理
*
* @return 全局跨域过滤器
*/
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig()); // 4 对接口配置跨域设置
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,40 +17,58 @@
import javax.validation.ValidationException;
import java.util.Set;

/**
* 全局异常处理器
*
* <p>统一拦截 Controller 层抛出的异常,避免异常堆栈直接暴露给前端。
* 所有被捕获的异常都会被转换成 {@link ResponseUtil} 约定的统一 JSON 结构
* (错误码 + 提示消息),同时打印错误日志便于后端排查。</p>
*
* <p>方法命名统一为 badArgument / serious 两类:参数类问题返回“参数错误”,
* 其余未预期异常统一返回“系统错误”。</p>
*
* @author litemall
* @since 1.0
*/
@ControllerAdvice
@Order
public class GlobalExceptionHandler {

private Log logger = LogFactory.getLog(GlobalExceptionHandler.class);

/** 非法入参:业务代码主动抛出的 IllegalArgumentException */
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
public Object badArgumentHandler(IllegalArgumentException e) {
logger.error(e.getMessage(), e);
return ResponseUtil.badArgumentValue();
}

/** 参数类型不匹配:如预期 Integer 却传入了字符串 */
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseBody
public Object badArgumentHandler(MethodArgumentTypeMismatchException e) {
logger.error(e.getMessage(), e);
return ResponseUtil.badArgumentValue();
}

/** 缺少必填的请求参数 */
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseBody
public Object badArgumentHandler(MissingServletRequestParameterException e) {
logger.error(e.getMessage(), e);
return ResponseUtil.badArgumentValue();
}

/** 请求体不可读:JSON 格式错误导致无法反序列化 */
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseBody
public Object badArgumentHandler(HttpMessageNotReadableException e) {
logger.error(e.getMessage(), e);
return ResponseUtil.badArgumentValue();
}

/** 参数校验异常:提取第一个校验失败字段及其提示信息,返回 402 */
@ExceptionHandler(ValidationException.class)
@ResponseBody
public Object badArgumentHandler(ValidationException e) {
Expand All @@ -66,6 +84,7 @@ public Object badArgumentHandler(ValidationException e) {
return ResponseUtil.badArgumentValue();
}

/** 兜底处理:所有未被上面匹配的异常,统一按系统错误返回 */
@ExceptionHandler(Exception.class)
@ResponseBody
public Object seriousHandler(Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,34 @@
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

/**
* Jackson JSON 序列化全局配置
*
* <p>统一规定后端返回给前端的 JSON 规则:</p>
* <ul>
* <li>时间类型按 “yyyy-MM-dd HH:mm:ss / yyyy-MM-dd / HH:mm:ss” 格式化,不再输出时间戳;</li>
* <li>值为 null 的字段不输出,减小报文体积;</li>
* <li>反序列化时忽略前端多传的未知字段,避免直接报错。</li>
* </ul>
*
* @author litemall
* @since 1.0
*/
@Configuration
public class JacksonConfig {

/**
* 自定义 ObjectMapper 构建器,最高优先级覆盖 Spring Boot 默认配置
*
* @return Jackson2ObjectMapperBuilderCustomizer 定制器
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public Jackson2ObjectMapperBuilderCustomizer customJackson() {
return new Jackson2ObjectMapperBuilderCustomizer() {
@Override
public void customize(Jackson2ObjectMapperBuilder builder) {
// LocalDateTime / LocalDate / LocalTime 的序列化与反序列化统一格式
builder.serializerByType(LocalDateTime.class,
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
builder.serializerByType(LocalDate.class,
Expand All @@ -41,8 +60,11 @@ public void customize(Jackson2ObjectMapperBuilder builder) {
new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
builder.deserializerByType(LocalTime.class,
new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
// null 字段不参与序列化
builder.serializationInclusion(JsonInclude.Include.NON_NULL);
// 前端多传未知字段时不报错,提升接口兼容性
builder.failOnUnknownProperties(false);
// 关闭把日期写成时间戳数组的默认行为
builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,30 @@
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

/**
* 参数校验器配置类
*
* <p>配置 JSR-303 参数校验(@Valid / @NotBlank 等)使用 Hibernate Validator 实现,
* 并开启 fail_fast:一旦遇到第一个校验失败就立即返回,不必等全部字段校验完,
* 便于接口快速给出明确的错误提示。</p>
*
* @author litemall
* @since 1.0
*/
@Configuration
public class ValidatorConfiguration {
/**
* 构造并注册全局 Validator
*
* @return fail-fast 模式的 Validator 实例
*/
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
.addProperty("hibernate.validator.fail_fast", "true")
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

return validator;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,24 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* 微信小程序与微信支付配置类
*
* <p>把 {@link WxProperties}(来自 application.yml 的 litemall.wx.* 配置)装配成
* 小程序服务 {@link WxMaService} 和微信支付服务 {@link WxPayService} 两个 bean,
* 供登录、解密用户数据、统一下单等业务直接注入使用。</p>
*
* @author litemall
* @since 1.0
*/
@Configuration
public class WxConfig {
@Autowired
private WxProperties properties;

/**
* 小程序连接配置:appid、secret、消息校验 token、消息加解密密钥
*/
@Bean
public WxMaConfig wxMaConfig() {
WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
Expand All @@ -26,14 +39,18 @@ public WxMaConfig wxMaConfig() {
return config;
}


/** 小程序服务:封装 code 换 session、获取用户信息等能力 */
@Bean
public WxMaService wxMaService(WxMaConfig maConfig) {
WxMaService service = new WxMaServiceImpl();
service.setWxMaConfig(maConfig);
return service;
}

/**
* 微信支付配置:商户号 mchId、商户密钥 mchKey、支付结果通知地址等,
* 交易类型固定 JSAPI(小程序内支付),签名方式 MD5
*/
@Bean
public WxPayConfig wxPayConfig() {
WxPayConfig payConfig = new WxPayConfig();
Expand All @@ -47,11 +64,11 @@ public WxPayConfig wxPayConfig() {
return payConfig;
}


/** 微信支付服务:封装统一下单、查询订单、退款等能力 */
@Bean
public WxPayService wxPayService(WxPayConfig payConfig) {
WxPayService wxPayService = new WxPayServiceImpl();
wxPayService.setConfig(payConfig);
return wxPayService;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,41 @@
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
* 微信小程序 / 微信支付配置属性
*
* <p>将 application.yml 中前缀为 {@code litemall.wx} 的配置项绑定到本类字段,
* 供 {@link WxConfig} 装配微信服务时读取。敏感信息(secret、mchKey)应放在配置文件或环境变量中,不要写死在代码里。</p>
*
* @author litemall
* @since 1.0
*/
@Configuration
@ConfigurationProperties(prefix = "litemall.wx")
public class WxProperties {

/** 小程序唯一标识(appid) */
private String appId;

/** 小程序 appSecret,用于 code 换 session_key */
private String appSecret;

/** 微信支付商户号 mch-id */
private String mchId;

/** 微信支付商户密钥 mch-key */
private String mchKey;

/** 支付结果异步通知地址 notify-url */
private String notifyUrl;

/** 商户证书本地路径 key-path */
private String keyPath;

/** 小程序服务器消息校验 token */
private String token;

/** 小程序消息加解密 aes-key */
private String aesKey;

public String getToken() {
Expand Down
Loading