diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/config/AsyncConfig.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/config/AsyncConfig.java index 02534a18e..f1e71362a 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/config/AsyncConfig.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/config/AsyncConfig.java @@ -3,6 +3,16 @@ import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; +/** + * 异步任务配置类 + * + *

通过 {@code @EnableAsync} 开启 Spring 对方法级异步调用的支持, + * 使发短信、发邮件、推送模板消息等非核心耗时操作可以从主请求线程中剥离、 + * 在独立线程池中后台执行,避免阻塞小程序接口的响应。

+ * + * @author litemall + * @since 1.0 + */ @Configuration @EnableAsync public class AsyncConfig { diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/config/CorsConfig.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/config/CorsConfig.java index 29f01ea25..d785fa22c 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/config/CorsConfig.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/config/CorsConfig.java @@ -6,25 +6,47 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; +/** + * 跨域资源共享(CORS)配置类 + * + *

浏览器中的 H5 或管理端与后端接口属于不同源(协议、域名、端口任一不同即跨域), + * 浏览器会拦截跨域请求。这里通过注册一个全局 CorsFilter,对 {@code /**} 所有接口 + * 放行跨域预检与实际请求,使前端能够正常调用后端 API。

+ * + *

注意:当前为方便演示,放行源、请求头、请求方法均为 "*",生产环境应收紧为具体域名。

+ * + * @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); } } diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/config/GlobalExceptionHandler.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/config/GlobalExceptionHandler.java index a14078ef4..646074eb4 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/config/GlobalExceptionHandler.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/config/GlobalExceptionHandler.java @@ -17,12 +17,26 @@ import javax.validation.ValidationException; import java.util.Set; +/** + * 全局异常处理器 + * + *

统一拦截 Controller 层抛出的异常,避免异常堆栈直接暴露给前端。 + * 所有被捕获的异常都会被转换成 {@link ResponseUtil} 约定的统一 JSON 结构 + * (错误码 + 提示消息),同时打印错误日志便于后端排查。

+ * + *

方法命名统一为 badArgument / serious 两类:参数类问题返回“参数错误”, + * 其余未预期异常统一返回“系统错误”。

+ * + * @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) { @@ -30,6 +44,7 @@ public Object badArgumentHandler(IllegalArgumentException e) { return ResponseUtil.badArgumentValue(); } + /** 参数类型不匹配:如预期 Integer 却传入了字符串 */ @ExceptionHandler(MethodArgumentTypeMismatchException.class) @ResponseBody public Object badArgumentHandler(MethodArgumentTypeMismatchException e) { @@ -37,6 +52,7 @@ public Object badArgumentHandler(MethodArgumentTypeMismatchException e) { return ResponseUtil.badArgumentValue(); } + /** 缺少必填的请求参数 */ @ExceptionHandler(MissingServletRequestParameterException.class) @ResponseBody public Object badArgumentHandler(MissingServletRequestParameterException e) { @@ -44,6 +60,7 @@ public Object badArgumentHandler(MissingServletRequestParameterException e) { return ResponseUtil.badArgumentValue(); } + /** 请求体不可读:JSON 格式错误导致无法反序列化 */ @ExceptionHandler(HttpMessageNotReadableException.class) @ResponseBody public Object badArgumentHandler(HttpMessageNotReadableException e) { @@ -51,6 +68,7 @@ public Object badArgumentHandler(HttpMessageNotReadableException e) { return ResponseUtil.badArgumentValue(); } + /** 参数校验异常:提取第一个校验失败字段及其提示信息,返回 402 */ @ExceptionHandler(ValidationException.class) @ResponseBody public Object badArgumentHandler(ValidationException e) { @@ -66,6 +84,7 @@ public Object badArgumentHandler(ValidationException e) { return ResponseUtil.badArgumentValue(); } + /** 兜底处理:所有未被上面匹配的异常,统一按系统错误返回 */ @ExceptionHandler(Exception.class) @ResponseBody public Object seriousHandler(Exception e) { diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/config/JacksonConfig.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/config/JacksonConfig.java index 74000936d..463bacad9 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/config/JacksonConfig.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/config/JacksonConfig.java @@ -20,15 +20,34 @@ import java.time.LocalTime; import java.time.format.DateTimeFormatter; +/** + * Jackson JSON 序列化全局配置 + * + *

统一规定后端返回给前端的 JSON 规则:

+ * + * + * @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, @@ -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); } }; diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/config/ValidatorConfiguration.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/config/ValidatorConfiguration.java index 73cc47431..b80442215 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/config/ValidatorConfiguration.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/config/ValidatorConfiguration.java @@ -8,8 +8,23 @@ import javax.validation.Validator; import javax.validation.ValidatorFactory; +/** + * 参数校验器配置类 + * + *

配置 JSR-303 参数校验(@Valid / @NotBlank 等)使用 Hibernate Validator 实现, + * 并开启 fail_fast:一旦遇到第一个校验失败就立即返回,不必等全部字段校验完, + * 便于接口快速给出明确的错误提示。

+ * + * @author litemall + * @since 1.0 + */ @Configuration public class ValidatorConfiguration { + /** + * 构造并注册全局 Validator + * + * @return fail-fast 模式的 Validator 实例 + */ @Bean public Validator validator() { ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) @@ -17,7 +32,6 @@ public Validator validator() { .addProperty("hibernate.validator.fail_fast", "true") .buildValidatorFactory(); Validator validator = validatorFactory.getValidator(); - return validator; } } diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/config/WxConfig.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/config/WxConfig.java index c97a92e53..ef79d3dbb 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/config/WxConfig.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/config/WxConfig.java @@ -11,11 +11,24 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +/** + * 微信小程序与微信支付配置类 + * + *

把 {@link WxProperties}(来自 application.yml 的 litemall.wx.* 配置)装配成 + * 小程序服务 {@link WxMaService} 和微信支付服务 {@link WxPayService} 两个 bean, + * 供登录、解密用户数据、统一下单等业务直接注入使用。

+ * + * @author litemall + * @since 1.0 + */ @Configuration public class WxConfig { @Autowired private WxProperties properties; + /** + * 小程序连接配置:appid、secret、消息校验 token、消息加解密密钥 + */ @Bean public WxMaConfig wxMaConfig() { WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl(); @@ -26,7 +39,7 @@ public WxMaConfig wxMaConfig() { return config; } - + /** 小程序服务:封装 code 换 session、获取用户信息等能力 */ @Bean public WxMaService wxMaService(WxMaConfig maConfig) { WxMaService service = new WxMaServiceImpl(); @@ -34,6 +47,10 @@ public WxMaService wxMaService(WxMaConfig maConfig) { return service; } + /** + * 微信支付配置:商户号 mchId、商户密钥 mchKey、支付结果通知地址等, + * 交易类型固定 JSAPI(小程序内支付),签名方式 MD5 + */ @Bean public WxPayConfig wxPayConfig() { WxPayConfig payConfig = new WxPayConfig(); @@ -47,11 +64,11 @@ public WxPayConfig wxPayConfig() { return payConfig; } - + /** 微信支付服务:封装统一下单、查询订单、退款等能力 */ @Bean public WxPayService wxPayService(WxPayConfig payConfig) { WxPayService wxPayService = new WxPayServiceImpl(); wxPayService.setConfig(payConfig); return wxPayService; } -} \ No newline at end of file +} diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/config/WxProperties.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/config/WxProperties.java index a5f3dace5..789635662 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/config/WxProperties.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/config/WxProperties.java @@ -3,24 +3,41 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; +/** + * 微信小程序 / 微信支付配置属性 + * + *

将 application.yml 中前缀为 {@code litemall.wx} 的配置项绑定到本类字段, + * 供 {@link WxConfig} 装配微信服务时读取。敏感信息(secret、mchKey)应放在配置文件或环境变量中,不要写死在代码里。

+ * + * @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() { diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/express/ExpressService.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/express/ExpressService.java index d6438da63..0d15c72de 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/express/ExpressService.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/express/ExpressService.java @@ -15,14 +15,14 @@ import java.util.Map; /** - * 物流查询服务 - *

- * 快递鸟即时查询API http://www.kdniao.com/api-track + * 封装快递鸟即时查询,为订单详情提供物流轨迹及快递公司名称。 + *

关闭开关或请求、解析异常时返回 null;非空响应仍须检查 Success, + * 不能把返回对象直接当作查询成功。 */ public class ExpressService { private final Log logger = LogFactory.getLog(ExpressService.class); - //请求url + // 服务商即时查询入口;enable 为 false 时不会发送请求。 private String ReqURL = "http://api.kdniao.com/Ebusiness/EbusinessOrderHandle.aspx"; private ExpressProperties properties; @@ -36,10 +36,9 @@ public void setProperties(ExpressProperties properties) { } /** - * 获取物流供应商名 - * - * @param vendorCode - * @return + * 将订单中的快递公司编码转换为页面展示名称。 + * @param vendorCode 与 vendors 配置中 code 对应的公司编码 + * @return 配置中的公司名称;没有匹配项时返回 null */ public String getVendorName(String vendorCode) { for (Map item : properties.getVendors()) { @@ -49,16 +48,20 @@ public String getVendorName(String vendorCode) { return null; } + /** + * 提供可选快递公司,供后台发货时选择。 + * @return 配置中的公司列表,每项包含 code 和 name;返回原列表而非副本 + */ public List> getVendors() { return properties.getVendors(); } /** - * 获取物流信息 - * - * @param expCode - * @param expNo - * @return + * 查询物流轨迹并补充本地快递公司名称。 + *

关闭查询或发生异常时返回 null;上游业务失败仍可能返回 Success=false 的对象。 + * @param expCode 快递公司编码,应与上游协议及本地 vendors 配置一致 + * @param expNo 运单号,保留字符串以避免丢失前导零 + * @return 查询响应;未启用或请求、解析异常时返回 null */ public ExpressInfo getExpressInfo(String expCode, String expNo) { if (!properties.isEnable()) { @@ -79,9 +82,12 @@ public ExpressInfo getExpressInfo(String expCode, String expNo) { } /** - * Json方式 查询订单物流轨迹 - * - * @throws Exception + * 按现有快递鸟即时查询协议提交运单查询。 + *

签名基于未 URL 编码的请求数据计算,再分别对请求数据和签名编码。 + * @param expCode 快递公司编码 + * @param expNo 运单号 + * @return 服务商返回的原始 JSON 字符串 + * @throws Exception 请求参数编码等处理发生异常时 */ private String getOrderTracesByJson(String expCode, String expNo) throws Exception { String requestData = "{'OrderCode':'','ShipperCode':'" + expCode + "','LogisticCode':'" + expNo + "'}"; @@ -89,6 +95,7 @@ private String getOrderTracesByJson(String expCode, String expNo) throws Excepti Map params = new HashMap(); params.put("RequestData", URLEncoder.encode(requestData, "UTF-8")); params.put("EBusinessID", properties.getAppId()); + // 1002 为本实现采用的即时物流查询类型,2 表示 JSON 响应。 params.put("RequestType", "1002"); String dataSign = encrypt(requestData, properties.getAppKey(), "UTF-8"); params.put("DataSign", URLEncoder.encode(dataSign, "UTF-8")); @@ -96,17 +103,16 @@ private String getOrderTracesByJson(String expCode, String expNo) throws Excepti String result = HttpUtil.sendPost(ReqURL, params); - //根据公司业务处理返回的信息...... return result; } /** - * MD5加密 - * - * @param str 内容 - * @param charset 编码方式 - * @throws Exception + * 计算协议签名需要的小写十六进制 MD5 摘要,不用于密码存储或内容加密。 + * @param str 待摘要的请求数据与密钥拼接结果 + * @param charset 将字符串转换为字节时使用的字符集 + * @return 32 位小写十六进制摘要 + * @throws Exception 算法不可用或字符集不受支持时 */ private String MD5(String str, String charset) throws Exception { MessageDigest md = MessageDigest.getInstance("MD5"); @@ -124,12 +130,12 @@ private String MD5(String str, String charset) throws Exception { } /** - * Sign签名生成 - * - * @param content 内容 - * @param keyValue Appkey - * @param charset 编码方式 - * @return DataSign签名 + * 将请求数据与 AppKey 拼接后的 MD5 十六进制字符串进行 Base64 编码。 + *

这里编码的是摘要字符串的字节,不是 MD5 原始摘要字节;需保持协议顺序。 + * @param content 未经 URL 编码的请求内容 + * @param keyValue 服务商 AppKey;为 null 时当前实现不拼接密钥 + * @param charset 摘要及字符串转换使用的字符集 + * @return 未 URL 编码的签名;计算异常时返回 null */ private String encrypt(String content, String keyValue, String charset) { if (keyValue != null) { diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/express/config/ExpressAutoConfiguration.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/express/config/ExpressAutoConfiguration.java index 4e7458ffe..9bb10f8f1 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/express/config/ExpressAutoConfiguration.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/express/config/ExpressAutoConfiguration.java @@ -5,6 +5,10 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +/** + * 将物流配置绑定到 ExpressService,供订单查询复用统一服务。 + *

服务 Bean 始终创建,是否发送外部请求由 ExpressService 的 enable 检查决定。 + */ @Configuration @EnableConfigurationProperties(ExpressProperties.class) public class ExpressAutoConfiguration { @@ -15,6 +19,10 @@ public ExpressAutoConfiguration(ExpressProperties properties) { this.properties = properties; } + /** + * 将配置交给物流服务,在查询时统一判断启用状态。 + * @return 已绑定配置的物流查询服务 + */ @Bean public ExpressService expressService() { ExpressService expressService = new ExpressService(); diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/express/config/ExpressProperties.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/express/config/ExpressProperties.java index 034cf963c..5bb02c333 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/express/config/ExpressProperties.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/express/config/ExpressProperties.java @@ -6,11 +6,19 @@ import java.util.List; import java.util.Map; +/** + * 绑定 litemall.express 配置,集中管理物流开关、查询凭据与快递公司列表。 + *

enable 默认 false,vendors 默认空列表;启用前需配置服务商所需凭据。 + */ @ConfigurationProperties(prefix = "litemall.express") public class ExpressProperties { + /** 是否启用外部物流查询,未配置时为 false。 */ private boolean enable; + /** 快递鸟商户标识,用于请求中的 EBusinessID。 */ private String appId; + /** 快递鸟签名密钥,仅由部署配置注入。 */ private String appKey; + /** 快递公司配置,每项含 code 与 name,默认空列表。 */ private List> vendors = new ArrayList<>(); public boolean isEnable() { diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/express/dao/ExpressInfo.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/express/dao/ExpressInfo.java index d281385da..af0c316af 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/express/dao/ExpressInfo.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/express/dao/ExpressInfo.java @@ -8,28 +8,36 @@ import java.util.List; /** - * Auto-generated: 2018-07-19 22:27:22 - * - * @author bejson.com (i@bejson.com) - * @website http://www.bejson.com/java2pojo/ + * 快递鸟物流查询响应,保留上游字段映射以兼容现有反序列化。 + *

Success 表示查询是否成功,Reason 提供失败原因;ShipperName 由本地配置补充。 + * @author bejson.com (原始模型生成来源) */ public class ExpressInfo { @JsonProperty("LogisticCode") + /** 运单号,保留服务商返回的字符串。 */ private String LogisticCode; @JsonProperty("ShipperCode") + /** 服务商快递公司编码。 */ private String ShipperCode; @JsonProperty("Traces") + /** 物流轨迹节点;查询无结果时可能为空或 null。 */ private List Traces; @JsonProperty("State") + /** 服务商物流状态原值,当前类不转换为本地枚举。 */ private String State; @JsonProperty("EBusinessID") + /** 响应关联的商户标识。 */ private String EBusinessID; @JsonProperty("Success") + /** 服务商业务查询结果,不能仅凭 HTTP 请求完成判断成功。 */ private boolean Success; @JsonProperty("Reason") + /** 服务商返回的失败或补充说明。 */ private String Reason; + /** 依据本地 vendors 配置补充的公司名称。 */ + private String ShipperName; public String getLogisticCode() { @@ -109,4 +117,4 @@ public String toString() { ", ShipperName='" + ShipperName + '\'' + '}'; } -} \ No newline at end of file +} diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/express/dao/Traces.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/express/dao/Traces.java index 24a070de9..a95d22711 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/express/dao/Traces.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/express/dao/Traces.java @@ -6,16 +6,17 @@ import com.fasterxml.jackson.annotation.JsonProperty; /** - * Auto-generated: 2018-07-19 22:27:22 - * - * @author bejson.com (i@bejson.com) - * @website http://www.bejson.com/java2pojo/ + * 物流轨迹中的一个节点,描述某一时间发生的揽收、转运或签收事件。 + *

时间保留服务商原始字符串,当前模型不进行时区或日期格式转换。 + * @author bejson.com (原始模型生成来源) */ public class Traces { @JsonProperty("AcceptStation") + /** 轨迹发生地点及事件说明。 */ private String AcceptStation; @JsonProperty("AcceptTime") + /** 服务商返回的事件时间字符串。 */ private String AcceptTime; public String getAcceptStation() { @@ -34,4 +35,4 @@ public void setAcceptTime(String AcceptTime) { this.AcceptTime = AcceptTime; } -} \ No newline at end of file +} diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/AliyunSmsSender.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/AliyunSmsSender.java index 5898e092f..fe06d913d 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/AliyunSmsSender.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/AliyunSmsSender.java @@ -16,17 +16,27 @@ import java.util.HashMap; import java.util.Map; -/* - * 阿里云短信服务 +/** + * 阿里云短信发送器。 + * + *

基于阿里云短信 API(dysmsapi)实现 {@link SmsSender}。 + * 注意:阿里云仅支持模板发送({@link #sendWithTemplate}), + * 直发 {@link #send} 为桩实现,始终返回发送失败;如需直发请走模板发送或改用其他渠道。 */ public class AliyunSmsSender implements SmsSender { + /** 日志 */ private final Log logger = LogFactory.getLog(AliyunSmsSender.class); + /** 阿里云区域 ID,如 cn-hangzhou */ private String regionId; + /** 阿里云 AccessKey ID */ private String accessKeyId; + /** 阿里云 AccessKey Secret */ private String accessKeySecret; + /** 短信签名 */ private String sign; + /** 阿里云短信发送成功的返回码 */ private final String okCode = "OK"; public String getRegionId() { @@ -61,6 +71,16 @@ public void setSign(String sign) { this.sign = sign; } + /** + * 直发短信(桩实现)。 + * + *

阿里云短信 API 不支持不指定模板的直发,此方法为占位实现,始终返回发送失败; + * 实际发送请使用 {@link #sendWithTemplate}。 + * + * @param phone 接收通知的电话号码 + * @param content 短消息内容 + * @return 固定返回 successful=false + */ @Override public SmsResult send(String phone, String content) { SmsResult smsResult = new SmsResult(); @@ -68,6 +88,17 @@ public SmsResult send(String phone, String content) { return smsResult; } + /** + * 通过短信模板发送短信息。 + * + *

阿里云模板参数约定:单参数用 code,多参数用 code/code1/code2...(见下方注释), + * 开发者需保证申请的模板参数名与此约定一致。 + * + * @param phone 接收通知的电话号码 + * @param templateId 通知模板ID + * @param params 模板参数值 + * @return 短信发送结果,失败时 successful=false + */ @Override public SmsResult sendWithTemplate(String phone, String templateId, String[] params) { DefaultProfile profile = DefaultProfile.getProfile(this.regionId, this.accessKeyId, this.accessKeySecret); diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/NotifyService.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/NotifyService.java index d47a6335d..0188ae80b 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/NotifyService.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/NotifyService.java @@ -9,22 +9,42 @@ import java.util.Map; /** - * 商城通知服务类 + * 商城通知服务类。 + * + *

统一封装邮件、短信两类通知能力,支持直发与模板发送。 + * 通过 {@link #isMailEnable()}、{@link #isSmsEnable()} 判断对应渠道是否已注入启用; + * 邮件与短信的装配在 {@code NotifyAutoConfiguration} 中完成。 */ public class NotifyService { + /** 邮件发送器,为 null 表示未启用邮件通知 */ private MailSender mailSender; + /** 邮件发件人地址(来自配置 spring.mail.sendfrom) */ private String sendFrom; + /** 邮件收件人地址(来自配置 spring.mail.sendto) */ private String sendTo; + /** 短信发送器,为 null 表示未启用短信通知 */ private SmsSender smsSender; + /** 短信模板列表,元素为 {name -> templateId} 的映射 */ private List> smsTemplate = new ArrayList<>(); + /** 预留的微信模板配置;保留现有设置入口,当前发送流程不读取该字段。 */ private List> wxTemplate = new ArrayList<>(); + /** + * 邮件通知是否启用 + * + * @return true 表示已注入邮件发送器 + */ public boolean isMailEnable() { return mailSender != null; } + /** + * 短信通知是否启用 + * + * @return true 表示已注入短信发送器 + */ public boolean isSmsEnable() { return smsSender != null; } @@ -70,7 +90,7 @@ public void notifySmsTemplate(String phoneNumber, NotifyType notifyType, String[ * @param phoneNumber 接收通知的电话号码 * @param notifyType 通知类别,通过该枚举值在配置文件中获取相应的模版ID * @param params 通知模版内容里的参数,类似"您的验证码为{1}"中{1}的值 - * @return + * @return 短信发送结果;短信未启用时返回 null,模板缺失时由具体发送器处理 */ public SmsResult notifySmsTemplateSync(String phoneNumber, NotifyType notifyType, String[] params) { if (smsSender == null) @@ -129,6 +149,10 @@ public void setSmsTemplate(List> smsTemplate) { this.smsTemplate = smsTemplate; } + /** + * 保存预留的微信模板配置,不启用额外的消息发送渠道。 + * @param wxTemplate 微信模板配置列表 + */ public void setWxTemplate(List> wxTemplate) { this.wxTemplate = wxTemplate; } diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/NotifyType.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/NotifyType.java index ae599627c..c012510a1 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/NotifyType.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/NotifyType.java @@ -1,11 +1,22 @@ package org.linlinjava.litemall.core.notify; +/** + * 商城通知类型枚举。 + * + *

每种通知类型对应短信/微信模板配置中的一个名称({@code type}), + * {@link NotifyService} 根据该名称在模板列表中查找对应的模板 ID。 + */ public enum NotifyType { + /** 支付成功通知 */ PAY_SUCCEED("paySucceed"), + /** 发货通知 */ SHIP("ship"), + /** 退款通知 */ REFUND("refund"), + /** 验证码通知(如短信验证码) */ CAPTCHA("captcha"); + /** 模板配置中使用的类型标识 */ private String type; NotifyType(String type) { diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/SmsResult.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/SmsResult.java index a35459947..e084b9433 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/SmsResult.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/SmsResult.java @@ -1,16 +1,20 @@ package org.linlinjava.litemall.core.notify; /** - * 发送短信的返回结果 + * 发送短信的返回结果。 + * + *

封装短信是否发送成功,以及短信渠道方返回的原始结果对象。 */ public class SmsResult { + /** 短信是否发送成功(true 表示成功) */ private boolean successful; + /** 短信渠道方返回的原始结果对象 */ private Object result; /** * 短信是否发送成功 * - * @return + * @return true 表示发送成功,false 表示发送失败 */ public boolean isSuccessful() { return successful; diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/SmsSender.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/SmsSender.java index b1ff00bfb..b49b4f3e2 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/SmsSender.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/SmsSender.java @@ -1,5 +1,11 @@ package org.linlinjava.litemall.core.notify; +/** + * 短信发送器接口。 + * + *

定义向指定手机号发送短信(直发或按模板发送)的能力, + * 由具体短信渠道实现,如腾讯云 {@link TencentSmsSender}、阿里云 {@link AliyunSmsSender}。 + */ public interface SmsSender { /** @@ -7,15 +13,18 @@ public interface SmsSender { * * @param phone 接收通知的电话号码 * @param content 短消息内容 + * @return 短信发送结果 */ SmsResult send(String phone, String content); /** * 通过短信模版发送短信息 - * @param phone 接收通知的电话号码 + * + * @param phone 接收通知的电话号码 * @param templateId 通知模板ID * @param params 通知模版内容里的参数,类似"您的验证码为{1}"中{1}的值 + * @return 短信发送结果 */ SmsResult sendWithTemplate(String phone, String templateId, String[] params); -} \ No newline at end of file +} diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/TencentSmsSender.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/TencentSmsSender.java index c4ad29dcc..b1c8914c8 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/TencentSmsSender.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/TencentSmsSender.java @@ -8,15 +8,22 @@ import java.io.IOException; -/* - * 腾讯云短信服务 +/** + * 腾讯云短信发送器。 + * + *

基于腾讯云短信 SDK(SmsSingleSender)实现 {@link SmsSender}, + * 支持直发与按模板发送;发送失败时返回 successful=false 并记录日志。 */ public class TencentSmsSender implements SmsSender { + /** 日志 */ private final Log logger = LogFactory.getLog(TencentSmsSender.class); + /** 腾讯云短信发送器(由配置注入) */ private SmsSingleSender sender; + /** 短信签名 */ private String sign; + //未使用 public SmsSingleSender getSender() { return sender; } @@ -25,6 +32,13 @@ public void setSender(SmsSingleSender sender) { this.sender = sender; } + /** + * 直发短信(不指定模板) + * + * @param phone 接收通知的电话号码 + * @param content 短消息内容 + * @return 短信发送结果,失败时 successful=false + */ @Override public SmsResult send(String phone, String content) { try { @@ -44,6 +58,14 @@ public SmsResult send(String phone, String content) { return smsResult; } + /** + * 通过短信模板发送短信息 + * + * @param phone 接收通知的电话号码 + * @param templateId 通知模板ID + * @param params 模板参数,对应模板中 {1}、{2} 等占位符 + * @return 短信发送结果,失败时 successful=false + */ @Override public SmsResult sendWithTemplate(String phone, String templateId, String[] params) { try { diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/config/NotifyAutoConfiguration.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/config/NotifyAutoConfiguration.java index bbf38e186..ff459232d 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/config/NotifyAutoConfiguration.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/config/NotifyAutoConfiguration.java @@ -12,16 +12,28 @@ import java.util.Properties; +/** + * 通知功能自动装配配置。 + * + *

根据 {@code litemall.notify} 配置构建 {@link NotifyService}, + * 邮件/短信启用时分别注入对应发送器;短信渠道按 {@code active} 选择腾讯云或阿里云。 + */ @Configuration @EnableConfigurationProperties(NotifyProperties.class) public class NotifyAutoConfiguration { + /** 通知配置 */ private final NotifyProperties properties; public NotifyAutoConfiguration(NotifyProperties properties) { this.properties = properties; } + /** + * 构建通知服务,按配置装配邮件/短信发送器 + * + * @return 配置完成的 NotifyService + */ @Bean public NotifyService notifyService() { NotifyService notifyService = new NotifyService(); @@ -48,6 +60,11 @@ else if(smsConfig.getActive().equals("aliyun")) { return notifyService; } + /** + * 构建 JavaMail 邮件发送器 + * + * @return 基于配置的 JavaMailSender + */ public JavaMailSender mailSender() { NotifyProperties.Mail mailConfig = properties.getMail(); JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); @@ -68,6 +85,11 @@ public JavaMailSender mailSender() { return mailSender; } + /** + * 构建腾讯云短信发送器 + * + * @return 基于配置的 TencentSmsSender + */ public TencentSmsSender tencentSmsSender() { NotifyProperties.Sms smsConfig = properties.getSms(); TencentSmsSender smsSender = new TencentSmsSender(); @@ -77,6 +99,11 @@ public TencentSmsSender tencentSmsSender() { return smsSender; } + /** + * 构建阿里云短信发送器 + * + * @return 基于配置的 AliyunSmsSender + */ public AliyunSmsSender aliyunSmsSender() { NotifyProperties.Sms smsConfig = properties.getSms(); AliyunSmsSender smsSender = new AliyunSmsSender(); diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/config/NotifyProperties.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/config/NotifyProperties.java index e1a3f5925..1170c750c 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/config/NotifyProperties.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/notify/config/NotifyProperties.java @@ -6,9 +6,16 @@ import java.util.List; import java.util.Map; +/** + * 商城通知配置。 + * + *

绑定前缀为 {@code litemall.notify} 的配置项,包含邮件与短信(腾讯云/阿里云)发送相关配置。 + */ @ConfigurationProperties(prefix = "litemall.notify") public class NotifyProperties { + /** 邮件通知配置 */ private Mail mail; + /** 短信通知配置 */ private Sms sms; public Mail getMail() { @@ -27,13 +34,23 @@ public void setSms(Sms sms) { this.sms = sms; } + /** + * 邮件发送配置,对应配置项 litemall.notify.mail.* + */ public static class Mail { + /** 是否启用邮件通知 */ private boolean enable; + /** SMTP 服务器地址 */ private String host; + /** SMTP 登录用户名 */ private String username; + /** SMTP 登录密码 */ private String password; + /** 发件人地址 */ private String sendfrom; + /** 收件人地址 */ private String sendto; + /** SMTP 端口 */ private Integer port; public boolean isEnable() { return enable; @@ -92,12 +109,21 @@ public void setPort(Integer port) { } } + /** + * 短信发送配置,对应配置项 litemall.notify.sms.* + */ public static class Sms { + /** 是否启用短信通知 */ private boolean enable; + /** 当前生效的短信渠道,取值 tencent 或 aliyun */ private String active; + /** 短信签名 */ private String sign; + /** 腾讯云短信配置 */ private Tencent tencent; + /** 阿里云短信配置 */ private Aliyun aliyun; + /** 短信模板列表,元素为 {name -> templateId} 的映射 */ private List> template = new ArrayList<>(); public boolean isEnable() { @@ -148,8 +174,13 @@ public void setAliyun(Aliyun aliyun) { this.aliyun = aliyun; } + /** + * 腾讯云短信配置 + */ public static class Tencent { + /** 腾讯云短信应用 ID */ private int appid; + /** 腾讯云短信应用密钥 */ private String appkey; public int getAppid() { @@ -169,9 +200,15 @@ public void setAppkey(String appkey) { } } + /** + * 阿里云短信配置 + */ public static class Aliyun { + /** 阿里云区域 ID,如 cn-hangzhou */ private String regionId; + /** 阿里云 AccessKey ID */ private String accessKeyId; + /** 阿里云 AccessKey Secret */ private String accessKeySecret; public String getRegionId() { diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/qcode/QCodeService.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/qcode/QCodeService.java index ead254c0d..92111930a 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/qcode/QCodeService.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/qcode/QCodeService.java @@ -18,16 +18,33 @@ import java.io.*; import java.net.URL; +/** + * 小程序二维码 / 分享图生成服务。 + * + *

生成小程序码,并将商品图片、商品名称绘制到分享图中, + * 最终保存到存储服务并返回可访问的图片 URL。 + */ @Service public class QCodeService { + /** 日志 */ private final Log logger = LogFactory.getLog(QCodeService.class); + /** 微信小程序服务 */ @Autowired WxMaService wxMaService; + /** 存储服务 */ @Autowired private StorageService storageService; + /** + * 创建拼团分享图 + * + * @param goodName 商品名称 + * @param goodPicUrl 商品图片地址 + * @param groupon 拼团信息,用于生成带 groupon 参数的二维码 + * @return 分享图 URL;生成失败时返回空字符串 + */ public String createGrouponShareImage(String goodName, String goodPicUrl, LitemallGroupon groupon) { try { //创建该商品的二维码 @@ -57,9 +74,10 @@ public String createGrouponShareImage(String goodName, String goodPicUrl, Litema /** * 创建商品分享图 * - * @param goodId - * @param goodPicUrl - * @param goodName + * @param goodId 商品 ID,用于生成带 goods 参数的二维码 + * @param goodPicUrl 商品图片地址 + * @param goodName 商品名称 + * @return 分享图 URL;未开启自动生成或生成失败时返回空字符串 */ public String createGoodShareImage(String goodId, String goodPicUrl, String goodName) { if (!SystemConfig.isAutoCreateShareImage()) @@ -88,6 +106,12 @@ public String createGoodShareImage(String goodId, String goodPicUrl, String good return ""; } + /** + * 生成分享图在存储服务中的对象键名 + * + * @param goodId 商品/拼团 ID + * @return 存储对象键名,如 GOOD_QCODE_123.jpg + */ private String getKeyName(String goodId) { return "GOOD_QCODE_" + goodId + ".jpg"; } @@ -98,8 +122,8 @@ private String getKeyName(String goodId) { * @param qrCodeImg 二维码图片 * @param goodPicUrl 商品图片地址 * @param goodName 商品名称 - * @return - * @throws IOException + * @return 合成的 JPG 图片字节数组 + * @throws IOException 图片读取失败时抛出 */ private byte[] drawPicture(InputStream qrCodeImg, String goodPicUrl, String goodName) throws IOException { //底图 @@ -131,10 +155,6 @@ private byte[] drawPicture(InputStream qrCodeImg, String goodPicUrl, String good //写上商品名称 drawTextInImg(baseImage, goodName, 65, 867); - //写上商城名称 - // drawTextInImgCenter(baseImage, shopName, 98); - - //转jpg BufferedImage result = new BufferedImage(baseImage.getWidth(), baseImage .getHeight(), BufferedImage.TYPE_3BYTE_BGR); @@ -146,27 +166,14 @@ private byte[] drawPicture(InputStream qrCodeImg, String goodPicUrl, String good return bs.toByteArray(); } - private void drawTextInImgCenter(BufferedImage baseImage, String textToWrite, int y) { - Graphics2D g2D = (Graphics2D) baseImage.getGraphics(); - g2D.setColor(new Color(167, 136, 69)); - - String fontName = "Microsoft YaHei"; - - Font f = new Font(fontName, Font.PLAIN, 28); - g2D.setFont(f); - g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - // 计算文字长度,计算居中的x点坐标 - FontMetrics fm = g2D.getFontMetrics(f); - int textWidth = fm.stringWidth(textToWrite); - int widthX = (baseImage.getWidth() - textWidth) / 2; - // 表示这段文字在图片上的位置(x,y) .第一个是你设置的内容。 - - g2D.drawString(textToWrite, widthX, y); - // 释放对象 - g2D.dispose(); - } - + /** + * 在图片上绘制文本 + * + * @param baseImage 目标画布 + * @param textToWrite 要绘制的文本 + * @param x 文本左上角 x 坐标 + * @param y 文本左上角 y 坐标 + */ private void drawTextInImg(BufferedImage baseImage, String textToWrite, int x, int y) { Graphics2D g2D = (Graphics2D) baseImage.getGraphics(); g2D.setColor(new Color(167, 136, 69)); @@ -179,6 +186,16 @@ private void drawTextInImg(BufferedImage baseImage, String textToWrite, int x, i g2D.dispose(); } + /** + * 将一张图片绘制到目标画布的指定区域 + * + * @param baseImage 目标画布 + * @param imageToWrite 待绘制的源图片 + * @param x 绘制区域左上角 x 坐标 + * @param y 绘制区域左上角 y 坐标 + * @param width 绘制区域宽度 + * @param heigth 绘制区域高度 + */ private void drawImgInImg(BufferedImage baseImage, BufferedImage imageToWrite, int x, int y, int width, int heigth) { Graphics2D g2D = (Graphics2D) baseImage.getGraphics(); diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/AliyunStorage.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/AliyunStorage.java index 30d84b00e..6164f8076 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/AliyunStorage.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/AliyunStorage.java @@ -16,9 +16,9 @@ import java.util.stream.Stream; /** + * 阿里云 OSS 存储适配器,以 bucket 和对象键定位商城文件。 + *

上传和删除异常仅写入日志,不向调用方报告失败;不提供本地路径与目录枚举。 * @author Yogeek - * @date 2018/7/16 16:10 - * @decrpt 阿里云对象存储服务 */ public class AliyunStorage implements Storage { @@ -75,12 +75,17 @@ private String getBaseUrl() { } /** - * 阿里云OSS对象存储简单上传实现 + * 保存文件内容,以对象键而非用户原始文件名定位对象。 + *

当前实现捕获上传异常并记录日志,正常返回不能作为上传成功凭据。 + * @param inputStream 文件内容输入流 + * @param contentLength 文件字节数 + * @param contentType 文件 MIME 类型 + * @param keyName 由调用方生成的可信对象键 */ @Override public void store(InputStream inputStream, long contentLength, String contentType, String keyName) { try { - // 简单文件上传, 最大支持 5 GB, 适用于小文件上传, 建议 20M以下的文件使用该接口 + // 设置长度和 MIME 类型,供对象存储识别内容及下游浏览器展示。 ObjectMetadata objectMetadata = new ObjectMetadata(); objectMetadata.setContentLength(contentLength); objectMetadata.setContentType(contentType); @@ -93,16 +98,30 @@ public void store(InputStream inputStream, long contentLength, String contentTyp } + /** + * 当前云适配器不实现目录枚举。 + * @return null + */ @Override public Stream loadAll() { return null; } + /** + * 当前云适配器不提供本地文件路径。 + * @param keyName 对象键 + * @return null + */ @Override public Path load(String keyName) { return null; } + /** + * 将对象键转换为可读取的资源,供文件下载使用。 + * @param keyName 存储对象键 + * @return 可用资源;地址无效或不可访问时可能返回 null + */ @Override public Resource loadAsResource(String keyName) { try { @@ -119,6 +138,10 @@ public Resource loadAsResource(String keyName) { } } + /** + * 删除底层存储对象,不负责删除数据库中的元数据记录。 + * @param keyName 待删除的对象键 + */ @Override public void delete(String keyName) { try { @@ -129,6 +152,11 @@ public void delete(String keyName) { } + /** + * 根据存储配置拼接访问地址,不检查对象存在性或生成签名凭证。 + * @param keyName 存储对象键 + * @return 对象访问地址 + */ @Override public String generateUrl(String keyName) { return getBaseUrl() + keyName; diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/LocalStorage.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/LocalStorage.java index 3c182b570..7f295bcdb 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/LocalStorage.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/LocalStorage.java @@ -16,7 +16,9 @@ import java.util.stream.Stream; /** - * 服务器本地对象存储服务 + * 以服务器磁盘保存商品图片等文件,并通过配置的 HTTP 地址生成访问链接。 + *

路径由 storagePath 指定,URL 由 address 指定,两者需对应同一资源映射。 + * 本类不检查路径穿越;调用方必须传入可信对象键。 */ public class LocalStorage implements Storage { @@ -51,6 +53,15 @@ public void setAddress(String address) { this.address = address; } + /** + * 保存文件内容,以对象键而非用户原始文件名定位对象。 + *

同名目标会被覆盖;contentLength 和 contentType 不参与磁盘复制。 + * @param inputStream 文件内容输入流 + * @param contentLength 文件字节数 + * @param contentType 文件 MIME 类型 + * @param keyName 由调用方生成的可信对象键 + * @throws RuntimeException 文件复制发生 I/O 错误时 + */ @Override public void store(InputStream inputStream, long contentLength, String contentType, String keyName) { try { @@ -60,6 +71,10 @@ public void store(InputStream inputStream, long contentLength, String contentTyp } } + /** + * 列出存储根目录下一层的相对路径。 + * @return 路径流,调用方使用后应关闭;云实现可能返回 null + */ @Override public Stream loadAll() { try { @@ -72,11 +87,21 @@ public Stream loadAll() { } + /** + * 按对象键定位文件;本方法不验证文件是否存在。 + * @param filename 存储对象键 + * @return 本地路径;云实现可能返回 null + */ @Override public Path load(String filename) { return rootLocation.resolve(filename); } + /** + * 将对象键转换为可读取的资源,供文件下载使用。 + * @param filename 存储对象键 + * @return 可用资源;地址无效或不可访问时可能返回 null + */ @Override public Resource loadAsResource(String filename) { try { @@ -93,6 +118,10 @@ public Resource loadAsResource(String filename) { } } + /** + * 删除底层存储对象,不负责删除数据库中的元数据记录。 + * @param filename 待删除的对象键 + */ @Override public void delete(String filename) { Path file = load(filename); @@ -103,9 +132,14 @@ public void delete(String filename) { } } + /** + * 根据存储配置拼接访问地址,不检查对象存在性或生成签名凭证。 + * @param keyName 存储对象键 + * @return 对象访问地址 + */ @Override public String generateUrl(String keyName) { return address + keyName; } -} \ No newline at end of file +} diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/QiniuStorage.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/QiniuStorage.java index 03827ec39..1671f8b76 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/QiniuStorage.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/QiniuStorage.java @@ -16,6 +16,11 @@ import java.nio.file.Path; import java.util.stream.Stream; +/** + * 七牛云对象存储适配器,上传凭证由桶名及访问密钥生成。 + *

访问地址使用配置的 endpoint;SDK 管理器无同步地延迟初始化, + * 首次并发调用可能重复创建对象,配置应在服务使用前完成。 + */ public class QiniuStorage implements Storage { private final Log logger = LogFactory.getLog(QiniuStorage.class); @@ -61,7 +66,12 @@ public void setBucketName(String bucketName) { } /** - * 七牛云OSS对象存储简单上传实现 + * 保存文件内容,以对象键而非用户原始文件名定位对象。 + *

当前实现捕获上传异常并记录日志,正常返回不能作为上传成功凭据。 + * @param inputStream 文件内容输入流 + * @param contentLength 文件字节数 + * @param contentType 文件 MIME 类型 + * @param keyName 由调用方生成的可信对象键 */ @Override public void store(InputStream inputStream, long contentLength, String contentType, String keyName) { @@ -80,16 +90,30 @@ public void store(InputStream inputStream, long contentLength, String contentTyp } } + /** + * 当前云适配器不实现目录枚举。 + * @return null + */ @Override public Stream loadAll() { return null; } + /** + * 当前云适配器不提供本地文件路径。 + * @param keyName 对象键 + * @return null + */ @Override public Path load(String keyName) { return null; } + /** + * 将对象键转换为可读取的资源,供文件下载使用。 + * @param keyName 存储对象键 + * @return 可用资源;地址无效或不可访问时可能返回 null + */ @Override public Resource loadAsResource(String keyName) { try { @@ -104,6 +128,10 @@ public Resource loadAsResource(String keyName) { return null; } + /** + * 删除底层存储对象,不负责删除数据库中的元数据记录。 + * @param keyName 待删除的对象键 + */ @Override public void delete(String keyName) { if (bucketManager == null) { @@ -119,6 +147,11 @@ public void delete(String keyName) { } } + /** + * 根据存储配置拼接访问地址,不检查对象存在性或生成签名凭证。 + * @param keyName 存储对象键 + * @return 对象访问地址 + */ @Override public String generateUrl(String keyName) { return endpoint + "/" + keyName; diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/Storage.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/Storage.java index 6c3ce0c83..93ff59b65 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/Storage.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/Storage.java @@ -7,27 +7,51 @@ import java.util.stream.Stream; /** - * 对象存储接口 + * 对象存储契约,使上传调用方不依赖本地磁盘或具体云厂商。 + *

对象键用于定位文件,不等同于原始文件名;资源可用性、异常处理和路径支持 + * 取决于实现。三个云实现的 loadAll/load 当前返回 null。 */ public interface Storage { /** - * 存储一个文件对象 - * - * @param inputStream 文件输入流 - * @param contentLength 文件长度 - * @param contentType 文件类型 - * @param keyName 文件名 + * 保存文件内容,以对象键而非用户原始文件名定位对象。 + * @param inputStream 文件内容输入流 + * @param contentLength 文件字节数 + * @param contentType 文件 MIME 类型 + * @param keyName 由调用方生成的可信对象键 */ void store(InputStream inputStream, long contentLength, String contentType, String keyName); + /** + * 枚举存储对象的相对路径,仅本地实现提供支持。 + * @return 路径流,调用方负责关闭;不支持时返回 null + */ Stream loadAll(); + /** + * 将对象键映射为本地路径,不检查存在性。 + * @param keyName 存储对象键 + * @return 本地路径;云实现返回 null + */ Path load(String keyName); + /** + * 将对象键转换为可读取的资源,供文件下载使用。 + * @param keyName 存储对象键 + * @return 可用资源;地址无效或不可访问时可能返回 null + */ Resource loadAsResource(String keyName); + /** + * 删除底层存储对象,不负责删除数据库中的元数据记录。 + * @param keyName 待删除的对象键 + */ void delete(String keyName); + /** + * 根据存储配置拼接访问地址,不检查对象存在性或生成签名凭证。 + * @param keyName 存储对象键 + * @return 对象访问地址 + */ String generateUrl(String keyName); -} \ No newline at end of file +} diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/StorageService.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/StorageService.java index 510f4c0c2..11e266a6b 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/StorageService.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/StorageService.java @@ -11,7 +11,9 @@ import java.util.stream.Stream; /** - * 提供存储服务类,所有存储服务均由该类对外提供 + * 商城文件存储入口,协调对象上传与 litemall_storage 元数据登记。 + *

先调用存储实现,再写入数据库,两者不构成跨系统原子事务; + * 云实现可能仅记录上传异常,因此返回元数据不等于远端文件已经可用。 */ public class StorageService { private String active; @@ -36,12 +38,14 @@ public void setStorage(Storage storage) { } /** - * 存储一个文件对象 - * - * @param inputStream 文件输入流 - * @param contentLength 文件长度 - * @param contentType 文件类型 - * @param fileName 文件索引名 + * 生成随机对象键、上传文件,再登记文件元数据。 + *

调用方必须提供包含扩展名的原始文件名;底层上传和数据库写入不是原子操作。 + * @param inputStream 文件内容输入流 + * @param contentLength 文件字节数,登记时转换为 int,调用方应限制上传大小 + * @param contentType 文件 MIME 类型 + * @param fileName 含扩展名的原始文件名,用于显示及提取后缀 + * @return 已写入数据库的文件元数据,不代表云端上传已经确认成功 + * @throws StringIndexOutOfBoundsException 文件名不包含点号,无法提取后缀时 */ public LitemallStorage store(InputStream inputStream, long contentLength, String contentType, String fileName) { String key = generateKey(fileName); @@ -59,6 +63,13 @@ public LitemallStorage store(InputStream inputStream, long contentLength, String return storageInfo; } + /** + * 保留文件后缀并生成随机键,避免不同用户的同名文件互相覆盖。 + *

查库重试降低重复风险,但不构成并发唯一性保证。 + * @param originalFilename 含点号及后缀的原始文件名 + * @return 20 位随机字符串与原始后缀组成的对象键 + * @throws StringIndexOutOfBoundsException 文件名不含点号时 + */ private String generateKey(String originalFilename) { int index = originalFilename.lastIndexOf('.'); String suffix = originalFilename.substring(index); @@ -75,22 +86,45 @@ private String generateKey(String originalFilename) { return key; } + /** + * 委托当前存储实现枚举对象。 + * @return 本地路径流(使用后应关闭);云实现返回 null + */ public Stream loadAll() { return storage.loadAll(); } + /** + * 委托当前存储实现定位对象。 + * @param keyName 存储对象键 + * @return 本地路径;云实现返回 null + */ public Path load(String keyName) { return storage.load(keyName); } + /** + * 将对象键转换为可读取的资源,供文件下载使用。 + * @param keyName 存储对象键 + * @return 可用资源;地址无效或不可访问时可能返回 null + */ public Resource loadAsResource(String keyName) { return storage.loadAsResource(keyName); } + /** + * 删除底层存储对象,不负责删除数据库中的元数据记录。 + * @param keyName 待删除的对象键 + */ public void delete(String keyName) { storage.delete(keyName); } + /** + * 根据存储配置拼接访问地址,不检查对象存在性或生成签名凭证。 + * @param keyName 存储对象键 + * @return 对象访问地址 + */ private String generateUrl(String keyName) { return storage.generateUrl(keyName); } diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/TencentStorage.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/TencentStorage.java index d899f671a..947454da8 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/TencentStorage.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/TencentStorage.java @@ -19,7 +19,9 @@ import java.util.stream.Stream; /** - * 腾讯对象存储服务 + * 腾讯云 COS 存储适配器,以地域、存储桶和对象键生成访问地址。 + *

客户端采用无同步的延迟初始化,首次并发调用可能重复创建客户端; + * 上传与删除异常仅记录日志,不提供本地路径和目录枚举。 */ public class TencentStorage implements Storage { @@ -66,9 +68,9 @@ public void setBucketName(String bucketName) { private COSClient getCOSClient() { if (cosClient == null) { - // 1 初始化用户身份信息(secretId, secretKey) + // 使用部署凭据创建客户端,避免每次文件操作都重复初始化。 COSCredentials cred = new BasicCOSCredentials(secretId, secretKey); - // 2 设置bucket的区域, COS地域的简称请参照 https://cloud.tencent.com/document/product/436/6224 + // 地域必须与目标存储桶一致,否则请求无法定位正确的桶。 ClientConfig clientConfig = new ClientConfig(new Region(region)); cosClient = new COSClient(cred, clientConfig); } @@ -80,10 +82,18 @@ private String getBaseUrl() { return "https://" + bucketName + ".cos." + region + ".myqcloud.com/"; } + /** + * 保存文件内容,以对象键而非用户原始文件名定位对象。 + *

当前实现捕获上传异常并记录日志,正常返回不能作为上传成功凭据。 + * @param inputStream 文件内容输入流 + * @param contentLength 文件字节数 + * @param contentType 文件 MIME 类型 + * @param keyName 由调用方生成的可信对象键 + */ @Override public void store(InputStream inputStream, long contentLength, String contentType, String keyName) { try { - // 简单文件上传, 最大支持 5 GB, 适用于小文件上传, 建议 20M以下的文件使用该接口 + // 设置长度和 MIME 类型,供对象存储识别内容及下游浏览器展示。 ObjectMetadata objectMetadata = new ObjectMetadata(); objectMetadata.setContentLength(contentLength); objectMetadata.setContentType(contentType); @@ -96,16 +106,30 @@ public void store(InputStream inputStream, long contentLength, String contentTyp } } + /** + * 当前云适配器不实现目录枚举。 + * @return null + */ @Override public Stream loadAll() { return null; } + /** + * 当前云适配器不提供本地文件路径。 + * @param keyName 对象键 + * @return null + */ @Override public Path load(String keyName) { return null; } + /** + * 将对象键转换为可读取的资源,供文件下载使用。 + * @param keyName 存储对象键 + * @return 可用资源;地址无效或不可访问时可能返回 null + */ @Override public Resource loadAsResource(String keyName) { try { @@ -120,6 +144,10 @@ public Resource loadAsResource(String keyName) { return null; } + /** + * 删除底层存储对象,不负责删除数据库中的元数据记录。 + * @param keyName 待删除的对象键 + */ @Override public void delete(String keyName) { try { @@ -130,6 +158,11 @@ public void delete(String keyName) { } + /** + * 根据存储配置拼接访问地址,不检查对象存在性或生成签名凭证。 + * @param keyName 存储对象键 + * @return 对象访问地址 + */ @Override public String generateUrl(String keyName) { return getBaseUrl() + keyName; diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/config/StorageAutoConfiguration.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/config/StorageAutoConfiguration.java index 20ecae2b3..49a6bd211 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/config/StorageAutoConfiguration.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/config/StorageAutoConfiguration.java @@ -5,6 +5,10 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +/** + * 装配四种存储适配器,并根据 active 为统一入口选择一个实现。 + *

各工厂均声明为 Bean;选择 active 不代表其余工厂不会被 Spring 初始化。 + */ @Configuration @EnableConfigurationProperties(StorageProperties.class) public class StorageAutoConfiguration { @@ -15,6 +19,12 @@ public StorageAutoConfiguration(StorageProperties properties) { this.properties = properties; } + /** + * 按 active 选择统一存储入口的委托实现,保持业务层调用方式一致。 + * @return 已绑定存储实现的服务 + * @throws RuntimeException active 不是 local、aliyun、tencent、qiniu 时 + * @throws NullPointerException active 未配置时 + */ @Bean public StorageService storageService() { StorageService storageService = new StorageService(); @@ -35,6 +45,10 @@ public StorageService storageService() { return storageService; } + /** + * 将本地磁盘路径与访问地址配置注入对应适配器。 + * @return 完成配置的存储适配器 + */ @Bean public LocalStorage localStorage() { LocalStorage localStorage = new LocalStorage(); @@ -44,6 +58,10 @@ public LocalStorage localStorage() { return localStorage; } + /** + * 将阿里云 OSS 凭据与桶配置注入对应适配器。 + * @return 完成配置的存储适配器 + */ @Bean public AliyunStorage aliyunStorage() { AliyunStorage aliyunStorage = new AliyunStorage(); @@ -55,6 +73,10 @@ public AliyunStorage aliyunStorage() { return aliyunStorage; } + /** + * 将腾讯云 COS 凭据、桶与地域配置注入对应适配器。 + * @return 完成配置的存储适配器 + */ @Bean public TencentStorage tencentStorage() { TencentStorage tencentStorage = new TencentStorage(); @@ -66,6 +88,10 @@ public TencentStorage tencentStorage() { return tencentStorage; } + /** + * 将七牛云凭据、桶与访问域名配置注入对应适配器。 + * @return 完成配置的存储适配器 + */ @Bean public QiniuStorage qiniuStorage() { QiniuStorage qiniuStorage = new QiniuStorage(); diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/config/StorageProperties.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/config/StorageProperties.java index a2c8b7b77..d69a6fe43 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/config/StorageProperties.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/storage/config/StorageProperties.java @@ -2,8 +2,13 @@ import org.springframework.boot.context.properties.ConfigurationProperties; +/** + * 绑定 litemall.storage 配置,为本地及三种云存储提供初始化参数。 + *

active 选择实际使用的实现,凭据属于部署配置,不应写入源码注释。 + */ @ConfigurationProperties(prefix = "litemall.storage") public class StorageProperties { + /** 启用的存储类型:local、aliyun、tencent 或 qiniu。 */ private String active; private Local local; private Aliyun aliyun; @@ -51,7 +56,9 @@ public void setQiniu(Qiniu qiniu) { } public static class Local { + /** 本地文件对外访问前缀,需与资源映射一致并包含所需分隔符。 */ private String address; + /** 服务器磁盘保存目录,初始化时尝试创建。 */ private String storagePath; public String getAddress() { @@ -72,9 +79,13 @@ public void setStoragePath(String storagePath) { } public static class Tencent { + /** 腾讯云访问密钥标识。 */ private String secretId; + /** 云服务访问密钥,由部署配置注入。 */ private String secretKey; + /** 腾讯云存储桶所属地域。 */ private String region; + /** 目标对象存储桶名称。 */ private String bucketName; public String getSecretId() { @@ -111,9 +122,13 @@ public void setBucketName(String bucketName) { } public static class Aliyun { + /** 服务端地址或访问域名,具体格式由相应适配器使用方式决定。 */ private String endpoint; + /** 阿里云访问密钥标识。 */ private String accessKeyId; + /** 阿里云访问密钥,由部署配置注入。 */ private String accessKeySecret; + /** 目标对象存储桶名称。 */ private String bucketName; public String getEndpoint() { @@ -150,9 +165,13 @@ public void setBucketName(String bucketName) { } public static class Qiniu { + /** 服务端地址或访问域名,具体格式由相应适配器使用方式决定。 */ private String endpoint; + /** 七牛云访问密钥标识。 */ private String accessKey; + /** 云服务访问密钥,由部署配置注入。 */ private String secretKey; + /** 目标对象存储桶名称。 */ private String bucketName; public String getEndpoint() { diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/system/SystemConfig.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/system/SystemConfig.java index 3e1fc0d09..dc9a4038f 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/system/SystemConfig.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/system/SystemConfig.java @@ -5,10 +5,17 @@ import java.util.Map; /** - * 系统设置 + * 系统运行配置(内存缓存) + * + *

后台“系统配置”页里的各项参数(首页展示条数、运费规则、订单超时时间、商场名称地址等) + * 都保存在数据库 litemall_system 表中。本类把它们加载到一个静态 Map 里做内存缓存, + * 业务代码通过静态 getter 直接读取,避免每次查库;后台修改配置后调用 updateConfigs 刷新缓存。

+ * + * @author litemall + * @since 1.0 */ public class SystemConfig { - // 小程序相关配置 + // —— 小程序首页展示相关配置 key —— public final static String LITEMALL_WX_INDEX_NEW = "litemall_wx_index_new"; public final static String LITEMALL_WX_INDEX_HOT = "litemall_wx_index_hot"; public final static String LITEMALL_WX_INDEX_BRAND = "litemall_wx_index_brand"; @@ -16,14 +23,14 @@ public class SystemConfig { public final static String LITEMALL_WX_INDEX_CATLOG_LIST = "litemall_wx_catlog_list"; public final static String LITEMALL_WX_INDEX_CATLOG_GOODS = "litemall_wx_catlog_goods"; public final static String LITEMALL_WX_SHARE = "litemall_wx_share"; - // 运费相关配置 + // —— 运费相关配置 key —— public final static String LITEMALL_EXPRESS_FREIGHT_VALUE = "litemall_express_freight_value"; public final static String LITEMALL_EXPRESS_FREIGHT_MIN = "litemall_express_freight_min"; - // 订单相关配置 + // —— 订单相关配置 key(超时时间,单位分钟/天)—— public final static String LITEMALL_ORDER_UNPAID = "litemall_order_unpaid"; public final static String LITEMALL_ORDER_UNCONFIRM = "litemall_order_unconfirm"; public final static String LITEMALL_ORDER_COMMENT = "litemall_order_comment"; - // 商场相关配置 + // —— 商场信息相关配置 key —— public final static String LITEMALL_MALL_NAME = "litemall_mall_name"; public final static String LITEMALL_MALL_ADDRESS = "litemall_mall_address"; public final static String LITEMALL_MALL_PHONE = "litemall_mall_phone"; @@ -31,7 +38,7 @@ public class SystemConfig { public final static String LITEMALL_MALL_LONGITUDE = "litemall_mall_longitude"; public final static String LITEMALL_MALL_Latitude = "litemall_mall_latitude"; - //所有的配置均保存在该 HashMap 中 + /** 全部系统配置的内存缓存,key 为配置名,value 为配置值 */ private static Map SYSTEM_CONFIGS = new HashMap<>(); private static String getConfig(String keyName) { @@ -50,50 +57,62 @@ private static BigDecimal getConfigBigDec(String keyName) { return new BigDecimal(SYSTEM_CONFIGS.get(keyName)); } + /** 首页“新品推荐”展示条数 */ public static Integer getNewLimit() { return getConfigInt(LITEMALL_WX_INDEX_NEW); } + /** 首页“热卖”展示条数 */ public static Integer getHotLimit() { return getConfigInt(LITEMALL_WX_INDEX_HOT); } + /** 首页“品牌”展示条数 */ public static Integer getBrandLimit() { return getConfigInt(LITEMALL_WX_INDEX_BRAND); } + /** 首页“专题”展示条数 */ public static Integer getTopicLimit() { return getConfigInt(LITEMALL_WX_INDEX_TOPIC); } + /** 分类页一级分类展示条数 */ public static Integer getCatlogListLimit() { return getConfigInt(LITEMALL_WX_INDEX_CATLOG_LIST); } + /** 分类下商品展示条数 */ public static Integer getCatlogMoreLimit() { return getConfigInt(LITEMALL_WX_INDEX_CATLOG_GOODS); } + /** 是否自动生成朋友圈分享图 */ public static boolean isAutoCreateShareImage() { return getConfigBoolean(LITEMALL_WX_SHARE); } + /** 运费金额(元) */ public static BigDecimal getFreight() { return getConfigBigDec(LITEMALL_EXPRESS_FREIGHT_VALUE); } + /** 免邮门槛(元) */ public static BigDecimal getFreightLimit() { return getConfigBigDec(LITEMALL_EXPRESS_FREIGHT_MIN); } + /** 未支付订单自动取消时长 */ public static Integer getOrderUnpaid() { return getConfigInt(LITEMALL_ORDER_UNPAID); } + /** 未确认收货自动确认时长 */ public static Integer getOrderUnconfirm() { return getConfigInt(LITEMALL_ORDER_UNCONFIRM); } + /** 订单自动好评时长 */ public static Integer getOrderComment() { return getConfigInt(LITEMALL_ORDER_COMMENT); } @@ -122,13 +141,15 @@ public static String getMallLatitude() { return getConfig(LITEMALL_MALL_Latitude); } + /** 启动时整体载入数据库配置到内存缓存 */ public static void setConfigs(Map configs) { SYSTEM_CONFIGS = configs; } + /** 后台修改配置后,增量更新内存缓存 */ public static void updateConfigs(Map data) { for (Map.Entry entry : data.entrySet()) { SYSTEM_CONFIGS.put(entry.getKey(), entry.getValue()); } } -} \ No newline at end of file +} diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/system/SystemInistService.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/system/SystemInistService.java index 37accbef1..0f9dbc7d6 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/system/SystemInistService.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/system/SystemInistService.java @@ -12,16 +12,29 @@ import java.util.Map; /** - * 系统启动服务,用于设置系统配置信息、检查系统状态及打印系统信息 + * 系统启动初始化服务 + * + *

在当前 Bean 完成依赖注入后({@link PostConstruct})自动执行:

+ *
    + *
  1. 从数据库加载全部系统配置;若某配置项缺失则用 DEFAULT_CONFIGS 兜底并回写数据库;
  2. + *
  3. 把配置整体载入 {@link SystemConfig} 的内存缓存;
  4. + *
  5. 在控制台打印一份系统信息横幅(端口、数据库、各模块开关、微信配置等),便于部署后核对。
  6. + *
+ * + *

注:类名 Inist 为原项目历史拼写,未改动以免影响引用。

+ * + * @author litemall + * @since 1.0 */ @Component class SystemInistService { + // 保存当前实例引用(保留原项目写法),不是 Spring 自注入。 private SystemInistService systemInistService; - @Autowired private Environment environment; + /** 容器初始化入口:先保存自身引用,再载入配置、打印启动横幅 */ @PostConstruct private void inist() { systemInistService = this; @@ -29,11 +42,11 @@ private void inist() { SystemInfoPrinter.printInfo("Litemall 初始化信息", getSystemInfo()); } - + /** 各项系统配置的默认值:数据库没有时以此为准 */ private final static Map DEFAULT_CONFIGS = new HashMap<>(); static { - // 小程序相关配置默认值 + // 小程序首页各区块默认展示条数 DEFAULT_CONFIGS.put(SystemConfig.LITEMALL_WX_INDEX_NEW, "6"); DEFAULT_CONFIGS.put(SystemConfig.LITEMALL_WX_INDEX_HOT, "6"); DEFAULT_CONFIGS.put(SystemConfig.LITEMALL_WX_INDEX_BRAND, "4"); @@ -41,14 +54,14 @@ private void inist() { DEFAULT_CONFIGS.put(SystemConfig.LITEMALL_WX_INDEX_CATLOG_LIST, "4"); DEFAULT_CONFIGS.put(SystemConfig.LITEMALL_WX_INDEX_CATLOG_GOODS, "4"); DEFAULT_CONFIGS.put(SystemConfig.LITEMALL_WX_SHARE, "false"); - // 运费相关配置默认值 + // 运费:默认 8 元,满 88 元免邮 DEFAULT_CONFIGS.put(SystemConfig.LITEMALL_EXPRESS_FREIGHT_VALUE, "8"); DEFAULT_CONFIGS.put(SystemConfig.LITEMALL_EXPRESS_FREIGHT_MIN, "88"); - // 订单相关配置默认值 + // 订单:未支付 30 分钟取消,未确认 7 天自动确认,7 天自动好评 DEFAULT_CONFIGS.put(SystemConfig.LITEMALL_ORDER_UNPAID, "30"); DEFAULT_CONFIGS.put(SystemConfig.LITEMALL_ORDER_UNCONFIRM, "7"); DEFAULT_CONFIGS.put(SystemConfig.LITEMALL_ORDER_COMMENT, "7"); - // 商城相关配置默认值 + // 商城信息默认值 DEFAULT_CONFIGS.put(SystemConfig.LITEMALL_MALL_NAME, "litemall"); DEFAULT_CONFIGS.put(SystemConfig.LITEMALL_MALL_ADDRESS, "上海"); DEFAULT_CONFIGS.put(SystemConfig.LITEMALL_MALL_Latitude, "31.201900"); @@ -60,16 +73,16 @@ private void inist() { @Autowired private LitemallSystemConfigService litemallSystemConfigService; + /** 读取数据库配置,缺项用默认值补齐并回写,最后载入内存缓存 */ private void initConfigs() { // 1. 读取数据库全部配置信息 Map configs = litemallSystemConfigService.queryAll(); - // 2. 分析DEFAULT_CONFIGS + // 2. 对默认配置中存在、但数据库缺失的项,补一条默认值并落库 for (Map.Entry entry : DEFAULT_CONFIGS.entrySet()) { if (configs.containsKey(entry.getKey())) { continue; } - configs.put(entry.getKey(), entry.getValue()); litemallSystemConfigService.addConfig(entry.getKey(), entry.getValue()); } @@ -77,18 +90,16 @@ private void initConfigs() { SystemConfig.setConfigs(configs); } + /** 汇总启动时要打印的系统信息(端口、数据源、模块开关、微信配置、系统设置) */ private Map getSystemInfo() { - Map infos = new LinkedHashMap<>(); infos.put(SystemInfoPrinter.CREATE_PART_COPPER + 0, "系统信息"); - // 测试获取application-db.yml配置信息 infos.put("服务器端口", environment.getProperty("server.port")); infos.put("数据库USER", environment.getProperty("spring.datasource.druid.username")); infos.put("数据库地址", environment.getProperty("spring.datasource.druid.url")); infos.put("调试级别", environment.getProperty("logging.level.org.linlinjava.litemall.wx")); - // 测试获取application-core.yml配置信息 infos.put(SystemInfoPrinter.CREATE_PART_COPPER + 1, "模块状态"); infos.put("邮件", environment.getProperty("litemall.notify.mail.enable")); infos.put("短信", environment.getProperty("litemall.notify.sms.enable")); @@ -100,7 +111,6 @@ private Map getSystemInfo() { infos.put("本地对象访问地址", environment.getProperty("litemall.storage.local.address")); infos.put("本地对象访问端口", environment.getProperty("litemall.storage.local.port")); - // 微信相关信息 infos.put(SystemInfoPrinter.CREATE_PART_COPPER + 2, "微信相关"); infos.put("微信APP KEY", environment.getProperty("litemall.wx.app-id")); infos.put("微信APP-SECRET", environment.getProperty("litemall.wx.app-secret")); @@ -108,7 +118,6 @@ private Map getSystemInfo() { infos.put("微信支付MCH-KEY", environment.getProperty("litemall.wx.mch-key")); infos.put("微信支付通知地址", environment.getProperty("litemall.wx.notify-url")); - //测试获取System表配置信息 infos.put(SystemInfoPrinter.CREATE_PART_COPPER + 3, "系统设置"); infos.put("自动创建朋友圈分享图", Boolean.toString(SystemConfig.isAutoCreateShareImage())); infos.put("商场名称", SystemConfig.getMallName()); diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/task/Task.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/task/Task.java index bdcc5fb13..187e0a768 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/task/Task.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/task/Task.java @@ -6,30 +6,64 @@ import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; +/** + * 延迟任务抽象基类。 + * + *

实现 {@link Delayed} 与 {@link Runnable},子类在 {@link #run()} 中定义到期要执行的具体逻辑; + * 任务以 id 标识,支持在延迟队列中的排序、相等判断与移除。 + */ public abstract class Task implements Delayed, Runnable{ + /** 任务唯一标识 */ private String id = ""; + /** 任务应执行的绝对时间戳(毫秒) */ private long start = 0; + /** + * 构造一个延迟任务 + * + * @param id 任务唯一标识 + * @param delayInMilliseconds 延迟执行的时间(毫秒) + */ public Task(String id, long delayInMilliseconds){ this.id = id; this.start = System.currentTimeMillis() + delayInMilliseconds; } + /** + * 获取任务标识 + * + * @return 任务唯一标识 + */ public String getId() { return id; } + /** + * 距任务执行的剩余延迟时间 + * + * @param unit 时间单位 + * @return 剩余延迟时间,按指定单位换算,可为负 + */ @Override public long getDelay(TimeUnit unit) { long diff = this.start - System.currentTimeMillis(); return unit.convert(diff, TimeUnit.MILLISECONDS); } + /** + * 按计划执行时间比较任务先后 + * + * @param o 待比较的延迟任务 + * @return 负数/零/正数,表示本任务早于/同时/晚于比较对象 + */ @Override public int compareTo(Delayed o) { return Ints.saturatedCast(this.start - ((Task) o).start); } + /** + * 任务相等性按 id 判断 + */ @Override public boolean equals(Object o) { if (this == o) return true; @@ -41,6 +75,9 @@ public boolean equals(Object o) { return this.id.equals(t.getId()); } + /** + * 哈希值基于任务 id + */ @Override public int hashCode() { return this.id.hashCode(); diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/task/TaskService.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/task/TaskService.java index 75c50a9fc..70c315ec2 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/task/TaskService.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/task/TaskService.java @@ -5,14 +5,26 @@ import java.util.concurrent.DelayQueue; import java.util.concurrent.Executors; +/** + * 延迟任务调度服务。 + * + *

内部使用 {@link DelayQueue} 保存待执行的延迟任务,并在启动时启动一个常驻线程 + * 消费队列中的到期任务;对外提供任务的添加与移除能力。 + */ @Component public class TaskService { - private TaskService taskService; + + /** 延迟任务队列,任务按到期时间排序 */ private DelayQueue delayQueue = new DelayQueue(); + /** + * 启动任务消费线程。 + * + *

单线程常驻消费 {@link DelayQueue},任务到期即执行;循环仅捕获 InterruptedException;任务抛出未捕获的运行时异常可能终止消费。 + */ @PostConstruct private void init() { - taskService = this; + Executors.newSingleThreadExecutor().execute(new Runnable() { @Override @@ -29,6 +41,11 @@ public void run() { }); } + /** + * 添加延迟任务,已存在(按 id 相等)的任务不重复添加 + * + * @param task 待添加的延迟任务 + */ public void addTask(Task task){ if(delayQueue.contains(task)){ return; @@ -36,6 +53,11 @@ public void addTask(Task task){ delayQueue.add(task); } + /** + * 移除延迟任务 + * + * @param task 待移除的延迟任务 + */ public void removeTask(Task task){ delayQueue.remove(task); } diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/util/BeanUtil.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/util/BeanUtil.java index 4f2dceb58..7bdbe3029 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/util/BeanUtil.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/util/BeanUtil.java @@ -5,19 +5,41 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; +/** + * Spring 容器工具类,用于在非 Spring 管理的类中获取 Bean 实例。 + */ @Component public class BeanUtil implements ApplicationContextAware { + /** 保存 Spring 容器上下文 */ protected static ApplicationContext context; + /** + * 注入 Spring 容器上下文。 + * + * @param applicationContext Spring 容器上下文 + */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { context = applicationContext; } + /** + * 根据 Bean 名称获取 Bean 实例。 + * + * @param name Bean 名称 + * @return Bean 实例 + */ public static Object getBean(String name) { return context.getBean(name); } + /** + * 根据 Bean 类型获取 Bean 实例。 + * + * @param c Bean 类型 + * @param Bean 类型泛型 + * @return Bean 实例 + */ public static T getBean(Class c){ return context.getBean(c); } diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/util/CharUtil.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/util/CharUtil.java index 628989bc4..84353049f 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/util/CharUtil.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/util/CharUtil.java @@ -2,12 +2,23 @@ import java.util.Random; +/** + * 随机字符串工具类。 + */ public class CharUtil { + /** + * 生成指定长度的随机字符串(包含小写字母和数字)。 + * + * @param num 随机字符串的长度 + * @return 随机字符串 + */ public static String getRandomString(Integer num) { + /* 随机字符池:小写字母与数字 */ String base = "abcdefghijklmnopqrstuvwxyz0123456789"; Random random = new Random(); StringBuffer sb = new StringBuffer(); + /* 循环从字符池中随机取 num 个字符拼接 */ for (int i = 0; i < num; i++) { int number = random.nextInt(base.length()); sb.append(base.charAt(number)); @@ -15,10 +26,18 @@ public static String getRandomString(Integer num) { return sb.toString(); } + /** + * 生成指定长度的随机数字字符串。 + * + * @param num 随机字符串的长度 + * @return 随机数字字符串 + */ public static String getRandomNum(Integer num) { + /* 随机字符池:仅数字 */ String base = "0123456789"; Random random = new Random(); StringBuffer sb = new StringBuffer(); + /* 循环从数字池中随机取 num 个字符拼接 */ for (int i = 0; i < num; i++) { int number = random.nextInt(base.length()); sb.append(base.charAt(number)); diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/util/DateTimeUtil.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/util/DateTimeUtil.java index 99764b978..63da7daec 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/util/DateTimeUtil.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/util/DateTimeUtil.java @@ -9,10 +9,10 @@ public class DateTimeUtil { /** - * 格式 yyyy年MM月dd日 HH:mm:ss + * 将日期时间格式化为 "yyyy年MM月dd日 HH:mm:ss" 形式的字符串。 * - * @param dateTime - * @return + * @param dateTime 待格式化的日期时间 + * @return 格式化后的日期时间字符串 */ public static String getDateTimeDisplayString(LocalDateTime dateTime) { DateTimeFormatter dtf2 = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss"); diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/util/HttpUtil.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/util/HttpUtil.java index 60713df1e..e0e5c1205 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/util/HttpUtil.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/util/HttpUtil.java @@ -11,11 +11,11 @@ import java.net.URL; import java.util.Map; + /** - * 向指定 URL 发送POST方法的请求 - * - * @return 远程资源的响应结果 + * HTTP 工具类,用于向指定 URL 发送 POST 请求。 */ + public class HttpUtil { private static final Log logger = LogFactory.getLog(HttpUtil.class); @@ -35,21 +35,21 @@ public static String sendPost(String url, Map params) { try { URL realUrl = new URL(url); HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection(); - // 发送POST请求必须设置如下两行 + /*发送POST请求必须设置如下两行*/ conn.setDoOutput(true); conn.setDoInput(true); - // POST方法 + /* POST方法 */ conn.setRequestMethod("POST"); - // 设置通用的请求属性 + /* 设置通用的请求属性 */ conn.setRequestProperty("accept", "*/*"); conn.setRequestProperty("connection", "Keep-Alive"); conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); conn.connect(); - // 获取URLConnection对象对应的输出流 + /* 获取URLConnection对象对应的输出流 */ out = new OutputStreamWriter(conn.getOutputStream(), "UTF-8"); - // 发送请求参数 + /* 发送请求参数 */ if (params != null) { StringBuilder param = new StringBuilder(); for (Map.Entry entry : params.entrySet()) { @@ -64,9 +64,9 @@ public static String sendPost(String url, Map params) { out.write(param.toString()); } - // flush输出流的缓冲 + /* flush输出流的缓冲 */ out.flush(); - // 定义BufferedReader输入流来读取URL的响应 + /* 定义BufferedReader输入流来读取URL的响应 */ in = new BufferedReader( new InputStreamReader(conn.getInputStream(), "UTF-8")); String line; @@ -76,7 +76,7 @@ public static String sendPost(String url, Map params) { } catch (Exception e) { logger.error(e.getMessage(), e); } - //使用finally块来关闭输出流、输入流 + /* 使用finally块来关闭输出流、输入流 */ finally { try { if (out != null) { diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/util/IpUtil.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/util/IpUtil.java index f753428e7..46e6ff2ab 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/util/IpUtil.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/util/IpUtil.java @@ -14,6 +14,15 @@ public class IpUtil { private static final Log logger = LogFactory.getLog(IpUtil.class); + /** + * 获取客户端真实 IP 地址。 + *

+ * 依次从请求头 x-forwarded-for、Proxy-Client-IP、WL-Proxy-Client-IP 中获取, + * 若都为空则回退到远程地址;经过多级代理时取第一个 IP。 + * + * @param request HTTP 请求 + * @return 客户端 IP 地址 + */ public static String getIpAddr(HttpServletRequest request) { String ipAddress; try { @@ -27,7 +36,7 @@ public static String getIpAddr(HttpServletRequest request) { if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getRemoteAddr(); if (ipAddress.equals("127.0.0.1")) { - // 根据网卡取本机配置的IP + /* 根据网卡取本机配置的IP */ InetAddress inet = null; try { inet = InetAddress.getLocalHost(); @@ -37,9 +46,9 @@ public static String getIpAddr(HttpServletRequest request) { ipAddress = inet.getHostAddress(); } } - // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 - if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length() - // = 15 + /* 通过多个代理时,第一个 IP 为客户端真实 IP,多个 IP 以逗号分隔; */ + /* 单个 IP 最长 15 个字符(如 255.255.255.255),超过 15 说明包含多个 IP */ + if (ipAddress != null && ipAddress.length() > 15) { if (ipAddress.indexOf(",") > 0) { ipAddress = ipAddress.substring(0, ipAddress.indexOf(",")); } diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/util/JacksonUtil.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/util/JacksonUtil.java index c64f9ff6d..2123ea228 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/util/JacksonUtil.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/util/JacksonUtil.java @@ -11,15 +11,27 @@ import java.util.List; import java.util.Map; +/** + * JSON 解析与序列化工具类,基于 Jackson 实现。 + */ public class JacksonUtil { private static final Log logger = LogFactory.getLog(JacksonUtil.class); + /** + * 解析 JSON 字符串中指定字段的字符串值。 + * + * @param body JSON 字符串 + * @param field 字段名 + * @return 字段对应的字符串值,不存在或解析失败时返回 null + */ public static String parseString(String body, String field) { ObjectMapper mapper = new ObjectMapper(); JsonNode node; try { + /* 将 JSON 字符串解析为树形结构 */ node = mapper.readTree(body); + /* 获取指定字段对应的节点 */ JsonNode leaf = node.get(field); if (leaf != null) return leaf.asText(); @@ -29,15 +41,23 @@ public static String parseString(String body, String field) { return null; } - + /** + * 解析 JSON 字符串中指定字段的字符串列表。 + * + * @param body JSON 字符串 + * @param field 字段名 + * @return 字段对应的字符串列表,不存在或解析失败时返回 null + */ public static List parseStringList(String body, String field) { ObjectMapper mapper = new ObjectMapper(); JsonNode node; try { + /* 将 JSON 字符串解析为树形结构 */ node = mapper.readTree(body); JsonNode leaf = node.get(field); if (leaf != null) + /* 将字段节点转换为字符串列表 */ return mapper.convertValue(leaf, new TypeReference>() { }); } catch (IOException e) { @@ -46,11 +66,20 @@ public static List parseStringList(String body, String field) { return null; } + /** + * 解析 JSON 字符串中指定字段的整数值。 + * + * @param body JSON 字符串 + * @param field 字段名 + * @return 字段对应的整数值,不存在或解析失败时返回 null + */ public static Integer parseInteger(String body, String field) { ObjectMapper mapper = new ObjectMapper(); JsonNode node; try { + /* 将 JSON 字符串解析为树形结构 */ node = mapper.readTree(body); + /* 获取指定字段对应的节点 */ JsonNode leaf = node.get(field); if (leaf != null) return leaf.asInt(); @@ -60,14 +89,23 @@ public static Integer parseInteger(String body, String field) { return null; } + /** + * 解析 JSON 字符串中指定字段的整数列表。 + * + * @param body JSON 字符串 + * @param field 字段名 + * @return 字段对应的整数列表,不存在或解析失败时返回 null + */ public static List parseIntegerList(String body, String field) { ObjectMapper mapper = new ObjectMapper(); JsonNode node; try { + /* 将 JSON 字符串解析为树形结构 */ node = mapper.readTree(body); JsonNode leaf = node.get(field); if (leaf != null) + /* 将字段节点转换为整数列表 */ return mapper.convertValue(leaf, new TypeReference>() { }); } catch (IOException e) { @@ -76,12 +114,20 @@ public static List parseIntegerList(String body, String field) { return null; } - + /** + * 解析 JSON 字符串中指定字段的布尔值。 + * + * @param body JSON 字符串 + * @param field 字段名 + * @return 字段对应的布尔值,不存在或解析失败时返回 null + */ public static Boolean parseBoolean(String body, String field) { ObjectMapper mapper = new ObjectMapper(); JsonNode node; try { + /* 将 JSON 字符串解析为树形结构 */ node = mapper.readTree(body); + /* 获取指定字段对应的节点 */ JsonNode leaf = node.get(field); if (leaf != null) return leaf.asBoolean(); @@ -91,13 +137,22 @@ public static Boolean parseBoolean(String body, String field) { return null; } + /** + * 解析 JSON 字符串中指定字段的短整数值。 + * + * @param body JSON 字符串 + * @param field 字段名 + * @return 字段对应的短整数值,不存在或解析失败时返回 null + */ public static Short parseShort(String body, String field) { ObjectMapper mapper = new ObjectMapper(); JsonNode node; try { + /* 将 JSON 字符串解析为树形结构 */ node = mapper.readTree(body); JsonNode leaf = node.get(field); if (leaf != null) { + /* 先按 int 取值再强转为 short */ Integer value = leaf.asInt(); return value.shortValue(); } @@ -107,13 +162,22 @@ public static Short parseShort(String body, String field) { return null; } + /** + * 解析 JSON 字符串中指定字段的字节值。 + * + * @param body JSON 字符串 + * @param field 字段名 + * @return 字段对应的字节值,不存在或解析失败时返回 null + */ public static Byte parseByte(String body, String field) { ObjectMapper mapper = new ObjectMapper(); JsonNode node; try { + /* 将 JSON 字符串解析为树形结构 */ node = mapper.readTree(body); JsonNode leaf = node.get(field); if (leaf != null) { + /* 先按 int 取值再强转为 byte */ Integer value = leaf.asInt(); return value.byteValue(); } @@ -123,12 +187,23 @@ public static Byte parseByte(String body, String field) { return null; } + /** + * 解析 JSON 字符串中指定字段为指定类型的对象。 + * + * @param body JSON 字符串 + * @param field 字段名 + * @param clazz 目标类型 + * @param 目标类型泛型 + * @return 字段对应的对象,不存在或解析失败时返回 null + */ public static T parseObject(String body, String field, Class clazz) { ObjectMapper mapper = new ObjectMapper(); JsonNode node; try { + /* 将 JSON 字符串解析为树形结构 */ node = mapper.readTree(body); node = node.get(field); + /* 将字段节点转换为指定类型的对象 */ return mapper.treeToValue(node, clazz); } catch (IOException e) { logger.error(e.getMessage(), e); @@ -136,13 +211,19 @@ public static T parseObject(String body, String field, Class clazz) { return null; } + /** + * 将 JSON 字符串解析为 JSON 节点。 + * + * @param json JSON 字符串 + * @return JSON 节点,解析失败时返回 null + */ public static Object toNode(String json) { if (json == null) { return null; } ObjectMapper mapper = new ObjectMapper(); try { - + /* 解析 JSON 字符串为 JSON 节点 */ return mapper.readTree(json); } catch (IOException e) { logger.error(e.getMessage(), e); @@ -151,9 +232,16 @@ public static Object toNode(String json) { return null; } + /** + * 将 JSON 字符串解析为 Map。 + * + * @param data JSON 字符串 + * @return Map 对象,解析失败时返回 null + */ public static Map toMap(String data) { ObjectMapper objectMapper = new ObjectMapper(); try { + /* 将 JSON 字符串反序列化为 Map */ return objectMapper.readValue(data, new TypeReference>() { }); } catch (IOException e) { @@ -162,9 +250,16 @@ public static Map toMap(String data) { return null; } + /** + * 将对象序列化为 JSON 字符串。 + * + * @param data 待序列化的对象 + * @return JSON 字符串,序列化失败时返回 null + */ public static String toJson(Object data) { ObjectMapper objectMapper = new ObjectMapper(); try { + /* 将对象序列化为 JSON 字符串 */ return objectMapper.writeValueAsString(data); } catch (JsonProcessingException e) { e.printStackTrace(); diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/util/ResponseUtil.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/util/ResponseUtil.java index c7a3521af..db21e87e4 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/util/ResponseUtil.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/util/ResponseUtil.java @@ -40,6 +40,11 @@ * */ public class ResponseUtil { + /** + * 构建成功响应。 + * + * @return 成功响应(不含数据) + */ public static Object ok() { Map obj = new HashMap(); obj.put("errno", 0); @@ -47,6 +52,12 @@ public static Object ok() { return obj; } + /** + * 构建成功响应。 + * + * @param data 响应数据 + * @return 成功响应(含数据) + */ public static Object ok(Object data) { Map obj = new HashMap(); obj.put("errno", 0); @@ -55,10 +66,17 @@ public static Object ok(Object data) { return obj; } + /** + * 构建分页列表成功响应。 + * + * @param list 列表数据(Page 类型时自动附带分页信息) + * @return 成功响应(含分页数据) + */ public static Object okList(List list) { Map data = new HashMap(); data.put("list", list); + /* PageHelper 分页对象会附带分页信息,否则按单页处理 */ if (list instanceof Page) { Page page = (Page) list; data.put("total", page.getTotal()); @@ -75,10 +93,18 @@ public static Object okList(List list) { return ok(data); } + /** + * 构建分页列表成功响应。 + * + * @param list 列表数据 + * @param pagedList 用于分页信息的分页对象 + * @return 成功响应(含分页数据) + */ public static Object okList(List list, List pagedList) { Map data = new HashMap(); data.put("list", list); + /* PageHelper 分页对象会附带分页信息,否则按单页处理 */ if (pagedList instanceof Page) { Page page = (Page) pagedList; data.put("total", page.getTotal()); @@ -95,6 +121,11 @@ public static Object okList(List list, List pagedList) { return ok(data); } + /** + * 构建失败响应。 + * + * @return 失败响应(错误码 -1) + */ public static Object fail() { Map obj = new HashMap(); obj.put("errno", -1); @@ -102,6 +133,13 @@ public static Object fail() { return obj; } + /** + * 构建失败响应。 + * + * @param errno 错误码 + * @param errmsg 错误消息 + * @return 失败响应 + */ public static Object fail(int errno, String errmsg) { Map obj = new HashMap(); obj.put("errno", errno); @@ -109,6 +147,14 @@ public static Object fail(int errno, String errmsg) { return obj; } + /** + * 构建带数据的失败响应。 + * + * @param errno 错误码 + * @param errmsg 错误消息 + * @param data 响应数据 + * @return 失败响应(含数据) + */ public static Object fail(int errno, String errmsg, String data) { Map obj = new HashMap(3); obj.put("errno", errno); @@ -117,36 +163,75 @@ public static Object fail(int errno, String errmsg, String data) { return obj; } + /** + * 构建参数错误响应(401)。 + * + * @return 参数错误响应 + */ public static Object badArgument() { return fail(401, "参数不对"); } + /** + * 构建参数值错误响应(402)。 + * + * @return 参数值错误响应 + */ public static Object badArgumentValue() { return fail(402, "参数值不对"); } + /** + * 构建未登录响应(501)。 + * + * @return 未登录响应 + */ public static Object unlogin() { return fail(501, "请登录"); } + /** + * 构建系统内部错误响应(502)。 + * + * @return 系统内部错误响应 + */ public static Object serious() { return fail(502, "系统内部错误"); } + /** + * 构建业务不支持响应(503)。 + * + * @return 业务不支持响应 + */ public static Object unsupport() { return fail(503, "业务不支持"); } + /** + * 构建更新数据失效响应(504)。 + * + * @return 更新数据失效响应 + */ public static Object updatedDateExpired() { return fail(504, "更新数据已经失效"); } + /** + * 构建更新数据失败响应(505)。 + * + * @return 更新数据失败响应 + */ public static Object updatedDataFailed() { return fail(505, "更新数据失败"); } + /** + * 构建无操作权限响应(506)。 + * + * @return 无操作权限响应 + */ public static Object unauthz() { return fail(506, "无操作权限"); } } - diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/util/SystemInfoPrinter.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/util/SystemInfoPrinter.java index 51a02f642..b90cba435 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/util/SystemInfoPrinter.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/util/SystemInfoPrinter.java @@ -5,16 +5,30 @@ import java.util.Map; +/** + * 系统信息打印工具类。 + *

+ * 用于在应用启动时以统一格式将系统配置信息打印到日志。 + */ public class SystemInfoPrinter { private static final Log logger = LogFactory.getLog(SystemInfoPrinter.class); public static final String CREATE_PART_COPPER = "XOXOXOXOX"; + /** 打印行的最大宽度,用于生成对齐的分隔横线 */ private static int maxSize = 0; + /** + * 打印系统信息。 + * + * @param title 信息标题 + * @param infos 键值对形式的信息集合 + */ public static void printInfo(String title, Map infos) { + /* 先根据内容计算最大行宽 */ setMaxSize(infos); + /* 依次打印头部、各信息行、结尾 */ printHeader(title); for (Map.Entry entry : infos.entrySet()) { @@ -24,6 +38,11 @@ public static void printInfo(String title, Map infos) { printEnd(); } + /** + * 计算打印每行内容的最大宽度。 + * + * @param infos 键值对形式的信息集合 + */ private static void setMaxSize(Map infos) { for (Map.Entry entry : infos.entrySet()) { if (entry.getValue() == null) @@ -35,9 +54,15 @@ private static void setMaxSize(Map infos) { maxSize = size; } + /* 额外留 30 个字符的空隙,保证横线不紧贴内容 */ maxSize = maxSize + 30; } + /** + * 打印信息头部。 + * + * @param title 信息标题 + */ private static void printHeader(String title) { logger.info(getLineCopper()); logger.info(""); @@ -45,11 +70,19 @@ private static void printHeader(String title) { logger.info(""); } + /** + * 打印信息结尾。 + */ private static void printEnd() { logger.info(" "); logger.info(getLineCopper()); } + /** + * 生成一条由等号组成的横线。 + * + * @return 横线字符串 + */ private static String getLineCopper() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < maxSize; i++) { @@ -59,15 +92,23 @@ private static String getLineCopper() { return sb.toString(); } + /** + * 打印一行信息。 + * + * @param head 键 + * @param line 值 + */ private static void printLine(String head, String line) { if (line == null) return; + /* 分区标题(以特殊前缀开头)单独高亮打印 */ if (head.startsWith(CREATE_PART_COPPER)) { logger.info(""); logger.info(" [[ " + line + " ]]"); logger.info(""); } else { + /* 普通键值对:key -> value */ logger.info(" " + head + " -> " + line); } } diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/validator/Order.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/validator/Order.java index dfbaa2b35..4e25178fa 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/validator/Order.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/validator/Order.java @@ -9,6 +9,11 @@ import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; +/** + * 排序类型校验注解。 + *

+ * 用于校验请求参数中的排序方式是否在允许范围内(默认仅支持 asc 和 desc)。 + */ @Target({METHOD, FIELD, PARAMETER}) @Retention(RUNTIME) @Documented diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/validator/OrderValidator.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/validator/OrderValidator.java index b163a117b..3dd97e78f 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/validator/OrderValidator.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/validator/OrderValidator.java @@ -6,12 +6,16 @@ import javax.validation.ConstraintValidatorContext; import java.util.List; +/** + * {@link Order} 注解的校验器,校验排序方式是否在允许范围内。 + */ public class OrderValidator implements ConstraintValidator { private List valueList; @Override public void initialize(Order order) { valueList = Lists.newArrayList(); + /* 将允许值统一转为大写,便于校验时忽略大小写 */ for (String val : order.accepts()) { valueList.add(val.toUpperCase()); } @@ -19,6 +23,7 @@ public void initialize(Order order) { @Override public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { + /* 校验时忽略大小写 */ if (!valueList.contains(s.toUpperCase())) { return false; } diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/validator/Sort.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/validator/Sort.java index 2568e7099..17d8300a0 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/validator/Sort.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/validator/Sort.java @@ -9,6 +9,11 @@ import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; +/** + * 排序字段校验注解。 + *

+ * 用于校验请求参数中的排序字段是否在允许范围内(默认仅支持 add_time 和 id)。 + */ @Target({METHOD, FIELD, PARAMETER}) @Retention(RUNTIME) @Documented diff --git a/litemall-core/src/main/java/org/linlinjava/litemall/core/validator/SortValidator.java b/litemall-core/src/main/java/org/linlinjava/litemall/core/validator/SortValidator.java index eca179bba..7fdd98b91 100644 --- a/litemall-core/src/main/java/org/linlinjava/litemall/core/validator/SortValidator.java +++ b/litemall-core/src/main/java/org/linlinjava/litemall/core/validator/SortValidator.java @@ -6,12 +6,16 @@ import javax.validation.ConstraintValidatorContext; import java.util.List; +/** + * {@link Sort} 注解的校验器,校验排序字段是否在允许范围内。 + */ public class SortValidator implements ConstraintValidator { private List valueList; @Override public void initialize(Sort sort) { valueList = Lists.newArrayList(); + /* 将允许值统一转为大写,便于校验时忽略大小写 */ for (String val : sort.accepts()) { valueList.add(val.toUpperCase()); } @@ -19,6 +23,7 @@ public void initialize(Sort sort) { @Override public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { + /* 校验时忽略大小写 */ if (!valueList.contains(s.toUpperCase())) { return false; }