Commit 7976759a81cb1a6fdfa80a3ad7525a65a9024442

Authored by huanggang
0 parents

dili cashier project init

Showing 113 changed files with 6367 additions and 0 deletions
.gitignore 0 → 100644
  1 +++ a/.gitignore
  1 +# Compiled class file
  2 +*.class
  3 +
  4 +# Ignore Gradle project-specific cache directory
  5 +.gradle
  6 +
  7 +# Ignore Gradle build output directory
  8 +build
  9 +
  10 +# Ignore Idea build output directory
  11 +.idea
  12 +
  13 +# Ignore log file
  14 +logs
  15 +*.log
... ...
build.gradle 0 → 100644
  1 +++ a/build.gradle
  1 +plugins {
  2 + // id 'org.springframework.boot' version '4.0.0' apply false
  3 + // id 'io.spring.dependency-management' version '1.1.7' apply false
  4 + alias(libs.plugins.spring.boot) apply false
  5 + alias(libs.plugins.spring.dependency.management) apply false
  6 + id 'com.github.johnrengelman.shadow' version '8.1.1' apply false
  7 +}
  8 +
  9 +allprojects {
  10 + group = 'com.diligrp'
  11 +// version = "${libs.versions.appVersion.get()}"
  12 + version = libs.versions.appVersion.get()
  13 +
  14 + repositories {
  15 + mavenLocal()
  16 + maven {
  17 + url 'https://maven.aliyun.com/repository/central'
  18 + }
  19 + maven {
  20 + url 'https://maven.aliyun.com/repository/public'
  21 + }
  22 + maven {
  23 + url 'https://maven.aliyun.com/repository/gradle-plugin'
  24 + }
  25 + mavenCentral()
  26 + }
  27 +}
  28 +
  29 +subprojects {
  30 + apply plugin: 'java'
  31 + apply plugin: 'java-library'
  32 + apply plugin: 'io.spring.dependency-management'
  33 +
  34 + java {
  35 + sourceCompatibility = JavaVersion.VERSION_21
  36 + targetCompatibility = JavaVersion.VERSION_21
  37 + // 可选:启用模块系统(JPMS)
  38 + modularity.inferModulePath = true
  39 + }
  40 +
  41 + dependencies {
  42 + implementation libs.spring.boot.starter.web
  43 + }
  44 +
  45 + dependencyManagement {
  46 + imports {
  47 + mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES
  48 + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${libs.versions.springCloudVersion.get()}"
  49 + }
  50 + }
  51 +
  52 + tasks.withType(JavaCompile) {
  53 + options.encoding = 'UTF-8'
  54 + options.compilerArgs += ['-Xlint:unchecked', '-Xlint:deprecation']
  55 + }
  56 +}
0 57 \ No newline at end of file
... ...
cashier-assistant/build.gradle 0 → 100644
  1 +++ a/cashier-assistant/build.gradle
  1 +dependencies {
  2 + api project(':cashier-shared')
  3 + implementation 'org.apache.commons:commons-text:1.15.0'
  4 + implementation('com.aliyun:dysmsapi20170525:4.3.1') {
  5 + // 排除指定模块, 否则将导致logback初始化时xml parse异常
  6 + exclude group: 'pull-parser', module: 'pull-parser'
  7 + }
  8 +}
0 9 \ No newline at end of file
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/AssistantConfiguration.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/AssistantConfiguration.java
  1 +package com.diligrp.cashier.assistant;
  2 +
  3 +import com.diligrp.cashier.assistant.pipeline.*;
  4 +import com.diligrp.cashier.shared.mybatis.MybatisMapperSupport;
  5 +import org.mybatis.spring.annotation.MapperScan;
  6 +import org.springframework.boot.context.properties.ConfigurationProperties;
  7 +import org.springframework.context.annotation.Bean;
  8 +import org.springframework.context.annotation.ComponentScan;
  9 +import org.springframework.context.annotation.Configuration;
  10 +
  11 +@Configuration
  12 +@ComponentScan("com.diligrp.cashier.assistant")
  13 +@MapperScan(basePackages = {"com.diligrp.cashier.assistant.dao"}, markerInterface = MybatisMapperSupport.class)
  14 +public class AssistantConfiguration {
  15 +
  16 + @Bean
  17 + @ConfigurationProperties("sms")
  18 + public SmsProperties smsProperties() {
  19 + return new SmsProperties();
  20 + }
  21 +
  22 + @Bean
  23 + public SmsPipelineManager smsPipelineManager(SmsProperties properties) {
  24 + SmsPipelineManager pipelineManager = new DefaultSmsPipelineManager();
  25 +
  26 + // TODO: 可利用数据库进行通道配置, 前期并没有必要
  27 + SmsProperties.AliSms aliSms = properties.getAlisms();
  28 + if (aliSms != null) {
  29 + SmsPipeline aliPipeline = new AliSmsPipeline(10, "阿里云短信服务通道", aliSms.getEndPoint(),
  30 + aliSms.getAccessKeyId(), aliSms.getAccessKeySecret());
  31 + pipelineManager.registerPipeline(aliPipeline);
  32 + }
  33 +
  34 + SmsProperties.SmsChinese chinese = properties.getSmschinese();
  35 + if (chinese != null) {
  36 + SmsPipeline pipeline = new SmsChinesePipeline(20, "网建短信服务通道", chinese.getUri(),
  37 + chinese.getUid(), chinese.getSecretKey());
  38 + pipelineManager.registerPipeline(pipeline);
  39 + }
  40 + return pipelineManager;
  41 + }
  42 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/SmsProperties.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/SmsProperties.java
  1 +package com.diligrp.cashier.assistant;
  2 +
  3 +import java.security.PrivateKey;
  4 +import java.security.PublicKey;
  5 +
  6 +public class SmsProperties {
  7 + // 私钥
  8 + private PrivateKey privateKey;
  9 + // 公钥
  10 + private PublicKey publicKey;
  11 + // 阿里云短信服务通道
  12 + private AliSms alisms;
  13 + // 网建短信服务通道
  14 + private SmsChinese smschinese;
  15 +
  16 + public PrivateKey getPrivateKey() {
  17 + return privateKey;
  18 + }
  19 +
  20 + public void setPrivateKey(PrivateKey privateKey) {
  21 + this.privateKey = privateKey;
  22 + }
  23 +
  24 + public PublicKey getPublicKey() {
  25 + return publicKey;
  26 + }
  27 +
  28 + public void setPublicKey(PublicKey publicKey) {
  29 + this.publicKey = publicKey;
  30 + }
  31 +
  32 + public AliSms getAlisms() {
  33 + return alisms;
  34 + }
  35 +
  36 + public void setAlisms(AliSms alisms) {
  37 + this.alisms = alisms;
  38 + }
  39 +
  40 + public SmsChinese getSmschinese() {
  41 + return smschinese;
  42 + }
  43 +
  44 + public void setSmschinese(SmsChinese smschinese) {
  45 + this.smschinese = smschinese;
  46 + }
  47 +
  48 + public static class AliSms {
  49 + private String endPoint;
  50 + private String accessKeyId;
  51 + private String accessKeySecret;
  52 +
  53 + public String getEndPoint() {
  54 + return endPoint;
  55 + }
  56 +
  57 + public void setEndPoint(String endPoint) {
  58 + this.endPoint = endPoint;
  59 + }
  60 +
  61 + public String getAccessKeyId() {
  62 + return accessKeyId;
  63 + }
  64 +
  65 + public void setAccessKeyId(String accessKeyId) {
  66 + this.accessKeyId = accessKeyId;
  67 + }
  68 +
  69 + public String getAccessKeySecret() {
  70 + return accessKeySecret;
  71 + }
  72 +
  73 + public void setAccessKeySecret(String accessKeySecret) {
  74 + this.accessKeySecret = accessKeySecret;
  75 + }
  76 + }
  77 +
  78 + public static class SmsChinese {
  79 + private String uri;
  80 + private String uid;
  81 + private String secretKey;
  82 +
  83 + public String getUri() {
  84 + return uri;
  85 + }
  86 +
  87 + public void setUri(String uri) {
  88 + this.uri = uri;
  89 + }
  90 +
  91 + public String getUid() {
  92 + return uid;
  93 + }
  94 +
  95 + public void setUid(String uid) {
  96 + this.uid = uid;
  97 + }
  98 +
  99 + public String getSecretKey() {
  100 + return secretKey;
  101 + }
  102 +
  103 + public void setSecretKey(String secretKey) {
  104 + this.secretKey = secretKey;
  105 + }
  106 + }
  107 +}
0 108 \ No newline at end of file
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/client/AliSmsHttpClient.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/client/AliSmsHttpClient.java
  1 +package com.diligrp.cashier.assistant.client;
  2 +
  3 +import com.aliyun.dysmsapi20170525.Client;
  4 +import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
  5 +import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
  6 +import com.aliyun.dysmsapi20170525.models.SendSmsResponseBody;
  7 +import com.aliyun.teaopenapi.models.Config;
  8 +import com.diligrp.cashier.assistant.domain.SmsMessage;
  9 +import com.diligrp.cashier.assistant.exception.AssistantServiceException;
  10 +import com.diligrp.cashier.shared.ErrorCode;
  11 +import org.slf4j.Logger;
  12 +import org.slf4j.LoggerFactory;
  13 +
  14 +import java.util.Objects;
  15 +import java.util.stream.Collectors;
  16 +
  17 +public class AliSmsHttpClient {
  18 +
  19 + private static final Logger LOGGER = LoggerFactory.getLogger(AliSmsHttpClient.class);
  20 +
  21 + private final Client client;
  22 +
  23 + public AliSmsHttpClient(String endPoint, String accessKeyId, String accessKeySecret) {
  24 + Config config = new Config().setAccessKeyId(accessKeyId).setAccessKeySecret(accessKeySecret);
  25 + config.endpoint = endPoint;
  26 + try {
  27 + this.client = new Client(config);
  28 + } catch (Exception e) {
  29 + throw new AssistantServiceException(ErrorCode.SERVICE_ACCESS_ERROR, "阿里云短信通道初始化错误");
  30 + }
  31 + }
  32 +
  33 + public String sendSmsMessage(SmsMessage message) {
  34 + String telephones = message.getTelephones().stream().reduce((telephone1, telephone2) ->
  35 + "".concat(telephone1).concat(",").concat(telephone2)).get();
  36 + SendSmsRequest request = new SendSmsRequest();
  37 + request.setPhoneNumbers(telephones);
  38 + request.setSignName(message.getSignature());
  39 + request.setTemplateCode(message.getTemplateId());
  40 + if (Objects.nonNull(message.getParams())) {
  41 + String json = message.getParams().entrySet().stream()
  42 + .filter(p -> Objects.nonNull(p.getKey()) && Objects.nonNull(p.getValue()))
  43 + .map(p -> String.format("\"%s\": \"%s\"", p.getKey(), p.getValue()))
  44 + .collect(Collectors.joining(",", "{", "}"));
  45 + request.setTemplateParam(json);
  46 + }
  47 +
  48 + SendSmsResponse response;
  49 + try {
  50 + response = this.client.sendSms(request);
  51 + } catch (Exception ex) {
  52 + LOGGER.error("Failed to send ali sms message", ex);
  53 + throw new AssistantServiceException(ErrorCode.SERVICE_ACCESS_ERROR, "发送短信失败: 未知错误");
  54 + }
  55 +
  56 + SendSmsResponseBody body = response.getBody();
  57 + if ("OK".equalsIgnoreCase(body.getCode())) {
  58 + return body.getBizId();
  59 + } else {
  60 + LOGGER.error("Failed to send ali sms message, {}:{}", body.getCode(), body.getMessage());
  61 + throw new AssistantServiceException(ErrorCode.SERVICE_ACCESS_ERROR, "发送短信失败: " + body.getMessage());
  62 + }
  63 + }
  64 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/client/SmsChineseHttpClient.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/client/SmsChineseHttpClient.java
  1 +package com.diligrp.cashier.assistant.client;
  2 +
  3 +import com.diligrp.cashier.assistant.exception.AssistantServiceException;
  4 +import com.diligrp.cashier.shared.ErrorCode;
  5 +import com.diligrp.cashier.shared.service.ServiceEndpointSupport;
  6 +import org.slf4j.Logger;
  7 +import org.slf4j.LoggerFactory;
  8 +
  9 +public class SmsChineseHttpClient extends ServiceEndpointSupport {
  10 +
  11 + private static final Logger LOGGER = LoggerFactory.getLogger(SmsChineseHttpClient.class);
  12 +
  13 + /**
  14 + * 接口地址
  15 + */
  16 + private final String uri;
  17 +
  18 + /**
  19 + * 用户名
  20 + */
  21 + private final String uid;
  22 +
  23 + /**
  24 + * 短信密钥
  25 + */
  26 + private final String secretKey;
  27 +
  28 + public SmsChineseHttpClient(String uri, String uid, String secretKey) {
  29 + this.uri = uri;
  30 + this.uid = uid;
  31 + this.secretKey = secretKey;
  32 + }
  33 +
  34 + public String sendSmsMessage(String telephone, String message, String signature) {
  35 + HttpParam[] params = new HttpParam[4];
  36 + params[0] = HttpParam.create("Uid", uid);
  37 + params[1] = HttpParam.create("Key", secretKey);
  38 + params[2] = HttpParam.create("smsMob", telephone);
  39 + params[3] = HttpParam.create("smsText", String.format("【%s】%s", signature, message));
  40 + HttpResult result = send(uri, params);
  41 + if (result.statusCode != 200) {
  42 + throw new AssistantServiceException(ErrorCode.SERVICE_ACCESS_ERROR, "调用网建短信服务失败");
  43 + }
  44 +
  45 + int code = Integer.parseInt(result.responseText);
  46 + if (code <= 0) {
  47 + LOGGER.error("发送短信失败, 错误码: {}", code);
  48 + throw new AssistantServiceException(ErrorCode.SERVICE_ACCESS_ERROR, "发送短信失败, " + errorMessage(code));
  49 + }
  50 + return null;
  51 + }
  52 +
  53 + private String errorMessage(int code) {
  54 + return switch (code) {
  55 + case -1, -11 -> "账号不存在或被禁用";
  56 + case -2, -21 -> "接口密钥错误";
  57 + case -3 -> "短信数量不足";
  58 + case -4, -41 -> "手机号码为空或格式不正确";
  59 + case -14, -42 -> "短信内容为空或出现非法字符";
  60 + case -51, -52 -> "短信签名太长或格式不正确";
  61 + default -> "未知错误";
  62 + };
  63 + }
  64 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/dao/SequenceKeyDao.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/dao/SequenceKeyDao.java
  1 +package com.diligrp.cashier.assistant.dao;
  2 +
  3 +import com.diligrp.cashier.assistant.model.PersistentSequenceKey;
  4 +import com.diligrp.cashier.shared.mybatis.MybatisMapperSupport;
  5 +import org.springframework.stereotype.Repository;
  6 +
  7 +import java.util.Optional;
  8 +
  9 +/**
  10 + * KeySequence数据操作
  11 + *
  12 + */
  13 +@Repository("sequenceKeyDao")
  14 +public interface SequenceKeyDao extends MybatisMapperSupport {
  15 + /**
  16 + * 注册SequenceKey
  17 + */
  18 + void insertSequenceKey(PersistentSequenceKey sequenceKey);
  19 +
  20 + /**
  21 + * 查询指定的SequenceKey
  22 + *
  23 + * @param key - SequenceKey唯一标识
  24 + * @return SequenceKey
  25 + */
  26 + Optional<PersistentSequenceKey> findSequenceKey(String key);
  27 +
  28 + /**
  29 + * 根据KeyId查询SequenceKey
  30 + *
  31 + * @param id - KeyId
  32 + * @return SequenceKey
  33 + */
  34 + Optional<PersistentSequenceKey> findSequenceKeyById(Long id);
  35 +
  36 + /**
  37 + * 悲观锁实现 - 根据数据库主键锁定数据记录
  38 + *
  39 + * @param id - KeyId
  40 + * @return SequenceKey
  41 + */
  42 + Optional<PersistentSequenceKey> lockSequenceKey(Long id);
  43 +
  44 + /**
  45 + * 悲观锁解锁实现 - 根据数据库主键解锁数据记录
  46 + *
  47 + * @param sequenceKey - 参数列表:id/newValue/expiredOn
  48 + * @return 1-更新成功,0-更新失败
  49 + */
  50 + int unlockSequenceKey(PersistentSequenceKey sequenceKey);
  51 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/dao/SmsMessageDao.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/dao/SmsMessageDao.java
  1 +package com.diligrp.cashier.assistant.dao;
  2 +
  3 +import com.diligrp.cashier.assistant.model.SmsMessageDO;
  4 +import com.diligrp.cashier.shared.mybatis.MybatisMapperSupport;
  5 +import org.springframework.stereotype.Repository;
  6 +
  7 +import java.util.List;
  8 +import java.util.Optional;
  9 +
  10 +@Repository("smsMessageDao")
  11 +public interface SmsMessageDao extends MybatisMapperSupport {
  12 + void insertSmsMessage(SmsMessageDO smsMessage);
  13 +
  14 + void insertSmsMessages(List<SmsMessageDO> smsMessages);
  15 +
  16 + Optional<SmsMessageDO> findSmsMessageById(String messageId);
  17 +
  18 + int updateSmsMessageState(SmsMessageDO smsMessage);
  19 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/dao/SmsTemplateDao.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/dao/SmsTemplateDao.java
  1 +package com.diligrp.cashier.assistant.dao;
  2 +
  3 +import com.diligrp.cashier.assistant.domain.TemplateStateDTO;
  4 +import com.diligrp.cashier.assistant.model.SmsTemplateDO;
  5 +import com.diligrp.cashier.shared.mybatis.MybatisMapperSupport;
  6 +import org.springframework.stereotype.Repository;
  7 +
  8 +import java.util.Optional;
  9 +
  10 +@Repository("smsTemplateDao")
  11 +public interface SmsTemplateDao extends MybatisMapperSupport {
  12 + void insertSmsTemplate(SmsTemplateDO template);
  13 +
  14 + Optional<SmsTemplateDO> findSmsTemplateById(String templateId);
  15 +
  16 + int updateSmsTemplate(SmsTemplateDO template);
  17 +
  18 + int deleteSmsTemplate(String templateId);
  19 +
  20 + int compareAndSetState(TemplateStateDTO stateDTO);
  21 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/domain/SequenceKey.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/domain/SequenceKey.java
  1 +package com.diligrp.cashier.assistant.domain;
  2 +
  3 +import java.time.LocalDate;
  4 +
  5 +public class SequenceKey {
  6 + private final long sequence;
  7 + private final LocalDate when;
  8 +
  9 + public SequenceKey(long sequence, LocalDate when) {
  10 + this.sequence = sequence;
  11 + this.when = when;
  12 + }
  13 +
  14 + public long getSequence() {
  15 + return sequence;
  16 + }
  17 +
  18 + public LocalDate getWhen() {
  19 + return when;
  20 + }
  21 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/domain/SequenceKeyDTO.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/domain/SequenceKeyDTO.java
  1 +package com.diligrp.cashier.assistant.domain;
  2 +
  3 +import java.time.LocalDate;
  4 +
  5 +public class SequenceKeyDTO {
  6 + /**
  7 + * KEY标识
  8 + */
  9 + private String key;
  10 + /**
  11 + * Key名称
  12 + */
  13 + private String name;
  14 + /**
  15 + * 起始值
  16 + */
  17 + private Long value;
  18 + /**
  19 + * 步长
  20 + */
  21 + private Integer step;
  22 + /**
  23 + * ID格式
  24 + */
  25 + private String pattern;
  26 + /**
  27 + * 有效日期
  28 + */
  29 + private LocalDate expiredOn;
  30 +
  31 + public String getKey() {
  32 + return key;
  33 + }
  34 +
  35 + public void setKey(String key) {
  36 + this.key = key;
  37 + }
  38 +
  39 + public String getName() {
  40 + return name;
  41 + }
  42 +
  43 + public void setName(String name) {
  44 + this.name = name;
  45 + }
  46 +
  47 + public Long getValue() {
  48 + return value;
  49 + }
  50 +
  51 + public void setValue(Long value) {
  52 + this.value = value;
  53 + }
  54 +
  55 + public Integer getStep() {
  56 + return step;
  57 + }
  58 +
  59 + public void setStep(Integer step) {
  60 + this.step = step;
  61 + }
  62 +
  63 + public String getPattern() {
  64 + return pattern;
  65 + }
  66 +
  67 + public void setPattern(String pattern) {
  68 + this.pattern = pattern;
  69 + }
  70 +
  71 + public LocalDate getExpiredOn() {
  72 + return expiredOn;
  73 + }
  74 +
  75 + public void setExpiredOn(LocalDate expiredOn) {
  76 + this.expiredOn = expiredOn;
  77 + }
  78 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/domain/SmsMessage.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/domain/SmsMessage.java
  1 +package com.diligrp.cashier.assistant.domain;
  2 +
  3 +import org.apache.commons.text.StringSubstitutor;
  4 +
  5 +import java.util.HashMap;
  6 +import java.util.List;
  7 +import java.util.Map;
  8 +import java.util.Objects;
  9 +
  10 +public class SmsMessage {
  11 + // 短信服务通道模版标识 - 支持短信模版的服务通道使用
  12 + private final String templateId;
  13 + // 手机号
  14 + private final List<String> telephones;
  15 + // 解析模版变量前的原始内容
  16 + private final String rawContent;
  17 + // 解析模版变量后的短信内容
  18 + private String content;
  19 + // 模版变量
  20 + private final Map<String, String> params;
  21 + // 消息签名
  22 + private final String signature;
  23 +
  24 + public SmsMessage(String templateId, List<String> telephones, String rawContent, Map<String, String> params, String signature) {
  25 + this.templateId = templateId;
  26 + this.telephones = telephones;
  27 + this.rawContent = rawContent;
  28 + this.params = Objects.nonNull(params) ? new HashMap<>(params) : null;
  29 + this.signature = signature;
  30 + }
  31 +
  32 + public String getTemplateId() {
  33 + return templateId;
  34 + }
  35 +
  36 + public List<String> getTelephones() {
  37 + return telephones;
  38 + }
  39 +
  40 + public synchronized String getContent() { // 延迟解析模版变量
  41 + if (content == null) {
  42 + content = parseContent();
  43 + }
  44 +
  45 + return content;
  46 + }
  47 +
  48 + public String getRawContent() {
  49 + return rawContent;
  50 + }
  51 +
  52 + public Map<String, String> getParams() {
  53 + return params;
  54 + }
  55 +
  56 + public String getSignature() {
  57 + return signature;
  58 + }
  59 +
  60 + private String parseContent() {
  61 + if (Objects.nonNull(this.params)) { // 解析模版变量
  62 + StringSubstitutor engine = new StringSubstitutor(this.params);
  63 + return engine.replace(this.rawContent);
  64 + }
  65 +
  66 + return rawContent;
  67 + }
  68 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/domain/SmsMessageDTO.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/domain/SmsMessageDTO.java
  1 +package com.diligrp.cashier.assistant.domain;
  2 +
  3 +import java.util.List;
  4 +import java.util.Map;
  5 +
  6 +public class SmsMessageDTO {
  7 + // 短信服务通道模版标识
  8 + private String templateId;
  9 + // 手机号
  10 + private String telephone;
  11 + // 手机号列表-批量发送时使用
  12 + private List<String> telephones;
  13 + // 模版变量
  14 + private Map<String, String> params;
  15 + // 消息签名
  16 + private String signature;
  17 + // 是否异步发送短信
  18 + private boolean async;
  19 +
  20 + public String getTemplateId() {
  21 + return templateId;
  22 + }
  23 +
  24 + public void setTemplateId(String templateId) {
  25 + this.templateId = templateId;
  26 + }
  27 +
  28 + public String getTelephone() {
  29 + return telephone;
  30 + }
  31 +
  32 + public void setTelephone(String telephone) {
  33 + this.telephone = telephone;
  34 + }
  35 +
  36 + public List<String> getTelephones() {
  37 + return telephones;
  38 + }
  39 +
  40 + public void setTelephones(List<String> telephones) {
  41 + this.telephones = telephones;
  42 + }
  43 +
  44 + public Map<String, String> getParams() {
  45 + return params;
  46 + }
  47 +
  48 + public void setParams(Map<String, String> params) {
  49 + this.params = params;
  50 + }
  51 +
  52 + public String getSignature() {
  53 + return signature;
  54 + }
  55 +
  56 + public void setSignature(String signature) {
  57 + this.signature = signature;
  58 + }
  59 +
  60 + public boolean isAsync() {
  61 + return async;
  62 + }
  63 +
  64 + public void setAsync(boolean async) {
  65 + this.async = async;
  66 + }
  67 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/domain/SmsTemplate.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/domain/SmsTemplate.java
  1 +package com.diligrp.cashier.assistant.domain;
  2 +
  3 +public class SmsTemplate {
  4 + // 模版ID
  5 + private final String id;
  6 + // 模版类型
  7 + private final Integer type;
  8 + // 模版名称
  9 + private final String name;
  10 + // 模版内容
  11 + private final String content;
  12 + // 模版描述
  13 + private final String description;
  14 +
  15 + public SmsTemplate(Integer type, String name, String content, String description) {
  16 + this(null, type, name, content, description);
  17 + }
  18 +
  19 + public SmsTemplate(String id, Integer type, String name, String content, String description) {
  20 + this.id = id;
  21 + this.type = type;
  22 + this.name = name;
  23 + this.content = content;
  24 + this.description = description;
  25 + }
  26 +
  27 + public String getId() {
  28 + return id;
  29 + }
  30 +
  31 + public Integer getType() {
  32 + return type;
  33 + }
  34 +
  35 + public String getName() {
  36 + return name;
  37 + }
  38 +
  39 + public String getContent() {
  40 + return content;
  41 + }
  42 +
  43 + public String getDescription() {
  44 + return description;
  45 + }
  46 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/domain/SmsTemplateDTO.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/domain/SmsTemplateDTO.java
  1 +package com.diligrp.cashier.assistant.domain;
  2 +
  3 +public class SmsTemplateDTO {
  4 + // 模版ID
  5 + private String templateId;
  6 + // 服务通道
  7 + private Integer pipeline;
  8 + // 模版类型
  9 + private Integer type;
  10 + // 模版名称
  11 + private String name;
  12 + // 模版内容
  13 + private String content;
  14 + // 外部短信模版ID - 比如阿里云短信模版ID
  15 + private String outTemplateId;
  16 + // 备注
  17 + private String description;
  18 +
  19 + public String getTemplateId() {
  20 + return templateId;
  21 + }
  22 +
  23 + public void setTemplateId(String templateId) {
  24 + this.templateId = templateId;
  25 + }
  26 +
  27 + public Integer getPipeline() {
  28 + return pipeline;
  29 + }
  30 +
  31 + public void setPipeline(Integer pipeline) {
  32 + this.pipeline = pipeline;
  33 + }
  34 +
  35 + public Integer getType() {
  36 + return type;
  37 + }
  38 +
  39 + public void setType(Integer type) {
  40 + this.type = type;
  41 + }
  42 +
  43 + public String getName() {
  44 + return name;
  45 + }
  46 +
  47 + public void setName(String name) {
  48 + this.name = name;
  49 + }
  50 +
  51 + public String getContent() {
  52 + return content;
  53 + }
  54 +
  55 + public void setContent(String content) {
  56 + this.content = content;
  57 + }
  58 +
  59 + public String getOutTemplateId() {
  60 + return outTemplateId;
  61 + }
  62 +
  63 + public void setOutTemplateId(String outTemplateId) {
  64 + this.outTemplateId = outTemplateId;
  65 + }
  66 +
  67 + public String getDescription() {
  68 + return description;
  69 + }
  70 +
  71 + public void setDescription(String description) {
  72 + this.description = description;
  73 + }
  74 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/domain/TemplateStateDTO.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/domain/TemplateStateDTO.java
  1 +package com.diligrp.cashier.assistant.domain;
  2 +
  3 +import java.time.LocalDateTime;
  4 +
  5 +public class TemplateStateDTO {
  6 + // 短信模板ID
  7 + private String templateId;
  8 + // 模板状态
  9 + private Integer state;
  10 + // 数据版本
  11 + private Integer version;
  12 + // 修改时间
  13 + private LocalDateTime modifiedTime;
  14 +
  15 + public static TemplateStateDTO of(String templateId, Integer state, Integer version, LocalDateTime modifiedTime) {
  16 + TemplateStateDTO stateDTO = new TemplateStateDTO();
  17 + stateDTO.setTemplateId(templateId);
  18 + stateDTO.setState(state);
  19 + stateDTO.setVersion(version);
  20 + stateDTO.setModifiedTime(modifiedTime);
  21 + return stateDTO;
  22 + }
  23 +
  24 + public String getTemplateId() {
  25 + return templateId;
  26 + }
  27 +
  28 + public void setTemplateId(String templateId) {
  29 + this.templateId = templateId;
  30 + }
  31 +
  32 + public Integer getState() {
  33 + return state;
  34 + }
  35 +
  36 + public void setState(Integer state) {
  37 + this.state = state;
  38 + }
  39 +
  40 + public Integer getVersion() {
  41 + return version;
  42 + }
  43 +
  44 + public void setVersion(Integer version) {
  45 + this.version = version;
  46 + }
  47 +
  48 + public LocalDateTime getModifiedTime() {
  49 + return modifiedTime;
  50 + }
  51 +
  52 + public void setModifiedTime(LocalDateTime modifiedTime) {
  53 + this.modifiedTime = modifiedTime;
  54 + }
  55 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/exception/AssistantServiceException.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/exception/AssistantServiceException.java
  1 +package com.diligrp.cashier.assistant.exception;
  2 +
  3 +import com.diligrp.cashier.shared.exception.PlatformServiceException;
  4 +
  5 +public class AssistantServiceException extends PlatformServiceException {
  6 + public AssistantServiceException(String message) {
  7 + super(message);
  8 + }
  9 +
  10 + public AssistantServiceException(int code, String message) {
  11 + super(code, message);
  12 + }
  13 +
  14 + public AssistantServiceException(String message, Throwable ex) {
  15 + super(message, ex);
  16 + }
  17 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/model/PersistentSequenceKey.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/model/PersistentSequenceKey.java
  1 +package com.diligrp.cashier.assistant.model;
  2 +
  3 +import java.time.LocalDate;
  4 +
  5 +/**
  6 + * KEY-ID生成器数据库模型
  7 + */
  8 +public class PersistentSequenceKey {
  9 + /**
  10 + * ID主键
  11 + */
  12 + private Long id;
  13 + /**
  14 + * KEY标识
  15 + */
  16 + private String key;
  17 + /**
  18 + * 名称
  19 + */
  20 + private String name;
  21 + /**
  22 + * 起始值
  23 + */
  24 + private Long value;
  25 + /**
  26 + * 步长
  27 + */
  28 + private Integer step;
  29 + /**
  30 + * ID格式
  31 + */
  32 + private String pattern;
  33 + /**
  34 + * 有效日期
  35 + */
  36 + private LocalDate expiredOn;
  37 + /**
  38 + * 当前日期-循环ID生成器使用
  39 + */
  40 + private LocalDate today;
  41 + /**
  42 + * 数据版本
  43 + */
  44 + private Integer version;
  45 +
  46 + public Long getId() {
  47 + return id;
  48 + }
  49 +
  50 + public void setId(Long id) {
  51 + this.id = id;
  52 + }
  53 +
  54 + public String getKey() {
  55 + return key;
  56 + }
  57 +
  58 + public void setKey(String key) {
  59 + this.key = key;
  60 + }
  61 +
  62 + public String getName() {
  63 + return name;
  64 + }
  65 +
  66 + public void setName(String name) {
  67 + this.name = name;
  68 + }
  69 +
  70 + public Long getValue() {
  71 + return value;
  72 + }
  73 +
  74 + public void setValue(Long value) {
  75 + this.value = value;
  76 + }
  77 +
  78 + public Integer getStep() {
  79 + return step;
  80 + }
  81 +
  82 + public void setStep(Integer step) {
  83 + this.step = step;
  84 + }
  85 +
  86 + public String getPattern() {
  87 + return pattern;
  88 + }
  89 +
  90 + public void setPattern(String pattern) {
  91 + this.pattern = pattern;
  92 + }
  93 +
  94 + public LocalDate getExpiredOn() {
  95 + return expiredOn;
  96 + }
  97 +
  98 + public void setExpiredOn(LocalDate expiredOn) {
  99 + this.expiredOn = expiredOn;
  100 + }
  101 +
  102 + public LocalDate getToday() {
  103 + return today;
  104 + }
  105 +
  106 + public void setToday(LocalDate today) {
  107 + this.today = today;
  108 + }
  109 +
  110 + public Integer getVersion() {
  111 + return version;
  112 + }
  113 +
  114 + public void setVersion(Integer version) {
  115 + this.version = version;
  116 + }
  117 +
  118 + public static Builder builder() {
  119 + return new PersistentSequenceKey().new Builder();
  120 + }
  121 +
  122 + public class Builder {
  123 + public Builder key(String key) {
  124 + PersistentSequenceKey.this.key = key;
  125 + return this;
  126 + }
  127 +
  128 + public Builder name(String name) {
  129 + PersistentSequenceKey.this.name = name;
  130 + return this;
  131 + }
  132 +
  133 + public Builder value(Long value) {
  134 + PersistentSequenceKey.this.value = value;
  135 + return this;
  136 + }
  137 +
  138 + public Builder step(Integer step) {
  139 + PersistentSequenceKey.this.step = step;
  140 + return this;
  141 + }
  142 +
  143 + public Builder pattern(String pattern) {
  144 + PersistentSequenceKey.this.pattern = pattern;
  145 + return this;
  146 + }
  147 +
  148 + public Builder expiredOn(LocalDate expiredOn) {
  149 + PersistentSequenceKey.this.expiredOn = expiredOn;
  150 + return this;
  151 + }
  152 +
  153 + public Builder version(Integer version) {
  154 + PersistentSequenceKey.this.version = version;
  155 + return this;
  156 + }
  157 +
  158 + public PersistentSequenceKey build() {
  159 + return PersistentSequenceKey.this;
  160 + }
  161 + }
  162 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/model/SmsMessageDO.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/model/SmsMessageDO.java
  1 +package com.diligrp.cashier.assistant.model;
  2 +
  3 +import com.diligrp.cashier.shared.domain.BaseDO;
  4 +
  5 +import java.time.LocalDateTime;
  6 +
  7 +public class SmsMessageDO extends BaseDO {
  8 + // 模版ID
  9 + private String templateId;
  10 + // 服务通道
  11 + private Integer pipeline;
  12 + // 短信类型
  13 + private Integer type;
  14 + // 消息ID
  15 + private String messageId;
  16 + // 电话号码
  17 + private String telephone;
  18 + // 消息内容
  19 + private String content;
  20 + // 消息状态
  21 + private Integer state;
  22 + // 外部请求ID
  23 + private String outMessageId;
  24 +
  25 + public String getTemplateId() {
  26 + return templateId;
  27 + }
  28 +
  29 + public void setTemplateId(String templateId) {
  30 + this.templateId = templateId;
  31 + }
  32 +
  33 + public Integer getPipeline() {
  34 + return pipeline;
  35 + }
  36 +
  37 + public void setPipeline(Integer pipeline) {
  38 + this.pipeline = pipeline;
  39 + }
  40 +
  41 + public Integer getType() {
  42 + return type;
  43 + }
  44 +
  45 + public void setType(Integer type) {
  46 + this.type = type;
  47 + }
  48 +
  49 + public String getMessageId() {
  50 + return messageId;
  51 + }
  52 +
  53 + public void setMessageId(String messageId) {
  54 + this.messageId = messageId;
  55 + }
  56 +
  57 + public String getTelephone() {
  58 + return telephone;
  59 + }
  60 +
  61 + public void setTelephone(String telephone) {
  62 + this.telephone = telephone;
  63 + }
  64 +
  65 + public String getContent() {
  66 + return content;
  67 + }
  68 +
  69 + public void setContent(String content) {
  70 + this.content = content;
  71 + }
  72 +
  73 + public Integer getState() {
  74 + return state;
  75 + }
  76 +
  77 + public void setState(Integer state) {
  78 + this.state = state;
  79 + }
  80 +
  81 + public String getOutMessageId() {
  82 + return outMessageId;
  83 + }
  84 +
  85 + public void setOutMessageId(String outMessageId) {
  86 + this.outMessageId = outMessageId;
  87 + }
  88 +
  89 + public static Builder builder() {
  90 + return new SmsMessageDO().new Builder();
  91 + }
  92 +
  93 + public class Builder {
  94 + public Builder templateId(String templateId) {
  95 + SmsMessageDO.this.templateId = templateId;
  96 + return this;
  97 + }
  98 +
  99 + public Builder pipeline(int pipeline) {
  100 + SmsMessageDO.this.pipeline = pipeline;
  101 + return this;
  102 + }
  103 +
  104 + public Builder type(int type) {
  105 + SmsMessageDO.this.type = type;
  106 + return this;
  107 + }
  108 +
  109 + public Builder messageId(String messageId) {
  110 + SmsMessageDO.this.messageId = messageId;
  111 + return this;
  112 + }
  113 +
  114 + public Builder telephone(String telephone) {
  115 + SmsMessageDO.this.telephone = telephone;
  116 + return this;
  117 + }
  118 +
  119 + public Builder content(String content) {
  120 + SmsMessageDO.this.content = content;
  121 + return this;
  122 + }
  123 +
  124 + public Builder state(int state) {
  125 + SmsMessageDO.this.state = state;
  126 + return this;
  127 + }
  128 +
  129 + public Builder outMessageId(String outMessageId) {
  130 + SmsMessageDO.this.outMessageId = outMessageId;
  131 + return this;
  132 + }
  133 +
  134 + public Builder createdTime(LocalDateTime createdTime) {
  135 + SmsMessageDO.this.createdTime = createdTime;
  136 + return this;
  137 + }
  138 +
  139 + public Builder modifiedTime(LocalDateTime modifiedTime) {
  140 + SmsMessageDO.this.modifiedTime = modifiedTime;
  141 + return this;
  142 + }
  143 +
  144 + public SmsMessageDO build() {
  145 + return SmsMessageDO.this;
  146 + }
  147 + }
  148 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/model/SmsTemplateDO.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/model/SmsTemplateDO.java
  1 +package com.diligrp.cashier.assistant.model;
  2 +
  3 +import com.diligrp.cashier.shared.domain.BaseDO;
  4 +
  5 +import java.time.LocalDateTime;
  6 +
  7 +public class SmsTemplateDO extends BaseDO {
  8 + // 模版ID
  9 + private String templateId;
  10 + // 服务通道
  11 + private Integer pipeline;
  12 + // 模版类型
  13 + private Integer type;
  14 + // 模版名称
  15 + private String name;
  16 + // 模版内容
  17 + private String content;
  18 + // 模版状态
  19 + private Integer state;
  20 + // 备注
  21 + private String description;
  22 + // 外部模版ID
  23 + private String outTemplateId;
  24 +
  25 + public String getTemplateId() {
  26 + return templateId;
  27 + }
  28 +
  29 + public void setTemplateId(String templateId) {
  30 + this.templateId = templateId;
  31 + }
  32 +
  33 + public Integer getPipeline() {
  34 + return pipeline;
  35 + }
  36 +
  37 + public void setPipeline(Integer pipeline) {
  38 + this.pipeline = pipeline;
  39 + }
  40 +
  41 + public Integer getType() {
  42 + return type;
  43 + }
  44 +
  45 + public void setType(Integer type) {
  46 + this.type = type;
  47 + }
  48 +
  49 + public String getName() {
  50 + return name;
  51 + }
  52 +
  53 + public void setName(String name) {
  54 + this.name = name;
  55 + }
  56 +
  57 + public String getContent() {
  58 + return content;
  59 + }
  60 +
  61 + public void setContent(String content) {
  62 + this.content = content;
  63 + }
  64 +
  65 + public Integer getState() {
  66 + return state;
  67 + }
  68 +
  69 + public void setState(Integer state) {
  70 + this.state = state;
  71 + }
  72 +
  73 + public String getDescription() {
  74 + return description;
  75 + }
  76 +
  77 + public void setDescription(String description) {
  78 + this.description = description;
  79 + }
  80 +
  81 + public String getOutTemplateId() {
  82 + return outTemplateId;
  83 + }
  84 +
  85 + public void setOutTemplateId(String outTemplateId) {
  86 + this.outTemplateId = outTemplateId;
  87 + }
  88 +
  89 + public static Builder builder() {
  90 + return new SmsTemplateDO().new Builder();
  91 + }
  92 +
  93 + public class Builder {
  94 + public Builder templateId(String templateId) {
  95 + SmsTemplateDO.this.templateId = templateId;
  96 + return this;
  97 + }
  98 +
  99 + public Builder pipeline(int pipeline) {
  100 + SmsTemplateDO.this.pipeline = pipeline;
  101 + return this;
  102 + }
  103 +
  104 + public Builder type(int type) {
  105 + SmsTemplateDO.this.type = type;
  106 + return this;
  107 + }
  108 +
  109 + public Builder name(String name) {
  110 + SmsTemplateDO.this.name = name;
  111 + return this;
  112 + }
  113 +
  114 + public Builder content(String content) {
  115 + SmsTemplateDO.this.content = content;
  116 + return this;
  117 + }
  118 +
  119 + public Builder state(int state) {
  120 + SmsTemplateDO.this.state = state;
  121 + return this;
  122 + }
  123 +
  124 + public Builder description(String description) {
  125 + SmsTemplateDO.this.description = description;
  126 + return this;
  127 + }
  128 +
  129 + public Builder outTemplateId(String outTemplateId) {
  130 + SmsTemplateDO.this.outTemplateId = outTemplateId;
  131 + return this;
  132 + }
  133 +
  134 + public Builder version(Integer version) {
  135 + SmsTemplateDO.this.version = version;
  136 + return this;
  137 + }
  138 +
  139 + public Builder createdTime(LocalDateTime createdTime) {
  140 + SmsTemplateDO.this.createdTime = createdTime;
  141 + return this;
  142 + }
  143 +
  144 + public Builder modifiedTime(LocalDateTime modifiedTime) {
  145 + SmsTemplateDO.this.modifiedTime = modifiedTime;
  146 + return this;
  147 + }
  148 +
  149 + public SmsTemplateDO build() {
  150 + return SmsTemplateDO.this;
  151 + }
  152 + }
  153 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/Converter.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/Converter.java
  1 +package com.diligrp.cashier.assistant.pattern;
  2 +
  3 +public abstract class Converter<T> {
  4 + private Converter<T> next;
  5 +
  6 + public abstract String convert(T t);
  7 +
  8 + public Converter<T> getNext() {
  9 + return next;
  10 + }
  11 +
  12 + public void setNext(Converter<T> next) {
  13 + this.next = next;
  14 + }
  15 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/DateConverter.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/DateConverter.java
  1 +package com.diligrp.cashier.assistant.pattern;
  2 +
  3 +import com.diligrp.cashier.assistant.domain.SequenceKey;
  4 +import com.diligrp.cashier.assistant.exception.AssistantServiceException;
  5 +import com.diligrp.cashier.shared.ErrorCode;
  6 +
  7 +import java.time.format.DateTimeFormatter;
  8 +
  9 +public class DateConverter extends Converter<SequenceKey> {
  10 + private static final String DEFAULT_FORMAT = "yyyyMMdd";
  11 +
  12 + private final String format;
  13 +
  14 + public DateConverter(String format) {
  15 + if (format != null) {
  16 + try {
  17 + DateTimeFormatter.ofPattern(format);
  18 + } catch (Exception ex) {
  19 + throw new AssistantServiceException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, "Invalid date format");
  20 + }
  21 + this.format = format;
  22 + } else {
  23 + this.format = DEFAULT_FORMAT;
  24 + }
  25 + }
  26 +
  27 + @Override
  28 + public String convert(SequenceKey context) {
  29 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
  30 + return context.getWhen().format(formatter);
  31 + }
  32 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/KeywordToken.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/KeywordToken.java
  1 +package com.diligrp.cashier.assistant.pattern;
  2 +
  3 +import com.diligrp.cashier.assistant.domain.SequenceKey;
  4 +import com.diligrp.cashier.assistant.exception.AssistantServiceException;
  5 +import com.diligrp.cashier.shared.ErrorCode;
  6 +
  7 +public class KeywordToken extends Token {
  8 + public KeywordToken(String token) {
  9 + super(token);
  10 + }
  11 +
  12 + @Override
  13 + Converter<SequenceKey> getConverter() {
  14 + if ("d".equals(token) || "date".equals(token)) {
  15 + return new DateConverter(option);
  16 + } else if ("n".equals(token)) {
  17 + return new SequenceConverter(option);
  18 + } else if ("r".equals(token)) {
  19 + return new RandomConverter(option);
  20 + } else {
  21 + throw new AssistantServiceException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, "Unrecognized keyword " + token);
  22 + }
  23 + }
  24 +
  25 + public String toString() {
  26 + return String.format("keyword(%s)", token);
  27 + }
  28 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/LiteralConverter.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/LiteralConverter.java
  1 +package com.diligrp.cashier.assistant.pattern;
  2 +
  3 +import com.diligrp.cashier.assistant.domain.SequenceKey;
  4 +
  5 +public class LiteralConverter extends Converter<SequenceKey> {
  6 + private final String literal;
  7 +
  8 + public LiteralConverter(String literal) {
  9 + this.literal = literal;
  10 + }
  11 +
  12 + @Override
  13 + public String convert(SequenceKey context) {
  14 + return literal;
  15 + }
  16 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/LiteralToken.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/LiteralToken.java
  1 +package com.diligrp.cashier.assistant.pattern;
  2 +
  3 +import com.diligrp.cashier.assistant.domain.SequenceKey;
  4 +
  5 +public class LiteralToken extends Token {
  6 + public LiteralToken(String token) {
  7 + super(token);
  8 + }
  9 +
  10 + @Override
  11 + Converter<SequenceKey> getConverter() {
  12 + return new LiteralConverter(token);
  13 + }
  14 +
  15 + public String toString() {
  16 + return String.format("literal(%s)", token);
  17 + }
  18 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/OptionToken.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/OptionToken.java
  1 +package com.diligrp.cashier.assistant.pattern;
  2 +
  3 +import com.diligrp.cashier.assistant.domain.SequenceKey;
  4 +import com.diligrp.cashier.assistant.exception.AssistantServiceException;
  5 +import com.diligrp.cashier.shared.ErrorCode;
  6 +
  7 +public class OptionToken extends Token {
  8 + public OptionToken(String token) {
  9 + super(token);
  10 + }
  11 +
  12 + public String getToken() {
  13 + return this.token;
  14 + }
  15 +
  16 + @Override
  17 + Converter<SequenceKey> getConverter() {
  18 + throw new AssistantServiceException(ErrorCode.OPERATION_NOT_ALLOWED, "Not supported converter");
  19 + }
  20 +
  21 + public String toString() {
  22 + return String.format("option(%s)", token);
  23 + }
  24 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/PatternLayout.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/PatternLayout.java
  1 +package com.diligrp.cashier.assistant.pattern;
  2 +
  3 +import com.diligrp.cashier.assistant.domain.SequenceKey;
  4 +
  5 +import java.util.ArrayList;
  6 +import java.util.List;
  7 +
  8 +public class PatternLayout {
  9 +
  10 + private final Converter<SequenceKey> head;
  11 +
  12 + public PatternLayout(String pattern) {
  13 + this.head = compile(pattern);
  14 + }
  15 +
  16 + public String doLayout(SequenceKey context) {
  17 + StringBuilder writer = new StringBuilder();
  18 + Converter<SequenceKey> converter = head;
  19 +
  20 + while (converter != null) {
  21 + writer.append(converter.convert(context));
  22 + converter = converter.getNext();
  23 + }
  24 + return writer.toString();
  25 + }
  26 +
  27 + private Converter<SequenceKey> compile(String pattern) {
  28 + PatternParser parser = new PatternParser(pattern);
  29 + List<Token> tokens = new ArrayList<>();
  30 + Token previous = null;
  31 + for (Token token : parser.parse()) {
  32 + if (token instanceof KeywordToken || token instanceof LiteralToken) {
  33 + previous = token;
  34 + tokens.add(token);
  35 + } else if (token instanceof OptionToken) {
  36 + if (previous != null) {
  37 + previous.setOption(((OptionToken) token).getToken());
  38 + }
  39 + }
  40 + }
  41 +
  42 + Converter<SequenceKey> first = tokens.get(0).getConverter();
  43 + Converter<SequenceKey> current = first;
  44 + for (int i = 1; i < tokens.size(); i++) {
  45 + Token token = tokens.get(i);
  46 + current.setNext(token.getConverter());
  47 + current = current.getNext();
  48 + }
  49 +
  50 + return first;
  51 + }
  52 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/PatternParser.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/PatternParser.java
  1 +package com.diligrp.cashier.assistant.pattern;
  2 +
  3 +import com.diligrp.cashier.assistant.exception.AssistantServiceException;
  4 +import com.diligrp.cashier.shared.ErrorCode;
  5 +
  6 +import java.util.ArrayList;
  7 +import java.util.List;
  8 +
  9 +public class PatternParser {
  10 + private final String pattern;
  11 + private final int length;
  12 + private TokenizerState state;
  13 + private int index;
  14 +
  15 + public PatternParser(String pattern) {
  16 + this.pattern = pattern;
  17 + this.length = pattern.length();
  18 + this.index = 0;
  19 + this.state = TokenizerState.LITERAL_STATE;
  20 + }
  21 +
  22 + public List<Token> parse() {
  23 + List<Token> tokens = new ArrayList<>();
  24 + StringBuilder buf = new StringBuilder();
  25 +
  26 + while(index < length) {
  27 + char c = pattern.charAt(index);
  28 + index ++;
  29 +
  30 + switch (this.state) {
  31 + case LITERAL_STATE:
  32 + handleLiteralState(c, tokens, buf);
  33 + break;
  34 + case KEYWORD_STATE:
  35 + handleKeywordState(c, tokens, buf);
  36 + break;
  37 + case OPTION_STATE:
  38 + handleOptionState(c, tokens, buf);
  39 + break;
  40 + }
  41 + }
  42 +
  43 + switch (state) {
  44 + case LITERAL_STATE:
  45 + handleLiteralState('%', tokens, buf);
  46 + break;
  47 + case KEYWORD_STATE:
  48 + handleKeywordState('%', tokens, buf);
  49 + break;
  50 + default:
  51 + throw new AssistantServiceException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, "Unexpected end of pattern string");
  52 + }
  53 +
  54 + return tokens;
  55 + }
  56 +
  57 + private void handleLiteralState(char c, List<Token> tokens, StringBuilder buf) {
  58 + switch (c) {
  59 + case '%':
  60 + addLiteralToken(buf, tokens);
  61 + state = TokenizerState.KEYWORD_STATE;
  62 + break;
  63 + default:
  64 + buf.append(c);
  65 + }
  66 + }
  67 +
  68 + private void handleKeywordState(char c, List<Token> tokens, StringBuilder buf) {
  69 + switch (c) {
  70 + case '%':
  71 + addKeywordToken(buf, tokens);
  72 + state = TokenizerState.KEYWORD_STATE;
  73 + break;
  74 + case '{':
  75 + this.addKeywordToken(buf, tokens);
  76 + this.state = TokenizerState.OPTION_STATE;
  77 + break;
  78 + default:
  79 + buf.append(c);
  80 + }
  81 + }
  82 +
  83 + private void handleOptionState(char c, List<Token> tokens, StringBuilder buf) {
  84 + switch (c) {
  85 + case '}':
  86 + addOptionToken(buf, tokens);
  87 + state = TokenizerState.LITERAL_STATE;
  88 + break;
  89 + default:
  90 + buf.append(c);
  91 + }
  92 + }
  93 +
  94 + private void addLiteralToken(StringBuilder buf, List<Token> tokens) {
  95 + if (!buf.isEmpty()) {
  96 + tokens.add(new LiteralToken(buf.toString()));
  97 + buf.setLength(0);
  98 + }
  99 + }
  100 +
  101 + private void addKeywordToken(StringBuilder buf, List<Token> tokens) {
  102 + if (!buf.isEmpty()) {
  103 + tokens.add(new KeywordToken(buf.toString()));
  104 + buf.setLength(0);
  105 + }
  106 + }
  107 +
  108 + private void addOptionToken(StringBuilder buf, List<Token> tokens) {
  109 + if (!buf.isEmpty()) {
  110 + tokens.add(new OptionToken(buf.toString()));
  111 + buf.setLength(0);
  112 + }
  113 + }
  114 +
  115 + private enum TokenizerState {
  116 + LITERAL_STATE,
  117 + KEYWORD_STATE,
  118 + OPTION_STATE
  119 + }
  120 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/RandomConverter.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/RandomConverter.java
  1 +package com.diligrp.cashier.assistant.pattern;
  2 +
  3 +import com.diligrp.cashier.assistant.domain.SequenceKey;
  4 +import com.diligrp.cashier.assistant.exception.AssistantServiceException;
  5 +import com.diligrp.cashier.shared.ErrorCode;
  6 +import com.diligrp.cashier.shared.util.RandomUtils;
  7 +
  8 +public class RandomConverter extends Converter<SequenceKey> {
  9 + private static final int DEFAULT_LENGTH = 1;
  10 +
  11 + private final int length;
  12 +
  13 + public RandomConverter(String length) {
  14 + if (length != null) {
  15 + try {
  16 + this.length = Integer.parseInt(length);
  17 + } catch (Exception ex) {
  18 + throw new AssistantServiceException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, "Invalid length for %r token");
  19 + }
  20 + } else {
  21 + this.length = DEFAULT_LENGTH;
  22 + }
  23 + }
  24 +
  25 + @Override
  26 + public String convert(SequenceKey context) {
  27 + return RandomUtils.randomNumber(length);
  28 + }
  29 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/SequenceConverter.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/SequenceConverter.java
  1 +package com.diligrp.cashier.assistant.pattern;
  2 +
  3 +import com.diligrp.cashier.assistant.domain.SequenceKey;
  4 +import com.diligrp.cashier.assistant.exception.AssistantServiceException;
  5 +import com.diligrp.cashier.shared.ErrorCode;
  6 +
  7 +public class SequenceConverter extends Converter<SequenceKey> {
  8 + private static final int DEFAULT_LENGTH = 4;
  9 +
  10 + private final int minLength;
  11 +
  12 + public SequenceConverter(String minLength) {
  13 + if (minLength != null) {
  14 + try {
  15 + this.minLength = Integer.parseInt(minLength);
  16 + } catch (Exception ex) {
  17 + throw new AssistantServiceException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, "Invalid minLength for %n token");
  18 + }
  19 + } else {
  20 + this.minLength = DEFAULT_LENGTH;
  21 + }
  22 + }
  23 +
  24 + @Override
  25 + public String convert(SequenceKey context) {
  26 + StringBuilder buffer = new StringBuilder();
  27 + buffer.append(context.getSequence());
  28 + int length = buffer.length();
  29 + if (length < minLength) {
  30 + for (int i = length; i < minLength; i++) {
  31 + buffer.insert(0, "0");
  32 + }
  33 + }
  34 + return buffer.toString();
  35 + }
  36 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/Token.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pattern/Token.java
  1 +package com.diligrp.cashier.assistant.pattern;
  2 +
  3 +import com.diligrp.cashier.assistant.domain.SequenceKey;
  4 +
  5 +public abstract class Token {
  6 + protected final String token;
  7 +
  8 + protected String option;
  9 +
  10 + public Token(String token) {
  11 + this.token = token;
  12 + }
  13 +
  14 + public void setOption(String option) {
  15 + this.option = option;
  16 + }
  17 +
  18 + abstract Converter<SequenceKey> getConverter();
  19 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pipeline/AliSmsPipeline.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pipeline/AliSmsPipeline.java
  1 +package com.diligrp.cashier.assistant.pipeline;
  2 +
  3 +import com.diligrp.cashier.assistant.client.AliSmsHttpClient;
  4 +import com.diligrp.cashier.assistant.domain.SmsMessage;
  5 +import com.diligrp.cashier.assistant.type.SmsPipelineType;
  6 +
  7 +public class AliSmsPipeline extends SmsPipeline {
  8 + private AliSmsHttpClient client;
  9 +
  10 + public AliSmsPipeline(int code, String name, String endPoint, String accessKeyId, String accessKeySecret) {
  11 + super(code, name, SmsPipelineType.SMS_ALI);
  12 + this.client = new AliSmsHttpClient(endPoint, accessKeyId, accessKeySecret);
  13 + }
  14 +
  15 + @Override
  16 + public String sendSmsMessage(SmsMessage message) {
  17 + return this.client.sendSmsMessage(message);
  18 + }
  19 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pipeline/DefaultSmsPipelineManager.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pipeline/DefaultSmsPipelineManager.java
  1 +package com.diligrp.cashier.assistant.pipeline;
  2 +
  3 +import org.springframework.beans.factory.DisposableBean;
  4 +
  5 +import java.util.ArrayList;
  6 +import java.util.List;
  7 +
  8 +public class DefaultSmsPipelineManager implements SmsPipelineManager, DisposableBean {
  9 + private List<SmsPipeline> pipelines;
  10 +
  11 + public DefaultSmsPipelineManager() {
  12 + this.pipelines = new ArrayList<>();
  13 + }
  14 +
  15 + @Override
  16 + public void registerPipeline(SmsPipeline pipeline) {
  17 + this.pipelines.add(pipeline);
  18 + }
  19 +
  20 + @Override
  21 + public List<SmsPipeline> pipelines() {
  22 + return this.pipelines;
  23 + }
  24 +
  25 + @Override
  26 + public void destroy() {
  27 + for (SmsPipeline pipeline : pipelines) {
  28 + pipeline.destroy();
  29 + }
  30 + }
  31 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pipeline/SmsChinesePipeline.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pipeline/SmsChinesePipeline.java
  1 +package com.diligrp.cashier.assistant.pipeline;
  2 +
  3 +import com.diligrp.cashier.assistant.client.SmsChineseHttpClient;
  4 +import com.diligrp.cashier.assistant.domain.SmsMessage;
  5 +import com.diligrp.cashier.assistant.type.SmsPipelineType;
  6 +
  7 +import java.util.stream.Collectors;
  8 +
  9 +/**
  10 + * 网建短信服务通道
  11 + */
  12 +public class SmsChinesePipeline extends SmsPipeline {
  13 +
  14 + private final SmsChineseHttpClient client;
  15 +
  16 + public SmsChinesePipeline(int code, String name, String uri, String uid, String secretKey) {
  17 + super(code, name, SmsPipelineType.SMS_CHINESE);
  18 + this.client = new SmsChineseHttpClient(uri, uid, secretKey);
  19 + }
  20 +
  21 + @Override
  22 + public String sendSmsMessage(SmsMessage message) {
  23 + String telephones = message.getTelephones().stream().collect(Collectors.joining());
  24 + return client.sendSmsMessage(telephones, message.getContent(), message.getSignature());
  25 + }
  26 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pipeline/SmsPipeline.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pipeline/SmsPipeline.java
  1 +package com.diligrp.cashier.assistant.pipeline;
  2 +
  3 +import com.diligrp.cashier.assistant.domain.SmsMessage;
  4 +import com.diligrp.cashier.assistant.type.SmsPipelineType;
  5 +
  6 +public abstract class SmsPipeline {
  7 + // 通道编码
  8 + protected final int code;
  9 + // 通道名称
  10 + protected final String name;
  11 + // 通道类型
  12 + protected final SmsPipelineType type;
  13 +
  14 + public SmsPipeline(int code, String name, SmsPipelineType type) {
  15 + this.code = code;
  16 + this.name = name;
  17 + this.type = type;
  18 + }
  19 +
  20 + /**
  21 + * 根据短信模版发送短信
  22 + *
  23 + * @param message - 短信
  24 + * @return 短信唯一标识
  25 + */
  26 + public abstract String sendSmsMessage(SmsMessage message);
  27 +
  28 + public void destroy() {
  29 + }
  30 +
  31 + /**
  32 + * 获取通道code
  33 + */
  34 + public int getCode() {
  35 + return this.code;
  36 + }
  37 +
  38 + /**
  39 + * 获取通道编码
  40 + */
  41 + public String getName() {
  42 + return this.name;
  43 + }
  44 +
  45 + /**
  46 + * 获取通道类型
  47 + */
  48 + public SmsPipelineType getType() {
  49 + return this.type;
  50 + }
  51 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pipeline/SmsPipelineManager.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/pipeline/SmsPipelineManager.java
  1 +package com.diligrp.cashier.assistant.pipeline;
  2 +
  3 +import com.diligrp.cashier.assistant.exception.AssistantServiceException;
  4 +import com.diligrp.cashier.shared.ErrorCode;
  5 +
  6 +import java.util.List;
  7 +import java.util.Optional;
  8 +
  9 +public interface SmsPipelineManager {
  10 +
  11 + void registerPipeline(SmsPipeline pipeline);
  12 +
  13 + List<SmsPipeline> pipelines();
  14 +
  15 + default SmsPipeline findPipelineByCode(int code) {
  16 + Optional<SmsPipeline> pipeline = pipelines().stream().filter(p -> p.code == code).findAny();
  17 + return pipeline.orElseThrow(() -> new AssistantServiceException(ErrorCode.OPERATION_NOT_ALLOWED, "系统未配置此短信服务通道"));
  18 + }
  19 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/service/KeyGenerator.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/service/KeyGenerator.java
  1 +package com.diligrp.cashier.assistant.service;
  2 +
  3 +/**
  4 + * SequenceKey基础类
  5 + */
  6 +public interface KeyGenerator {
  7 + /**
  8 + * 获取下一个ID
  9 + *
  10 + * @return 下一个ID
  11 + */
  12 + String nextId();
  13 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/service/SequenceKeyService.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/service/SequenceKeyService.java
  1 +package com.diligrp.cashier.assistant.service;
  2 +
  3 +import com.diligrp.cashier.assistant.model.PersistentSequenceKey;
  4 +
  5 +/**
  6 + * SequenceKey数据同步基础类
  7 + *
  8 + * @author: brenthuang
  9 + * @date: 2020/03/24
  10 + */
  11 +public interface SequenceKeyService {
  12 + /**
  13 + * 注册SequenceKey
  14 + */
  15 + void registerSequenceKey(PersistentSequenceKey sequenceKey);
  16 +
  17 + /**
  18 + * 查找指定的SequenceKey
  19 + *
  20 + * @param key - SequenceKey的唯一标识
  21 + * @return SequenceKey
  22 + */
  23 + PersistentSequenceKey findSequenceKey(String key);
  24 +
  25 + /**
  26 + * 根据KeyId查询SequenceKey
  27 + *
  28 + * @param id - KeyId
  29 + * @return SequenceKey
  30 + */
  31 + PersistentSequenceKey findSequenceKeyById(Long id);
  32 +
  33 + /**
  34 + * 通过悲观锁实现同步从数据库获取基于过期日期的SequenceKey
  35 + *
  36 + * 根据数据库主键锁定数据记录(加行锁),根据SequenceKey的过期日期更新下一个startWith值
  37 + * 当SequenceKey过期时value将重新设置为1,否则value + 1
  38 + *
  39 + * @param id - KeyId
  40 + * @return SequenceKey
  41 + */
  42 + PersistentSequenceKey synchronizeSequenceKey(Long id);
  43 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/service/SmsMessageService.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/service/SmsMessageService.java
  1 +package com.diligrp.cashier.assistant.service;
  2 +
  3 +import java.util.List;
  4 +import java.util.Map;
  5 +
  6 +public interface SmsMessageService {
  7 + /**
  8 + * 发送短信
  9 + *
  10 + * @param templateId - 短信模版ID
  11 + * @param telephones - 手机号列表
  12 + * @param params - 参数
  13 + * @param signature - 网建短信签名
  14 + */
  15 + void sendSmsMessage(String templateId, List<String> telephones, Map<String, String> params, String signature);
  16 +
  17 + /**
  18 + * 进行必要性检查后异步发送短信
  19 + *
  20 + * @param templateId - 短信模版ID
  21 + * @param telephones - 手机号列表
  22 + * @param params - 参数
  23 + * @param signature - 网建短信签名
  24 + */
  25 + void asyncSendSmsMessage(String templateId, List<String> telephones, Map<String, String> params, String signature);
  26 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/service/SmsTemplateService.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/service/SmsTemplateService.java
  1 +package com.diligrp.cashier.assistant.service;
  2 +
  3 +import com.diligrp.cashier.assistant.domain.SmsTemplateDTO;
  4 +
  5 +public interface SmsTemplateService {
  6 + /**
  7 + * 创建短信模版
  8 + *
  9 + * @param smsTemplate - 短信模版
  10 + * @return 模版ID
  11 + */
  12 + String createSmsTemplate(SmsTemplateDTO smsTemplate);
  13 +
  14 + /**
  15 + * 修改短信模版
  16 + *
  17 + * @param smsTemplate - 短信模版
  18 + */
  19 + void modifySmsTemplate(SmsTemplateDTO smsTemplate);
  20 +
  21 + /**
  22 + * 短信模版审批通过
  23 + *
  24 + * @param templateId - 短信模版ID
  25 + */
  26 + void approveSmsTemplate(String templateId);
  27 +
  28 + /**
  29 + * 禁用短信模版
  30 + *
  31 + * @param templateId - 短信模版ID
  32 + */
  33 + void disableSmsTemplate(String templateId);
  34 +
  35 + /**
  36 + * 启用短信模版
  37 + *
  38 + * @param templateId - 短信模版ID
  39 + */
  40 + void enableSmsTemplate(String templateId);
  41 +
  42 + /**
  43 + * 删除短信模版
  44 + *
  45 + * @param templateId - 短信模版ID
  46 + */
  47 + void deleteSmsTemplate(String templateId);
  48 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/service/impl/KeyGeneratorManager.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/service/impl/KeyGeneratorManager.java
  1 +package com.diligrp.cashier.assistant.service.impl;
  2 +
  3 +import com.diligrp.cashier.assistant.model.PersistentSequenceKey;
  4 +import com.diligrp.cashier.assistant.domain.SequenceKey;
  5 +import com.diligrp.cashier.assistant.pattern.PatternLayout;
  6 +import com.diligrp.cashier.assistant.service.KeyGenerator;
  7 +import com.diligrp.cashier.assistant.service.SequenceKeyService;
  8 +import com.diligrp.cashier.shared.util.AssertUtils;
  9 +import org.springframework.stereotype.Service;
  10 +
  11 +import java.time.LocalDate;
  12 +import java.util.concurrent.ConcurrentHashMap;
  13 +import java.util.concurrent.ConcurrentMap;
  14 +import java.util.concurrent.TimeUnit;
  15 +import java.util.concurrent.locks.Lock;
  16 +import java.util.concurrent.locks.ReentrantLock;
  17 +
  18 +@Service("keyGeneratorManager")
  19 +public class KeyGeneratorManager {
  20 + private final SequenceKeyService sequenceKeyService;
  21 +
  22 + private final Lock locker = new ReentrantLock();
  23 +
  24 + private final ConcurrentMap<String, KeyGenerator> keyGenerators = new ConcurrentHashMap<>();
  25 +
  26 + public KeyGeneratorManager(SequenceKeyService sequenceKeyService) {
  27 + this.sequenceKeyService = sequenceKeyService;
  28 + }
  29 +
  30 + public KeyGenerator getKeyGenerator(String key) {
  31 + AssertUtils.notNull(key, "Miss key parameter");
  32 +
  33 + KeyGenerator keyGenerator = keyGenerators.get(key);
  34 + // First check, no need synchronize code block
  35 + if (keyGenerator == null) {
  36 + boolean result = false;
  37 + try {
  38 + result = locker.tryLock(15, TimeUnit.SECONDS);
  39 + if (!result) {
  40 + throw new RuntimeException("Timeout to get KeyGenerator for " + key);
  41 + }
  42 +
  43 + // Double check for performance purpose
  44 + if ((keyGenerator = keyGenerators.get(key)) == null) {
  45 + PersistentSequenceKey persistentKey = sequenceKeyService.findSequenceKey(key);
  46 + if (persistentKey.getExpiredOn() == null) {
  47 + keyGenerator = new KeyGeneratorImpl(persistentKey.getId(), key, persistentKey.getPattern());
  48 + } else {
  49 + keyGenerator = new ExpiredKeyGeneratorImpl(persistentKey.getId(), key, persistentKey.getPattern());
  50 + }
  51 + keyGenerators.put(key, keyGenerator);
  52 + }
  53 + } catch (InterruptedException iex) {
  54 + throw new RuntimeException("Interrupted to get KeyGenerator for " + key, iex);
  55 + } finally {
  56 + if (result) {
  57 + locker.unlock();
  58 + }
  59 + }
  60 + }
  61 +
  62 + return keyGenerator;
  63 + }
  64 +
  65 + private class KeyGeneratorImpl implements KeyGenerator {
  66 + private final long id;
  67 + private final String key;
  68 + private final PatternLayout layout;
  69 + private long startWith;
  70 + private long endWith;
  71 + private final Lock keyLocker = new ReentrantLock();
  72 +
  73 + public KeyGeneratorImpl(long id, String key, String pattern) {
  74 + this.id = id;
  75 + this.key = key;
  76 + this.startWith = 0;
  77 + this.endWith = -1;
  78 + if (pattern != null) {
  79 + this.layout = new PatternLayout(pattern);
  80 + } else {
  81 + this.layout = null;
  82 + }
  83 + }
  84 +
  85 + @Override
  86 + public String nextId() {
  87 + boolean result = false;
  88 + try {
  89 + result = keyLocker.tryLock(15L, TimeUnit.SECONDS);
  90 + if (!result) {
  91 + throw new RuntimeException("Timeout to get KeyGenerator for " + key);
  92 + }
  93 +
  94 + if (this.startWith <= this.endWith) {
  95 + if (this.layout != null) {
  96 + SequenceKey context = new SequenceKey(this.startWith++, LocalDate.now());
  97 + return layout.doLayout(context);
  98 + } else {
  99 + return String.valueOf(this.startWith++);
  100 + }
  101 + } else {
  102 + PersistentSequenceKey sequenceKey = sequenceKeyService.synchronizeSequenceKey(id);
  103 + long newValue = sequenceKey.getValue() + sequenceKey.getStep();
  104 + this.startWith = sequenceKey.getValue();
  105 + this.endWith = newValue - 1;
  106 +
  107 + // Then recursive call for a next ID
  108 + return nextId();
  109 + }
  110 + } catch (InterruptedException iex) {
  111 + throw new RuntimeException("Interrupted to get KeyGenerator for " + key, iex);
  112 + } finally {
  113 + if (result) {
  114 + keyLocker.unlock();
  115 + }
  116 + }
  117 + }
  118 + }
  119 +
  120 + private class ExpiredKeyGeneratorImpl implements KeyGenerator {
  121 + private final long id;
  122 + private final String key;
  123 + private final PatternLayout layout;
  124 +
  125 + private ExpiredKeyGeneratorImpl(long id, String key, String pattern) {
  126 + this.id = id;
  127 + this.key = key;
  128 + if (pattern != null) {
  129 + this.layout = new PatternLayout(pattern);
  130 + } else {
  131 + this.layout = null;
  132 + }
  133 + }
  134 +
  135 + @Override
  136 + public String nextId() {
  137 + //悲观锁添加行锁 - 多JVM多线程场景下自动实现线程同步
  138 + PersistentSequenceKey persistentKey = sequenceKeyService.synchronizeSequenceKey(id);
  139 + if (persistentKey == null) {
  140 + throw new RuntimeException("Unregistered service key generator: " + key);
  141 + }
  142 +
  143 + if (this.layout != null) {
  144 + SequenceKey context = new SequenceKey(persistentKey.getValue(), persistentKey.getToday());
  145 + return layout.doLayout(context);
  146 + } else {
  147 + return String.valueOf(persistentKey.getValue());
  148 + }
  149 + }
  150 + }
  151 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/service/impl/SequenceKeyServiceImpl.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/service/impl/SequenceKeyServiceImpl.java
  1 +package com.diligrp.cashier.assistant.service.impl;
  2 +
  3 +import com.diligrp.cashier.assistant.dao.SequenceKeyDao;
  4 +import com.diligrp.cashier.assistant.model.PersistentSequenceKey;
  5 +import com.diligrp.cashier.assistant.exception.AssistantServiceException;
  6 +import com.diligrp.cashier.assistant.service.SequenceKeyService;
  7 +import com.diligrp.cashier.shared.ErrorCode;
  8 +import org.springframework.stereotype.Service;
  9 +import org.springframework.transaction.annotation.Transactional;
  10 +
  11 +import java.time.LocalDate;
  12 +
  13 +/**
  14 + * SequenceKey数据同步的实现类
  15 + */
  16 +@Service("sequenceKeyService")
  17 +public class SequenceKeyServiceImpl implements SequenceKeyService {
  18 +
  19 + private SequenceKeyDao sequenceKeyDao;
  20 +
  21 + public SequenceKeyServiceImpl(SequenceKeyDao sequenceKeyDao) {
  22 + this.sequenceKeyDao = sequenceKeyDao;
  23 + }
  24 +
  25 + @Override
  26 + @Transactional(rollbackFor = Exception.class)
  27 + public void registerSequenceKey(PersistentSequenceKey sequenceKey) {
  28 + sequenceKeyDao.insertSequenceKey(sequenceKey);
  29 + }
  30 +
  31 + @Override
  32 + public PersistentSequenceKey findSequenceKey(String key) {
  33 + return sequenceKeyDao.findSequenceKey(key).orElseThrow(() ->
  34 + new AssistantServiceException(ErrorCode.OBJECT_NOT_FOUND, "没有配置该SequenceKey"));
  35 + }
  36 +
  37 + @Override
  38 + public PersistentSequenceKey findSequenceKeyById(Long id) {
  39 + return sequenceKeyDao.findSequenceKeyById(id).orElseThrow(() ->
  40 + new AssistantServiceException(ErrorCode.OBJECT_NOT_FOUND, "没有配置该SequenceKey"));
  41 + }
  42 +
  43 + /**
  44 + * 数据库的行锁只有在事务提交之后才会释放,这里使用业务层的Spring事务,因此行锁将不能很快释放,这样势必会降低此代码块的并发性能。
  45 + * 如果新开一个Spring事务Propagation.REQUIRES_NEW,与业务层事务独立将无法保证ID的连续性(不能随着业务层的失败回滚生成的ID)
  46 + */
  47 + @Override
  48 +// @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
  49 + @Transactional(rollbackFor = Exception.class)
  50 + public PersistentSequenceKey synchronizeSequenceKey(Long id) {
  51 + // 悲观锁添加行锁 - 多JVM多线程场景下自动实现线程同步
  52 + // 通过SELECT FOR UPDATE锁定了行,当事务提交时将自动释放行锁
  53 + PersistentSequenceKey persistentKey = sequenceKeyDao.lockSequenceKey(id).orElseThrow(() ->
  54 + new AssistantServiceException(ErrorCode.OBJECT_NOT_FOUND, "没有配置该SequenceKey"));
  55 +
  56 + // 当PersistentKey设置了过期时间并且已经过期时, 则value重新设置成1并刷新过期日期为今天,否则设置value+=step
  57 + LocalDate today = persistentKey.getToday();
  58 + LocalDate expiredDay = persistentKey.getExpiredOn();
  59 + if (expiredDay != null && today.isAfter(persistentKey.getExpiredOn())) {
  60 + persistentKey.setValue(1L);
  61 + expiredDay = today;
  62 + }
  63 +
  64 + PersistentSequenceKey newKey = new PersistentSequenceKey();
  65 + newKey.setId(persistentKey.getId());
  66 + newKey.setValue(persistentKey.getValue() + persistentKey.getStep());
  67 + newKey.setExpiredOn(expiredDay);
  68 +
  69 + sequenceKeyDao.unlockSequenceKey(newKey);
  70 +
  71 + return persistentKey;
  72 + }
  73 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/service/impl/SmsMessageServiceImpl.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/service/impl/SmsMessageServiceImpl.java
  1 +package com.diligrp.cashier.assistant.service.impl;
  2 +
  3 +import com.diligrp.cashier.assistant.dao.SmsMessageDao;
  4 +import com.diligrp.cashier.assistant.dao.SmsTemplateDao;
  5 +import com.diligrp.cashier.assistant.domain.SmsMessage;
  6 +import com.diligrp.cashier.assistant.exception.AssistantServiceException;
  7 +import com.diligrp.cashier.assistant.model.SmsMessageDO;
  8 +import com.diligrp.cashier.assistant.model.SmsTemplateDO;
  9 +import com.diligrp.cashier.assistant.pipeline.SmsPipeline;
  10 +import com.diligrp.cashier.assistant.pipeline.SmsPipelineManager;
  11 +import com.diligrp.cashier.assistant.service.SmsMessageService;
  12 +import com.diligrp.cashier.assistant.type.SmsState;
  13 +import com.diligrp.cashier.assistant.type.TemplateState;
  14 +import com.diligrp.cashier.shared.ErrorCode;
  15 +import com.diligrp.cashier.shared.service.ThreadPoolService;
  16 +import com.diligrp.cashier.shared.util.RandomUtils;
  17 +import jakarta.annotation.Resource;
  18 +import org.springframework.stereotype.Service;
  19 +import org.springframework.transaction.annotation.Transactional;
  20 +
  21 +import java.time.LocalDateTime;
  22 +import java.util.List;
  23 +import java.util.Map;
  24 +import java.util.stream.Collectors;
  25 +
  26 +@Service("smsMessageService")
  27 +public class SmsMessageServiceImpl implements SmsMessageService {
  28 +
  29 + @Resource
  30 + private SmsTemplateDao smsTemplateDao;
  31 +
  32 + @Resource
  33 + private SmsMessageDao smsMessageDao;
  34 +
  35 + @Resource
  36 + private SmsPipelineManager smsPipelineManager;
  37 +
  38 + @Override
  39 + @Transactional(rollbackFor = Exception.class)
  40 + public void sendSmsMessage(String templateId, List<String> telephones, Map<String, String> params, String signature) {
  41 + SmsTemplateDO smsTemplate = smsTemplateDao.findSmsTemplateById(templateId)
  42 + .orElseThrow(() -> new AssistantServiceException(ErrorCode.OBJECT_NOT_FOUND, "短信模版不存在"));
  43 + if (TemplateState.PENDING.equalsTo(smsTemplate.getState()) || TemplateState.FAILED.equalsTo(smsTemplate.getState())) {
  44 + throw new AssistantServiceException(ErrorCode.OPERATION_NOT_ALLOWED, "短信模版未审核通过,不能发送短信");
  45 + }
  46 + if (TemplateState.DISABLED.equalsTo(smsTemplate.getState())) {
  47 + throw new AssistantServiceException(ErrorCode.OPERATION_NOT_ALLOWED, "短信模版被禁用,不能发送短信");
  48 + }
  49 +
  50 + LocalDateTime now = LocalDateTime.now();
  51 + SmsPipeline pipeline = smsPipelineManager.findPipelineByCode(smsTemplate.getPipeline());
  52 + SmsMessage smsMessage = new SmsMessage(smsTemplate.getOutTemplateId(), telephones, smsTemplate.getContent(), params, signature);
  53 +
  54 + String outMessageId = pipeline.sendSmsMessage(smsMessage);
  55 + List<SmsMessageDO> smsMessages = telephones.stream().map(telephone -> SmsMessageDO.builder()
  56 + .templateId(templateId).pipeline(smsTemplate.getPipeline()).type(smsTemplate.getType())
  57 + .messageId(RandomUtils.randomUUID()).telephone(telephone).content(smsMessage.getContent())
  58 + .state(SmsState.SUCCESS.getCode()).outMessageId(outMessageId).createdTime(now).modifiedTime(now).build())
  59 + .collect(Collectors.toList());
  60 + smsMessageDao.insertSmsMessages(smsMessages);
  61 + }
  62 +
  63 + @Override
  64 + public void asyncSendSmsMessage(String templateId, List<String> telephones, Map<String, String> params, String signature) {
  65 + SmsTemplateDO smsTemplate = smsTemplateDao.findSmsTemplateById(templateId)
  66 + .orElseThrow(() -> new AssistantServiceException(ErrorCode.OBJECT_NOT_FOUND, "短信模版不存在"));
  67 + if (TemplateState.PENDING.equalsTo(smsTemplate.getState()) || TemplateState.FAILED.equalsTo(smsTemplate.getState())) {
  68 + throw new AssistantServiceException(ErrorCode.OPERATION_NOT_ALLOWED, "短信模版未审核通过,不能发送短信");
  69 + }
  70 + if (TemplateState.DISABLED.equalsTo(smsTemplate.getState())) {
  71 + throw new AssistantServiceException(ErrorCode.OPERATION_NOT_ALLOWED, "短信模版被禁用,不能发送短信");
  72 + }
  73 +
  74 + LocalDateTime now = LocalDateTime.now();
  75 + SmsPipeline pipeline = smsPipelineManager.findPipelineByCode(smsTemplate.getPipeline());
  76 +
  77 + ThreadPoolService.getIoThreadPoll().submit(() -> {
  78 + SmsMessage smsMessage = new SmsMessage(smsTemplate.getOutTemplateId(), telephones, smsTemplate.getContent(), params, signature);
  79 + String outMessageId = pipeline.sendSmsMessage(smsMessage);
  80 + List<SmsMessageDO> smsMessages = telephones.stream().map(telephone -> SmsMessageDO.builder()
  81 + .templateId(templateId).pipeline(smsTemplate.getPipeline()).type(smsTemplate.getType())
  82 + .messageId(RandomUtils.randomUUID()).telephone(telephone).content(smsMessage.getContent())
  83 + .state(SmsState.SUCCESS.getCode()).outMessageId(outMessageId).createdTime(now).modifiedTime(now).build())
  84 + .collect(Collectors.toList());
  85 + smsMessageDao.insertSmsMessages(smsMessages);
  86 + });
  87 + }
  88 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/service/impl/SmsTemplateServiceImpl.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/service/impl/SmsTemplateServiceImpl.java
  1 +package com.diligrp.cashier.assistant.service.impl;
  2 +
  3 +import com.diligrp.cashier.assistant.dao.SmsTemplateDao;
  4 +import com.diligrp.cashier.assistant.domain.SmsTemplateDTO;
  5 +import com.diligrp.cashier.assistant.domain.TemplateStateDTO;
  6 +import com.diligrp.cashier.assistant.exception.AssistantServiceException;
  7 +import com.diligrp.cashier.assistant.model.SmsTemplateDO;
  8 +import com.diligrp.cashier.assistant.service.SmsTemplateService;
  9 +import com.diligrp.cashier.assistant.type.SmsType;
  10 +import com.diligrp.cashier.assistant.type.TemplateState;
  11 +import com.diligrp.cashier.shared.ErrorCode;
  12 +import jakarta.annotation.Resource;
  13 +import org.springframework.stereotype.Service;
  14 +import org.springframework.transaction.annotation.Transactional;
  15 +
  16 +import java.time.LocalDateTime;
  17 +
  18 +@Service("smsTemplateService")
  19 +public class SmsTemplateServiceImpl implements SmsTemplateService {
  20 +
  21 + @Resource
  22 + private SmsTemplateDao smsTemplateDao;
  23 +
  24 + @Resource
  25 + private KeyGeneratorManager keyGeneratorManager;
  26 +
  27 + @Override
  28 + @Transactional(rollbackFor = Exception.class)
  29 + public String createSmsTemplate(SmsTemplateDTO smsTemplate) {
  30 + LocalDateTime now = LocalDateTime.now();
  31 + SmsType.getType(smsTemplate.getType()).orElseThrow(() ->
  32 + new AssistantServiceException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, "不能识别的短信模版类型"));
  33 +
  34 + SmsTemplateDO.Builder template = SmsTemplateDO.builder();
  35 + String templateId = keyGeneratorManager.getKeyGenerator("SMS_TEMPLATE_KEY").nextId();
  36 + template.templateId(templateId).pipeline(smsTemplate.getPipeline()).type(smsTemplate.getType())
  37 + .name(smsTemplate.getName()).content(smsTemplate.getContent()).state(TemplateState.PENDING.getCode())
  38 + .description(smsTemplate.getDescription()).outTemplateId(smsTemplate.getOutTemplateId()).version(0)
  39 + .createdTime(now).modifiedTime(now);
  40 + smsTemplateDao.insertSmsTemplate(template.build());
  41 + return templateId;
  42 + }
  43 +
  44 + @Override
  45 + @Transactional(rollbackFor = Exception.class)
  46 + public void modifySmsTemplate(SmsTemplateDTO smsTemplate) {
  47 + LocalDateTime now = LocalDateTime.now();
  48 +
  49 + SmsTemplateDO template = smsTemplateDao.findSmsTemplateById(smsTemplate.getTemplateId())
  50 + .orElseThrow(() -> new AssistantServiceException(ErrorCode.OBJECT_NOT_FOUND, "短信模版不存在"));
  51 + // 与阿里云模版管理逻辑保持一致:只允许修改未审核通过的模版,审核通过的模版只能删除
  52 + if (!TemplateState.PENDING.equalsTo(template.getState()) && !TemplateState.FAILED.equalsTo(template.getState())) {
  53 + throw new AssistantServiceException(ErrorCode.OPERATION_NOT_ALLOWED, "短信模版已审核通过,不允许修改");
  54 + }
  55 + if (smsTemplate.getType() != null) {
  56 + SmsType.getType(smsTemplate.getType()).orElseThrow(() -> new AssistantServiceException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, "不能识别的短信模版类型"));
  57 + }
  58 +
  59 + SmsTemplateDO smsTemplateDo = SmsTemplateDO.builder().templateId(smsTemplate.getTemplateId())
  60 + .type(smsTemplate.getType()).name(smsTemplate.getName()).content(smsTemplate.getContent())
  61 + .description(smsTemplate.getDescription()).modifiedTime(now).build();
  62 + smsTemplateDao.updateSmsTemplate(smsTemplateDo);
  63 + }
  64 +
  65 + @Override
  66 + @Transactional(rollbackFor = Exception.class)
  67 + public void approveSmsTemplate(String templateId) {
  68 + LocalDateTime now = LocalDateTime.now();
  69 +
  70 + SmsTemplateDO smsTemplate = smsTemplateDao.findSmsTemplateById(templateId)
  71 + .orElseThrow(() -> new AssistantServiceException(ErrorCode.OBJECT_NOT_FOUND, "短信模版不存在"));
  72 + if (TemplateState.SUCCESS.equalsTo(smsTemplate.getState())) {
  73 + throw new AssistantServiceException(ErrorCode.OPERATION_NOT_ALLOWED, "短信模版已审批通过");
  74 + }
  75 + if (TemplateState.DISABLED.equalsTo(smsTemplate.getState())) {
  76 + throw new AssistantServiceException(ErrorCode.OPERATION_NOT_ALLOWED, "短信模版已被禁用");
  77 + }
  78 +
  79 + TemplateStateDTO stateDTO = TemplateStateDTO.of(templateId, TemplateState.SUCCESS.getCode(),
  80 + smsTemplate.getVersion(), now);
  81 + if (smsTemplateDao.compareAndSetState(stateDTO) == 0) {
  82 + throw new AssistantServiceException(ErrorCode.OPERATION_NOT_ALLOWED, "系统忙,请稍后重试");
  83 + }
  84 + }
  85 +
  86 + @Override
  87 + public void disableSmsTemplate(String templateId) {
  88 + LocalDateTime now = LocalDateTime.now();
  89 + SmsTemplateDO smsTemplate = smsTemplateDao.findSmsTemplateById(templateId)
  90 + .orElseThrow(() -> new AssistantServiceException(ErrorCode.OBJECT_NOT_FOUND, "短信模版不存在"));
  91 + if (!TemplateState.SUCCESS.equalsTo(smsTemplate.getState())) {
  92 + String error = String.format("不能禁用状态为\"%s\"的短信模版", TemplateState.getName(smsTemplate.getState()));
  93 + throw new AssistantServiceException(ErrorCode.OPERATION_NOT_ALLOWED, error);
  94 + }
  95 +
  96 + // 仅仅允许审核通过的短信模版被禁用
  97 + TemplateStateDTO stateDTO = TemplateStateDTO.of(templateId, TemplateState.DISABLED.getCode(),
  98 + smsTemplate.getVersion(), now);
  99 + if (smsTemplateDao.compareAndSetState(stateDTO) == 0) {
  100 + throw new AssistantServiceException(ErrorCode.OPERATION_NOT_ALLOWED, "系统忙,请稍后重试");
  101 + }
  102 + }
  103 +
  104 + @Override
  105 + @Transactional(rollbackFor = Exception.class)
  106 + public void enableSmsTemplate(String templateId) {
  107 + LocalDateTime now = LocalDateTime.now();
  108 + SmsTemplateDO smsTemplate = smsTemplateDao.findSmsTemplateById(templateId)
  109 + .orElseThrow(() -> new AssistantServiceException(ErrorCode.OBJECT_NOT_FOUND, "短信模版不存在"));
  110 + if (!TemplateState.DISABLED.equalsTo(smsTemplate.getState())) {
  111 + throw new AssistantServiceException(ErrorCode.OPERATION_NOT_ALLOWED, "仅仅被禁用的短信模版才能被启用");
  112 + }
  113 +
  114 + TemplateStateDTO stateDTO = TemplateStateDTO.of(templateId, TemplateState.SUCCESS.getCode(),
  115 + smsTemplate.getVersion(), now);
  116 + if (smsTemplateDao.compareAndSetState(stateDTO) == 0) {
  117 + throw new AssistantServiceException(ErrorCode.OPERATION_NOT_ALLOWED, "系统忙,请稍后重试");
  118 + }
  119 + }
  120 +
  121 + @Override
  122 + @Transactional(rollbackFor = Exception.class)
  123 + public void deleteSmsTemplate(String templateId) {
  124 + SmsTemplateDO smsTemplate = smsTemplateDao.findSmsTemplateById(templateId)
  125 + .orElseThrow(() -> new AssistantServiceException(ErrorCode.OBJECT_NOT_FOUND, "短信模版不存在"));
  126 +
  127 + smsTemplateDao.deleteSmsTemplate(templateId);
  128 + }
  129 +}
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/service/impl/SnowflakeKeyManager.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/service/impl/SnowflakeKeyManager.java
  1 +package com.diligrp.cashier.assistant.service.impl;
  2 +
  3 +import com.diligrp.cashier.assistant.service.KeyGenerator;
  4 +import com.diligrp.cashier.shared.security.HexUtils;
  5 +import com.diligrp.cashier.shared.util.AssertUtils;
  6 +import com.diligrp.cashier.shared.util.DateUtils;
  7 +import org.springframework.stereotype.Service;
  8 +
  9 +import java.net.InetAddress;
  10 +import java.net.NetworkInterface;
  11 +import java.nio.charset.StandardCharsets;
  12 +import java.security.MessageDigest;
  13 +import java.time.LocalDateTime;
  14 +import java.time.ZoneOffset;
  15 +import java.util.*;
  16 +import java.util.concurrent.ConcurrentHashMap;
  17 +import java.util.concurrent.ConcurrentMap;
  18 +import java.util.concurrent.TimeUnit;
  19 +import java.util.concurrent.locks.Lock;
  20 +import java.util.concurrent.locks.ReentrantLock;
  21 +
  22 +/**
  23 + * 雪花算法KeyManager实现
  24 + */
  25 +@Service("snowflakeKeyManager")
  26 +public class SnowflakeKeyManager {
  27 +
  28 + private final Lock locker = new ReentrantLock();
  29 +
  30 + private final ConcurrentMap<String, KeyGenerator> keyGenerators = new ConcurrentHashMap<>();
  31 +
  32 + public KeyGenerator getKeyGenerator(SnowflakeKey snowflakeKey) {
  33 + AssertUtils.notNull(snowflakeKey, "Miss key parameter");
  34 +
  35 + String cachedKey = snowflakeKey.identifier();
  36 + KeyGenerator keyGenerator = keyGenerators.get(cachedKey);
  37 + // First check, no need synchronize code block
  38 + if (keyGenerator == null) {
  39 + boolean result = false;
  40 + try {
  41 + result = locker.tryLock(15, TimeUnit.SECONDS);
  42 + if (!result) {
  43 + throw new RuntimeException("Timeout to get SnowflakeKeyGenerator for " + cachedKey);
  44 + }
  45 +
  46 + // Double check for performance purpose
  47 + if ((keyGenerator = keyGenerators.get(cachedKey)) == null) {
  48 + keyGenerator = new SnowflakeKeyGenerator(snowflakeKey);
  49 + keyGenerators.put(cachedKey, keyGenerator);
  50 + }
  51 + } catch (InterruptedException iex) {
  52 + throw new RuntimeException("Interrupted to get SnowflakeKeyGenerator for " + cachedKey, iex);
  53 + } finally {
  54 + if (result) {
  55 + locker.unlock();
  56 + }
  57 + }
  58 + }
  59 +
  60 + return keyGenerator;
  61 + }
  62 +
  63 + /**
  64 + * 雪花算法ID生成器实现
  65 + */
  66 + private static class SnowflakeKeyGenerator implements KeyGenerator {
  67 +
  68 + /**
  69 + * Customer based epoch, unit as second. until 2024-08-08 00:00:00
  70 + */
  71 + private final long epochSeconds = LocalDateTime.of(2024, 8, 8, 0, 0, 0)
  72 + .toEpochSecond(ZoneOffset.of("+8"));
  73 +
  74 + /**
  75 + * Stable fields after spring bean initializing
  76 + */
  77 + private final BitsAllocator bitsAllocator;
  78 + private final long workerId;
  79 +
  80 + /**
  81 + * Volatile fields caused by nextId()
  82 + */
  83 + private long sequence = 0L;
  84 + private long lastSecond = -1L;
  85 +
  86 + public SnowflakeKeyGenerator(SnowflakeKey snowflakeKey) {
  87 + this.bitsAllocator = new BitsAllocator(snowflakeKey.timeBits(), snowflakeKey.workerBits(), snowflakeKey.seqBits());
  88 + // using pid in case two instances running on the same machine
  89 + String serviceFeature = String.format("%s@%s", snowflakeKey.identifier(), ProcessHandle.current().pid());
  90 + this.workerId = new WorkerIdGenerator(snowflakeKey.workerBits()).assignWorkerId(serviceFeature);
  91 + }
  92 +
  93 + @Override
  94 + public synchronized String nextId() {
  95 + long currentSecond = getCurrentSecond();
  96 +
  97 + // Clock moved backwards, refuse to generate uid
  98 + if (currentSecond < lastSecond) {
  99 + long refusedSeconds = lastSecond - currentSecond;
  100 + throw new RuntimeException(String.format("Clock moved backwards. Refusing for %d seconds", refusedSeconds));
  101 + }
  102 +
  103 + // At the same second, increase service
  104 + if (currentSecond == lastSecond) {
  105 + sequence = (sequence + 1) & bitsAllocator.maxSequence;
  106 + // Exceed the max service, we wait the next second to generate uid
  107 + if (sequence == 0) {
  108 + currentSecond = getNextSecond(lastSecond);
  109 + }
  110 +
  111 + // At the different second, service restart from zero
  112 + } else {
  113 + sequence = 0L;
  114 + }
  115 +
  116 + lastSecond = currentSecond;
  117 +
  118 + // Allocate bits for UID
  119 + return String.valueOf(bitsAllocator.allocate(currentSecond - epochSeconds, workerId, sequence));
  120 + }
  121 +
  122 + public String parseId(long uid) {
  123 + long totalBits = BitsAllocator.TOTAL_BITS;
  124 + long signBits = bitsAllocator.signBits;
  125 + long timestampBits = bitsAllocator.timestampBits;
  126 + long workerIdBits = bitsAllocator.workerIdBits;
  127 + long sequenceBits = bitsAllocator.sequenceBits;
  128 +
  129 + // parse UID
  130 + long sequence = (uid << (totalBits - sequenceBits)) >>> (totalBits - sequenceBits);
  131 + long workerId = (uid << (timestampBits + signBits)) >>> (totalBits - workerIdBits);
  132 + long deltaSeconds = uid >>> (workerIdBits + sequenceBits);
  133 +
  134 + LocalDateTime when = LocalDateTime.ofEpochSecond(epochSeconds + deltaSeconds, 0, ZoneOffset.of("+8"));
  135 + String thatTime = DateUtils.formatDateTime(when);
  136 +
  137 + // format as string
  138 + return String.format("{\"UID\":\"%d\",\"timestamp\":\"%s\",\"workerId\":\"%d\",\"service\":\"%d\"}",
  139 + uid, thatTime, workerId, sequence);
  140 + }
  141 +
  142 + /**
  143 + * Get next millisecond
  144 + */
  145 + private long getNextSecond(long lastTimestamp) {
  146 + long timestamp = getCurrentSecond();
  147 + while (timestamp <= lastTimestamp) {
  148 + timestamp = getCurrentSecond();
  149 + }
  150 +
  151 + return timestamp;
  152 + }
  153 +
  154 + /**
  155 + * Get current second
  156 + */
  157 + private long getCurrentSecond() {
  158 + long currentSecond = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
  159 + if (currentSecond - epochSeconds > bitsAllocator.maxDeltaSeconds) {
  160 + throw new RuntimeException("Timestamp bits is exhausted. Refusing UID generate. Now: " + currentSecond);
  161 + }
  162 +
  163 + return currentSecond;
  164 + }
  165 + }
  166 +
  167 + /**
  168 + * Allocate 64 bits for the UID(long)<br>
  169 + * sign (fixed 1bit) -> deltaSecond -> workerId -> service(within the same second)
  170 + */
  171 + private static class BitsAllocator {
  172 + /**
  173 + * Total 64 bits
  174 + */
  175 + public static final int TOTAL_BITS = 1 << 6;
  176 +
  177 + /**
  178 + * Bits for [sign-> second-> workId-> service]
  179 + */
  180 + private final int signBits = 1;
  181 + private final int timestampBits;
  182 + private final int workerIdBits;
  183 + private final int sequenceBits;
  184 +
  185 + /**
  186 + * Max value for workId & service
  187 + */
  188 + private final long maxDeltaSeconds;
  189 + private final long maxSequence;
  190 +
  191 + /**
  192 + * Shift for timestamp & workerId
  193 + */
  194 + private final int timestampShift;
  195 + private final int workerIdShift;
  196 +
  197 + /**
  198 + * Constructor with timestampBits, workerIdBits, sequenceBits<br>
  199 + * The highest bit used for sign, so <code>63</code> bits for timestampBits, workerIdBits, sequenceBits
  200 + */
  201 + public BitsAllocator(int timestampBits, int workerIdBits, int sequenceBits) {
  202 + // make sure allocated 64 bits
  203 + int allocateTotalBits = signBits + timestampBits + workerIdBits + sequenceBits;
  204 + AssertUtils.isTrue(allocateTotalBits == TOTAL_BITS, "allocate not enough 64 bits");
  205 +
  206 + // initialize bits
  207 + this.timestampBits = timestampBits;
  208 + this.workerIdBits = workerIdBits;
  209 + this.sequenceBits = sequenceBits;
  210 +
  211 + // initialize max value
  212 + this.maxDeltaSeconds = ~(-1L << timestampBits);
  213 + this.maxSequence = ~(-1L << sequenceBits);
  214 +
  215 + // initialize shift
  216 + this.timestampShift = workerIdBits + sequenceBits;
  217 + this.workerIdShift = sequenceBits;
  218 + }
  219 +
  220 + /**
  221 + * Allocate bits for UID according to delta seconds & workerId & service<br>
  222 + * <b>Note that: </b>The highest bit will always be 0 for sign
  223 + */
  224 + public long allocate(long deltaSeconds, long workerId, long sequence) {
  225 + return (deltaSeconds << timestampShift) | (workerId << workerIdShift) | sequence;
  226 + }
  227 + }
  228 +
  229 + /**
  230 + * Assign worker id using host and service feature, avoid worker id conflict
  231 + */
  232 + private static class WorkerIdGenerator {
  233 + /**
  234 + * Host feature
  235 + */
  236 + private static final int UNIQUE_HOST_ID = getUniqueHostId();
  237 +
  238 + private final int workerIdBits;
  239 +
  240 + public WorkerIdGenerator(int workerIdBits) {
  241 + this.workerIdBits = workerIdBits;
  242 + }
  243 +
  244 + public long assignWorkerId(String serviceFeature) {
  245 + int uniqueServiceId = convertUniqueInt(serviceFeature);
  246 + long workerId = ((long) UNIQUE_HOST_ID << 32) | uniqueServiceId;
  247 + long maxWorkerId = ~(-1L << workerIdBits);
  248 + return workerId & maxWorkerId;
  249 + }
  250 +
  251 + private static int getUniqueHostId() {
  252 + StringBuilder uniqueFeatures = new StringBuilder();
  253 + try {
  254 + Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
  255 + List<String> macs = new ArrayList<>();
  256 + while (interfaces.hasMoreElements()) {
  257 + NetworkInterface networkInterface = interfaces.nextElement();
  258 + if (networkInterface.isLoopback() || networkInterface.isVirtual() || !networkInterface.isUp()) {
  259 + continue;
  260 + }
  261 + byte[] macBytes = networkInterface.getHardwareAddress();
  262 + if (macBytes == null || macBytes.length == 0) {
  263 + continue;
  264 + }
  265 + macs.add(HexUtils.encodeHexStr(macBytes));
  266 + }
  267 +
  268 + if (!macs.isEmpty()) {
  269 + Collections.sort(macs);
  270 + String macList = macs.stream().reduce((mac1, mac2) -> mac1 + "," + mac2).orElse("unknown-mac");
  271 + uniqueFeatures.append(macList);
  272 + } else {
  273 + uniqueFeatures.append(UUID.randomUUID());
  274 + }
  275 + } catch (Exception ex) {
  276 + throw new RuntimeException("Failed to generate unique host feature", ex);
  277 + }
  278 +
  279 + try {
  280 + String hostName = InetAddress.getLocalHost().getHostName();
  281 + uniqueFeatures.append("@").append(hostName);
  282 + } catch (Exception ex) {
  283 + uniqueFeatures.append("@unknown-host");
  284 + }
  285 +
  286 + return convertUniqueInt(uniqueFeatures.toString());
  287 + }
  288 +
  289 + private static int convertUniqueInt(String feature) {
  290 + MessageDigest sha;
  291 + try {
  292 + sha = MessageDigest.getInstance("SHA-256");
  293 + } catch (Exception ex) {
  294 + throw new RuntimeException("Failed to init snowflake key", ex);
  295 + }
  296 +
  297 + sha.update(feature.getBytes(StandardCharsets.UTF_8));
  298 + byte[] bytes = new byte[4];
  299 + System.arraycopy(sha.digest(), 4, bytes, 0, 4);
  300 +
  301 + // Big endian
  302 + int intValue = 0;
  303 + intValue |= (bytes[3] & 0xFF);
  304 + intValue |= (bytes[2] & 0xFF) << 8;
  305 + intValue |= (bytes[1] & 0xFF) << 16;
  306 + intValue |= (bytes[0] & 0xFF) << 24;
  307 + return intValue;
  308 + }
  309 + }
  310 +
  311 + public interface SnowflakeKey {
  312 + // 默认时间戳位数
  313 + int timeBits = 32;
  314 + // 默认机器标识位数
  315 + int workerBits = 19;
  316 + // 默认序号位数
  317 + int seqBits = 12;
  318 +
  319 + default int timeBits() {
  320 + return timeBits;
  321 + }
  322 +
  323 + default int workerBits() {
  324 + return workerBits;
  325 + }
  326 +
  327 + default int seqBits() {
  328 + return seqBits;
  329 + }
  330 +
  331 + default String identifier() {
  332 + return this.toString();
  333 + }
  334 + }
  335 +}
0 336 \ No newline at end of file
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/type/SmsPipelineType.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/type/SmsPipelineType.java
  1 +package com.diligrp.cashier.assistant.type;
  2 +
  3 +import com.diligrp.cashier.shared.type.IEnumType;
  4 +
  5 +import java.util.Arrays;
  6 +import java.util.List;
  7 +import java.util.Optional;
  8 +import java.util.stream.Stream;
  9 +
  10 +public enum SmsPipelineType implements IEnumType {
  11 + SMS_ALI("阿里短信服务", 1),
  12 +
  13 + SMS_CHINESE("网建短信服务", 2);
  14 +
  15 +
  16 + private final String name;
  17 + private final int code;
  18 +
  19 + SmsPipelineType(String name, int code) {
  20 + this.name = name;
  21 + this.code = code;
  22 + }
  23 +
  24 + public static Optional<SmsPipelineType> getType(int code) {
  25 + Stream<SmsPipelineType> types = Arrays.stream(values());
  26 + return types.filter(type -> type.getCode() == code).findFirst();
  27 + }
  28 +
  29 + public static String getName(int code) {
  30 + Stream<SmsPipelineType> TYPES = Arrays.stream(values());
  31 + Optional<String> result = TYPES.filter(type -> type.getCode() == code)
  32 + .map(SmsPipelineType::getName).findFirst();
  33 + return result.orElse(null);
  34 + }
  35 +
  36 + public static List<SmsPipelineType> getTypes() {
  37 + return Arrays.asList(values());
  38 + }
  39 +
  40 + public String getName() {
  41 + return this.name;
  42 + }
  43 +
  44 + public int getCode() {
  45 + return this.code;
  46 + }
  47 +
  48 + public String toString() {
  49 + return this.name;
  50 + }
  51 +}
0 52 \ No newline at end of file
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/type/SmsState.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/type/SmsState.java
  1 +package com.diligrp.cashier.assistant.type;
  2 +
  3 +import com.diligrp.cashier.shared.type.IEnumType;
  4 +
  5 +import java.util.Arrays;
  6 +import java.util.List;
  7 +import java.util.Optional;
  8 +import java.util.stream.Stream;
  9 +
  10 +public enum SmsState implements IEnumType {
  11 + SUBMITTED("已提交", 1),
  12 +
  13 + SUCCESS("发送成功", 2),
  14 +
  15 + FAILED("发送失败", 3);
  16 +
  17 + private final String name;
  18 + private final int code;
  19 +
  20 + SmsState(String name, int code) {
  21 + this.name = name;
  22 + this.code = code;
  23 + }
  24 +
  25 + public static Optional<SmsState> getState(int code) {
  26 + Stream<SmsState> states = Arrays.stream(values());
  27 + return states.filter(state -> state.getCode() == code).findFirst();
  28 + }
  29 +
  30 + public static String getName(int code) {
  31 + Stream<SmsState> states = Arrays.stream(values());
  32 + Optional<String> result = states.filter(state -> state.getCode() == code)
  33 + .map(SmsState::getName).findFirst();
  34 + return result.isPresent() ? result.get() : null;
  35 + }
  36 +
  37 + public static List<SmsState> getStates() {
  38 + return Arrays.asList(values());
  39 + }
  40 +
  41 + public String getName() {
  42 + return this.name;
  43 + }
  44 +
  45 + public int getCode() {
  46 + return this.code;
  47 + }
  48 +
  49 + public String toString() {
  50 + return this.name;
  51 + }
  52 +}
0 53 \ No newline at end of file
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/type/SmsType.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/type/SmsType.java
  1 +package com.diligrp.cashier.assistant.type;
  2 +
  3 +import com.diligrp.cashier.shared.type.IEnumType;
  4 +
  5 +import java.util.Arrays;
  6 +import java.util.List;
  7 +import java.util.Optional;
  8 +import java.util.stream.Stream;
  9 +
  10 +/**
  11 + * 与阿里云短信服务中的模版类型保持一致,请谨慎修改
  12 + */
  13 +public enum SmsType implements IEnumType {
  14 + CAPTCHA("验证码", 0),
  15 +
  16 + INFORMATION("短信通知", 1),
  17 +
  18 + PROMOTION("推广短信", 2);
  19 +
  20 + private final String name;
  21 + private final int code;
  22 +
  23 + SmsType(String name, int code) {
  24 + this.name = name;
  25 + this.code = code;
  26 + }
  27 +
  28 + public static Optional<SmsType> getType(int code) {
  29 + Stream<SmsType> types = Arrays.stream(values());
  30 + return types.filter(type -> type.getCode() == code).findFirst();
  31 + }
  32 +
  33 + public static String getName(int code) {
  34 + Stream<SmsType> TYPES = Arrays.stream(values());
  35 + Optional<String> result = TYPES.filter(type -> type.getCode() == code)
  36 + .map(SmsType::getName).findFirst();
  37 + return result.orElse(null);
  38 + }
  39 +
  40 + public static List<SmsType> getTypes() {
  41 + return Arrays.asList(values());
  42 + }
  43 +
  44 + public String getName() {
  45 + return this.name;
  46 + }
  47 +
  48 + public int getCode() {
  49 + return this.code;
  50 + }
  51 +
  52 + public String toString() {
  53 + return this.name;
  54 + }
  55 +}
0 56 \ No newline at end of file
... ...
cashier-assistant/src/main/java/com/diligrp/cashier/assistant/type/TemplateState.java 0 → 100644
  1 +++ a/cashier-assistant/src/main/java/com/diligrp/cashier/assistant/type/TemplateState.java
  1 +package com.diligrp.cashier.assistant.type;
  2 +
  3 +import com.diligrp.cashier.shared.type.IEnumType;
  4 +
  5 +import java.util.Arrays;
  6 +import java.util.List;
  7 +import java.util.Optional;
  8 +import java.util.stream.Stream;
  9 +
  10 +/**
  11 + * 允许的状态变更
  12 + *
  13 + * 待审核 -> 审核通过/审核失败
  14 + * 审核通过 -> 禁用
  15 + */
  16 +public enum TemplateState implements IEnumType {
  17 + PENDING("待审核", 1),
  18 +
  19 + SUCCESS("审核通过", 2),
  20 +
  21 + FAILED("审核失败", 3),
  22 +
  23 + DISABLED("禁用", 0);
  24 +
  25 + private final String name;
  26 + private final int code;
  27 +
  28 + TemplateState(String name, int code) {
  29 + this.name = name;
  30 + this.code = code;
  31 + }
  32 +
  33 + public static Optional<TemplateState> getState(int code) {
  34 + Stream<TemplateState> states = Arrays.stream(values());
  35 + return states.filter(state -> state.getCode() == code).findFirst();
  36 + }
  37 +
  38 + public static String getName(int code) {
  39 + Stream<TemplateState> states = Arrays.stream(values());
  40 + Optional<String> result = states.filter(state -> state.getCode() == code)
  41 + .map(TemplateState::getName).findFirst();
  42 + return result.orElse(null);
  43 + }
  44 +
  45 + public static List<TemplateState> getStates() {
  46 + return Arrays.asList(values());
  47 + }
  48 +
  49 + public String getName() {
  50 + return this.name;
  51 + }
  52 +
  53 + public int getCode() {
  54 + return this.code;
  55 + }
  56 +
  57 + public boolean equalsTo(int code) {
  58 + return this.code == code;
  59 + }
  60 +
  61 + public String toString() {
  62 + return this.name;
  63 + }
  64 +}
0 65 \ No newline at end of file
... ...
cashier-assistant/src/main/resources/com/diligrp/cashier/dao/mapper/SequenceKeyDao.xml 0 → 100644
  1 +++ a/cashier-assistant/src/main/resources/com/diligrp/cashier/dao/mapper/SequenceKeyDao.xml
  1 +<?xml version="1.0" encoding="UTF-8" ?>
  2 +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  3 + "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  4 +
  5 +<mapper namespace="com.diligrp.cashier.assistant.dao.SequenceKeyDao">
  6 + <resultMap id="PersistentKeyMap" type="com.diligrp.cashier.assistant.model.PersistentSequenceKey">
  7 + <id column="id" property="id"/>
  8 + <result column="key" property="key"/>
  9 + <result column="name" property="name"/>
  10 + <result column="value" property="value"/>
  11 + <result column="step" property="step"/>
  12 + <result column="pattern" property="pattern"/>
  13 + <result column="expired_on" property="expiredOn"/>
  14 + <result column="today" property="today"/>
  15 + <result column="version" property="version"/>
  16 + </resultMap>
  17 +
  18 + <insert id="insertSequenceKey" parameterType="com.diligrp.cashier.assistant.model.PersistentSequenceKey">
  19 + INSERT INTO uid_sequence_key(`key`, name, value, step, pattern, expired_on, version)
  20 + VALUES (#{key}, #{name}, #{value}, #{step}, #{pattern}, #{expiredOn}, #{version})
  21 + </insert>
  22 +
  23 + <select id="findSequenceKey" parameterType="string" resultMap="PersistentKeyMap">
  24 + SELECT
  25 + id, `key`, name, value, step, pattern, expired_on, curdate() AS today, version
  26 + FROM
  27 + uid_sequence_key
  28 + WHERE
  29 + `key` = #{key}
  30 + </select>
  31 +
  32 + <select id="findSequenceKeyById" parameterType="long" resultMap="PersistentKeyMap">
  33 + SELECT
  34 + id, `key`, name, value, step, pattern, expired_on, curdate() AS today, version
  35 + FROM
  36 + uid_sequence_key
  37 + WHERE
  38 + id = #{id}
  39 + </select>
  40 +
  41 + <select id="lockSequenceKey" parameterType="long" resultMap="PersistentKeyMap">
  42 + SELECT
  43 + id, `key`, name, value, step, pattern, expired_on, curdate() AS today, version
  44 + FROM
  45 + uid_sequence_key
  46 + WHERE
  47 + id = #{id}
  48 + FOR UPDATE
  49 + </select>
  50 +
  51 + <update id="unlockSequenceKey" parameterType="com.diligrp.cashier.assistant.model.PersistentSequenceKey">
  52 + UPDATE
  53 + uid_sequence_key
  54 + SET
  55 + value = #{value}, expired_on = #{expiredOn}, version = version + 1
  56 + WHERE
  57 + id = #{id}
  58 + </update>
  59 +</mapper>
0 60 \ No newline at end of file
... ...
cashier-assistant/src/main/resources/com/diligrp/cashier/dao/mapper/SmsMessageDao.xml 0 → 100644
  1 +++ a/cashier-assistant/src/main/resources/com/diligrp/cashier/dao/mapper/SmsMessageDao.xml
  1 +<?xml version="1.0" encoding="UTF-8" ?>
  2 +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  3 + "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  4 +
  5 +<mapper namespace="com.diligrp.assistant.sms.dao.SmsMessageDao">
  6 + <resultMap id="SmsMessageMap" type="com.diligrp.cashier.assistant.model.SmsMessageDO">
  7 + <id column="id" property="id"/>
  8 + <result column="template_id" property="templateId"/>
  9 + <result column="pipeline" property="pipeline"/>
  10 + <result column="type" property="type"/>
  11 + <result column="message_id" property="messageId"/>
  12 + <result column="telephone" property="telephone"/>
  13 + <result column="content" property="content"/>
  14 + <result column="state" property="state"/>
  15 + <result column="out_message_id" property="outMessageId"/>
  16 + <result column="created_time" property="createdTime"/>
  17 + <result column="modified_time" property="modifiedTime"/>
  18 + </resultMap>
  19 +
  20 + <insert id="insertSmsMessage" parameterType="com.diligrp.cashier.assistant.model.SmsTemplateDO">
  21 + INSERT INTO sms_message
  22 + (template_id, pipeline, type, message_id, telephone, content, state, out_message_id, created_time, modified_time)
  23 + VALUES
  24 + (#{templateId}, #{pipeline}, #{type}, #{messageId}, #{telephone}, #{content}, #{state}, #{outMessageId}, #{createdTime}, #{modifiedTime})
  25 + </insert>
  26 +
  27 + <insert id="insertSmsMessages">
  28 + INSERT INTO sms_message
  29 + (template_id, pipeline, type, message_id, telephone, content, state, out_message_id, created_time, modified_time)
  30 + VALUES
  31 + <foreach collection="list" item="item" separator=",">
  32 + (#{item.templateId}, #{item.pipeline}, #{item.type}, #{item.messageId}, #{item.telephone}, #{item.content}, #{item.state}, #{item.outMessageId}, #{item.createdTime}, #{item.modifiedTime})
  33 + </foreach>
  34 + </insert>
  35 +
  36 + <select id="findSmsMessageById" parameterType="string" resultMap="SmsMessageMap">
  37 + SELECT * FROM sms_message WHERE message_id = #{messageId}
  38 + </select>
  39 +
  40 + <update id="updateSmsMessageState" parameterType="com.diligrp.cashier.assistant.model.SmsMessageDO">
  41 + UPDATE sms_message SET state = #{state}, modified_time = #{modifiedTime}
  42 + WHERE message_id = #{messageId}
  43 + </update>
  44 +</mapper>
... ...
cashier-assistant/src/main/resources/com/diligrp/cashier/dao/mapper/SmsTemplateDao.xml 0 → 100644
  1 +++ a/cashier-assistant/src/main/resources/com/diligrp/cashier/dao/mapper/SmsTemplateDao.xml
  1 +<?xml version="1.0" encoding="UTF-8" ?>
  2 +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  3 + "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  4 +
  5 +<mapper namespace="com.diligrp.assistant.sms.dao.SmsTemplateDao">
  6 + <resultMap id="SmsTemplateMap" type="com.diligrp.cashier.assistant.model.SmsTemplateDO">
  7 + <id column="id" property="id"/>
  8 + <result column="template_id" property="templateId"/>
  9 + <result column="pipeline" property="pipeline"/>
  10 + <result column="type" property="type"/>
  11 + <result column="name" property="name"/>
  12 + <result column="content" property="content"/>
  13 + <result column="state" property="state"/>
  14 + <result column="description" property="description"/>
  15 + <result column="out_template_id" property="outTemplateId"/>
  16 + <result column="version" property="version"/>
  17 + <result column="created_time" property="createdTime"/>
  18 + <result column="modified_time" property="modifiedTime"/>
  19 + </resultMap>
  20 +
  21 + <insert id="insertSmsTemplate" parameterType="com.diligrp.cashier.assistant.model.SmsTemplateDO">
  22 + INSERT INTO sms_template
  23 + (template_id, pipeline, type, name, content, state, description, out_template_id, version, created_time, modified_time)
  24 + VALUES
  25 + (#{templateId}, #{pipeline}, #{type}, #{name}, #{content}, #{state}, #{description}, #{outTemplateId}, #{version}, #{createdTime}, #{modifiedTime})
  26 + </insert>
  27 +
  28 + <select id="findSmsTemplateById" parameterType="string" resultMap="SmsTemplateMap">
  29 + SELECT * FROM sms_template WHERE template_id = #{templateId}
  30 + </select>
  31 +
  32 + <update id="updateSmsTemplate" parameterType="com.diligrp.cashier.assistant.model.SmsTemplateDO">
  33 + UPDATE sms_template SET modified_time = #{modifiedTime}
  34 + <if test="type != null">
  35 + , type = #{type}
  36 + </if>
  37 + <if test="name != null">
  38 + , name = #{name}
  39 + </if>
  40 + <if test="content != null">
  41 + , content = #{content}
  42 + </if>
  43 + <if test="state != null">
  44 + , state = #{state}
  45 + </if>
  46 + <if test="description != null">
  47 + , description = #{description}
  48 + </if>
  49 + WHERE template_id = #{templateId}
  50 + </update>
  51 +
  52 + <update id="compareAndSetState" parameterType="com.diligrp.cashier.assistant.domain.TemplateStateDTO">
  53 + UPDATE sms_template SET state = #{state}, version = version + 1
  54 + <if test="modifiedTime != null">
  55 + , modified_time = #{modifiedTime}
  56 + </if>
  57 + WHERE
  58 + template_id = #{templateId} AND version = #{version}
  59 + </update>
  60 +
  61 + <update id="deleteSmsTemplate" parameterType="string">
  62 + DELETE FROM sms_template WHERE template_id = #{templateId}
  63 + </update>
  64 +</mapper>
... ...
cashier-boss/build.gradle 0 → 100644
  1 +++ a/cashier-boss/build.gradle
  1 +plugins {
  2 + id 'java'
  3 + id 'org.springframework.boot'
  4 + id 'com.github.johnrengelman.shadow'
  5 +}
  6 +
  7 +dependencies {
  8 + implementation project(':cashier-trade')
  9 + implementation project(':cashier-rtmart')
  10 + implementation 'org.springframework.cloud:spring-cloud-starter'
  11 + implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap'
  12 + implementation libs.spring.cloud.nacos.discovery
  13 + implementation libs.spring.cloud.nacos.config
  14 + // implementation 'org.springframework.boot:spring-boot-starter-actuator'
  15 + // testImplementation 'org.springframework.boot:spring-boot-starter-test'
  16 +}
  17 +
  18 +springBoot {
  19 + mainClass = 'com.diligrp.cashier.boss.CashierServiceBootstrap'
  20 +}
  21 +
  22 +tasks.bootJar {
  23 + archiveFileName = "cashier-service-${version}.jar"
  24 + destinationDirectory = file("$rootProject.buildDir/libs")
  25 +}
0 26 \ No newline at end of file
... ...
cashier-boss/src/main/java/com/diligrp/cashier/boss/BossConfiguration.java 0 → 100644
  1 +++ a/cashier-boss/src/main/java/com/diligrp/cashier/boss/BossConfiguration.java
  1 +package com.diligrp.cashier.boss;
  2 +
  3 +import org.springframework.context.annotation.ComponentScan;
  4 +import org.springframework.context.annotation.Configuration;
  5 +
  6 +@Configuration
  7 +@ComponentScan("com.diligrp.cashier.boss")
  8 +public class BossConfiguration {
  9 +}
... ...
cashier-boss/src/main/java/com/diligrp/cashier/boss/CashierServiceBootstrap.java 0 → 100644
  1 +++ a/cashier-boss/src/main/java/com/diligrp/cashier/boss/CashierServiceBootstrap.java
  1 +package com.diligrp.cashier.boss;
  2 +
  3 +import com.diligrp.cashier.assistant.AssistantConfiguration;
  4 +import org.springframework.boot.SpringApplication;
  5 +import org.springframework.boot.SpringBootConfiguration;
  6 +import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
  7 +import org.springframework.boot.autoconfigure.SpringBootApplication;
  8 +import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
  9 +import org.springframework.context.annotation.Import;
  10 +
  11 +@SpringBootConfiguration
  12 +@EnableAutoConfiguration
  13 +@Import({BossConfiguration.class, AssistantConfiguration.class})
  14 +@EnableDiscoveryClient
  15 +public class CashierServiceBootstrap {
  16 + public static void main(String[] args) {
  17 + SpringApplication.run(CashierServiceBootstrap.class, args);
  18 + }
  19 +}
0 20 \ No newline at end of file
... ...
cashier-boss/src/main/resources/bootstrap.properties 0 → 100644
  1 +++ a/cashier-boss/src/main/resources/bootstrap.properties
  1 +spring.profiles.active=dev
  2 +spring.application.name=cashier-service
  3 +
  4 +spring.config.import[0]=nacos:${spring.application.name}.properties
  5 +spring.config.import[1]=nacos:${spring.application.name}-${spring.profiles.active}.properties
  6 +
  7 +spring.cloud.nacos.discovery.enabled=true
  8 +spring.cloud.nacos.discovery.group=MICROSERVICE
  9 +spring.cloud.nacos.discovery.server-addr=nacos.diligrp.com:8848
  10 +spring.cloud.nacos.discovery.namespace=2267e673-b41f-458d-9643-2a03e4fd92fb
  11 +
  12 +spring.cloud.nacos.config.enabled=true
  13 +spring.cloud.nacos.config.group=MICROSERVICE
  14 +spring.cloud.nacos.config.server-addr=nacos.diligrp.com:8848
  15 +spring.cloud.nacos.config.namespace=2267e673-b41f-458d-9643-2a03e4fd92fb
0 16 \ No newline at end of file
... ...
cashier-boss/src/main/resources/logback-spring.xml 0 → 100644
  1 +++ a/cashier-boss/src/main/resources/logback-spring.xml
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<configuration scan="true" scanPeriod="30 seconds" debug="false">
  3 + <property name="LOG_NAME" value="cashier-service" />
  4 +
  5 + <property name="APP_BASE_DIR" value="${user.dir}" />
  6 +
  7 + <property name="LOG_HOME" value="${APP_BASE_DIR}/logs" />
  8 +
  9 + <property name="LOG_HISTORY_DIR" value="${LOG_HOME}/history" />
  10 +
  11 + <property name="LOG_CHARSET" value="UTF-8" />
  12 +
  13 + <property name="ASYNC_QUEUE_SIZE" value="1024" />
  14 +
  15 + <springProperty scope="context" name="profile" source="spring.profiles.active" defaultValue="dev" />
  16 +
  17 + <property name="LOG_PATTERN" value="%d %-5level [${LOG_NAME}-${profile}] [%t] [%c:%L] -| %msg%n" />
  18 +
  19 + <statusListener class="ch.qos.logback.core.status.NopStatusListener" />
  20 +
  21 + <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
  22 + <target>System.out</target>
  23 + <encoder>
  24 + <charset>${LOG_CHARSET}</charset>
  25 + <pattern>${LOG_PATTERN}</pattern>
  26 + </encoder>
  27 + <!-- 过滤器:只输出INFO及以上级别,减少控制台冗余 -->
  28 + <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
  29 + <level>INFO</level>
  30 + </filter>
  31 + </appender>
  32 +
  33 + <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
  34 + <file>${LOG_HOME}/${LOG_NAME}.log</file>
  35 + <append>true</append>
  36 + <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
  37 + <!-- 日志命名:按天分类,按大小分卷,自动压缩为zip -->
  38 + <fileNamePattern>${LOG_HISTORY_DIR}/${LOG_NAME}_%d{yyyy-MM-dd}_%i.log.zip</fileNamePattern>
  39 + <!-- 日志保留30天,自动清理过期日志 -->
  40 + <maxHistory>30</maxHistory>
  41 + <!-- 单个日志文件最大60MB(减少切割次数) -->
  42 + <maxFileSize>60MB</maxFileSize>
  43 + <!-- 总日志大小上限10GB,防止磁盘占满 -->
  44 + <totalSizeCap>10GB</totalSizeCap>
  45 + </rollingPolicy>
  46 + <encoder>
  47 + <charset>${LOG_CHARSET}</charset>
  48 + <pattern>${LOG_PATTERN}</pattern>
  49 + </encoder>
  50 + <!-- 过滤器:只输出DEBUG及以上级别 -->
  51 + <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
  52 + <level>DEBUG</level>
  53 + </filter>
  54 + </appender>
  55 +
  56 + <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
  57 + <appender-ref ref="FILE" />
  58 + <!-- 队列大小:根据业务并发调整 -->
  59 + <queueSize>${ASYNC_QUEUE_SIZE}</queueSize>
  60 + <!-- 超出队列时不阻塞业务线程(生产环境关键) -->
  61 + <neverBlock>true</neverBlock>
  62 + <!-- 丢弃策略:超出队列时丢弃TRACE/DEBUG级日志 -->
  63 + <discardingThreshold>0</discardingThreshold>
  64 + <!-- 包含调用者数据,避免异步日志丢失异常栈 -->
  65 + <includeCallerData>true</includeCallerData>
  66 + </appender>
  67 +
  68 + <!-- ************************ 多环境日志配置 ************************ -->
  69 + <!-- 开发环境:仅控制台输出 -->
  70 + <springProfile name="dev">
  71 + <root level="INFO">
  72 + <appender-ref ref="CONSOLE" />
  73 + </root>
  74 +
  75 + <logger name="com.diligrp.cashier" level="DEBUG" additivity="false">
  76 + <appender-ref ref="CONSOLE" />
  77 + </logger>
  78 +
  79 + <logger name="com.alibaba" level="ERROR" additivity="false">
  80 + <appender-ref ref="CONSOLE" />
  81 + </logger>
  82 + </springProfile>
  83 +
  84 + <!-- 测试环境:控制台+异步文件输出 -->
  85 + <springProfile name="test">
  86 + <root level="INFO">
  87 + <appender-ref ref="CONSOLE" />
  88 + </root>
  89 +
  90 + <logger name="com.diligrp.cashier" level="DEBUG" additivity="false">
  91 + <appender-ref ref="ASYNC_FILE" />
  92 + </logger>
  93 +
  94 + <logger name="com.alibaba" level="ERROR" additivity="false">
  95 + <appender-ref ref="CONSOLE" />
  96 + <appender-ref ref="ASYNC_FILE" />
  97 + </logger>
  98 + </springProfile>
  99 +
  100 + <!-- 灰度/生产环境:控制台+异步文件输出 -->
  101 + <springProfile name="pre,prod">
  102 + <root level="INFO">
  103 + <appender-ref ref="CONSOLE" />
  104 + </root>
  105 +
  106 + <logger name="com.diligrp.cashier" level="DEBUG" additivity="false">
  107 + <appender-ref ref="ASYNC_FILE" />
  108 + </logger>
  109 +
  110 + <logger name="com.alibaba" level="ERROR" additivity="false">
  111 + <appender-ref ref="CONSOLE" />
  112 + <appender-ref ref="ASYNC_FILE" />
  113 + </logger>
  114 + </springProfile>
  115 +
  116 + <!-- ************************ 通用框架日志配置 ************************ -->
  117 + <logger name="org.springframework" level="WARN" additivity="false">
  118 + <appender-ref ref="CONSOLE" />
  119 + </logger>
  120 +
  121 + <logger name="org.mybatis" level="WARN" additivity="false">
  122 + <appender-ref ref="CONSOLE" />
  123 + </logger>
  124 +</configuration>
0 125 \ No newline at end of file
... ...
cashier-pipeline/build.gradle 0 → 100644
  1 +++ a/cashier-pipeline/build.gradle
  1 +dependencies {
  2 + api project(':cashier-shared')
  3 + implementation fileTree(dir: "$rootProject.projectDir/libs", includes: ['*jar'])
  4 +}
0 5 \ No newline at end of file
... ...
cashier-rtmart/build.gradle 0 → 100644
  1 +++ a/cashier-rtmart/build.gradle
  1 +dependencies {
  2 + implementation project(':cashier-shared')
  3 + implementation project(':cashier-trade')
  4 +}
0 5 \ No newline at end of file
... ...
cashier-shared/build.gradle 0 → 100644
  1 +++ a/cashier-shared/build.gradle
  1 +dependencies {
  2 + //api 'org.springframework.boot:spring-boot-starter-data-mongodb'
  3 + api libs.mybatis.starter
  4 + api 'org.springframework.boot:spring-boot-starter-data-redis'
  5 + //api 'org.redisson:redisson-spring-boot-starter:3.52.0'
  6 + api 'org.springframework.boot:spring-boot-starter-amqp'
  7 + api 'org.springframework.cloud:spring-cloud-starter-loadbalancer'
  8 + api 'org.springframework.cloud:spring-cloud-starter-openfeign'
  9 + api libs.cache.caffeine
  10 +
  11 + runtimeOnly libs.mysql.driver
  12 +}
0 13 \ No newline at end of file
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/Constants.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/Constants.java
  1 +package com.diligrp.cashier.shared;
  2 +
  3 +public final class Constants {
  4 + public static final String SIGN_ALGORITHM = "SHA1WithRSA";
  5 +
  6 + public static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
  7 +
  8 + public static final String DATE_FORMAT = "yyyy-MM-dd";
  9 +
  10 + public static final String TIME_FORMAT = "HH:mm:ss";
  11 +
  12 + public static final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors() + 1;
  13 +
  14 + public static final int MAX_POOL_SIZE = 200;
  15 +
  16 + public final static String CONTENT_TYPE = "application/json;charset=UTF-8";
  17 +}
0 18 \ No newline at end of file
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/ErrorCode.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/ErrorCode.java
  1 +package com.diligrp.cashier.shared;
  2 +
  3 +/**
  4 + * 系统错误码列表 - 错误码前三位用于区分模块
  5 + */
  6 +public class ErrorCode {
  7 + // 系统未知异常
  8 + public static final int SYSTEM_UNKNOWN_ERROR = 300000;
  9 + // 无效参数错误
  10 + public static final int ILLEGAL_ARGUMENT_ERROR = 300001;
  11 + // 访问未授权
  12 + public static final int UNAUTHORIZED_ACCESS_ERROR = 300002;
  13 + // 操作不允许
  14 + public static final int OPERATION_NOT_ALLOWED = 300003;
  15 + // 对象不存在
  16 + public static final int OBJECT_NOT_FOUND = 300004;
  17 + // 对象已存在
  18 + public static final int OBJECT_ALREADY_EXISTS = 300005;
  19 + // 数据并发修改
  20 + public static final int SYSTEM_BUSY_ERROR = 301000;
  21 + // 无效对象状态
  22 + public static final int INVALID_OBJECT_STATE = 301001;
  23 + // 远程服务访问错误
  24 + public static final int SERVICE_ACCESS_ERROR = 301002;
  25 +
  26 + public static final String MESSAGE_UNKNOWN_ERROR = "收银台系统未知异常,请联系管理员";
  27 +
  28 + public static final String MESSAGE_ACCESS_DENIED = "未授权的系统访问";
  29 +
  30 + public static final String MESSAGE_SYSTEM_BUSY = "收银台系统正忙,请稍后重试";
  31 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/SharedConfiguration.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/SharedConfiguration.java
  1 +package com.diligrp.cashier.shared;
  2 +
  3 +import com.diligrp.cashier.shared.exception.PlatformServiceException;
  4 +import com.diligrp.cashier.shared.security.RsaCipher;
  5 +import com.diligrp.cashier.shared.util.JsonUtils;
  6 +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
  7 +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
  8 +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
  9 +import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
  10 +import org.springframework.context.annotation.Bean;
  11 +import org.springframework.context.annotation.ComponentScan;
  12 +import org.springframework.context.annotation.Configuration;
  13 +import org.springframework.core.convert.converter.Converter;
  14 +import org.springframework.data.redis.connection.RedisConnectionFactory;
  15 +import org.springframework.data.redis.core.RedisTemplate;
  16 +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
  17 +import org.springframework.data.redis.serializer.StringRedisSerializer;
  18 +import org.springframework.stereotype.Component;
  19 +import org.springframework.util.StringUtils;
  20 +import org.springframework.web.servlet.config.annotation.CorsRegistry;
  21 +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  22 +
  23 +import java.security.PrivateKey;
  24 +import java.security.PublicKey;
  25 +import java.text.SimpleDateFormat;
  26 +import java.time.LocalDate;
  27 +import java.time.LocalDateTime;
  28 +import java.time.LocalTime;
  29 +import java.time.format.DateTimeFormatter;
  30 +import java.util.Date;
  31 +
  32 +@Configuration
  33 +@ComponentScan("com.diligrp.cashier.shared")
  34 +public class SharedConfiguration {
  35 + @Bean
  36 + public WebMvcConfigurer corsConfigurer() {
  37 + return new WebMvcConfigurer() {
  38 + @Override
  39 + public void addCorsMappings(CorsRegistry registry) {
  40 + registry.addMapping("/**")
  41 + .allowedOriginPatterns("*")
  42 + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
  43 + .allowedHeaders("*")
  44 + .allowCredentials(true)
  45 + .maxAge(3600);
  46 + }
  47 + };
  48 + }
  49 +
  50 + @Bean
  51 + public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
  52 + RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
  53 + template.setConnectionFactory(factory);
  54 + template.setKeySerializer(new StringRedisSerializer());
  55 + template.setHashKeySerializer(new StringRedisSerializer());
  56 + template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
  57 + template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
  58 + template.afterPropertiesSet();
  59 + return template;
  60 + }
  61 +
  62 + // Jackson DataBinding所有配置
  63 + @Bean
  64 + @ConditionalOnClass(JavaTimeModule.class)
  65 + public Jackson2ObjectMapperBuilderCustomizer customizeJacksonConfig() {
  66 + return JsonUtils::initObjectMapperBuilder;
  67 + }
  68 +
  69 + @Bean
  70 + public Converter<String, LocalDateTime> localDateTimeConverter() {
  71 + // 不能使用lambda表达式,否则导致springboot启动问题
  72 + return new Converter<String, LocalDateTime>() {
  73 + @Override
  74 + public LocalDateTime convert(String source) {
  75 + try {
  76 + return StringUtils.hasText(source) ? LocalDateTime.parse(source, DateTimeFormatter.ofPattern(Constants.DATE_TIME_FORMAT)) : null;
  77 + } catch (Exception ex) {
  78 + throw new IllegalArgumentException(String.format("Error parse %s to LocalDateTime", source), ex);
  79 + }
  80 + }
  81 + };
  82 + }
  83 +
  84 + @Bean
  85 + public Converter<String, LocalDate> localDateConverter() {
  86 + // 不能使用lambda表达式,否则导致springboot启动问题
  87 + return new Converter<String, LocalDate>() {
  88 + @Override
  89 + public LocalDate convert(String source) {
  90 + try {
  91 + return StringUtils.hasText(source) ? LocalDate.parse(source, DateTimeFormatter.ofPattern(Constants.DATE_FORMAT)) : null;
  92 + } catch (Exception ex) {
  93 + throw new IllegalArgumentException(String.format("Error parse %s to LocalDate", source), ex);
  94 + }
  95 + }
  96 + };
  97 + }
  98 +
  99 + @Bean
  100 + public Converter<String, LocalTime> localTimeConverter() {
  101 + // 不能使用lambda表达式,否则导致springboot启动问题
  102 + return new Converter<String, LocalTime>() {
  103 + @Override
  104 + public LocalTime convert(String source) {
  105 + try {
  106 + return StringUtils.hasText(source) ? LocalTime.parse(source, DateTimeFormatter.ofPattern(Constants.TIME_FORMAT)) : null;
  107 + } catch (Exception ex) {
  108 + throw new IllegalArgumentException(String.format("Error parse %s to LocalTime", source), ex);
  109 + }
  110 + }
  111 + };
  112 + }
  113 +
  114 + @Bean
  115 + public Converter<String, Date> dateConverter() {
  116 + // 不能使用lambda表达式,否则导致springboot启动问题
  117 + return new Converter<String, Date>() {
  118 + @Override
  119 + public Date convert(String source) {
  120 + try {
  121 + return StringUtils.hasText(source) ? new SimpleDateFormat(Constants.DATE_TIME_FORMAT).parse(source) : null;
  122 + } catch (Exception ex) {
  123 + throw new IllegalArgumentException(String.format("Error parse %s to Date", source), ex);
  124 + }
  125 + }
  126 + };
  127 + }
  128 +
  129 + @Component
  130 + @ConfigurationPropertiesBinding
  131 + public class PrivateKeyConverter implements Converter<String, PrivateKey> {
  132 + @Override
  133 + public PrivateKey convert(String source) {
  134 + try {
  135 + return RsaCipher.getPrivateKey(source);
  136 + } catch (Exception ex) {
  137 + throw new PlatformServiceException("privateKey configuration failed", ex);
  138 + }
  139 + }
  140 + }
  141 +
  142 + @Component
  143 + @ConfigurationPropertiesBinding
  144 + public class PublicKeyConverter implements Converter<String, PublicKey> {
  145 + @Override
  146 + public PublicKey convert(String source) {
  147 + try {
  148 + return RsaCipher.getPublicKey(source);
  149 + } catch (Exception ex) {
  150 + throw new PlatformServiceException("publicKey configuration failed", ex);
  151 + }
  152 + }
  153 + }
  154 +}
0 155 \ No newline at end of file
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/codec/ByteDecoder.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/codec/ByteDecoder.java
  1 +package com.diligrp.cashier.shared.codec;
  2 +
  3 +import java.io.IOException;
  4 +
  5 +public interface ByteDecoder<T> {
  6 + T decode(byte[] payload) throws IOException;
  7 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/codec/ByteEncoder.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/codec/ByteEncoder.java
  1 +package com.diligrp.cashier.shared.codec;
  2 +
  3 +import java.io.IOException;
  4 +
  5 +public interface ByteEncoder<T> {
  6 + byte[] encode(T payload) throws IOException;
  7 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/codec/StringCodec.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/codec/StringCodec.java
  1 +package com.diligrp.cashier.shared.codec;
  2 +
  3 +import java.nio.charset.StandardCharsets;
  4 +
  5 +public final class StringCodec {
  6 + public static ByteEncoder<String> getEncoder() {
  7 + return StringEncoder.INSTANCE;
  8 + }
  9 +
  10 + public static ByteDecoder<String> getDecoder() {
  11 + return StringDecoder.INSTANCE;
  12 + }
  13 +
  14 + static class StringEncoder implements ByteEncoder<String> {
  15 +
  16 + static final ByteEncoder<String> INSTANCE = new StringEncoder();
  17 +
  18 + @Override
  19 + public byte[] encode(String payload) {
  20 + return payload.getBytes(StandardCharsets.UTF_8);
  21 + }
  22 + }
  23 +
  24 + static class StringDecoder implements ByteDecoder<String> {
  25 +
  26 + static final ByteDecoder<String> INSTANCE = new StringDecoder();
  27 +
  28 + @Override
  29 + public String decode(byte[] payload) {
  30 + return new String(payload, StandardCharsets.UTF_8);
  31 + }
  32 + }
  33 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/domain/BaseDO.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/domain/BaseDO.java
  1 +package com.diligrp.cashier.shared.domain;
  2 +
  3 +import java.time.LocalDateTime;
  4 +
  5 +public class BaseDO {
  6 + // 数据库主键
  7 + protected Long id;
  8 + // 数据版本
  9 + protected Integer version;
  10 + // 创建时间
  11 + protected LocalDateTime createdTime;
  12 + // 修改时间
  13 + protected LocalDateTime modifiedTime;
  14 +
  15 + public Long getId() {
  16 + return id;
  17 + }
  18 +
  19 + public void setId(Long id) {
  20 + this.id = id;
  21 + }
  22 +
  23 + public Integer getVersion() {
  24 + return version;
  25 + }
  26 +
  27 + public void setVersion(Integer version) {
  28 + this.version = version;
  29 + }
  30 +
  31 + public LocalDateTime getCreatedTime() {
  32 + return createdTime;
  33 + }
  34 +
  35 + public void setCreatedTime(LocalDateTime createdTime) {
  36 + this.createdTime = createdTime;
  37 + }
  38 +
  39 + public LocalDateTime getModifiedTime() {
  40 + return modifiedTime;
  41 + }
  42 +
  43 + public void setModifiedTime(LocalDateTime modifiedTime) {
  44 + this.modifiedTime = modifiedTime;
  45 + }
  46 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/domain/ContainerSupport.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/domain/ContainerSupport.java
  1 +package com.diligrp.cashier.shared.domain;
  2 +
  3 +import java.util.HashMap;
  4 +import java.util.List;
  5 +import java.util.Optional;
  6 +
  7 +public abstract class ContainerSupport extends HashMap<String, Object> {
  8 + public ContainerSupport attach(Object object) {
  9 + put(object.getClass().getName(), object);
  10 + return this;
  11 + }
  12 +
  13 + public ContainerSupport attach(String key, Object object) {
  14 + put(key, object);
  15 + return this;
  16 + }
  17 +
  18 + public Long getLong(String param) {
  19 + Object value = get(param);
  20 + if (value != null) {
  21 + return value instanceof Long ? (Long)value : Long.parseLong(value.toString());
  22 + }
  23 + return null;
  24 + }
  25 +
  26 + public Integer getInteger(String param) {
  27 + Object value = get(param);
  28 + if (value != null) {
  29 + return value instanceof Integer ? (Integer)value : Integer.parseInt(value.toString());
  30 + }
  31 + return null;
  32 + }
  33 +
  34 + public String getString(String param) {
  35 + Object value = get(param);
  36 + return value != null ? value.toString() : null;
  37 + }
  38 +
  39 + public <T> T getObject(String param, Class<T> type) {
  40 + Object value = get(param);
  41 + return value == null ? null : type.cast(value);
  42 + }
  43 +
  44 + public <T> T getObject(Class<T> type) {
  45 + Object value = get(type.getName());
  46 + return value == null ? null : type.cast(value);
  47 + }
  48 +
  49 + @SuppressWarnings("unchecked")
  50 + public <T> Optional<T> getObject(String param) {
  51 + Object value = get(param);
  52 + return Optional.ofNullable ((T) value);
  53 + }
  54 +
  55 + @SuppressWarnings("unchecked")
  56 + public <T> Optional<List<T>> getObjects(String param) {
  57 + Object value = get(param);
  58 + return Optional.ofNullable ((List<T>) value);
  59 + }
  60 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/domain/Message.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/domain/Message.java
  1 +package com.diligrp.cashier.shared.domain;
  2 +
  3 +import com.diligrp.cashier.shared.ErrorCode;
  4 +
  5 +public class Message<T> {
  6 + protected static final int CODE_SUCCESS = 200;
  7 + protected static final int CODE_FAILURE = ErrorCode.SYSTEM_UNKNOWN_ERROR;
  8 + protected static final String MSG_SUCCESS = "success";
  9 +
  10 + private Integer code;
  11 + private String message;
  12 + private T data;
  13 +
  14 + public Message() {
  15 + }
  16 +
  17 + public Integer getCode() {
  18 + return this.code;
  19 + }
  20 +
  21 + public void setCode(Integer code) {
  22 + this.code = code;
  23 + }
  24 +
  25 + public String getMessage() {
  26 + return this.message;
  27 + }
  28 +
  29 + public void setMessage(String message) {
  30 + this.message = message;
  31 + }
  32 +
  33 + public T getData() {
  34 + return this.data;
  35 + }
  36 +
  37 + public void setData(T data) {
  38 + this.data = data;
  39 + }
  40 +
  41 + public static Message<?> success() {
  42 + Message<?> result = new Message<>();
  43 + result.code = 200;
  44 + result.message = "success";
  45 + return result;
  46 + }
  47 +
  48 + public static <E> Message<E> success(E data) {
  49 + Message<E> result = new Message<>();
  50 + result.code = 200;
  51 + result.data = data;
  52 + result.message = "success";
  53 + return result;
  54 + }
  55 +
  56 + public static Message<?> failure(String message) {
  57 + Message<?> result = new Message<>();
  58 + result.code = 1000;
  59 + result.message = message;
  60 + return result;
  61 + }
  62 +
  63 + public static Message<?> failure(int code, String message) {
  64 + Message<?> result = new Message<>();
  65 + result.code = code;
  66 + result.message = message;
  67 + return result;
  68 + }
  69 +}
0 70 \ No newline at end of file
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/domain/PageMessage.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/domain/PageMessage.java
  1 +package com.diligrp.cashier.shared.domain;
  2 +
  3 +import java.util.List;
  4 +
  5 +/**
  6 + * 分页数据模型
  7 + */
  8 +public class PageMessage<E> extends Message<List<E>>{
  9 + // 总记录数
  10 + private long total;
  11 +
  12 + public long getTotal() {
  13 + return total;
  14 + }
  15 +
  16 + public void setTotal(long total) {
  17 + this.total = total;
  18 + }
  19 +
  20 + public static <T> PageMessage<T> success(long total, List<T> data) {
  21 + PageMessage<T> page = new PageMessage<>();
  22 + page.setCode(CODE_SUCCESS);
  23 + page.setTotal(total);
  24 + page.setData(data);
  25 + page.setMessage(MSG_SUCCESS);
  26 + return page;
  27 + }
  28 +
  29 + public static PageMessage<?> failure(String message) {
  30 + return failure(CODE_FAILURE, message);
  31 + }
  32 +
  33 + public static PageMessage<?> failure(int code, String message) {
  34 + PageMessage<?> page = new PageMessage<>();
  35 + page.setCode(code);
  36 + page.setTotal(0);
  37 + page.setData(null);
  38 + page.setMessage(message);
  39 + return page;
  40 + }
  41 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/domain/PageQuery.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/domain/PageQuery.java
  1 +package com.diligrp.cashier.shared.domain;
  2 +
  3 +/**
  4 + * 分页查询领域模型
  5 + */
  6 +public class PageQuery {
  7 + private Integer pageNo = 1;
  8 + // 每页记录数
  9 + private Integer pageSize = 20;
  10 +
  11 + // 起始行下标
  12 + protected Integer start;
  13 + // 获取的记录行数
  14 + protected Integer limit;
  15 +
  16 + public Integer getStart() {
  17 + return start;
  18 + }
  19 +
  20 + public void setStart(Integer start) {
  21 + this.start = start;
  22 + }
  23 +
  24 + public Integer getLimit() {
  25 + return limit;
  26 + }
  27 +
  28 + public void setLimit(Integer limit) {
  29 + this.limit = limit;
  30 + }
  31 +
  32 + public Integer getPageNo() {
  33 + return pageNo;
  34 + }
  35 +
  36 + public void setPageNo(Integer pageNo) {
  37 + this.pageNo = pageNo;
  38 + }
  39 +
  40 + public Integer getPageSize() {
  41 + return pageSize;
  42 + }
  43 +
  44 + public void setPageSize(Integer pageSize) {
  45 + this.pageSize = pageSize;
  46 + }
  47 +
  48 + /**
  49 + * 通过页号、每页记录数计算起始行下标
  50 + */
  51 + public void from(int pageNo, int pageSize) {
  52 + this.start = (pageNo - 1) * pageSize;
  53 + this.limit = pageSize;
  54 + }
  55 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/exception/DefaultExceptionHandler.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/exception/DefaultExceptionHandler.java
  1 +package com.diligrp.cashier.shared.exception;
  2 +
  3 +import com.diligrp.cashier.shared.ErrorCode;
  4 +import com.diligrp.cashier.shared.domain.Message;
  5 +import org.slf4j.Logger;
  6 +import org.slf4j.LoggerFactory;
  7 +import org.springframework.web.bind.annotation.ExceptionHandler;
  8 +import org.springframework.web.bind.annotation.RestControllerAdvice;
  9 +
  10 +@RestControllerAdvice
  11 +public class DefaultExceptionHandler {
  12 + private final Logger LOG = LoggerFactory.getLogger(this.getClass());
  13 +
  14 + @ExceptionHandler(PlatformServiceException.class)
  15 + public Message<?> platformServiceException(PlatformServiceException ex) {
  16 + LOG.warn("assistant platform service exception", ex);
  17 + return Message.failure(ex.getCode(), ex.getMessage());
  18 + }
  19 +
  20 + @ExceptionHandler(IllegalArgumentException.class)
  21 + public Message<?> illegalArgumentException(IllegalArgumentException ex) {
  22 + LOG.warn("assistant platform service exception", ex);
  23 + return Message.failure(ErrorCode.ILLEGAL_ARGUMENT_ERROR, ex.getMessage());
  24 + }
  25 +
  26 + @ExceptionHandler(Exception.class)
  27 + public Message<?> defaultExceptionHandler(Exception ex) {
  28 + LOG.warn("assistant platform service exception", ex);
  29 + return Message.failure(ErrorCode.SYSTEM_UNKNOWN_ERROR, ErrorCode.MESSAGE_UNKNOWN_ERROR);
  30 + }
  31 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/exception/PlatformServiceException.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/exception/PlatformServiceException.java
  1 +package com.diligrp.cashier.shared.exception;
  2 +
  3 +import com.diligrp.cashier.shared.ErrorCode;
  4 +
  5 +/**
  6 + * 所有模块异常类的基类
  7 + */
  8 +public class PlatformServiceException extends RuntimeException {
  9 + /**
  10 + * 错误码
  11 + */
  12 + private int code = ErrorCode.SYSTEM_UNKNOWN_ERROR;
  13 +
  14 + /**
  15 + * 是否打印异常栈
  16 + */
  17 + private boolean stackTrace = true;
  18 +
  19 + public PlatformServiceException(String message) {
  20 + super(message);
  21 + }
  22 +
  23 + public PlatformServiceException(int code, String message) {
  24 + super(message);
  25 + this.code = code;
  26 + this.stackTrace = false;
  27 + }
  28 +
  29 + public PlatformServiceException(String message, Throwable ex) {
  30 + super(message, ex);
  31 + }
  32 +
  33 + @Override
  34 + public Throwable fillInStackTrace() {
  35 + return stackTrace ? super.fillInStackTrace() : this;
  36 + }
  37 +
  38 + public int getCode() {
  39 + return code;
  40 + }
  41 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/exception/ServiceAccessException.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/exception/ServiceAccessException.java
  1 +package com.diligrp.cashier.shared.exception;
  2 +
  3 +/**
  4 + * 远程服务访问异常
  5 + */
  6 +public class ServiceAccessException extends PlatformServiceException {
  7 + public ServiceAccessException(String message) {
  8 + super(message);
  9 + }
  10 +
  11 + public ServiceAccessException(int code, String message) {
  12 + super(code, message);
  13 + }
  14 +
  15 + public ServiceAccessException(String message, Throwable ex) {
  16 + super(message, ex);
  17 + }
  18 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/exception/ServiceConnectException.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/exception/ServiceConnectException.java
  1 +package com.diligrp.cashier.shared.exception;
  2 +
  3 +/**
  4 + * 远程服务连接异常
  5 + */
  6 +public class ServiceConnectException extends ServiceAccessException {
  7 +
  8 + public ServiceConnectException(String message) {
  9 + super(message);
  10 + }
  11 +
  12 + public ServiceConnectException(int code, String message) {
  13 + super(code, message);
  14 + }
  15 +
  16 + public ServiceConnectException(String message, Throwable ex) {
  17 + super(message, ex);
  18 + }
  19 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/exception/ServiceTimeoutException.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/exception/ServiceTimeoutException.java
  1 +package com.diligrp.cashier.shared.exception;
  2 +
  3 +/**
  4 + * 远程服务访问超时异常
  5 + */
  6 +public class ServiceTimeoutException extends ServiceAccessException {
  7 +
  8 + public ServiceTimeoutException(String message) {
  9 + super(message);
  10 + }
  11 +
  12 + public ServiceTimeoutException(int code, String message) {
  13 + super(code, message);
  14 + }
  15 +
  16 + public ServiceTimeoutException(String message, Throwable ex) {
  17 + super(message, ex);
  18 + }
  19 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/mybatis/GenericEnumTypeHandler.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/mybatis/GenericEnumTypeHandler.java
  1 +package com.diligrp.cashier.shared.mybatis;
  2 +
  3 +import com.diligrp.cashier.shared.type.IEnumType;
  4 +import org.apache.ibatis.type.BaseTypeHandler;
  5 +import org.apache.ibatis.type.JdbcType;
  6 +
  7 +import java.sql.CallableStatement;
  8 +import java.sql.PreparedStatement;
  9 +import java.sql.ResultSet;
  10 +import java.sql.SQLException;
  11 +import java.util.Arrays;
  12 +
  13 +public class GenericEnumTypeHandler<E extends IEnumType> extends BaseTypeHandler<E> {
  14 + private final E[] enums;
  15 +
  16 + public GenericEnumTypeHandler(Class<E> type) {
  17 + if (type == null) {
  18 + throw new IllegalArgumentException("Type argument cannot be null");
  19 + } else {
  20 + this.enums = type.getEnumConstants();
  21 + if (this.enums == null) {
  22 + throw new IllegalArgumentException(type.getSimpleName() + " does not represent an enum type.");
  23 + }
  24 + }
  25 + }
  26 +
  27 + @Override
  28 + public void setNonNullParameter(PreparedStatement preparedStatement, int i, E e, JdbcType jdbcType) throws SQLException {
  29 + preparedStatement.setInt(i, e.getCode());
  30 + }
  31 +
  32 + @Override
  33 + public E getNullableResult(ResultSet resultSet, String columnName) throws SQLException {
  34 + int code = resultSet.getInt(columnName);
  35 + if (resultSet.wasNull()) {
  36 + return null;
  37 + } else {
  38 + return getEnumType(code);
  39 + }
  40 + }
  41 +
  42 + @Override
  43 + public E getNullableResult(ResultSet resultSet, int columnIndex) throws SQLException {
  44 + int code = resultSet.getInt(columnIndex);
  45 + if (resultSet.wasNull()) {
  46 + return null;
  47 + } else {
  48 + return getEnumType(code);
  49 + }
  50 + }
  51 +
  52 + @Override
  53 + public E getNullableResult(CallableStatement callableStatement, int columnIndex) throws SQLException {
  54 + int code = callableStatement.getInt(columnIndex);
  55 + if (callableStatement.wasNull()) {
  56 + return null;
  57 + } else {
  58 + return getEnumType(code);
  59 + }
  60 + }
  61 +
  62 + private E getEnumType(int code) {
  63 + return Arrays.stream(enums).filter(item -> item.getCode() == code).findFirst().orElse(null);
  64 + }
  65 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/mybatis/MybatisMapperSupport.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/mybatis/MybatisMapperSupport.java
  1 +package com.diligrp.cashier.shared.mybatis;
  2 +
  3 +public interface MybatisMapperSupport {
  4 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/security/AesCipher.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/security/AesCipher.java
  1 +package com.diligrp.cashier.shared.security;
  2 +
  3 +import javax.crypto.Cipher;
  4 +import javax.crypto.KeyGenerator;
  5 +import javax.crypto.SecretKey;
  6 +import javax.crypto.spec.SecretKeySpec;
  7 +import java.security.Key;
  8 +import java.util.Base64;
  9 +
  10 +/**
  11 + * AES算法工具类
  12 + */
  13 +public class AesCipher {
  14 + private static final String KEY_ALGORITHM = "AES";
  15 +
  16 + private static final String CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
  17 +
  18 + public static String generateSecretKey() throws Exception {
  19 + KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
  20 + keyGenerator.init(128);
  21 +
  22 + SecretKey secretKey = keyGenerator.generateKey();
  23 + return Base64.getEncoder().encodeToString(secretKey.getEncoded());
  24 + }
  25 +
  26 + public static byte[] encrypt(byte[] data, String secretKey) throws Exception {
  27 + Key key = toKey(secretKey);
  28 + Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
  29 + cipher.init(Cipher.ENCRYPT_MODE, key);
  30 +
  31 + return cipher.doFinal(data);
  32 + }
  33 +
  34 + public static byte[] decrypt(byte[] data, String secretKey) throws Exception {
  35 + Key key = toKey(secretKey);
  36 + Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
  37 + cipher.init(Cipher.DECRYPT_MODE, key);
  38 +
  39 + return cipher.doFinal(data);
  40 + }
  41 +
  42 + private static Key toKey(String secretKey) {
  43 + byte[] key = Base64.getDecoder().decode(secretKey);
  44 + return new SecretKeySpec(key, KEY_ALGORITHM);
  45 + }
  46 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/security/HexUtils.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/security/HexUtils.java
  1 +package com.diligrp.cashier.shared.security;
  2 +
  3 +/**
  4 + * 字节数组与十六进制字符串转化工具类
  5 + */
  6 +public class HexUtils {
  7 +
  8 + private static final int GUARD_CHAR = 0x01;
  9 +
  10 + private static final char[] DIGITS_LOWER = {'0', '1', '2', '3', '4', '5',
  11 + '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
  12 +
  13 + private static final char[] DIGITS_UPPER = {'0', '1', '2', '3', '4', '5',
  14 + '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
  15 +
  16 + public static char[] encodeHex(byte[] data) {
  17 + return encodeHex(data, true);
  18 + }
  19 +
  20 + public static char[] encodeHex(byte[] data, boolean toLowerCase) {
  21 + return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER);
  22 + }
  23 +
  24 + public static String encodeHexStr(byte[] data) {
  25 + return encodeHexStr(data, true);
  26 + }
  27 +
  28 + public static String encodeHexStr(byte[] data, boolean toLowerCase) {
  29 + return encodeHexStr(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER);
  30 + }
  31 +
  32 + public static byte[] decodeHex(String data) {
  33 + return decodeHex(data.toCharArray());
  34 + }
  35 +
  36 + public static byte[] decodeHex(char[] data) {
  37 + int len = data.length;
  38 + if ((len & GUARD_CHAR) != 0) {
  39 + throw new RuntimeException("Unknown char");
  40 + }
  41 +
  42 + byte[] out = new byte[len >> 1];
  43 + for (int i = 0, j = 0; j < len; i++) {
  44 + int f = toDigit(data[j], j) << 4;
  45 + j++;
  46 + f = f | toDigit(data[j], j);
  47 + j++;
  48 + out[i] = (byte) (f & 0xFF);
  49 + }
  50 +
  51 + return out;
  52 + }
  53 +
  54 + private static char[] encodeHex(byte[] data, char[] toDigits) {
  55 + int l = data.length;
  56 + char[] out = new char[l << 1];
  57 + for (int i = 0, j = 0; i < l; i++) {
  58 + out[j++] = toDigits[(0xF0 & data[i]) >>> 4];
  59 + out[j++] = toDigits[0x0F & data[i]];
  60 + }
  61 + return out;
  62 + }
  63 +
  64 + private static String encodeHexStr(byte[] data, char[] toDigits) {
  65 + return new String(encodeHex(data, toDigits));
  66 + }
  67 +
  68 + private static int toDigit(char ch, int index) {
  69 + int digit = Character.digit(ch, 16);
  70 + if (digit == -1) {
  71 + throw new RuntimeException("Invalid hex char " + ch + ", index at " + index);
  72 + }
  73 + return digit;
  74 + }
  75 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/security/KeyStoreUtils.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/security/KeyStoreUtils.java
  1 +package com.diligrp.cashier.shared.security;
  2 +
  3 +import com.diligrp.cashier.shared.util.ClassUtils;
  4 +
  5 +import java.io.InputStream;
  6 +import java.security.KeyStore;
  7 +import java.security.PrivateKey;
  8 +import java.security.PublicKey;
  9 +import java.security.cert.Certificate;
  10 +import java.security.cert.CertificateFactory;
  11 +import java.util.Base64;
  12 +
  13 +/**
  14 + * 数字证书工具类
  15 + */
  16 +public class KeyStoreUtils {
  17 + public static String getPrivateKeyStr(String keyStorePath, String storeType, String storePass,
  18 + String alias, String keyPass) throws Exception {
  19 + InputStream in = ClassUtils.getDefaultClassLoader().getResourceAsStream(keyStorePath);
  20 + PrivateKey privateKey = getPrivateKey(in, storeType, storePass, alias, keyPass);
  21 + return Base64.getEncoder().encodeToString(privateKey.getEncoded());
  22 + }
  23 +
  24 + public static PrivateKey getPrivateKey(String keyStorePath, String storeType, String storePass,
  25 + String alias, String keyPass) throws Exception {
  26 + InputStream in = ClassUtils.getDefaultClassLoader().getResourceAsStream(keyStorePath);
  27 + return getPrivateKey(in, storeType, storePass, alias, keyPass);
  28 + }
  29 +
  30 + public static PrivateKey getPrivateKey(InputStream in, String storeType, String storePass, String alias, String keyPass) throws Exception {
  31 + KeyStore ks = getKeyStore(in, storeType, storePass);
  32 + PrivateKey key = (PrivateKey) ks.getKey(alias, keyPass.toCharArray());
  33 + return key;
  34 + }
  35 +
  36 + public static String getPublicKeyStr(String keyStorePath, String storeType, String storePass, String alias) throws Exception {
  37 + InputStream in = ClassUtils.getDefaultClassLoader().getResourceAsStream(keyStorePath);
  38 + PublicKey publicKey = getPublicKey(in, storeType, storePass, alias);
  39 + return Base64.getEncoder().encodeToString(publicKey.getEncoded());
  40 + }
  41 +
  42 + public static PublicKey getPublicKey(String keyStorePath, String storeType, String storePass, String alias) throws Exception {
  43 + InputStream in = ClassUtils.getDefaultClassLoader().getResourceAsStream(keyStorePath);
  44 + return getPublicKey(in, storeType, storePass, alias);
  45 + }
  46 +
  47 + public static PublicKey getPublicKey(InputStream in, String storeType, String storePass, String alias) throws Exception {
  48 + KeyStore ks = getKeyStore(in, storeType, storePass);
  49 + Certificate cert = ks.getCertificate(alias);
  50 + return cert.getPublicKey();
  51 + }
  52 +
  53 + public static String getPublicKeyStr(String certificatePath) throws Exception {
  54 + PublicKey publicKey = getPublicKey(certificatePath);
  55 + return Base64.getEncoder().encodeToString(publicKey.getEncoded());
  56 + }
  57 +
  58 + public static PublicKey getPublicKey(String certificatePath) throws Exception {
  59 + InputStream in = ClassUtils.getDefaultClassLoader().getResourceAsStream(certificatePath);
  60 + Certificate x509Cert = CertificateFactory.getInstance("X509").generateCertificate(in);
  61 + return x509Cert.getPublicKey();
  62 + }
  63 +
  64 + public static KeyStore getKeyStore(InputStream in, String storeType, String storePass) throws Exception {
  65 + KeyStore ks = KeyStore.getInstance(storeType);
  66 + ks.load(in, storePass.toCharArray());
  67 + in.close();
  68 + return ks;
  69 + }
  70 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/security/Md5Cipher.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/security/Md5Cipher.java
  1 +package com.diligrp.cashier.shared.security;
  2 +
  3 +import java.security.MessageDigest;
  4 +
  5 +/**
  6 + * MD5算法工具类
  7 + */
  8 +public class Md5Cipher {
  9 + private static final String KEY_MD5 = "MD5";
  10 +
  11 + public static byte[] encrypt(byte[] data) throws Exception {
  12 + MessageDigest md5 = MessageDigest.getInstance(KEY_MD5);
  13 + md5.update(data);
  14 + return md5.digest();
  15 + }
  16 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/security/PasswordUtils.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/security/PasswordUtils.java
  1 +package com.diligrp.cashier.shared.security;
  2 +
  3 +import java.nio.charset.StandardCharsets;
  4 +
  5 +/**
  6 + * 密码加密散列工具类
  7 + */
  8 +public class PasswordUtils {
  9 + public static String generateSecretKey() {
  10 + try {
  11 + return AesCipher.generateSecretKey();
  12 + } catch (Exception ex) {
  13 + throw new RuntimeException("Generate password secret key error", ex);
  14 + }
  15 + }
  16 +
  17 + public static String encrypt(String password, String secretKey) {
  18 + try {
  19 + byte[] data = password.getBytes(StandardCharsets.UTF_8);
  20 + return HexUtils.encodeHexStr(ShaCipher.encrypt(AesCipher.encrypt(data, secretKey)));
  21 + } catch (Exception ex) {
  22 + throw new RuntimeException("Encrypt password error", ex);
  23 + }
  24 + }
  25 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/security/PbeCipher.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/security/PbeCipher.java
  1 +package com.diligrp.cashier.shared.security;
  2 +
  3 +import javax.crypto.Cipher;
  4 +import javax.crypto.SecretKey;
  5 +import javax.crypto.SecretKeyFactory;
  6 +import javax.crypto.spec.PBEKeySpec;
  7 +import javax.crypto.spec.PBEParameterSpec;
  8 +import java.security.Key;
  9 +import java.util.Random;
  10 +
  11 +/**
  12 + * PBE算法工具类
  13 + */
  14 +public class PbeCipher {
  15 + private static final String ALGORITHM = "PBEWithSHA1AndDESede";
  16 +
  17 + public static byte[] encrypt(byte[] data, String password, byte[] salt) throws Exception {
  18 + Key key = toKey(password);
  19 +
  20 + PBEParameterSpec paramSpec = new PBEParameterSpec(salt, 100);
  21 + Cipher cipher = Cipher.getInstance(ALGORITHM);
  22 + cipher.init(Cipher.ENCRYPT_MODE, key, paramSpec);
  23 +
  24 + return cipher.doFinal(data);
  25 + }
  26 +
  27 + public static byte[] decrypt(byte[] data, String password, byte[] salt) throws Exception {
  28 + Key key = toKey(password);
  29 +
  30 + PBEParameterSpec paramSpec = new PBEParameterSpec(salt, 100);
  31 + Cipher cipher = Cipher.getInstance(ALGORITHM);
  32 + cipher.init(Cipher.DECRYPT_MODE, key, paramSpec);
  33 +
  34 + return cipher.doFinal(data);
  35 + }
  36 +
  37 + private static byte[] initSalt() throws Exception {
  38 + byte[] salt = new byte[8];
  39 + Random random = new Random();
  40 + random.nextBytes(salt);
  41 + return salt;
  42 + }
  43 +
  44 + private static Key toKey(String password) throws Exception {
  45 + PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
  46 + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(ALGORITHM);
  47 + SecretKey secretKey = keyFactory.generateSecret(keySpec);
  48 +
  49 + return secretKey;
  50 + }
  51 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/security/RsaCipher.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/security/RsaCipher.java
  1 +package com.diligrp.cashier.shared.security;
  2 +
  3 +import javax.crypto.Cipher;
  4 +import java.security.*;
  5 +import java.security.spec.PKCS8EncodedKeySpec;
  6 +import java.security.spec.X509EncodedKeySpec;
  7 +import java.util.Base64;
  8 +
  9 +/**
  10 + * RSA算法工具类
  11 + */
  12 +public class RsaCipher {
  13 + private static final String KEY_ALGORITHM = "RSA";
  14 +
  15 + private static final String SIGN_ALGORITHMS = "SHA1WithRSA";
  16 +
  17 + public static String[] generateRSAKeyPair() throws Exception {
  18 + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
  19 + keyPairGen.initialize(512, new SecureRandom());
  20 + KeyPair keyPair = keyPairGen.generateKeyPair();
  21 + PrivateKey privateKey = keyPair.getPrivate();
  22 + PublicKey publicKey = keyPair.getPublic();
  23 +
  24 + String[] keyPairArray = new String[2];
  25 + keyPairArray[0] = Base64.getEncoder().encodeToString(privateKey.getEncoded());
  26 + keyPairArray[1] = Base64.getEncoder().encodeToString(publicKey.getEncoded());
  27 + return keyPairArray;
  28 + }
  29 +
  30 + public static byte[] encrypt(byte[] data, Key secretKey) throws Exception {
  31 + Cipher cipher = Cipher.getInstance(KEY_ALGORITHM);
  32 + cipher.init(Cipher.ENCRYPT_MODE, secretKey);
  33 + return cipher.doFinal(data);
  34 + }
  35 +
  36 + public static byte[] decrypt(byte[] data, Key secretKey) throws Exception {
  37 + Cipher cipher = Cipher.getInstance(KEY_ALGORITHM);
  38 + cipher.init(Cipher.DECRYPT_MODE, secretKey);
  39 + return cipher.doFinal(data);
  40 + }
  41 +
  42 + public static byte[] sign(byte[] data, PrivateKey privateKey) throws Exception {
  43 + Signature signature = Signature.getInstance(SIGN_ALGORITHMS);
  44 + signature.initSign(privateKey, new SecureRandom());
  45 + signature.update(data);
  46 + return signature.sign();
  47 + }
  48 +
  49 + public static boolean verify(byte[] data, byte[] sign, PublicKey publicKey) throws Exception {
  50 + Signature signature = Signature.getInstance(SIGN_ALGORITHMS);
  51 + signature.initVerify(publicKey);
  52 + signature.update(data);
  53 + return signature.verify(sign);
  54 + }
  55 +
  56 + public static PrivateKey getPrivateKey(String key) throws Exception {
  57 + byte[] keyBytes = Base64.getDecoder().decode(key);
  58 + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
  59 + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
  60 + return keyFactory.generatePrivate(keySpec);
  61 + }
  62 +
  63 + public static PublicKey getPublicKey(String key) throws Exception {
  64 + byte[] keyBytes = Base64.getDecoder().decode(key);
  65 + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
  66 + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
  67 + return keyFactory.generatePublic(keySpec);
  68 + }
  69 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/security/ShaCipher.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/security/ShaCipher.java
  1 +package com.diligrp.cashier.shared.security;
  2 +
  3 +import java.security.MessageDigest;
  4 +
  5 +/**
  6 + * SHA散列算法工具类
  7 + */
  8 +public class ShaCipher {
  9 + private static final String KEY_SHA = "SHA-256";
  10 +
  11 + public static byte[] encrypt(byte[] data) throws Exception {
  12 + MessageDigest sha = MessageDigest.getInstance(KEY_SHA);
  13 + sha.update(data);
  14 +
  15 + return sha.digest();
  16 + }
  17 +
  18 + public static byte[] encrypt(byte[] data, String algorithm) throws Exception {
  19 + MessageDigest sha = MessageDigest.getInstance(algorithm);
  20 + sha.update(data);
  21 +
  22 + return sha.digest();
  23 + }
  24 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/service/ILifeCycle.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/service/ILifeCycle.java
  1 +package com.diligrp.cashier.shared.service;
  2 +
  3 +public interface ILifeCycle
  4 +{
  5 + void start() throws Exception;
  6 +
  7 + void stop() throws Exception;
  8 +
  9 + boolean isRunning();
  10 +
  11 + boolean isStarted();
  12 +
  13 + String getState();
  14 +}
0 15 \ No newline at end of file
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/service/LifeCycle.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/service/LifeCycle.java
  1 +package com.diligrp.cashier.shared.service;
  2 +
  3 +public abstract class LifeCycle implements ILifeCycle {
  4 + private final static int FAILED = -1;
  5 + private final static int STOPPED = 0;
  6 + private final static int STARTING = 1;
  7 + private final static int STARTED = 2;
  8 + private final static int STOPPING = 3;
  9 +
  10 + private final Object lock = new Object();
  11 + private volatile int state = STOPPED;
  12 +
  13 + @Override
  14 + public void start() throws Exception {
  15 + // Double check
  16 + if (state == STARTED || state == STARTING) {
  17 + return;
  18 + }
  19 +
  20 + synchronized (lock) {
  21 + try {
  22 + if (state == STARTED || state == STARTING) {
  23 + return;
  24 + }
  25 +
  26 + setState(STARTING);
  27 + doStart();
  28 + setState(STARTED);
  29 + } catch (Throwable ex) {
  30 + setState(FAILED);
  31 + throw new Exception("Start the component exception", ex);
  32 + }
  33 + }
  34 + }
  35 +
  36 + @Override
  37 + public void stop() throws Exception {
  38 + // Double check
  39 + if (state == STOPPING || state == STOPPED) {
  40 + return;
  41 + }
  42 +
  43 + synchronized (lock) {
  44 + try {
  45 + if (state == STOPPING || state == STOPPED) {
  46 + return;
  47 + }
  48 + setState(STOPPING);
  49 + doStop();
  50 + setState(STOPPED);
  51 + } catch (Throwable ex) {
  52 + setState(FAILED);
  53 + throw new Exception("Stop the component exception", ex);
  54 + }
  55 + }
  56 + }
  57 +
  58 + protected void doStart() throws Exception {
  59 + }
  60 +
  61 + protected void doStop() throws Exception {
  62 + }
  63 +
  64 + @Override
  65 + public boolean isRunning() {
  66 + final int _state = state;
  67 + return _state == STARTED || _state == STARTING;
  68 + }
  69 +
  70 + @Override
  71 + public boolean isStarted() {
  72 + return state == STARTED;
  73 + }
  74 +
  75 + @Override
  76 + public String getState() {
  77 + switch (state) {
  78 + case STARTED:
  79 + return "started";
  80 + case STOPPED:
  81 + return "stopped";
  82 + case STARTING:
  83 + return "starting";
  84 + case STOPPING:
  85 + return "stopping";
  86 + case FAILED:
  87 + return "failed";
  88 + default:
  89 + return "unknown";
  90 + }
  91 + }
  92 +
  93 + private void setState(int state) {
  94 + this.state = state;
  95 + }
  96 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/service/ServiceEndpointSupport.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/service/ServiceEndpointSupport.java
  1 +package com.diligrp.cashier.shared.service;
  2 +
  3 +import com.diligrp.cashier.shared.exception.ServiceAccessException;
  4 +import com.diligrp.cashier.shared.exception.ServiceConnectException;
  5 +import com.diligrp.cashier.shared.exception.ServiceTimeoutException;
  6 +import com.diligrp.cashier.shared.util.ObjectUtils;
  7 +
  8 +import javax.net.ssl.SSLContext;
  9 +import javax.net.ssl.SSLParameters;
  10 +import java.net.ConnectException;
  11 +import java.net.URI;
  12 +import java.net.URLEncoder;
  13 +import java.net.http.*;
  14 +import java.nio.charset.StandardCharsets;
  15 +import java.time.Duration;
  16 +import java.util.*;
  17 +import java.util.concurrent.Executor;
  18 +
  19 +public abstract class ServiceEndpointSupport {
  20 + protected static final int MAX_CONNECT_TIMEOUT_TIME = 10000;
  21 +
  22 + protected static final int MAX_REQUEST_TIMEOUT_TIME = 30000;
  23 +
  24 + protected static final String CONTENT_TYPE = "Content-Type";
  25 +
  26 + protected static final String CONTENT_TYPE_JSON = "application/json;charset=UTF-8";
  27 +
  28 + protected static final String CONTENT_TYPE_FORM = "application/x-www-form-urlencoded;charset=UTF-8";
  29 +
  30 + protected static final String CONTENT_TYPE_XML = "text/xml;charset=UTF-8";
  31 +
  32 + protected volatile HttpClient httpClient;
  33 +
  34 + protected final Object lock = new Object();
  35 +
  36 + /**
  37 + * @throws ServiceConnectException, ServiceAccessException, ServiceTimeoutException
  38 + */
  39 + public HttpResult send(String requestUrl, String body) {
  40 + return send(requestUrl, null, body);
  41 + }
  42 +
  43 + /**
  44 + * @throws ServiceConnectException, ServiceAccessException, ServiceTimeoutException
  45 + */
  46 + public HttpResult send(String requestUrl, HttpHeader[] headers, String body) {
  47 + if (ObjectUtils.isEmpty(requestUrl)) {
  48 + throw new IllegalArgumentException("Invalid http request url, url=" + requestUrl);
  49 + }
  50 +
  51 + HttpRequest.Builder request = HttpRequest.newBuilder().uri(URI.create(requestUrl))
  52 + .version(HttpClient.Version.HTTP_2)
  53 + .timeout(Duration.ofMillis(MAX_REQUEST_TIMEOUT_TIME))
  54 + .header(CONTENT_TYPE, CONTENT_TYPE_JSON)
  55 + .POST(HttpRequest.BodyPublishers.ofString(body));
  56 + // Wrap the HTTP headers
  57 + if (headers != null && headers.length > 0) {
  58 + // request.headers(Arrays.stream(headers).flatMap(h -> Stream.of(h.param, h.value)).toArray(String[]::new));
  59 + Arrays.stream(headers).filter(Objects::nonNull).forEach(h -> request.header(h.param, h.value));
  60 + }
  61 +
  62 + return execute(request.build());
  63 + }
  64 +
  65 + /**
  66 + * @throws ServiceConnectException, ServiceAccessException, ServiceTimeoutException
  67 + */
  68 + public HttpResult send(String requestUrl, HttpParam[] params) {
  69 + return send(requestUrl, null, params);
  70 + }
  71 +
  72 + /**
  73 + * @throws ServiceConnectException, ServiceAccessException, ServiceTimeoutException
  74 + */
  75 + public HttpResult send(String requestUrl, HttpHeader[] headers, HttpParam[] params) {
  76 + if (ObjectUtils.isEmpty(requestUrl)) {
  77 + throw new IllegalArgumentException("Invalid http request url, url=" + requestUrl);
  78 + }
  79 +
  80 + HttpRequest.Builder request = HttpRequest.newBuilder().uri(URI.create(requestUrl))
  81 + .version(HttpClient.Version.HTTP_2)
  82 + .timeout(Duration.ofMillis(MAX_REQUEST_TIMEOUT_TIME))
  83 + .header(CONTENT_TYPE, CONTENT_TYPE_FORM);
  84 + // Wrap the HTTP headers
  85 + if (headers != null && headers.length > 0) {
  86 + Arrays.stream(headers).filter(Objects::nonNull).forEach(h -> request.header(h.param, h.value));
  87 + }
  88 + if (params != null && params.length > 0) {
  89 + // [key1, value1, key2, value2] -> key1=value1&key2=value2
  90 + String body = Arrays.stream(params).filter(Objects::nonNull)
  91 + .map(p -> "".concat(p.param).concat("=").concat(URLEncoder.encode(p.value, StandardCharsets.UTF_8)))
  92 + .reduce((value1, value2) -> "".concat(value1).concat("&").concat(value2)).orElse("");
  93 + request.POST(HttpRequest.BodyPublishers.ofString(body));
  94 + }
  95 +
  96 + return execute(request.build());
  97 + }
  98 +
  99 + /**
  100 + * @throws ServiceConnectException, ServiceAccessException, ServiceTimeoutException
  101 + */
  102 + public HttpResult sendXml(String requestUrl, HttpHeader[] headers, String xml) {
  103 + if (ObjectUtils.isEmpty(requestUrl)) {
  104 + throw new IllegalArgumentException("Invalid http request url, url=" + requestUrl);
  105 + }
  106 +
  107 + HttpRequest.Builder request = HttpRequest.newBuilder().uri(URI.create(requestUrl))
  108 + .version(HttpClient.Version.HTTP_2)
  109 + .timeout(Duration.ofMillis(MAX_REQUEST_TIMEOUT_TIME))
  110 + .header(CONTENT_TYPE, CONTENT_TYPE_XML)
  111 + .POST(HttpRequest.BodyPublishers.ofString(xml));
  112 + // Wrap the HTTP headers
  113 + if (headers != null && headers.length > 0) {
  114 + Arrays.stream(headers).filter(Objects::nonNull).forEach(h -> request.header(h.param, h.value));
  115 + }
  116 +
  117 + return execute(request.build());
  118 + }
  119 +
  120 + protected HttpClient getHttpClient() {
  121 + if (httpClient == null) {
  122 + synchronized (lock) {
  123 + // Double check for performance purpose
  124 + if (httpClient == null) {
  125 + HttpClient.Builder builder = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2)
  126 + // 认证,默认情况下Authenticator.getDefault()是null值,会报错
  127 +// .authenticator(Authenticator.getDefault())
  128 + // 缓存,默认情况下 CookieHandler.getDefault()是null值,会报错
  129 +// .cookieHandler(CookieHandler.getDefault())
  130 + .connectTimeout(Duration.ofMillis(MAX_CONNECT_TIMEOUT_TIME))
  131 + .followRedirects(HttpClient.Redirect.NEVER);
  132 + // Build SSL
  133 + Optional<SSLContext> sslContext = buildSSLContext();
  134 + sslContext.ifPresent(builder::sslContext);
  135 + Optional<SSLParameters> parameters = buildSslParameters();
  136 + parameters.ifPresent(builder::sslParameters);
  137 + // Build thread pool
  138 + Optional<Executor> executor = buildThreadPool();
  139 + executor.ifPresent(builder::executor);
  140 + httpClient = builder.build();
  141 + }
  142 + }
  143 + }
  144 +
  145 + return httpClient;
  146 + }
  147 +
  148 + protected Optional<SSLContext> buildSSLContext() {
  149 + return Optional.empty();
  150 + }
  151 +
  152 + protected Optional<SSLParameters> buildSslParameters() {
  153 + return Optional.empty();
  154 + }
  155 +
  156 + protected Optional<Executor> buildThreadPool() {
  157 + return Optional.empty();
  158 + }
  159 +
  160 + /**
  161 + * @throws ServiceConnectException, ServiceAccessException, ServiceTimeoutException
  162 + */
  163 + protected HttpResult execute(HttpRequest request) {
  164 + try {
  165 + HttpResponse<String> response = getHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
  166 +
  167 + HttpResult result = HttpResult.create();
  168 + result.statusCode = response.statusCode();
  169 + result.responseText = response.body();
  170 + result.headers = response.headers() == null ? null : response.headers().map();
  171 + return result;
  172 + } catch (ConnectException | HttpConnectTimeoutException cex) {
  173 + throw new ServiceConnectException("Remote service connection exception", cex);
  174 + } catch (HttpTimeoutException hex) {
  175 + throw new ServiceTimeoutException("Remote service access timeout", hex);
  176 + } catch (Exception ex) {
  177 + throw new ServiceAccessException("Remote service access exception", ex);
  178 + }
  179 + }
  180 +
  181 + public static class HttpParam {
  182 + public String param;
  183 + public String value;
  184 +
  185 + private HttpParam(String param, String value) {
  186 + this.param = param;
  187 + this.value = value;
  188 + }
  189 +
  190 + public static HttpParam create(String param, String value) {
  191 + return new HttpParam(param, value);
  192 + }
  193 + }
  194 +
  195 + public static class HttpHeader {
  196 + public String param;
  197 + public String value;
  198 +
  199 + private HttpHeader(String param, String value) {
  200 + this.param = param;
  201 + this.value = value;
  202 + }
  203 +
  204 + public static HttpHeader create(String param, String value) {
  205 + return new HttpHeader(param, value);
  206 + }
  207 + }
  208 +
  209 + public static class HttpResult {
  210 + public int statusCode = -1;
  211 + public String responseText;
  212 + public Map<String, List<String>> headers;
  213 +
  214 + public static HttpResult create() {
  215 + return new HttpResult();
  216 + }
  217 +
  218 + public String header(String key) {
  219 + if (headers == null) {
  220 + return null;
  221 + }
  222 +
  223 + List<String> values = headers.get(key);
  224 + if (values == null || values.isEmpty()) {
  225 + return null;
  226 + }
  227 +
  228 + return values.getFirst();
  229 + }
  230 + }
  231 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/service/ThreadPoolService.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/service/ThreadPoolService.java
  1 +package com.diligrp.cashier.shared.service;
  2 +
  3 +import java.util.concurrent.ExecutorService;
  4 +import java.util.concurrent.LinkedBlockingQueue;
  5 +import java.util.concurrent.ThreadPoolExecutor;
  6 +import java.util.concurrent.TimeUnit;
  7 +
  8 +/**
  9 + * 请谨慎使用此线程池工具类,通常建议根据特定的使用场景设置线程池参数,不建议使用统一的线程池配置
  10 + * JDK的线程池类并不能很好区分"计算密集型"和"IO密集型"任务类型,并根据不同的任务类型去配置不同的参数
  11 + */
  12 +public final class ThreadPoolService {
  13 +
  14 + private static final int CPU_CORE_NUM = Runtime.getRuntime().availableProcessors();
  15 +
  16 + private static final int CPU_MAX_POOL_SIZE = 100;
  17 +
  18 + private static final int IO_MAX_POOL_SIZE = 1000;
  19 +
  20 + // CPU运算密集型任务的线程池实例
  21 + private static volatile ExecutorService cpuThreadPoll;
  22 +
  23 + // IO密集型任务的线程池实例
  24 + private static volatile ExecutorService ioThreadPoll;
  25 +
  26 + private ThreadPoolService() {
  27 + }
  28 +
  29 + /**
  30 + * 获取运算密集型任务的线程池实例
  31 + * 通常建议根据特定的使用场景设置线程池参数,不建议使用统一的线程池配置
  32 + */
  33 + public static ExecutorService getCpuThreadPoll() {
  34 + if (cpuThreadPoll == null) {
  35 + synchronized (ThreadPoolService.class) {
  36 + if (cpuThreadPoll == null) {
  37 + cpuThreadPoll = new ThreadPoolExecutor(CPU_CORE_NUM + 1, CPU_MAX_POOL_SIZE,
  38 + 20, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100),
  39 + new ThreadPoolExecutor.AbortPolicy());
  40 + }
  41 + }
  42 + }
  43 + return cpuThreadPoll;
  44 + }
  45 +
  46 + /**
  47 + * 获取IO密集型任务的线程池实例
  48 + * 通常建议根据特定的使用场景设置线程池参数,不建议使用统一的线程池配置
  49 + */
  50 + public static ExecutorService getIoThreadPoll() {
  51 + if (ioThreadPoll == null) {
  52 + synchronized (ThreadPoolService.class) {
  53 + if (ioThreadPoll == null) {
  54 + ioThreadPoll = new ThreadPoolExecutor(CPU_CORE_NUM + 1, IO_MAX_POOL_SIZE,
  55 + 20, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000),
  56 + new ThreadPoolExecutor.AbortPolicy());
  57 + }
  58 + }
  59 + }
  60 + return ioThreadPoll;
  61 + }
  62 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/type/Gender.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/type/Gender.java
  1 +package com.diligrp.cashier.shared.type;
  2 +
  3 +import com.diligrp.cashier.shared.ErrorCode;
  4 +import com.diligrp.cashier.shared.exception.PlatformServiceException;
  5 +
  6 +import java.util.Arrays;
  7 +import java.util.List;
  8 +import java.util.Optional;
  9 +import java.util.stream.Stream;
  10 +
  11 +public enum Gender implements IEnumType {
  12 +
  13 + MALE("男", 1),
  14 +
  15 + FEMALE("女", 2);
  16 +
  17 + private String name;
  18 + private int code;
  19 +
  20 + Gender(String name, int code) {
  21 + this.name = name;
  22 + this.code = code;
  23 + }
  24 +
  25 + public static Optional<Gender> getGender(int code) {
  26 + Stream<Gender> GENDERS = Arrays.stream(values());
  27 + return GENDERS.filter((gender) -> gender.getCode() == code).findFirst();
  28 + }
  29 +
  30 + public static Gender getByCode(int code) {
  31 + return getGender(code).orElseThrow(() -> new PlatformServiceException(ErrorCode.ILLEGAL_ARGUMENT_ERROR, "invalid gender"));
  32 + }
  33 +
  34 + public static String getName(int code) {
  35 + Stream<Gender> GENDERS = Arrays.stream(values());
  36 + Optional<String> result = GENDERS.filter((gender) -> gender.getCode() == code)
  37 + .map(Gender::getName).findFirst();
  38 + return result.orElse(null);
  39 + }
  40 +
  41 + public static List<Gender> getGenders() {
  42 + return Arrays.asList(values());
  43 + }
  44 +
  45 + public String getName() {
  46 + return this.name;
  47 + }
  48 +
  49 + public int getCode() {
  50 + return this.code;
  51 + }
  52 +
  53 + public String toString() {
  54 + return this.name;
  55 + }
  56 +}
0 57 \ No newline at end of file
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/type/IEnumType.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/type/IEnumType.java
  1 +package com.diligrp.cashier.shared.type;
  2 +
  3 +public interface IEnumType {
  4 + int getCode();
  5 +
  6 + String getName();
  7 +
  8 + String toString();
  9 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/util/AssertUtils.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/util/AssertUtils.java
  1 +package com.diligrp.cashier.shared.util;
  2 +
  3 +import java.util.Collection;
  4 +import java.util.Map;
  5 +
  6 +/**
  7 + * 断言工具类
  8 + */
  9 +public class AssertUtils {
  10 + public static void notNull(Object object) {
  11 + notNull(object, "[Assertion failed] - this argument is required; it must not be null");
  12 + }
  13 +
  14 + public static void notNull(Object object, String message) {
  15 + if (object == null) {
  16 + throw new IllegalArgumentException(message);
  17 + }
  18 + }
  19 +
  20 + public static void notEmpty(String str) {
  21 + notEmpty(str, "[Assertion failed] - this argument is required; it must not be empty");
  22 + }
  23 +
  24 + public static void notEmpty(String str, String message) {
  25 + if (ObjectUtils.isEmpty(str)) {
  26 + throw new IllegalArgumentException(message);
  27 + }
  28 + }
  29 +
  30 + public static void isTrue(boolean expression, String message) {
  31 + if (!expression) {
  32 + throw new IllegalArgumentException(message);
  33 + }
  34 + }
  35 +
  36 + public static void isTrue(boolean expression) {
  37 + isTrue(expression, "[Assertion failed] - this expression must be true");
  38 + }
  39 +
  40 + public static void notEmpty(Collection<?> collection, String message) {
  41 + if (collection == null || collection.isEmpty()) {
  42 + throw new IllegalArgumentException(message);
  43 + }
  44 + }
  45 +
  46 + public static void notEmpty(Collection<?> collection) {
  47 + notEmpty(collection, "[Assertion failed] - this collection must not be empty");
  48 + }
  49 +
  50 + public static void notEmpty(Map<?, ?> map, String message) {
  51 + if (map == null || map.isEmpty()) {
  52 + throw new IllegalArgumentException(message);
  53 + }
  54 + }
  55 +
  56 + public static void notEmpty(Object[] array) {
  57 + notEmpty(array, "[Assertion failed] - this array must not be empty");
  58 + }
  59 +
  60 + public static void notEmpty(Object[] array, String message) {
  61 + if (array == null || array.length == 0) {
  62 + throw new IllegalArgumentException(message);
  63 + }
  64 + }
  65 +
  66 + public static void notEmpty(Map<?, ?> map) {
  67 + notEmpty(map, "[Assertion failed] - this map must not be empty");
  68 + }
  69 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/util/ClassUtils.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/util/ClassUtils.java
  1 +package com.diligrp.cashier.shared.util;
  2 +
  3 +/**
  4 + * Classloader工具类
  5 + */
  6 +public class ClassUtils {
  7 + public static ClassLoader getDefaultClassLoader() {
  8 + ClassLoader cl = null;
  9 + try {
  10 + cl = Thread.currentThread().getContextClassLoader();
  11 + } catch (Throwable ex) {
  12 + // Cannot access thread context ClassLoader - falling back...
  13 + }
  14 +
  15 + if (cl == null) {
  16 + // No thread context class loader -> use class loader of this class.
  17 + cl = ClassUtils.class.getClassLoader();
  18 + if (cl == null) {
  19 + // getClassLoader() returning null indicates the bootstrap ClassLoader
  20 + try {
  21 + cl = ClassLoader.getSystemClassLoader();
  22 + } catch (Throwable ex) {
  23 + // Cannot access system ClassLoader - oh well, maybe the caller can live with null...
  24 + }
  25 + }
  26 + }
  27 + return cl;
  28 + }
  29 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/util/CurrencyUtils.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/util/CurrencyUtils.java
  1 +package com.diligrp.cashier.shared.util;
  2 +
  3 +import java.math.BigDecimal;
  4 +import java.math.BigInteger;
  5 +import java.math.RoundingMode;
  6 +import java.text.FieldPosition;
  7 +import java.text.NumberFormat;
  8 +import java.util.Locale;
  9 +
  10 +/**
  11 + * 金额格式转化工具类
  12 + */
  13 +public class CurrencyUtils {
  14 + private static final int DEFAULT_SCALE = 2;
  15 + private static final Locale CURRENT_LOCALE = Locale.CHINA;
  16 + private static final BigDecimal YUAN_CENT_UNIT = new BigDecimal(100);
  17 +
  18 + public static String toCurrency(Long cent) {
  19 + if (cent == null) {
  20 + return null;
  21 + }
  22 +
  23 + BigDecimal amount = new BigDecimal(cent);
  24 + BigDecimal yuan = amount.divide(YUAN_CENT_UNIT).setScale(DEFAULT_SCALE, RoundingMode.HALF_UP);
  25 +
  26 + NumberFormat numberFormat = NumberFormat.getCurrencyInstance(CURRENT_LOCALE);
  27 + StringBuffer currency = new StringBuffer();
  28 + numberFormat.format(yuan, currency, new FieldPosition(0));
  29 + if (cent < 0) {
  30 + correctSymbol(currency);
  31 + }
  32 + return currency.toString();
  33 + }
  34 +
  35 + public static String toNoSymbolCurrency(Long cent) {
  36 + if (cent == null) {
  37 + return null;
  38 + }
  39 +
  40 + BigDecimal amount = new BigDecimal(cent);
  41 + BigDecimal yuan = amount.divide(YUAN_CENT_UNIT).setScale(DEFAULT_SCALE, RoundingMode.HALF_UP);
  42 +
  43 + NumberFormat numberFormat = NumberFormat.getCurrencyInstance(CURRENT_LOCALE);
  44 + StringBuffer currency = new StringBuffer();
  45 + numberFormat.format(yuan, currency, new FieldPosition(0));
  46 + if (cent < 0) {
  47 + correctSymbol(currency);
  48 + }
  49 + return currency.substring(1);
  50 + }
  51 +
  52 + public static String cent2TenNoSymbol(Long cent) {
  53 +
  54 + BigDecimal yuan = point2ten(cent);
  55 + if (null == yuan) {
  56 + yuan = new BigDecimal(0L);
  57 + }
  58 + return yuan.toString();
  59 + }
  60 +
  61 + public static Long yuan2Cent(BigDecimal yuan) {
  62 + if (yuan == null) {
  63 + return null;
  64 + }
  65 +
  66 + BigDecimal amount = yuan.setScale(DEFAULT_SCALE, RoundingMode.HALF_UP);
  67 + BigDecimal cent = amount.multiply(YUAN_CENT_UNIT);
  68 + return cent.longValue();
  69 + }
  70 +
  71 + private static BigDecimal point2ten(Long point) {
  72 + if (null == point) {
  73 + point = 0L;
  74 + }
  75 + BigDecimal centBigDecimal = new BigDecimal(point);
  76 + BigInteger divisor = BigInteger.valueOf(1L);
  77 + for (int i = 0; i < DEFAULT_SCALE; i++) {
  78 + divisor = divisor.multiply(BigInteger.valueOf(10L));
  79 + }
  80 + return centBigDecimal.divide(new BigDecimal(divisor)).setScale(DEFAULT_SCALE);
  81 + }
  82 +
  83 + public static String convert2Percent(Long total, Long percentNumber) {
  84 + if (percentNumber == 0L || total == 0L) {
  85 + return "0.00%";
  86 + }
  87 + double percent = percentNumber.doubleValue() / total.doubleValue() * 100;
  88 + BigDecimal bigDecimal = new BigDecimal(percent);
  89 + return bigDecimal.setScale(2, RoundingMode.HALF_UP) + "%";
  90 + }
  91 +
  92 + /**
  93 + * $-100.00 => -$100.00
  94 + */
  95 + private static void correctSymbol(StringBuffer currency) {
  96 + char negativeSymbol = currency.charAt(0);
  97 + char currencySymbol = currency.charAt(1);
  98 + currency.setCharAt(0, currencySymbol);
  99 + currency.setCharAt(1, negativeSymbol);
  100 + }
  101 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/util/DateUtils.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/util/DateUtils.java
  1 +package com.diligrp.cashier.shared.util;
  2 +
  3 +import com.diligrp.cashier.shared.Constants;
  4 +
  5 +import java.text.SimpleDateFormat;
  6 +import java.time.LocalDate;
  7 +import java.time.LocalDateTime;
  8 +import java.time.ZoneOffset;
  9 +import java.time.format.DateTimeFormatter;
  10 +import java.util.Date;
  11 +
  12 +/**
  13 + * 日期格式转化工具类 - JDK1.8 TIME API
  14 + */
  15 +public class DateUtils {
  16 +
  17 + public static String formatDateTime(LocalDateTime when, String format) {
  18 + if (ObjectUtils.isNull(when)) {
  19 + return null;
  20 + }
  21 +
  22 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
  23 + return when.format(formatter);
  24 + }
  25 +
  26 + public static String formatDateTime(LocalDateTime when) {
  27 + return formatDateTime(when, Constants.DATE_TIME_FORMAT);
  28 + }
  29 +
  30 + public static String formatDate(LocalDate when, String format) {
  31 + if (ObjectUtils.isNull(when)) {
  32 + return null;
  33 + }
  34 +
  35 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
  36 + return when.format(formatter);
  37 + }
  38 +
  39 + public static String formatDate(LocalDate when) {
  40 + return formatDate(when, Constants.DATE_FORMAT);
  41 + }
  42 +
  43 + public static String formatNow(String format) {
  44 + return formatDateTime(LocalDateTime.now(), format);
  45 + }
  46 +
  47 + public static String formatNow() {
  48 + return formatNow(Constants.DATE_TIME_FORMAT);
  49 + }
  50 +
  51 + public static String format(Date date) {
  52 + return format(date, Constants.DATE_TIME_FORMAT);
  53 + }
  54 +
  55 + public static LocalDateTime addDays(long amount) {
  56 + LocalDateTime localDateTime = LocalDateTime.now();
  57 + localDateTime.plusDays(amount);
  58 + return localDateTime;
  59 + }
  60 +
  61 + public static String format(Date date, String format) {
  62 + if (ObjectUtils.isNull(date)) {
  63 + return null;
  64 + }
  65 +
  66 + SimpleDateFormat sdf = new SimpleDateFormat(format);
  67 + return sdf.format(date);
  68 +
  69 + }
  70 +
  71 + public static LocalDateTime parseDateTime(String datetimeStr, String format) {
  72 + if (ObjectUtils.isEmpty(datetimeStr)) {
  73 + return null;
  74 + }
  75 +
  76 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
  77 + return LocalDateTime.parse(datetimeStr, formatter);
  78 + }
  79 +
  80 + public static LocalDateTime parseDateTime(String datetimeStr) {
  81 + return parseDateTime(datetimeStr, Constants.DATE_TIME_FORMAT);
  82 + }
  83 +
  84 + public static LocalDate parseDate(String dateStr, String format) {
  85 + if (ObjectUtils.isEmpty(dateStr)) {
  86 + return null;
  87 + }
  88 +
  89 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
  90 + return LocalDate.parse(dateStr, formatter);
  91 + }
  92 +
  93 + public static LocalDate parseDate(String dateStr) {
  94 + return parseDate(dateStr, Constants.DATE_FORMAT);
  95 + }
  96 +
  97 + public static Date parse(String dateStr) {
  98 + return parse(dateStr, Constants.DATE_TIME_FORMAT);
  99 + }
  100 +
  101 + public static Date parse(String dateStr, String format) {
  102 + if (ObjectUtils.isEmpty(dateStr)) {
  103 + return null;
  104 + }
  105 +
  106 + try {
  107 + SimpleDateFormat sdf = new SimpleDateFormat(format);
  108 + return sdf.parse(dateStr);
  109 + } catch (Exception ex) {
  110 + throw new IllegalArgumentException("Invalid date format", ex);
  111 + }
  112 + }
  113 +
  114 + /**
  115 + * 获取时间戳
  116 + */
  117 + public static long parseMilliSecond(LocalDateTime localDateTime){
  118 + return parseMilliSecond(localDateTime,null);
  119 + }
  120 +
  121 + public static long parseMilliSecond(LocalDateTime localDateTime, String zoneNumStr){
  122 + //默认东八区
  123 + if (ObjectUtils.isEmpty(zoneNumStr)){
  124 + zoneNumStr = "+8";
  125 + }
  126 + return localDateTime.toInstant(ZoneOffset.of(zoneNumStr)).toEpochMilli();
  127 + }
  128 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/util/HttpUtils.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/util/HttpUtils.java
  1 +package com.diligrp.cashier.shared.util;
  2 +
  3 +import com.diligrp.cashier.shared.Constants;
  4 +import jakarta.servlet.http.HttpServletRequest;
  5 +import jakarta.servlet.http.HttpServletResponse;
  6 +import org.slf4j.Logger;
  7 +import org.slf4j.LoggerFactory;
  8 +
  9 +import java.io.BufferedReader;
  10 +import java.io.IOException;
  11 +import java.nio.charset.StandardCharsets;
  12 +
  13 +/**
  14 + * HTTP工具类
  15 + */
  16 +public final class HttpUtils {
  17 +
  18 + private static final Logger LOG = LoggerFactory.getLogger(HttpUtils.class);
  19 +
  20 + public static String httpBody(HttpServletRequest request) {
  21 + StringBuilder payload = new StringBuilder();
  22 + try {
  23 + String line;
  24 + BufferedReader reader = request.getReader();
  25 + while ((line = reader.readLine()) != null) {
  26 + payload.append(line);
  27 + }
  28 + } catch (IOException iex) {
  29 + LOG.error("Failed to extract http body", iex);
  30 + }
  31 +
  32 + return payload.toString();
  33 + }
  34 +
  35 + public static void sendResponse(HttpServletResponse response, String payload) {
  36 + try {
  37 + response.setContentType(Constants.CONTENT_TYPE);
  38 + byte[] responseBytes = payload.getBytes(StandardCharsets.UTF_8);
  39 + response.setContentLength(responseBytes.length);
  40 + response.getOutputStream().write(responseBytes);
  41 + response.flushBuffer();
  42 + } catch (IOException iex) {
  43 + LOG.error("Failed to write data packet back");
  44 + }
  45 + }
  46 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/util/JsonUtils.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/util/JsonUtils.java
  1 +package com.diligrp.cashier.shared.util;
  2 +
  3 +import com.diligrp.cashier.shared.Constants;
  4 +import com.fasterxml.jackson.annotation.JsonInclude;
  5 +import com.fasterxml.jackson.core.JsonProcessingException;
  6 +import com.fasterxml.jackson.core.type.TypeReference;
  7 +import com.fasterxml.jackson.databind.DeserializationFeature;
  8 +import com.fasterxml.jackson.databind.JsonNode;
  9 +import com.fasterxml.jackson.databind.ObjectMapper;
  10 +import com.fasterxml.jackson.databind.SerializationFeature;
  11 +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
  12 +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
  13 +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
  14 +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
  15 +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
  16 +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
  17 +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
  18 +
  19 +import java.text.SimpleDateFormat;
  20 +import java.time.LocalDate;
  21 +import java.time.LocalDateTime;
  22 +import java.time.LocalTime;
  23 +import java.time.ZoneOffset;
  24 +import java.time.format.DateTimeFormatter;
  25 +import java.util.TimeZone;
  26 +
  27 +public class JsonUtils {
  28 +
  29 + private static ObjectMapper objectMapper = initObjectMapper();
  30 +
  31 + private static ObjectMapper initObjectMapper(){
  32 + Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder = new Jackson2ObjectMapperBuilder();
  33 + initObjectMapperBuilder(jackson2ObjectMapperBuilder);
  34 + ObjectMapper objectMapper = jackson2ObjectMapperBuilder.createXmlMapper(false).build();
  35 + objectMapper.setSerializerFactory(objectMapper.getSerializerFactory());
  36 + return objectMapper;
  37 + }
  38 +
  39 + public static void initObjectMapperBuilder(Jackson2ObjectMapperBuilder builder) {
  40 + //序列化java.util.Date类型
  41 + builder.dateFormat(new SimpleDateFormat(Constants.DATE_TIME_FORMAT));
  42 + builder.timeZone(TimeZone.getTimeZone(ZoneOffset.of("+8")));
  43 + builder.serializationInclusion(JsonInclude.Include.NON_NULL);
  44 + builder.featuresToDisable(
  45 + DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, // Json串的属性无JavaBean字段对应时,避免抛出异常
  46 + DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, // JavaBean中primitive类型的字段无Json属性时,避免抛出异常
  47 + DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS, // Json串数字类型属性,赋值JavaBean中Enum字段时,避免抛出异常
  48 + SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
  49 + SerializationFeature.FAIL_ON_EMPTY_BEANS
  50 + );
  51 + builder.featuresToEnable(
  52 + DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT,
  53 + DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY
  54 + );
  55 +
  56 + var dateTimeFormatter = DateTimeFormatter.ofPattern(Constants.DATE_TIME_FORMAT);
  57 + var dateFormatter = DateTimeFormatter.ofPattern(Constants.DATE_FORMAT);
  58 + var timeFormatter = DateTimeFormatter.ofPattern(Constants.TIME_FORMAT);
  59 + // 添加自定义序列化
  60 + builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(dateTimeFormatter));
  61 + builder.serializerByType(LocalDate.class, new LocalDateSerializer(dateFormatter));
  62 + builder.serializerByType(LocalTime.class, new LocalTimeSerializer(timeFormatter));
  63 + // 添加自定义反序列化
  64 + builder.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(dateTimeFormatter));
  65 + builder.deserializerByType(LocalDate.class, new LocalDateDeserializer(dateFormatter));
  66 + builder.deserializerByType(LocalTime.class, new LocalTimeDeserializer(timeFormatter));
  67 + }
  68 +
  69 + public static <T> T fromJsonString(String json, Class<T> type) {
  70 + try {
  71 + return objectMapper.readValue(json, type);
  72 + } catch (JsonProcessingException ex) {
  73 + throw new IllegalArgumentException("Deserialize json exception", ex);
  74 + }
  75 + }
  76 +
  77 + public static <T> T fromJsonString(String json, TypeReference<T> typeReference){
  78 + try {
  79 + return objectMapper.readValue(json, typeReference);
  80 + } catch (JsonProcessingException ex) {
  81 + throw new IllegalArgumentException("Deserialize json array exception", ex);
  82 + }
  83 + }
  84 +
  85 + public static void fromJsonString(Object javaBean, String json) {
  86 + try {
  87 + objectMapper.readerForUpdating(javaBean).readValue(json);
  88 + } catch (JsonProcessingException ex) {
  89 + throw new IllegalArgumentException("Deserialize json exception", ex);
  90 + }
  91 + }
  92 +
  93 + public static JsonNode fromJsonString(String json) {
  94 + try {
  95 + return objectMapper.readTree(json);
  96 + } catch (JsonProcessingException ex) {
  97 + throw new IllegalArgumentException("Deserialize json exception", ex);
  98 + }
  99 + }
  100 +
  101 + public static <T> T convertValue(Object value, TypeReference<T> typeReference) {
  102 + return objectMapper.convertValue(value, typeReference);
  103 + }
  104 +
  105 + public static <T> String toJsonString(T object) {
  106 + try {
  107 + return objectMapper.writeValueAsString(object);
  108 + } catch (JsonProcessingException ex) {
  109 + throw new IllegalArgumentException("Serialize json exception", ex);
  110 + }
  111 + }
  112 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/util/MathUtils.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/util/MathUtils.java
  1 +package com.diligrp.cashier.shared.util;
  2 +
  3 +import java.math.BigDecimal;
  4 +import java.math.RoundingMode;
  5 +
  6 +/**
  7 + * 精确的浮点数运算
  8 + */
  9 +public class MathUtils {
  10 +
  11 + /**
  12 + * 默认除法运算精度
  13 + */
  14 + private static final int DEF_DIV_SCALE = 10;
  15 +
  16 + /**
  17 + * 提供精确的加法运算
  18 + */
  19 + public static double add(double v1, double v2) {
  20 + BigDecimal b1 = new BigDecimal(Double.toString(v1));
  21 + BigDecimal b2 = new BigDecimal(Double.toString(v2));
  22 + return b1.add(b2).doubleValue();
  23 + }
  24 +
  25 + /**
  26 + * 提供精确的减法运算。
  27 + */
  28 + public static double sub(double v1, double v2) {
  29 + BigDecimal b1 = new BigDecimal(Double.toString(v1));
  30 + BigDecimal b2 = new BigDecimal(Double.toString(v2));
  31 + return b1.subtract(b2).doubleValue();
  32 + }
  33 +
  34 + /**
  35 + * 提供精确的乘法运算。
  36 + */
  37 + public static double mul(double v1, double v2) {
  38 + BigDecimal b1 = new BigDecimal(Double.toString(v1));
  39 + BigDecimal b2 = new BigDecimal(Double.toString(v2));
  40 + return b1.multiply(b2).doubleValue();
  41 + }
  42 +
  43 + /**
  44 + * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到小数点以后10位,
  45 + * 以后的数字四舍五入。
  46 + */
  47 + public static double div(double v1, double v2) {
  48 + return div(v1, v2, DEF_DIV_SCALE);
  49 + }
  50 +
  51 + /**
  52 + * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指定精度,
  53 + * 以后的数字四舍五入。
  54 + */
  55 + public static double div(double v1, double v2, int scale) {
  56 + if (scale < 0) {
  57 + throw new IllegalArgumentException(
  58 + "The scale must be a positive integer or zero");
  59 + }
  60 + BigDecimal b1 = new BigDecimal(Double.toString(v1));
  61 + BigDecimal b2 = new BigDecimal(Double.toString(v2));
  62 + if (b1.compareTo(BigDecimal.ZERO) == 0) {
  63 + return BigDecimal.ZERO.doubleValue();
  64 + }
  65 + return b1.divide(b2, scale, RoundingMode.HALF_UP).doubleValue();
  66 + }
  67 +
  68 + /**
  69 + * 提供精确的小数位四舍五入处理。
  70 + */
  71 + public static double round(double v, int scale) {
  72 + if (scale < 0) {
  73 + throw new IllegalArgumentException(
  74 + "The scale must be a positive integer or zero");
  75 + }
  76 + BigDecimal b = new BigDecimal(Double.toString(v));
  77 + BigDecimal one = new BigDecimal("1");
  78 + return b.divide(one, scale, RoundingMode.HALF_UP).doubleValue();
  79 + }
  80 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/util/NumberUtils.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/util/NumberUtils.java
  1 +package com.diligrp.cashier.shared.util;
  2 +
  3 +import org.slf4j.Logger;
  4 +import org.slf4j.LoggerFactory;
  5 +
  6 +/**
  7 + * 字符串-数值转化工具类
  8 + */
  9 +public class NumberUtils {
  10 + private static Logger LOG = LoggerFactory.getLogger(NumberUtils.class);
  11 +
  12 + public static int str2Int(String number, int defaultValue) {
  13 + if (ObjectUtils.isEmpty(number)) {
  14 + return defaultValue;
  15 + }
  16 +
  17 + try {
  18 + return Integer.parseInt(number);
  19 + } catch (NumberFormatException nfe) {
  20 + // Never ignore any exception
  21 + LOG.error("Invalid number format", nfe);
  22 + return defaultValue;
  23 + }
  24 + }
  25 +
  26 + public static long str2Long(String number, long defaultValue) {
  27 + if (ObjectUtils.isEmpty(number)) {
  28 + return defaultValue;
  29 + }
  30 +
  31 + try {
  32 + return Long.parseLong(number);
  33 + } catch (NumberFormatException nfe) {
  34 + // Never ignore any exception
  35 + LOG.error("Invalid number format", nfe);
  36 + return defaultValue;
  37 + }
  38 + }
  39 +
  40 + public static boolean isNumeric(String str) {
  41 + if (ObjectUtils.isEmpty(str)) {
  42 + return false;
  43 + } else {
  44 + int sz = str.length();
  45 +
  46 + for(int i = 0; i < sz; ++i) {
  47 + if (!Character.isDigit(str.charAt(i))) {
  48 + return false;
  49 + }
  50 + }
  51 +
  52 + return true;
  53 + }
  54 + }
  55 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/util/ObjectUtils.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/util/ObjectUtils.java
  1 +package com.diligrp.cashier.shared.util;
  2 +
  3 +import java.util.ArrayList;
  4 +import java.util.List;
  5 +
  6 +/**
  7 + * 通用工具类
  8 + */
  9 +public class ObjectUtils {
  10 + public static boolean equals(String str1, String str2) {
  11 + if (str1 == str2) {
  12 + return true;
  13 + } else if (str1 != null && str2 != null) {
  14 + if (str1.length() != str2.length()) {
  15 + return false;
  16 + } else if (str1 instanceof String && str2 instanceof String) {
  17 + return str1.equals(str2);
  18 + } else {
  19 + int length = str1.length();
  20 +
  21 + for(int i = 0; i < length; ++i) {
  22 + if (str1.charAt(i) != str2.charAt(i)) {
  23 + return false;
  24 + }
  25 + }
  26 +
  27 + return true;
  28 + }
  29 + } else {
  30 + return false;
  31 + }
  32 + }
  33 +
  34 + public static boolean equals(Object object1, Object object2) {
  35 + if (object1 == object2) {
  36 + return true;
  37 + }
  38 + return object1 != null && object2 != null ? object1.equals(object2) : false;
  39 + }
  40 +
  41 + public static String[] split(String str, char separator) {
  42 + if (str == null) {
  43 + return null;
  44 + } else {
  45 + int len = str.length();
  46 + if (len == 0) {
  47 + return new String[0];
  48 + } else {
  49 + int i = 0;
  50 + int start = 0;
  51 + boolean match = false;
  52 + boolean lastMatch = false;
  53 + boolean preserveAllTokens = false;
  54 + List<String> list = new ArrayList<String>();
  55 +
  56 + while(true) {
  57 + while(i < len) {
  58 + if (str.charAt(i) == separator) {
  59 + if (match || preserveAllTokens) {
  60 + list.add(str.substring(start, i));
  61 + match = false;
  62 + lastMatch = true;
  63 + }
  64 +
  65 + ++i;
  66 + start = i;
  67 + } else {
  68 + lastMatch = false;
  69 + match = true;
  70 + ++i;
  71 + }
  72 + }
  73 +
  74 + if (match || preserveAllTokens && lastMatch) {
  75 + list.add(str.substring(start, i));
  76 + }
  77 +
  78 + return (String[])list.toArray(new String[list.size()]);
  79 + }
  80 + }
  81 + }
  82 + }
  83 +
  84 + public static boolean isEmpty(String str) {
  85 + return str == null || str.length() == 0;
  86 + }
  87 +
  88 + public static boolean isNotEmpty(String str) {
  89 + return !isEmpty(str);
  90 + }
  91 +
  92 + public static <T> boolean isEmpty(List<T> array) {
  93 + return array == null || array.isEmpty();
  94 + }
  95 +
  96 + public static <T> boolean isNotEmpty(List<T> array) {
  97 + return array != null && !array.isEmpty();
  98 + }
  99 +
  100 + public static String trimToEmpty(String str) {
  101 + return str == null ? "" : str.trim();
  102 + }
  103 +
  104 + public static boolean isNull(Object obj) {
  105 + return null == obj;
  106 + }
  107 +}
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/util/RandomUtils.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/util/RandomUtils.java
  1 +package com.diligrp.cashier.shared.util;
  2 +
  3 +import java.util.Random;
  4 +import java.util.UUID;
  5 +import java.util.concurrent.ThreadLocalRandom;
  6 +
  7 +/**
  8 + * 随机数工具类
  9 + */
  10 +public final class RandomUtils {
  11 +
  12 + /**
  13 + * 生成随机字符串, a-z A-Z 0-9
  14 + */
  15 + public static String randomString(int length){
  16 + String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
  17 + Random random = new Random();
  18 + StringBuilder sb = new StringBuilder();
  19 + for(int i = 0; i < length; i++){
  20 + int number = random.nextInt(62);
  21 + sb.append(str.charAt(number));
  22 + }
  23 + return sb.toString();
  24 + }
  25 +
  26 + /**
  27 + * 生成固定长度的随机数字字符串
  28 + */
  29 + public static String randomNumber(int length) {
  30 + AssertUtils.isTrue(length > 0, "invalid length");
  31 + StringBuilder builder = new StringBuilder();
  32 + for (int i = 0; i < length; i ++) {
  33 + int next = ThreadLocalRandom.current().nextInt(10);
  34 + builder.append((char) (48 + next));
  35 + }
  36 +
  37 + return builder.toString();
  38 + }
  39 +
  40 + /**
  41 + * 生成8位随机码(数字0~9+大写字母A~Z)
  42 + */
  43 + public static String randomCode() {
  44 + StringBuilder sb = new StringBuilder();
  45 + for (int i = 0; i < 8; i++) {
  46 + int a = Math.abs((new Random()).nextInt(32));
  47 + if (a <= 9) {
  48 + sb.append((char) (a + 48));
  49 + } else if (a < 33) {
  50 + if ((a + 55) == 79 || (a + 55) == 73) {
  51 + sb.append((char) (a + 63));
  52 + } else {
  53 + sb.append((char) (a + 55));
  54 + }
  55 + }
  56 + }
  57 + return sb.toString();
  58 + }
  59 +
  60 +
  61 + /**
  62 + * 功能描述:获取随机字符串
  63 + */
  64 + public static String randomString(int length, String original) {
  65 + if (length <= 0 || ObjectUtils.isEmpty(original)) {
  66 + return null;
  67 + }
  68 + StringBuilder sb = new StringBuilder();
  69 + int len = original.length();
  70 + for (int i = 0; i < length; i++) {
  71 + int round = (int) Math.round(Math.random() * (len - 1));
  72 + sb.append(original.charAt(round));
  73 + }
  74 + return sb.toString();
  75 + }
  76 +
  77 + /**
  78 + * UUID随机数,移除 "-"
  79 + */
  80 + public static String randomUUID() {
  81 + return randomUUID(true);
  82 + }
  83 +
  84 + public static String randomUUID(boolean upperCase) {
  85 + UUID uuid = UUID.randomUUID();
  86 + long mostSigBits = uuid.getMostSignificantBits();
  87 + long leastSigBits = uuid.getLeastSignificantBits();
  88 + return (digits(mostSigBits >> 32, 8, upperCase) +
  89 + digits(mostSigBits >> 16, 4, upperCase) +
  90 + digits(mostSigBits, 4, upperCase) +
  91 + digits(leastSigBits >> 48, 4, upperCase) +
  92 + digits(leastSigBits, 12, upperCase));
  93 + }
  94 +
  95 + public static String randomCaptcha() {
  96 + return String.valueOf((int) ((Math.random() * ((1 << 3) + 1) + 1) * 1000));
  97 + }
  98 +
  99 + /**
  100 + * 生成随机区间数字
  101 + */
  102 + public static int randomInt(int min, int max) {
  103 + return min + (int)(Math.random() * (max + 1 - min));
  104 + }
  105 +
  106 + private static String digits(long val, int digits, boolean upperCase) {
  107 + long hi = 1L << (digits * 4);
  108 + String hexDigits = Long.toHexString(hi | (val & (hi - 1))).substring(1);
  109 + return upperCase ? hexDigits.toUpperCase() : hexDigits;
  110 + }
  111 +}
0 112 \ No newline at end of file
... ...
cashier-shared/src/main/java/com/diligrp/cashier/shared/util/ReflectUtils.java 0 → 100644
  1 +++ a/cashier-shared/src/main/java/com/diligrp/cashier/shared/util/ReflectUtils.java
  1 +package com.diligrp.cashier.shared.util;
  2 +
  3 +import java.lang.reflect.Field;
  4 +import java.lang.reflect.InvocationTargetException;
  5 +import java.lang.reflect.Method;
  6 +import java.util.Arrays;
  7 +
  8 +/**
  9 + * 利用反射进行操作的一个工具类
  10 + */
  11 +public class ReflectUtils {
  12 + /**
  13 + * 利用反射获取指定对象的指定属性
  14 + */
  15 + public static Object getFieldValue(Object target, String fieldName) {
  16 + Object result = null;
  17 + Field field = ReflectUtils.getField(target, fieldName);
  18 + if (field != null) {
  19 + field.setAccessible(true);
  20 + try {
  21 + result = field.get(target);
  22 + } catch (IllegalArgumentException | IllegalAccessException ex) {
  23 + throw new RuntimeException("Illegal access or argument exception", ex);
  24 + }
  25 + }
  26 + return result;
  27 + }
  28 +
  29 + /**
  30 + * 利用反射获取指定对象里面的指定属性
  31 + */
  32 + private static Field getField(Object obj, String fieldName) {
  33 + Field field = null;
  34 + for (Class<?> clazz = obj.getClass();
  35 + clazz != Object.class; clazz = clazz.getSuperclass()) {
  36 + try {
  37 + field = clazz.getDeclaredField(fieldName);
  38 + break;
  39 + } catch (NoSuchFieldException e) {
  40 + // 当前类没有此方法则向父类查找,都没有就返回NULL
  41 + }
  42 + }
  43 + return field;
  44 + }
  45 +
  46 + /**
  47 + * 利用反射设置指定对象的指定属性为指定的值
  48 + */
  49 + public static void setFieldValue(Object obj, String fieldName, String fieldValue) {
  50 + Field field = ReflectUtils.getField(obj, fieldName);
  51 + if (field != null) {
  52 + try {
  53 + field.setAccessible(true);
  54 + field.set(obj, fieldValue);
  55 + } catch (IllegalArgumentException | IllegalAccessException ex) {
  56 + throw new RuntimeException("Illegal access or argument exception", ex);
  57 + }
  58 + }
  59 + }
  60 +
  61 + /**
  62 + * 调用对象方法, 包含private/protected修饰的方法.
  63 + */
  64 + public static Object invokeMethod(final Object target, final String methodName, final Class<?>[] parameterTypes,
  65 + final Object[] parameters) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
  66 +
  67 + Method method = getDeclaredMethod(target, methodName, parameterTypes);
  68 + if (method == null) {
  69 + throw new IllegalArgumentException("Could not find method ["
  70 + + methodName + "] parameterType " + Arrays.toString(parameterTypes)
  71 + + " on target [" + target + "]");
  72 + }
  73 + method.setAccessible(true);
  74 + return method.invoke(target, parameters);
  75 + }
  76 +
  77 + /**
  78 + * 在target对象上查找方法,如果当前类定义未定义则向父类查找,都未查找到则返回Null.
  79 + */
  80 + protected static Method getDeclaredMethod(Object target, String methodName, Class<?>[] parameterTypes) {
  81 + AssertUtils.notNull(target, "target must be not null");
  82 + for (Class<?> superClass = target.getClass(); superClass != Object.class;
  83 + superClass = superClass.getSuperclass()) {
  84 + try {
  85 + return superClass.getDeclaredMethod(methodName, parameterTypes);
  86 + } catch (NoSuchMethodException sme) {
  87 + // 当前类未定义Method则向父类查找
  88 + }
  89 + }
  90 + return null;
  91 + }
  92 +}
0 93 \ No newline at end of file
... ...
cashier-trade/build.gradle 0 → 100644
  1 +++ a/cashier-trade/build.gradle
  1 +dependencies {
  2 + api project(':cashier-shared')
  3 + api project(':cashier-assistant')
  4 + api project(':cashier-pipeline')
  5 +}
0 6 \ No newline at end of file
... ...
gradle.properties 0 → 100644
  1 +++ a/gradle.properties
  1 +org.gradle.console=verbose
  2 +org.gradle.caching=true
  3 +org.gradle.configuration-cache=true
  4 +org.gradle.jvmargs=-Dfile.encoding=UTF-8
0 5 \ No newline at end of file
... ...
gradle/libs.versions.toml 0 → 100644
  1 +++ a/gradle/libs.versions.toml
  1 +[versions]
  2 +appVersion = "1.0.0"
  3 +springBootVersion = "3.5.8"
  4 +springCloudVersion = "2025.0.0" #springboot4的cloud版本为2025.1.0
  5 +springDependencyVersion = "1.1.7"
  6 +springCloudAlibabaVersion = "2025.0.0.0" #目前没有支持springboot4的版本
  7 +mybatisVersion = "3.0.5" #支持springboot4的版本是4.0.0
  8 +mysqlDriverVersion = "8.0.33"
  9 +caffeineVersion = "3.2.3"
  10 +
  11 +[libraries]
  12 +#spring-boot-starter = {module = 'org.springframework.boot:spring-boot-starter', version.ref = 'springBootVersion'}
  13 +spring-boot-starter-web = {module = 'org.springframework.boot:spring-boot-starter-web', version.ref = 'springBootVersion'}
  14 +spring-cloud-nacos-discovery = {module = 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery', version.ref = 'springCloudAlibabaVersion'}
  15 +spring-cloud-nacos-config = {module = 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-config', version.ref = 'springCloudAlibabaVersion'}
  16 +mybatis-starter = {module = 'org.mybatis.spring.boot:mybatis-spring-boot-starter', version.ref = 'mybatisVersion'}
  17 +mysql-driver = {module = 'mysql:mysql-connector-java', version.ref = 'mysqlDriverVersion'}
  18 +cache-caffeine = {module = 'com.github.ben-manes.caffeine:caffeine', version.ref = 'caffeineVersion'}
  19 +
  20 +[plugins]
  21 +spring-boot = {id = 'org.springframework.boot', version.ref = 'springBootVersion'}
  22 +spring-dependency-management = {id = 'io.spring.dependency-management', version.ref = 'springDependencyVersion'}
0 23 \ No newline at end of file
... ...
gradle/wrapper/gradle-wrapper.jar 0 → 100644
No preview for this file type
gradle/wrapper/gradle-wrapper.properties 0 → 100644
  1 +++ a/gradle/wrapper/gradle-wrapper.properties
  1 +distributionBase=GRADLE_USER_HOME
  2 +distributionPath=wrapper/dists
  3 +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-9.2.1-bin.zip
  4 +networkTimeout=10000
  5 +validateDistributionUrl=true
  6 +zipStoreBase=GRADLE_USER_HOME
  7 +zipStorePath=wrapper/dists
... ...
gradlew 0 → 100755
  1 +++ a/gradlew
  1 +#!/bin/sh
  2 +
  3 +#
  4 +# Copyright © 2015 the original authors.
  5 +#
  6 +# Licensed under the Apache License, Version 2.0 (the "License");
  7 +# you may not use this file except in compliance with the License.
  8 +# You may obtain a copy of the License at
  9 +#
  10 +# https://www.apache.org/licenses/LICENSE-2.0
  11 +#
  12 +# Unless required by applicable law or agreed to in writing, software
  13 +# distributed under the License is distributed on an "AS IS" BASIS,
  14 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15 +# See the License for the specific language governing permissions and
  16 +# limitations under the License.
  17 +#
  18 +# SPDX-License-Identifier: Apache-2.0
  19 +#
  20 +
  21 +##############################################################################
  22 +#
  23 +# Gradle start up script for POSIX generated by Gradle.
  24 +#
  25 +# Important for running:
  26 +#
  27 +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
  28 +# noncompliant, but you have some other compliant shell such as ksh or
  29 +# bash, then to run this script, type that shell name before the whole
  30 +# command line, like:
  31 +#
  32 +# ksh Gradle
  33 +#
  34 +# Busybox and similar reduced shells will NOT work, because this script
  35 +# requires all of these POSIX shell features:
  36 +# * functions;
  37 +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
  38 +# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
  39 +# * compound commands having a testable exit status, especially «case»;
  40 +# * various built-in commands including «command», «set», and «ulimit».
  41 +#
  42 +# Important for patching:
  43 +#
  44 +# (2) This script targets any POSIX shell, so it avoids extensions provided
  45 +# by Bash, Ksh, etc; in particular arrays are avoided.
  46 +#
  47 +# The "traditional" practice of packing multiple parameters into a
  48 +# space-separated string is a well documented source of bugs and security
  49 +# problems, so this is (mostly) avoided, by progressively accumulating
  50 +# options in "$@", and eventually passing that to Java.
  51 +#
  52 +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
  53 +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
  54 +# see the in-line comments for details.
  55 +#
  56 +# There are tweaks for specific operating systems such as AIX, CygWin,
  57 +# Darwin, MinGW, and NonStop.
  58 +#
  59 +# (3) This script is generated from the Groovy template
  60 +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
  61 +# within the Gradle project.
  62 +#
  63 +# You can find Gradle at https://github.com/gradle/gradle/.
  64 +#
  65 +##############################################################################
  66 +
  67 +# Attempt to set APP_HOME
  68 +
  69 +# Resolve links: $0 may be a link
  70 +app_path=$0
  71 +
  72 +# Need this for daisy-chained symlinks.
  73 +while
  74 + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
  75 + [ -h "$app_path" ]
  76 +do
  77 + ls=$( ls -ld "$app_path" )
  78 + link=${ls#*' -> '}
  79 + case $link in #(
  80 + /*) app_path=$link ;; #(
  81 + *) app_path=$APP_HOME$link ;;
  82 + esac
  83 +done
  84 +
  85 +# This is normally unused
  86 +# shellcheck disable=SC2034
  87 +APP_BASE_NAME=${0##*/}
  88 +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
  89 +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
  90 +
  91 +# Use the maximum available, or set MAX_FD != -1 to use that value.
  92 +MAX_FD=maximum
  93 +
  94 +warn () {
  95 + echo "$*"
  96 +} >&2
  97 +
  98 +die () {
  99 + echo
  100 + echo "$*"
  101 + echo
  102 + exit 1
  103 +} >&2
  104 +
  105 +# OS specific support (must be 'true' or 'false').
  106 +cygwin=false
  107 +msys=false
  108 +darwin=false
  109 +nonstop=false
  110 +case "$( uname )" in #(
  111 + CYGWIN* ) cygwin=true ;; #(
  112 + Darwin* ) darwin=true ;; #(
  113 + MSYS* | MINGW* ) msys=true ;; #(
  114 + NONSTOP* ) nonstop=true ;;
  115 +esac
  116 +
  117 +
  118 +
  119 +# Determine the Java command to use to start the JVM.
  120 +if [ -n "$JAVA_HOME" ] ; then
  121 + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
  122 + # IBM's JDK on AIX uses strange locations for the executables
  123 + JAVACMD=$JAVA_HOME/jre/sh/java
  124 + else
  125 + JAVACMD=$JAVA_HOME/bin/java
  126 + fi
  127 + if [ ! -x "$JAVACMD" ] ; then
  128 + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
  129 +
  130 +Please set the JAVA_HOME variable in your environment to match the
  131 +location of your Java installation."
  132 + fi
  133 +else
  134 + JAVACMD=java
  135 + if ! command -v java >/dev/null 2>&1
  136 + then
  137 + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
  138 +
  139 +Please set the JAVA_HOME variable in your environment to match the
  140 +location of your Java installation."
  141 + fi
  142 +fi
  143 +
  144 +# Increase the maximum file descriptors if we can.
  145 +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
  146 + case $MAX_FD in #(
  147 + max*)
  148 + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
  149 + # shellcheck disable=SC2039,SC3045
  150 + MAX_FD=$( ulimit -H -n ) ||
  151 + warn "Could not query maximum file descriptor limit"
  152 + esac
  153 + case $MAX_FD in #(
  154 + '' | soft) :;; #(
  155 + *)
  156 + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
  157 + # shellcheck disable=SC2039,SC3045
  158 + ulimit -n "$MAX_FD" ||
  159 + warn "Could not set maximum file descriptor limit to $MAX_FD"
  160 + esac
  161 +fi
  162 +
  163 +# Collect all arguments for the java command, stacking in reverse order:
  164 +# * args from the command line
  165 +# * the main class name
  166 +# * -classpath
  167 +# * -D...appname settings
  168 +# * --module-path (only if needed)
  169 +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
  170 +
  171 +# For Cygwin or MSYS, switch paths to Windows format before running java
  172 +if "$cygwin" || "$msys" ; then
  173 + APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
  174 +
  175 + JAVACMD=$( cygpath --unix "$JAVACMD" )
  176 +
  177 + # Now convert the arguments - kludge to limit ourselves to /bin/sh
  178 + for arg do
  179 + if
  180 + case $arg in #(
  181 + -*) false ;; # don't mess with options #(
  182 + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
  183 + [ -e "$t" ] ;; #(
  184 + *) false ;;
  185 + esac
  186 + then
  187 + arg=$( cygpath --path --ignore --mixed "$arg" )
  188 + fi
  189 + # Roll the args list around exactly as many times as the number of
  190 + # args, so each arg winds up back in the position where it started, but
  191 + # possibly modified.
  192 + #
  193 + # NB: a `for` loop captures its iteration list before it begins, so
  194 + # changing the positional parameters here affects neither the number of
  195 + # iterations, nor the values presented in `arg`.
  196 + shift # remove old arg
  197 + set -- "$@" "$arg" # push replacement arg
  198 + done
  199 +fi
  200 +
  201 +
  202 +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
  203 +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
  204 +
  205 +# Collect all arguments for the java command:
  206 +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
  207 +# and any embedded shellness will be escaped.
  208 +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
  209 +# treated as '${Hostname}' itself on the command line.
  210 +
  211 +set -- \
  212 + "-Dorg.gradle.appname=$APP_BASE_NAME" \
  213 + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
  214 + "$@"
  215 +
  216 +# Stop when "xargs" is not available.
  217 +if ! command -v xargs >/dev/null 2>&1
  218 +then
  219 + die "xargs is not available"
  220 +fi
  221 +
  222 +# Use "xargs" to parse quoted args.
  223 +#
  224 +# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
  225 +#
  226 +# In Bash we could simply go:
  227 +#
  228 +# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
  229 +# set -- "${ARGS[@]}" "$@"
  230 +#
  231 +# but POSIX shell has neither arrays nor command substitution, so instead we
  232 +# post-process each arg (as a line of input to sed) to backslash-escape any
  233 +# character that might be a shell metacharacter, then use eval to reverse
  234 +# that process (while maintaining the separation between arguments), and wrap
  235 +# the whole thing up as a single "set" statement.
  236 +#
  237 +# This will of course break if any of these variables contains a newline or
  238 +# an unmatched quote.
  239 +#
  240 +
  241 +eval "set -- $(
  242 + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
  243 + xargs -n1 |
  244 + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
  245 + tr '\n' ' '
  246 + )" '"$@"'
  247 +
  248 +exec "$JAVACMD" "$@"
... ...
gradlew.bat 0 → 100755
  1 +++ a/gradlew.bat
  1 +@rem
  2 +@rem Copyright 2015 the original author or authors.
  3 +@rem
  4 +@rem Licensed under the Apache License, Version 2.0 (the "License");
  5 +@rem you may not use this file except in compliance with the License.
  6 +@rem You may obtain a copy of the License at
  7 +@rem
  8 +@rem https://www.apache.org/licenses/LICENSE-2.0
  9 +@rem
  10 +@rem Unless required by applicable law or agreed to in writing, software
  11 +@rem distributed under the License is distributed on an "AS IS" BASIS,
  12 +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +@rem See the License for the specific language governing permissions and
  14 +@rem limitations under the License.
  15 +@rem
  16 +@rem SPDX-License-Identifier: Apache-2.0
  17 +@rem
  18 +
  19 +@if "%DEBUG%"=="" @echo off
  20 +@rem ##########################################################################
  21 +@rem
  22 +@rem Gradle startup script for Windows
  23 +@rem
  24 +@rem ##########################################################################
  25 +
  26 +@rem Set local scope for the variables with windows NT shell
  27 +if "%OS%"=="Windows_NT" setlocal
  28 +
  29 +set DIRNAME=%~dp0
  30 +if "%DIRNAME%"=="" set DIRNAME=.
  31 +@rem This is normally unused
  32 +set APP_BASE_NAME=%~n0
  33 +set APP_HOME=%DIRNAME%
  34 +
  35 +@rem Resolve any "." and ".." in APP_HOME to make it shorter.
  36 +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
  37 +
  38 +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
  39 +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
  40 +
  41 +@rem Find java.exe
  42 +if defined JAVA_HOME goto findJavaFromJavaHome
  43 +
  44 +set JAVA_EXE=java.exe
  45 +%JAVA_EXE% -version >NUL 2>&1
  46 +if %ERRORLEVEL% equ 0 goto execute
  47 +
  48 +echo. 1>&2
  49 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
  50 +echo. 1>&2
  51 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2
  52 +echo location of your Java installation. 1>&2
  53 +
  54 +goto fail
  55 +
  56 +:findJavaFromJavaHome
  57 +set JAVA_HOME=%JAVA_HOME:"=%
  58 +set JAVA_EXE=%JAVA_HOME%/bin/java.exe
  59 +
  60 +if exist "%JAVA_EXE%" goto execute
  61 +
  62 +echo. 1>&2
  63 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
  64 +echo. 1>&2
  65 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2
  66 +echo location of your Java installation. 1>&2
  67 +
  68 +goto fail
  69 +
  70 +:execute
  71 +@rem Setup the command line
  72 +
  73 +
  74 +
  75 +@rem Execute Gradle
  76 +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
  77 +
  78 +:end
  79 +@rem End local scope for the variables with windows NT shell
  80 +if %ERRORLEVEL% equ 0 goto mainEnd
  81 +
  82 +:fail
  83 +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
  84 +rem the _cmd.exe /c_ return code!
  85 +set EXIT_CODE=%ERRORLEVEL%
  86 +if %EXIT_CODE% equ 0 set EXIT_CODE=1
  87 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
  88 +exit /b %EXIT_CODE%
  89 +
  90 +:mainEnd
  91 +if "%OS%"=="Windows_NT" endlocal
  92 +
  93 +:omega
... ...
scripts/dili-cashier.sql 0 → 100644
  1 +++ a/scripts/dili-cashier.sql
... ...
settings.gradle 0 → 100644
  1 +++ a/settings.gradle
  1 +rootProject.name = 'dili-cashier'
  2 +include 'cashier-shared'
  3 +include 'cashier-assistant'
  4 +include 'cashier-pipeline'
  5 +include 'cashier-trade'
  6 +include 'cashier-rtmart'
  7 +include 'cashier-boss'
0 8 \ No newline at end of file
... ...