Merge tag '1.0.12' into develop

农商行接口修改,消费接口新加支付场景
diff --git a/.gitignore b/.gitignore
index 470effd..7499f48 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,3 +49,4 @@
 # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
 # gradle/wrapper/gradle-wrapper.properties
 
+/log
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 83b1ec8..fe5a7de 100644
--- a/build.gradle
+++ b/build.gradle
@@ -101,7 +101,6 @@
         implementation "org.slf4j:slf4j-api:${slf4jVersion}"
         implementation "org.postgresql:postgresql:${postgresVersion}"
         implementation "io.github.microutils:kotlin-logging:${kotlnLogVersion}"
-        implementation "org.slf4j:slf4j-parent:${slf4jVersion}"
         implementation "com.google.code.gson:gson:${gsonVersion}"
         implementation "commons-dbcp:commons-dbcp:${dbcpVersion}"
         implementation "commons-codec:commons-codec:${commonCodecVersion}"
diff --git a/bus-qrcode/build.gradle b/bus-qrcode/build.gradle
new file mode 100644
index 0000000..8f2e1f1
--- /dev/null
+++ b/bus-qrcode/build.gradle
@@ -0,0 +1,54 @@
+plugins {
+    id 'java'
+    id 'maven-publish'
+    id "com.palantir.git-version"
+}
+
+group = rootProject.group
+
+def sdkVersion = gitVersion()
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+
+publishing {
+    publications {
+        mavenJava(MavenPublication) {
+            groupId = project.group
+            artifactId = 'bus-qrcode'
+            version = sdkVersion
+            from components.java
+        }
+    }
+    repositories {
+        maven {
+            // change URLs to point to your repos, e.g. http://my.org/repo
+            def releasesRepoUrl = "http://ykt-nx.supwisdom.com/repository/ecard-repo/"
+            def snapshotsRepoUrl = "http://ykt-nx.supwisdom.com/repository/ecard-repo/snapshot/"
+            url = version.endsWith('dirty') ? snapshotsRepoUrl : releasesRepoUrl
+            credentials(PasswordCredentials) {
+                username = nxUser
+                password = nxPassword
+            }
+        }
+    }
+}
+
+dependencies {
+    implementation "org.apache.commons:commons-lang3:3.7"
+    implementation 'com.eatthepath:java-otp:0.1.0'
+    implementation 'org.slf4j:slf4j-api:1.7.25'
+    implementation 'commons-codec:commons-codec:1.9'
+    runtime 'org.slf4j:slf4j-parent:1.7.25'
+    runtime 'org.slf4j:slf4j-simple:1.7.25'
+    testImplementation 'junit:junit:4.12'
+}
+
+jar {
+    enabled = true
+    baseName = "bus-qrcode"
+    manifest {
+        attributes('Bus-QRcode-Version': sdkVersion)
+    }
+}
+
+publish.dependsOn(jar)
diff --git a/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/BinUtil.java b/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/BinUtil.java
new file mode 100644
index 0000000..ebb4f05
--- /dev/null
+++ b/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/BinUtil.java
@@ -0,0 +1,67 @@
+package com.supwisdom.dlpay.busqrcode;
+
+import org.apache.commons.codec.binary.Base64;
+
+public class BinUtil {
+  public static byte[] decodeHex(String data) {
+    if (data == null || data.length() % 2 != 0) {
+      throw new RuntimeException("decodeHex data length must be divided by 2");
+    }
+    byte[] result = new byte[data.length() / 2];
+    for (int i = 0; i < data.length(); i += 2) {
+      result[i / 2] = (byte) Integer.parseInt(data.substring(i, i + 2), 16);
+    }
+    return result;
+  }
+
+  public static String encodeHex(byte[] data) {
+    if (data == null) {
+      throw new RuntimeException("encodeHex data must be not null");
+    }
+    StringBuilder sb = new StringBuilder();
+    for (byte datum : data) {
+      sb.append(String.format("%02x", datum & 0xFF));
+    }
+    return sb.toString();
+  }
+
+  public static byte[] encodeBCD(String data) {
+    if (data == null || data.length() % 2 != 0) {
+      throw new RuntimeException("encodeBCD data length must be divided by 2");
+    }
+    byte[] result = new byte[data.length() / 2];
+    int base = (int)'0';
+    for (int i = 0; i < data.length(); i += 2) {
+      int t = (int)data.charAt(i) - base;
+      int t1 = (int)data.charAt(i + 1) - base;
+      if (t < 0 || t > 9 || t1 < 0 || t1 > 9) {
+        throw new RuntimeException("encodeBCD char must be '0'~'9'");
+      }
+      result[i / 2] = (byte) (((t << 4) | t1) & 0xFF);
+    }
+    return result;
+  }
+
+  public static String decodeBCD(byte[] data) {
+    if (data == null) {
+      throw new RuntimeException("decodeBCD data must be not null");
+    }
+    int base = (int)'0';
+    StringBuilder sb = new StringBuilder();
+    for (byte datum : data) {
+      int t1 = ((datum >> 4) & 0x0F) + base;
+      int t2 = (datum & 0x0F) + base;
+      sb.append((char) t1)
+          .append((char) t2);
+    }
+    return sb.toString();
+  }
+
+  public static String encodeBase64(byte[] data) {
+    return Base64.encodeBase64String(data);
+  }
+
+  public static byte[] decodeBase64(String data) {
+    return Base64.decodeBase64(data);
+  }
+}
diff --git a/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/CryptoUtil.java b/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/CryptoUtil.java
new file mode 100644
index 0000000..839e3c0
--- /dev/null
+++ b/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/CryptoUtil.java
@@ -0,0 +1,117 @@
+package com.supwisdom.dlpay.busqrcode;
+
+import javax.crypto.*;
+import javax.crypto.spec.DESKeySpec;
+import javax.crypto.spec.DESedeKeySpec;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import java.util.Arrays;
+
+public class CryptoUtil {
+
+  private static byte[] doCipher(Cipher cipher, SecretKey key, int mode, byte[] data) {
+    try {
+      cipher.init(mode, key);
+      return cipher.doFinal(data);
+    } catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
+      e.printStackTrace();
+    }
+    return null;
+  }
+
+  private static byte[] doDesCipher(byte[] key, int cipherMode, byte[] data) {
+    try {
+      String mode;
+      KeySpec keySpec;
+      if (key.length == 8) {
+        mode = "DES";
+        keySpec = new DESKeySpec(key);
+      } else if (key.length == 16) {
+        mode = "DESede";
+        byte[] keyEDE = new byte[24];
+        System.arraycopy(key, 0, keyEDE, 0, 16);
+        System.arraycopy(key, 0, keyEDE, 16, 8);
+        keySpec = new DESedeKeySpec(keyEDE);
+      } else {
+        throw new RuntimeException("DES Key length must be 8 or 16 bytes");
+      }
+      Cipher keyCipher = Cipher.getInstance(mode + "/ECB/NoPadding");
+      SecretKey secret = SecretKeyFactory.getInstance(mode).generateSecret(keySpec);
+      return doCipher(keyCipher, secret, cipherMode, data);
+    } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeySpecException | InvalidKeyException e) {
+      e.printStackTrace();
+    }
+    return null;
+  }
+
+  public static byte[] desEncryptECB(byte[] key, byte[] plain) {
+    return doDesCipher(key, Cipher.ENCRYPT_MODE, plain);
+  }
+
+  public static byte[] desDecryptECB(byte[] key, byte[] cipher) {
+    return doDesCipher(key, Cipher.DECRYPT_MODE, cipher);
+  }
+
+  private static byte[] messageDigest(String alg, byte[] data) {
+    try {
+      MessageDigest md = MessageDigest.getInstance(alg);
+      md.update(data);
+      return md.digest();
+    } catch (NoSuchAlgorithmException e) {
+      e.printStackTrace();
+    }
+    return null;
+  }
+
+  public static byte[] md5(byte[] data) {
+    return messageDigest("MD5", data);
+  }
+
+  public static byte[] sha256(byte[] data) {
+    return messageDigest("SHA-256", data);
+  }
+
+  public static byte[] sha1(byte[] data) {
+    return messageDigest("SHA-1", data);
+  }
+
+  private static byte[] doAESCipher(byte[] key, byte[] iv, byte[] data, int mode) {
+    try {
+      Cipher keyCipher = Cipher.getInstance("AES/CFB/NoPadding");
+      IvParameterSpec ivSpec = new IvParameterSpec(iv);
+      SecretKeySpec secret = new SecretKeySpec(key, "AES");
+      keyCipher.init(mode, secret, ivSpec);
+      return keyCipher.doFinal(data);
+    } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
+        | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
+      e.printStackTrace();
+    }
+    return null;
+  }
+
+  public static byte[] aesEncryptCFB(byte[] key, byte[] plain) {
+    byte[] iv = new byte[16];
+    Arrays.fill(iv, (byte) 0);
+    return aesEncryptCFB(key, plain, iv);
+  }
+
+  public static byte[] aesEncryptCFB(byte[] key, byte[] plain, byte[] iv) {
+    return doAESCipher(key, iv, plain, Cipher.ENCRYPT_MODE);
+  }
+
+  public static byte[] aesDecryptCFB(byte[] key, byte[] cipher, byte[] iv) {
+    return doAESCipher(key, iv, cipher, Cipher.DECRYPT_MODE);
+  }
+
+  public static byte[] aesDecryptCFB(byte[] key, byte[] cipher) {
+    byte[] iv = new byte[16];
+    Arrays.fill(iv, (byte) 0);
+    return aesDecryptCFB(key, cipher, iv);
+  }
+}
diff --git a/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/PbocAlgorithem.java b/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/PbocAlgorithem.java
new file mode 100644
index 0000000..3c26f8b
--- /dev/null
+++ b/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/PbocAlgorithem.java
@@ -0,0 +1,93 @@
+package com.supwisdom.dlpay.busqrcode;
+
+import java.util.Arrays;
+
+import static com.supwisdom.dlpay.busqrcode.CryptoUtil.desDecryptECB;
+import static com.supwisdom.dlpay.busqrcode.CryptoUtil.desEncryptECB;
+
+
+public class PbocAlgorithem {
+  private static final byte[] PADDING = new byte[]{(byte) 0x80, 0, 0, 0, 0, 0, 0, 0};
+
+  private static byte[] doDeliveryKey(byte[] key, byte[] factor) {
+    if (key.length == 16) {
+      byte[] factor1 = new byte[8];
+      for (int i = 0; i < 8; i++) {
+        factor1[i] = (byte) ((~((int) factor[i] & 0xFF)) & 0xFF);
+      }
+      byte[] d1 = desEncryptECB(key, factor);
+      byte[] d2 = desEncryptECB(key, factor1);
+      byte[] result = new byte[16];
+      System.arraycopy(d1, 0, result, 0, 8);
+      System.arraycopy(d2, 0, result, 8, 8);
+      return result;
+    } else if (key.length == 8) {
+      return desEncryptECB(key, factor);
+    } else {
+      throw new RuntimeException("Des key length must be 8 or 16");
+    }
+  }
+
+  public static byte[] deliveryKey(byte[] rootKey, byte[] factory) {
+    if (factory.length % 8 != 0) {
+      throw new RuntimeException("delivery key factory length must be divived by 8");
+    }
+    if (factory.length / 8 > 3) {
+      throw new RuntimeException("delivery key factor length must be less or equal 24");
+    }
+
+    byte[] deliveryKey = rootKey;
+
+    for (int i = 0; i < factory.length; i += 8) {
+      deliveryKey = doDeliveryKey(deliveryKey, Arrays.copyOfRange(factory, i, i + 8));
+    }
+    return deliveryKey;
+  }
+
+  private static byte[] doDesMac(byte[] key, byte[] init, byte[] data) {
+    byte[] temp = Arrays.copyOf(init, 8);
+    for (int offset = 0; offset < data.length; offset += 8) {
+      int i;
+      for (i = 0; i < 8 && i + offset < data.length; ++i) {
+        temp[i] = (byte) (((int) temp[i] & 0xFF) ^ ((int) data[i + offset] & 0xFF));
+      }
+      for (int j = i; i < 8; ++i) {
+        temp[i] = (byte) (((int) temp[i] & 0xFF) ^ ((int) PADDING[i - j] & 0xFF));
+      }
+      temp = desEncryptECB(key, temp);
+    }
+    return temp;
+  }
+
+  public static byte[] desMac(byte[] key, byte[] init, byte[] data) {
+    byte[] buffer = Arrays.copyOfRange(doDesMac(key, init, data), 0, 4);
+    if (data.length % 8 == 0) {
+      buffer = doDesMac(key, buffer, PADDING);
+    }
+    return Arrays.copyOfRange(buffer, 0, 4);
+  }
+
+  public static byte[] desMac(byte[] key, byte[] data) {
+    byte[] init = new byte[8];
+    Arrays.fill(init, (byte) 0);
+    return desMac(key, init, data);
+  }
+
+  public static byte[] des3Mac(byte[] key, byte[] init, byte[] data) {
+    byte[] k1 = Arrays.copyOfRange(key, 0, 8);
+    byte[] k2 = Arrays.copyOfRange(key, 8, 16);
+    byte[] buffer = doDesMac(k1, init, data);
+    if (data.length % 8 == 0) {
+      buffer = doDesMac(key, buffer, PADDING);
+    }
+    buffer = desDecryptECB(k2, buffer);
+    buffer = desEncryptECB(k1, buffer);
+    return Arrays.copyOfRange(buffer, 0, 4);
+  }
+
+  public static byte[] des3Mac(byte[] key, byte[] data) {
+    byte[] init = new byte[8];
+    Arrays.fill(init, (byte) 0);
+    return des3Mac(key, init, data);
+  }
+}
diff --git a/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/QrCode.java b/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/QrCode.java
new file mode 100644
index 0000000..93f6b60
--- /dev/null
+++ b/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/QrCode.java
@@ -0,0 +1,279 @@
+package com.supwisdom.dlpay.busqrcode;
+
+import com.eatthepath.otp.TimeBasedOneTimePasswordGenerator;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+import static com.supwisdom.dlpay.busqrcode.BinUtil.*;
+import static com.supwisdom.dlpay.busqrcode.CryptoUtil.*;
+
+public class QrCode {
+  private static Logger logger = LoggerFactory.getLogger(QrCode.class);
+
+  public static final String DELIMITER = ":";
+  public static final String SCOPE_DELIMITER = ",";
+  public static final String DEFAULT_SCOPE = "1"; //默认仅支持 1-大理车载消费场景
+  public static final int TOTP_OFFSET = 10; //TODO: 前后偏移10步,30*10=300s=5m 约前后5分钟内totp有效
+
+//  static final ArrayList<String> DEFAULT_SCOPES = new ArrayList<>();
+//  static {
+//    DEFAULT_SCOPES.add("1"); //默认仅支持 1-大理车载消费场景
+//  }
+
+  private Builder qrBuilder;
+
+  private QrCode(Builder b) {
+    this.qrBuilder = b;
+  }
+
+  public static class Builder {
+    private String appid;
+    private String uid;
+    private String scope;
+    private String seed = "a8aae1a41ffab089428e8e21381cc34af3b614ac7b7cba50f58470b8e50c6072";
+    private boolean resetSeed = false;
+    private byte[] iv = decodeHex("55b6f5b3287c535f8274a99354676d0b");
+    private boolean resetIv = false;
+    private byte[] rootKey;
+    private byte[] sKey;
+    private byte[] des3Key;
+    private String prefix;
+    private boolean debug;
+
+    public QrCode create() {
+      return new QrCode(this);
+    }
+
+    public Builder appid(String appid) {
+      if (StringUtils.isBlank(appid) || !StringUtils.isAsciiPrintable(appid)) {
+        throw new RuntimeException("appid is invalid");
+      }
+      this.appid = appid;
+      this.des3Key = md5(appid.getBytes());
+      return this;
+    }
+
+    public Builder uid(String uid) {
+      if (StringUtils.isBlank(uid) || !StringUtils.isAsciiPrintable(uid)) {
+        throw new RuntimeException("uid is invalid");
+      }
+      this.uid = uid;
+      return this;
+    }
+
+    public Builder scope(String scope) {
+      if (StringUtils.isBlank(scope)) {
+        throw new RuntimeException("scope is empty");
+      }
+      this.scope = scope;
+      return this;
+    }
+
+    public Builder rootkey(String key) {
+      this.rootKey = decodeHex(key);
+      return this;
+    }
+
+    public Builder rootkey(String appid, String skey){
+      if (StringUtils.isBlank(appid) || !StringUtils.isAsciiPrintable(appid)) {
+        throw new RuntimeException("appid is invalid");
+      }
+      if (StringUtils.isBlank(skey)) {
+        throw new RuntimeException("sKey is empty");
+      }
+      this.appid = appid;
+      this.sKey = decodeHex(skey);
+      this.des3Key = md5(appid.getBytes());
+      this.rootKey = desEncryptECB(this.des3Key, this.sKey);
+      return this;
+    }
+
+    public Builder seed(String s) {
+      if (StringUtils.isBlank(s)) {
+        throw new RuntimeException("seed is empty");
+      }
+      this.seed = s;
+      this.resetSeed = true;
+      return this;
+    }
+
+    public Builder ivHex(String v) {
+      this.iv = decodeHex(v);
+      this.resetIv = true;
+      return this;
+    }
+
+    public Builder prefix(String p) {
+      if (p == null) {
+        throw new RuntimeException("prefix must not be null");
+      }
+      this.prefix = p;
+      return this;
+    }
+
+    public Builder debug(boolean on) {
+      this.debug = on;
+      return this;
+    }
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  private static String getRandomString(int length) {
+    String base = "abcdefghijklmnopqrstuvwxyz234567";
+    Random random = new Random(System.currentTimeMillis());
+    StringBuilder sb = new StringBuilder();
+    for (int i = 0; i < length; i++) {
+      int number = random.nextInt(base.length());
+      sb.append(base.charAt(number));
+    }
+    return sb.toString();
+  }
+
+  private String genTOTPWithSeed(String seed, int pl) {
+    final TimeBasedOneTimePasswordGenerator alg = totpGenerator(pl);
+    final SecretKey secretKey = new SecretKeySpec(decodeHex(seed),
+        TimeBasedOneTimePasswordGenerator.TOTP_ALGORITHM_HMAC_SHA256);
+    try {
+      Date now = Calendar.getInstance().getTime();
+      return StringUtils.leftPad(String.format("%d", alg.generateOneTimePassword(secretKey, now)),
+          pl, '0');
+    } catch (InvalidKeyException e) {
+      throw new RuntimeException("TOTP Error", e);
+    }
+  }
+
+  private TimeBasedOneTimePasswordGenerator totpGenerator(int passwordLength) {
+    try {
+      return new TimeBasedOneTimePasswordGenerator(30, TimeUnit.SECONDS, passwordLength,
+          TimeBasedOneTimePasswordGenerator.TOTP_ALGORITHM_HMAC_SHA256);
+    } catch (NoSuchAlgorithmException e) {
+      throw new RuntimeException("TOTP Error", e);
+    }
+  }
+
+  private boolean checkScope(List<String> scopes) {
+    return scopes.contains(DEFAULT_SCOPE);
+  }
+
+  private boolean verifyTotp(String totp, String secret, int offset) {
+    String second = "30";
+    String seed = secret;
+    long T0 = 0;
+    String[] keys = new String[offset * 2 + 1];
+    long time = Calendar.getInstance().getTime().getTime() / 1000;
+    for (int i = 0; i < keys.length; i++) {
+      String steps = "0";
+      try {
+        long T = (time - T0) / Long.parseLong(second);
+        steps = Long.toHexString(T + (i - offset)).toUpperCase();
+        while (steps.length() < 16) {
+          steps = "0" + steps;
+        }
+        String key = TOTP.generateTOTP(secret, steps, "8", TimeBasedOneTimePasswordGenerator.TOTP_ALGORITHM_HMAC_SHA256);
+        keys[i] = key;
+      } catch (final Exception e) {
+        System.out.println("Error : " + e);
+      }
+    }
+    for (String key : keys) {
+      if (key.equals(totp)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public String qrcode() {
+    if (qrBuilder.debug) {
+      logger.info("appid=" + qrBuilder.appid);
+      logger.info("uid=" + qrBuilder.uid);
+      logger.info("scope=" + qrBuilder.scope);
+      if (qrBuilder.resetSeed) logger.info("seed=" + qrBuilder.seed);
+      if (qrBuilder.resetIv) logger.info("iv=" + encodeHex(qrBuilder.iv));
+      if (null != qrBuilder.sKey) logger.info("sKey=" + encodeHex(qrBuilder.sKey).toUpperCase());
+      if (null != qrBuilder.prefix) logger.info("prefix=" + qrBuilder.prefix);
+
+      logger.info("rootkey=" + encodeHex(qrBuilder.rootKey).toUpperCase());
+      logger.info("=======================================================");
+    }
+
+    final String totp = genTOTPWithSeed(qrBuilder.seed, 8);
+    final String encDataPlain = new StringJoin(DELIMITER)
+        .add(qrBuilder.uid)
+        .add(qrBuilder.scope)
+        .add(totp).toString();
+
+    final byte[] encData = aesEncryptCFB(qrBuilder.rootKey, encDataPlain.getBytes(), qrBuilder.iv);
+    final String code = encodeBase64(encData);
+    if (qrBuilder.debug) {
+      logger.info("encdata plain= " + encDataPlain);
+    }
+
+    if (qrBuilder.prefix != null) {
+      return qrBuilder.prefix + code;
+    }
+    return code;
+  }
+
+  public String decodeUid(String qrcode) {
+    if (qrBuilder.debug) {
+      logger.info("appid=" + qrBuilder.appid);
+      if (qrBuilder.resetSeed) logger.info("seed=" + qrBuilder.seed);
+      if (qrBuilder.resetIv) logger.info("iv=" + encodeHex(qrBuilder.iv));
+      if (null != qrBuilder.sKey) logger.info("sKey=" + encodeHex(qrBuilder.sKey).toUpperCase());
+      if (null != qrBuilder.prefix) logger.info("prefix=" + qrBuilder.prefix);
+      logger.info("=======================================================");
+    }
+    if (null == qrBuilder.rootKey) {
+      throw new RuntimeException("qr decode root key must not be null");
+    }
+    if (qrBuilder.prefix != null) {
+      qrcode = qrcode.substring(qrBuilder.prefix.length());
+    }
+    final String encDataPlain = new String(aesDecryptCFB(qrBuilder.rootKey, decodeBase64(qrcode), qrBuilder.iv));
+    if (qrBuilder.debug) {
+      logger.info("Decode data : <" + encDataPlain + ">");
+    }
+    String[] fields = encDataPlain.split(DELIMITER);
+    if (fields.length < 3) {
+      throw new RuntimeException("qrcode plain text format error!");
+    }
+    String uid = fields[0];
+    String[] scopes = fields[1].split(SCOPE_DELIMITER);
+    String totp = fields[2];
+
+    if (!verifyTotp(totp, qrBuilder.seed, TOTP_OFFSET)) {
+      throw new RuntimeException("qrcode is invalid!"); //TOTP校验
+    }
+
+    if (!checkScope(Arrays.asList(scopes))) {
+      throw new RuntimeException("qrcode is not supported!"); //应用场景校验
+    }
+
+    return uid;
+  }
+
+  public String tac(String uid, Integer amount, String transdate, String transtime) {
+    if (null == qrBuilder.appid) {
+      throw new RuntimeException("appid is null");
+    }
+    if (null == qrBuilder.sKey) {
+      throw new RuntimeException("sKey is null");
+    }
+    final String encdata = uid + qrBuilder.appid + amount + transdate + transtime + "{" + encodeHex(qrBuilder.sKey).toUpperCase() + "}";
+    return encodeHex(sha256(encdata.getBytes())).toUpperCase();
+  }
+
+}
diff --git a/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/StringJoin.java b/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/StringJoin.java
new file mode 100644
index 0000000..ae3b0c7
--- /dev/null
+++ b/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/StringJoin.java
@@ -0,0 +1,30 @@
+package com.supwisdom.dlpay.busqrcode;
+
+public class StringJoin {
+  private String delimiter;
+  private StringBuilder builder;
+
+  public StringJoin(String delimiter) {
+    if(delimiter == null) {
+      throw new RuntimeException("Delimiter must not be null");
+    }
+    this.delimiter = delimiter;
+    builder = new StringBuilder();
+  }
+
+  public StringJoin add(String field) {
+    if(field == null) {
+      throw new RuntimeException("added value must not be null");
+    }
+    if(builder.length() > 0) {
+      builder.append(this.delimiter);
+    }
+    builder.append(field);
+    return this;
+  }
+
+  @Override
+  public String toString() {
+    return builder.toString();
+  }
+}
diff --git a/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/TOTP.java b/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/TOTP.java
new file mode 100644
index 0000000..450ca25
--- /dev/null
+++ b/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/TOTP.java
@@ -0,0 +1,264 @@
+package com.supwisdom.dlpay.busqrcode;
+
+/**
+ Copyright (c) 2011 IETF Trust and the persons identified as
+ authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, is permitted pursuant to, and subject to the license
+ terms contained in, the Simplified BSD License set forth in Section
+ 4.c of the IETF Trust's Legal Provisions Relating to IETF Documents
+ (http://trustee.ietf.org/license-info).
+ */
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.lang.reflect.UndeclaredThrowableException;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+
+/**
+ * This is an example implementation of the OATH TOTP algorithm. Visit
+ * www.openauthentication.org for more information.
+ * 
+ * @author Johan Rydell, PortWise, Inc.
+ */
+public class TOTP {
+	private static final int[] DIGITS_POWER
+	// 0 1 2 3 4 5 6 7 8
+	= { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000 };
+
+	private TOTP() {
+	}
+
+	/**
+	 * This method uses the JCE to provide the crypto algorithm. HMAC computes a
+	 * Hashed Message Authentication Code with the crypto hash algorithm as a
+	 * parameter.
+	 * 
+	 * @param crypto
+	 *            : the crypto algorithm (HmacSHA1, HmacSHA256, HmacSHA512)
+	 * @param keyBytes
+	 *            : the bytes to use for the HMAC key
+	 * @param text
+	 *            : the message or text to be authenticated
+	 */
+	private static byte[] hmac_sha(String crypto, byte[] keyBytes, byte[] text) {
+		try {
+			Mac hmac;
+			hmac = Mac.getInstance(crypto);
+
+			SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW");
+			hmac.init(macKey);
+
+			return hmac.doFinal(text);
+		} catch (GeneralSecurityException gse) {
+			throw new UndeclaredThrowableException(gse);
+		}
+	}
+
+	/**
+	 * This method converts a HEX string to Byte[]
+	 * 
+	 * @param hex
+	 *            : the HEX string
+	 * 
+	 * @return: a byte array
+	 */
+	private static byte[] hexStr2Bytes(String hex) {
+		// Adding one byte to get the right conversion
+		// Values starting with "0" can be converted
+		byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();
+
+		// Copy all the REAL bytes, not the "first"
+		byte[] ret = new byte[bArray.length - 1];
+
+		for (int i = 0; i < ret.length; i++)
+			ret[i] = bArray[i + 1];
+
+		return ret;
+	}
+
+	/**
+	 * This method generates a TOTP value for the given set of parameters.
+	 * 
+	 * @param key
+	 *            : the shared secret, HEX encoded
+	 * @param time
+	 *            : a value that reflects a time
+	 * @param returnDigits
+	 *            : number of digits to return
+	 * 
+	 * @return: a numeric String in base 10 that includes
+	 *          {@link truncationDigits} digits
+	 */
+	public static String generateTOTP(String key, String time,
+			String returnDigits) {
+		return generateTOTP(key, time, returnDigits, "HmacSHA1");
+	}
+
+	/**
+	 * This method generates a TOTP value for the given set of parameters.
+	 * 
+	 * @param key
+	 *            : the shared secret, HEX encoded
+	 * @param time
+	 *            : a value that reflects a time
+	 * @param returnDigits
+	 *            : number of digits to return
+	 * 
+	 * @return: a numeric String in base 10 that includes
+	 *          {@link truncationDigits} digits
+	 */
+	public static String generateTOTP256(String key, String time,
+			String returnDigits) {
+		return generateTOTP(key, time, returnDigits, "HmacSHA256");
+	}
+
+	/**
+	 * This method generates a TOTP value for the given set of parameters.
+	 * 
+	 * @param key
+	 *            : the shared secret, HEX encoded
+	 * @param time
+	 *            : a value that reflects a time
+	 * @param returnDigits
+	 *            : number of digits to return
+	 * 
+	 * @return: a numeric String in base 10 that includes
+	 *          {@link truncationDigits} digits
+	 */
+	public static String generateTOTP512(String key, String time,
+			String returnDigits) {
+		return generateTOTP(key, time, returnDigits, "HmacSHA512");
+	}
+
+	/**
+	 * This method generates a TOTP value for the given set of parameters.
+	 * 
+	 * @param key
+	 *            : the shared secret, HEX encoded
+	 * @param time
+	 *            : a value that reflects a time
+	 * @param returnDigits
+	 *            : number of digits to return
+	 * @param crypto
+	 *            : the crypto function to use
+	 * 
+	 * @return: a numeric String in base 10 that includes
+	 *          {@link truncationDigits} digits
+	 */
+	public static String generateTOTP(String key, String time,
+			String returnDigits, String crypto) {
+		int codeDigits = Integer.decode(returnDigits).intValue();
+		String result = null;
+
+		// Using the counter
+		// First 8 bytes are for the movingFactor
+		// Compliant with base RFC 4226 (HOTP)
+		while (time.length() < 16)
+			time = "0" + time;
+
+		// Get the HEX in a Byte[]
+		byte[] msg = hexStr2Bytes(time);
+		byte[] k = hexStr2Bytes(key);
+		byte[] hash = hmac_sha(crypto, k, msg);
+
+		// put selected bytes into result int
+		int offset = hash[hash.length - 1] & 0xf;
+
+		int binary = ((hash[offset] & 0x7f) << 24)
+				| ((hash[offset + 1] & 0xff) << 16)
+				| ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff);
+
+		int otp = binary % DIGITS_POWER[codeDigits];
+
+		result = Integer.toString(otp);
+
+		while (result.length() < codeDigits) {
+			result = "0" + result;
+		}
+
+		return result;
+	}
+
+	public static void main(String[] args) {
+		long X = 30;
+		long T0 = 0;
+		String steps = "0";
+		long time = System.currentTimeMillis() / 1000;
+		System.out.println(time);
+		long T = (time - T0) / X;
+		steps = Long.toHexString(T).toUpperCase();
+		while (steps.length() < 16) {
+			steps = "0" + steps;
+		}
+		String seed = "9e8bb3d2df73741c041aef39e37ee015fc98233720a1350fcd67d5c3027896ac";
+		String totp = TOTP.generateTOTP(seed, steps, "8", "HmacSHA256");
+		System.out.println(totp);
+
+		/*
+
+
+		SecureRandom a = new SecureRandom();
+		byte[] b = a.generateSeed(8);
+		// Seed for HMAC-SHA1 - 20 bytes
+		String seed = "3132333435363738393031323334353637383930";
+
+		// Seed for HMAC-SHA256 - 32 bytes
+		//String seed32 = "3132333435363738393031323334353637383930313233343536373839303132";
+		String seed32 = "e5b72370f61687c8075b8383266f4fbcbb3b55da178eed76329666600c134093";
+
+		// Seed for HMAC-SHA512 - 64 bytes
+		String seed64 = "3132333435363738393031323334353637383930"
+				+ "3132333435363738393031323334353637383930"
+				+ "3132333435363738393031323334353637383930" + "31323334";
+		long T0 = 0;
+		long X = 30;
+		long[] testTime = { 59L, 1111111109L, 1111111111L, 1234567890L,
+				2000000000L, 20000000000L };
+
+		String steps = "0";
+		DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+		df.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+		try {
+			System.out.println("+---------------+-----------------------+"
+					+ "------------------+--------+--------+");
+			System.out.println("|  Time(sec)    |   Time (UTC format)   "
+					+ "| Value of T(Hex)  |  TOTP  | Mode   |");
+			System.out.println("+---------------+-----------------------+"
+					+ "------------------+--------+--------+");
+
+			for (int i = 0; i < testTime.length; i++) {
+				long T = (testTime[i] - T0) / X;
+				steps = Long.toHexString(T).toUpperCase();
+
+				while (steps.length() < 16)
+					steps = "0" + steps;
+
+				String fmtTime = String.format("%1$-11s", testTime[i]);
+				String utcTime = df.format(new Date(testTime[i] * 1000));
+				System.out.print("|  " + fmtTime + "  |  " + utcTime + "  | "
+						+ steps + " |");
+				System.out.println(generateTOTP(seed, steps, "8", "HmacSHA1")
+						+ "| SHA1   |");
+				System.out.print("|  " + fmtTime + "  |  " + utcTime + "  | "
+						+ steps + " |");
+				System.out.println(generateTOTP(seed32, steps, "8",
+						"HmacSHA256") + "| SHA256 |");
+				System.out.print("|  " + fmtTime + "  |  " + utcTime + "  | "
+						+ steps + " |");
+				System.out.println(generateTOTP(seed64, steps, "8",
+						"HmacSHA512") + "| SHA512 |");
+
+				System.out.println("+---------------+-----------------------+"
+						+ "------------------+--------+--------+");
+			}
+		} catch (final Exception e) {
+			System.out.println("Error : " + e);
+		}
+
+		*/
+	}
+}
\ No newline at end of file
diff --git a/bus-qrcode/src/test/java/com/supwisdom/dlpay/busqrcode/QrcodeTest.java b/bus-qrcode/src/test/java/com/supwisdom/dlpay/busqrcode/QrcodeTest.java
new file mode 100644
index 0000000..9e8bae1
--- /dev/null
+++ b/bus-qrcode/src/test/java/com/supwisdom/dlpay/busqrcode/QrcodeTest.java
@@ -0,0 +1,61 @@
+package com.supwisdom.dlpay.busqrcode;
+
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static com.supwisdom.dlpay.busqrcode.BinUtil.*;
+import static com.supwisdom.dlpay.busqrcode.CryptoUtil.*;
+import static org.junit.Assert.*;
+
+public class QrcodeTest {
+    private Logger logger = LoggerFactory.getLogger(QrcodeTest.class);
+
+    @Test
+    public void testBusQrcode() {
+        String prefix = "dlsmk_";
+        String appid = "10001";
+        String uid = "ff8080816cf13e79016cf15d63f40011";
+        String scope = "1,2";
+        String rkey = "0A5DE6CE985D43989B7EBE64AD8EB9C3";
+        Integer amount = 100;
+        String transdate = "20200220";
+        String transtime = "090921";
+
+        String skey = encodeHex(desDecryptECB(md5(appid.getBytes()), decodeHex(rkey))).toUpperCase();
+        logger.info("skey=[" + skey + "]");
+        String rootKey = encodeHex(desEncryptECB(md5(appid.getBytes()), decodeHex(skey))).toUpperCase();
+        assertEquals("rootkey与skey互为3des加密关系错误!", rkey, rootKey);
+
+        QrCode encQr = QrCode.builder()
+                .appid(appid)
+                .rootkey(rkey)
+                .uid(uid)
+                .scope(scope)
+                .prefix(prefix)
+                .debug(true)
+                .create();
+        String qrcode = encQr.qrcode();
+        logger.info("qrcode=[" + qrcode + "]");
+
+        QrCode decQr = QrCode.builder()
+                .rootkey(appid, skey)
+                .prefix(prefix)
+                .debug(true)
+                .create();
+
+//        try {
+//            Thread.sleep(6*60*1000); //totp测试
+//        } catch (InterruptedException e) {
+//            e.printStackTrace();
+//        }
+
+        String decUid = decQr.decodeUid(qrcode);
+        assertEquals("解码uid错误!", uid, decUid);
+
+        String tac = decQr.tac(uid, amount, transdate, transtime);
+        String tacData = uid + appid + amount + transdate + transtime + "{" + skey + "}";
+        String testTac = encodeHex(sha256(tacData.getBytes())).toUpperCase();
+        assertEquals("tac计算错误", tac, testTac);
+    }
+}
\ No newline at end of file
diff --git a/payapi/build.gradle b/payapi/build.gradle
index 184dbb5..2938317 100644
--- a/payapi/build.gradle
+++ b/payapi/build.gradle
@@ -82,6 +82,15 @@
     runtime("org.springframework.boot:spring-boot-devtools")
 
     implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5"
+    implementation 'org.bitbucket.b_c:jose4j:0.6.5'
+    implementation 'com.github.penggle:kaptcha:2.3.2'
+    implementation group: 'com.sun.jersey', name: 'jersey-client', version: '1.19'
+    implementation group: 'javax.servlet', name: 'jstl', version: '1.2'
+    implementation group: 'taglibs', name: 'standard', version: '1.1.2'
+    implementation files('libs/masmgc.sdk.sms-0.0.1-SNAPSHOT.jar')
+    implementation 'commons-beanutils:commons-beanutils:1.9.3'
+    implementation 'com.eatthepath:java-otp:0.1.0'
+    implementation project(':payapi-common')
 
     implementation "org.apache.commons:commons-lang3:${lang3Version}"
     implementation "net.javacrumbs.shedlock:shedlock-spring:${shedlockVersion}"
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/api/bean/KafkaXgMessage.java b/payapi/src/main/java/com/supwisdom/dlpay/api/bean/KafkaXgMessage.java
index 9bc836a..1378bcf 100644
--- a/payapi/src/main/java/com/supwisdom/dlpay/api/bean/KafkaXgMessage.java
+++ b/payapi/src/main/java/com/supwisdom/dlpay/api/bean/KafkaXgMessage.java
@@ -6,18 +6,20 @@
   private String content;//提醒消息内容
   private String gids;//gid发送列表,逗号隔开
   private Boolean alltarget;//是否平台全发送
-  private String platform;//android,ios
+  private String platform;//android,ios,all
   private int type;//TYPE_NOTIFICATION = 1,TYPE_MESSAGE = 2
 
-  private String url;//跳转url
   private String expiretime;//过期时间
   private Boolean callback;//是否回调返回
-  private String msg_type;//提醒类型 字典 epay_pos_scan_code_pay、
-  private String refno;//流水参考号
   private int retries;//最多重试次数
-  private String amount;//消费金额,元,小数点后两位
   private String custom; //扩展参数,原样传递
 
+  //TODO: 下面的参数在epaymessager中已不用,要赋值请放入custom的json串中
+//  private String url;//跳转url
+//  private String msg_type;//提醒类型 字典 epay_pos_scan_code_pay、
+//  private String refno;//流水参考号
+//  private String amount;//消费金额,元,小数点后两位
+
 
   public String getTitle() {
     return title;
@@ -67,14 +69,6 @@
     this.type = type;
   }
 
-  public String getUrl() {
-    return url;
-  }
-
-  public void setUrl(String url) {
-    this.url = url;
-  }
-
   public String getExpiretime() {
     return expiretime;
   }
@@ -91,22 +85,6 @@
     this.callback = callback;
   }
 
-  public String getMsg_type() {
-    return msg_type;
-  }
-
-  public void setMsg_type(String msg_type) {
-    this.msg_type = msg_type;
-  }
-
-  public String getRefno() {
-    return refno;
-  }
-
-  public void setRefno(String refno) {
-    this.refno = refno;
-  }
-
   public int getRetries() {
     return retries;
   }
@@ -115,14 +93,6 @@
     this.retries = retries;
   }
 
-  public String getAmount() {
-    return amount;
-  }
-
-  public void setAmount(String amount) {
-    this.amount = amount;
-  }
-
   public String getCustom() {
     return custom;
   }
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/framework/dao/NoticeDao.java b/payapi/src/main/java/com/supwisdom/dlpay/framework/dao/NoticeDao.java
new file mode 100644
index 0000000..800c10e
--- /dev/null
+++ b/payapi/src/main/java/com/supwisdom/dlpay/framework/dao/NoticeDao.java
@@ -0,0 +1,19 @@
+package com.supwisdom.dlpay.framework.dao;
+
+import com.supwisdom.dlpay.framework.domain.TNotice;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Lock;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+
+import javax.persistence.LockModeType;
+
+@Repository
+public interface NoticeDao extends JpaRepository<TNotice, String>, JpaSpecificationExecutor<TNotice> {
+  TNotice getById(String id);
+
+  @Lock(LockModeType.PESSIMISTIC_WRITE)
+  @Query("from TNotice t where t.id=?1 ")
+  TNotice getByIdForUpdate(String id);
+}
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/framework/dao/NoticeMsgDao.java b/payapi/src/main/java/com/supwisdom/dlpay/framework/dao/NoticeMsgDao.java
new file mode 100644
index 0000000..35a6dd4
--- /dev/null
+++ b/payapi/src/main/java/com/supwisdom/dlpay/framework/dao/NoticeMsgDao.java
@@ -0,0 +1,30 @@
+package com.supwisdom.dlpay.framework.dao;
+
+import com.supwisdom.dlpay.framework.domain.TNoticeMsg;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Lock;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.jpa.repository.QueryHints;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.persistence.LockModeType;
+import javax.persistence.QueryHint;
+import java.util.List;
+
+@Repository
+public interface NoticeMsgDao extends JpaRepository<TNoticeMsg, String> {
+  TNoticeMsg getByMsgid(String msgid);
+
+  @Transactional
+  @Lock(LockModeType.PESSIMISTIC_WRITE)
+  @QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value = "0")})
+  @Query("from TNoticeMsg where msgid=?1")
+  TNoticeMsg getByMsgidForUpdateNowait(String msgid);
+
+  @Query("from TNoticeMsg where noticeId=?1 order by createtime desc")
+  List<TNoticeMsg> findAllByNoticeId(String noticeid);
+
+  @Query("from TNoticeMsg t where t.status='normal' and t.pushmode='delay' and t.pushSettime<=?1 and t.sendkafka=false order by t.createtime")
+  List<TNoticeMsg> findAllDelayNoticeByDatetime(String datetime);
+}
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/framework/dao/RoleFunctionDao.java b/payapi/src/main/java/com/supwisdom/dlpay/framework/dao/RoleFunctionDao.java
index e125f7a..945f510 100644
--- a/payapi/src/main/java/com/supwisdom/dlpay/framework/dao/RoleFunctionDao.java
+++ b/payapi/src/main/java/com/supwisdom/dlpay/framework/dao/RoleFunctionDao.java
@@ -19,11 +19,11 @@
     List<TRoleFunction> findByRoleId(String roleId);
 
     @Query(value = "select tt.id,tt.pid,tt.name,tt.checked,tt.open from  " +
-            " ( select f.id||'' as id ,f.parentid||'' as pid,f.name,case when rf.id is null then 0 else 1 end as checked,case when f.parentid=-1 then 1 else 0 end as open from tb_function f " +
+            " ( select f.id||'' as id ,f.parentid||'' as pid,f.name,case when rf.id is null then 0 else 1 end as checked,case when f.parentid=-1 then 1 else 0 end as open,f.ordernum from tb_function f " +
             " left join tb_role_function rf on rf.functionid = f.id and rf.roleid=?1  " +
             " union all " +
-            " select r.id||'_res' as id,r.function_id||'' as pid,r.name,case when p.id is null then 0 else 1 end as checked,0 as open from tb_resource  r " +
-            " left join tb_permission p on p.resid = r.id and p.roleid=?1 ) tt order by tt.id " , nativeQuery = true)
+            " select r.id||'_res' as id,r.function_id||'' as pid,r.name,case when p.id is null then 0 else 1 end as checked,0 as open,999999 as ordernum from tb_resource  r " +
+            " left join tb_permission p on p.resid = r.id and p.roleid=?1 ) tt order by tt.pid,tt.ordernum" , nativeQuery = true)
     List<NodeData> findByRoleIdNative(String roleId);
 
     void deleteByRoleId(String roleId);
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/framework/domain/TNotice.java b/payapi/src/main/java/com/supwisdom/dlpay/framework/domain/TNotice.java
new file mode 100644
index 0000000..08e645d
--- /dev/null
+++ b/payapi/src/main/java/com/supwisdom/dlpay/framework/domain/TNotice.java
@@ -0,0 +1,149 @@
+package com.supwisdom.dlpay.framework.domain;
+
+import org.hibernate.annotations.GenericGenerator;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+
+/**
+ * 通知公告表(全体通知)
+ * */
+@Entity
+@Table(name = "TB_NOTICE")
+public class TNotice {
+  @Id
+  @GenericGenerator(name = "nidGenerator", strategy = "uuid")
+  @GeneratedValue(generator = "nidGenerator")
+  @Column(name = "ID", nullable = false, length = 32)
+  private String id;
+
+  @Column(name = "TITLE", nullable = false, length = 200)
+  private String title; //标题
+
+  @Column(name = "CONTENT", nullable = false, length = 2000)
+  private String content; //内容
+
+  @Column(name = "PLATFORM", nullable = false, length = 10)
+  private String platform; //all-全部;ios-苹果;android-安卓
+
+  @Column(name = "CREATEDATE", nullable = false, length = 8)
+  private String createdate; //创建日期yyyyMMdd
+
+  @Column(name = "CREATETIME", length = 6)
+  private String createtime; //创建日期hh24miss
+
+  @Column(name = "OPERID", length = 32)
+  private String operid; //创建者的operid
+
+  @Column(name = "CREATOR", length = 200)
+  private String creator; //创建者名称
+
+  @Column(name = "SENDCNT", nullable = false, precision = 9)
+  private Integer sendcnt = 0;
+
+  @Column(name = "LINKURL", length = 1000)
+  private String linkurl;
+
+  @Column(name = "LASTSAVED", length = 14)
+  private String lastsaved; //最后更新时间
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantId;
+
+  public String getId() {
+    return id;
+  }
+
+  public void setId(String id) {
+    this.id = id;
+  }
+
+  public String getTitle() {
+    return title;
+  }
+
+  public void setTitle(String title) {
+    this.title = title;
+  }
+
+  public String getContent() {
+    return content;
+  }
+
+  public void setContent(String content) {
+    this.content = content;
+  }
+
+  public String getPlatform() {
+    return platform;
+  }
+
+  public void setPlatform(String platform) {
+    this.platform = platform;
+  }
+
+  public String getCreatedate() {
+    return createdate;
+  }
+
+  public void setCreatedate(String createdate) {
+    this.createdate = createdate;
+  }
+
+  public String getCreatetime() {
+    return createtime;
+  }
+
+  public void setCreatetime(String createtime) {
+    this.createtime = createtime;
+  }
+
+  public String getOperid() {
+    return operid;
+  }
+
+  public void setOperid(String operid) {
+    this.operid = operid;
+  }
+
+  public String getCreator() {
+    return creator;
+  }
+
+  public void setCreator(String creator) {
+    this.creator = creator;
+  }
+
+  public Integer getSendcnt() {
+    return sendcnt;
+  }
+
+  public void setSendcnt(Integer sendcnt) {
+    this.sendcnt = sendcnt;
+  }
+
+  public String getLinkurl() {
+    return linkurl;
+  }
+
+  public void setLinkurl(String linkurl) {
+    this.linkurl = linkurl;
+  }
+
+  public String getLastsaved() {
+    return lastsaved;
+  }
+
+  public void setLastsaved(String lastsaved) {
+    this.lastsaved = lastsaved;
+  }
+
+  public String getTenantId() {
+    return tenantId;
+  }
+
+  public void setTenantId(String tenantId) {
+    this.tenantId = tenantId;
+  }
+}
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/framework/domain/TNoticeMsg.java b/payapi/src/main/java/com/supwisdom/dlpay/framework/domain/TNoticeMsg.java
new file mode 100644
index 0000000..2a417e0
--- /dev/null
+++ b/payapi/src/main/java/com/supwisdom/dlpay/framework/domain/TNoticeMsg.java
@@ -0,0 +1,160 @@
+package com.supwisdom.dlpay.framework.domain;
+
+import org.hibernate.annotations.GenericGenerator;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+
+/**
+ * 通知公告发布明细表(全体通知)
+ * */
+@Entity
+@Table(name = "TB_NOTICE_MSG")
+public class TNoticeMsg {
+  @Id
+  @GenericGenerator(name = "midGenerator", strategy = "uuid")
+  @GeneratedValue(generator = "midGenerator")
+  @Column(name = "msgid", nullable = false, length = 32)
+  private String msgid;
+
+  @Column(name = "notice_id", nullable = false, length = 32)
+  private String noticeId;
+
+  @Column(name = "alltarget", nullable = false, length = 10)
+  private Boolean alltarget = true; //是否全体通知公告
+
+  @Column(name = "pushmode", nullable = false, length = 10)
+  private String pushmode = "atonce"; //atonce-立即发布,delay-延迟定时发布
+
+  @Column(name = "push_settime", length = 14)
+  private String pushSettime; //定时发布的设定时间
+
+  @Column(name = "publisher", length = 200)
+  private String publisher; //发布者
+
+  @Column(name = "operid", length = 32)
+  private String operid; //发布者operid
+
+  @Column(name = "status", nullable = false, length = 10)
+  private String status;
+
+  @Column(name = "sendkafka", nullable = false, length = 10)
+  private Boolean sendkafka=false;
+
+  @Column(name = "pushtime", length = 14)
+  private String pushtime;
+
+  @Column(name = "createtime", length = 14)
+  private String createtime;
+
+  @Column(name = "pushresult", length = 600)
+  private String pushresult;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantId;
+
+  public String getMsgid() {
+    return msgid;
+  }
+
+  public void setMsgid(String msgid) {
+    this.msgid = msgid;
+  }
+
+  public String getNoticeId() {
+    return noticeId;
+  }
+
+  public void setNoticeId(String noticeId) {
+    this.noticeId = noticeId;
+  }
+
+  public Boolean getAlltarget() {
+    return alltarget;
+  }
+
+  public void setAlltarget(Boolean alltarget) {
+    this.alltarget = alltarget;
+  }
+
+  public String getPushmode() {
+    return pushmode;
+  }
+
+  public void setPushmode(String pushmode) {
+    this.pushmode = pushmode;
+  }
+
+  public String getPushSettime() {
+    return pushSettime;
+  }
+
+  public void setPushSettime(String pushSettime) {
+    this.pushSettime = pushSettime;
+  }
+
+  public String getPublisher() {
+    return publisher;
+  }
+
+  public void setPublisher(String publisher) {
+    this.publisher = publisher;
+  }
+
+  public String getOperid() {
+    return operid;
+  }
+
+  public void setOperid(String operid) {
+    this.operid = operid;
+  }
+
+  public String getStatus() {
+    return status;
+  }
+
+  public void setStatus(String status) {
+    this.status = status;
+  }
+
+  public Boolean getSendkafka() {
+    return sendkafka;
+  }
+
+  public void setSendkafka(Boolean sendkafka) {
+    this.sendkafka = sendkafka;
+  }
+
+  public String getPushtime() {
+    return pushtime;
+  }
+
+  public void setPushtime(String pushtime) {
+    this.pushtime = pushtime;
+  }
+
+  public String getCreatetime() {
+    return createtime;
+  }
+
+  public void setCreatetime(String createtime) {
+    this.createtime = createtime;
+  }
+
+  public String getPushresult() {
+    return pushresult;
+  }
+
+  public void setPushresult(String pushresult) {
+    this.pushresult = pushresult;
+  }
+
+  public String getTenantId() {
+    return tenantId;
+  }
+
+  public void setTenantId(String tenantId) {
+    this.tenantId = tenantId;
+  }
+}
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/framework/tenant/HibernateConfig.java b/payapi/src/main/java/com/supwisdom/dlpay/framework/tenant/HibernateConfig.java
deleted file mode 100644
index bff7098..0000000
--- a/payapi/src/main/java/com/supwisdom/dlpay/framework/tenant/HibernateConfig.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package com.supwisdom.dlpay.framework.tenant;
-
-import java.util.HashMap;
-import java.util.Map;
-import javax.sql.DataSource;
-
-import lombok.extern.slf4j.Slf4j;
-import org.hibernate.MultiTenancyStrategy;
-import org.hibernate.cfg.Environment;
-import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
-import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties;
-import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings;
-import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.orm.jpa.JpaVendorAdapter;
-import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
-import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
-
-
-@Configuration
-@Slf4j
-public class HibernateConfig {
-
-  private final JpaProperties jpaProperties;
-
-  private final HibernateProperties hibernateProperties;
-
-  public HibernateConfig(@Autowired JpaProperties jpaProperties,
-                         HibernateProperties hibernateProperties) {
-    this.jpaProperties = jpaProperties;
-    this.hibernateProperties = hibernateProperties;
-  }
-
-  @Bean
-  public JpaVendorAdapter getJpaVendorAdapter() {
-    HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
-    adapter.setGenerateDdl(true);
-    return adapter;
-  }
-
-  @Bean("entityManagerFactory")
-  public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(DataSource dataSource,
-                                                                         MultiTenantConnectionProvider multiTenantConnectionProvider,
-                                                                         CurrentTenantIdentifierResolver currentTenantIdentifierResolver) {
-    Map<String, Object> properties = new HashMap<>();
-    properties.putAll(hibernateProperties
-        .determineHibernateProperties(jpaProperties.getProperties(),
-            new HibernateSettings()));
-    properties.put(Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);
-    properties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
-    properties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolver);
-
-    LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
-    em.setDataSource(dataSource);
-    em.setPackagesToScan("com.supwisdom");
-    em.setJpaPropertyMap(properties);
-    em.setJpaVendorAdapter(getJpaVendorAdapter());
-    log.info("setup multi-tenant entityManagerFactor");
-    return em;
-  }
-
-}
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/system/controller/NoticeController.java b/payapi/src/main/java/com/supwisdom/dlpay/system/controller/NoticeController.java
new file mode 100644
index 0000000..71bfae7
--- /dev/null
+++ b/payapi/src/main/java/com/supwisdom/dlpay/system/controller/NoticeController.java
@@ -0,0 +1,192 @@
+package com.supwisdom.dlpay.system.controller;
+
+import com.supwisdom.dlpay.api.bean.JsonResult;
+import com.supwisdom.dlpay.api.service.KafkaSendMsgService;
+import com.supwisdom.dlpay.framework.data.SystemDateTime;
+import com.supwisdom.dlpay.framework.domain.TNotice;
+import com.supwisdom.dlpay.framework.domain.TNoticeMsg;
+import com.supwisdom.dlpay.framework.domain.TOperator;
+import com.supwisdom.dlpay.framework.service.SystemUtilService;
+import com.supwisdom.dlpay.framework.util.DateUtil;
+import com.supwisdom.dlpay.framework.util.PageResult;
+import com.supwisdom.dlpay.framework.util.StringUtil;
+import com.supwisdom.dlpay.framework.util.WebConstant;
+import com.supwisdom.dlpay.system.service.NoticeService;
+import com.supwisdom.dlpay.util.ConstantUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+
+@Controller
+@RequestMapping("/notice")
+public class NoticeController {
+  @Autowired
+  private NoticeService noticeService;
+  @Autowired
+  private SystemUtilService systemUtilService;
+  @Autowired
+  private KafkaSendMsgService kafkaSendMsgService;
+
+  @GetMapping("/index")
+  public String noticeIndex() {
+    return "system/notice/index";
+  }
+
+  @GetMapping("/list")
+  @PreAuthorize("hasPermission('/notice/index','')")
+  @ResponseBody
+  public PageResult<TNotice> searchNotice(@RequestParam("page") Integer pageNo,
+                                          @RequestParam("limit") Integer pageSize,
+                                          @RequestParam(value = "startdate", required = false) String startdate,
+                                          @RequestParam(value = "enddate", required = false) String enddate,
+                                          @RequestParam(value = "searchkey", required = false) String searchkey) {
+    try {
+      if (null == pageNo || pageNo < 1) pageNo = WebConstant.PAGENO_DEFAULT;
+      if (null == pageSize || pageSize < 1) pageSize = WebConstant.PAGESIZE_DEFAULT;
+      return noticeService.getNotice(startdate, enddate, searchkey, pageNo, pageSize);
+    } catch (Exception e) {
+      e.printStackTrace();
+      return new PageResult<>(99, "系统查询错误");
+    }
+  }
+
+  @GetMapping("/load4add")
+  @PreAuthorize("hasPermission('/notice/load4add','')")
+  public String load4Add() {
+    return "system/notice/add";
+  }
+
+  @PostMapping("/edit")
+  @PreAuthorize("hasPermission('/notice/edit','')")
+  @ResponseBody
+  public JsonResult edit(@RequestParam("noticeId") String noticeId,
+                         @RequestParam("title") String title,
+                         @RequestParam("content") String content,
+                         @RequestParam("platform") String platform,
+                         @RequestParam(value = "linkurl", required = false) String linkurl,
+                         @AuthenticationPrincipal UserDetails operUser) {
+    try {
+      TOperator oper = (TOperator) operUser;
+      String prefix = "";
+      TNotice notice = null;
+      if (StringUtil.isEmpty(noticeId)) {
+        prefix = "新增";
+        notice = new TNotice();
+        SystemDateTime dt = systemUtilService.getSysdatetime();
+        notice.setCreatedate(dt.getHostdate());
+        notice.setCreatetime(dt.getHosttime());
+        notice.setOperid(oper.getOperid());
+        notice.setCreator(oper.getOpername());
+        notice.setTenantId(oper.getTenantId());
+      } else {
+        prefix = "修改";
+        notice = noticeService.getById(noticeId);
+        if (null == notice) return JsonResult.error("未找到原始记录,请重新查询后在操作。");
+        if (notice.getSendcnt() > 0) return JsonResult.error("公告已发布,不能修改");
+      }
+      if (StringUtil.isEmpty(title)) return JsonResult.error("公告标题不能为空");
+      if (StringUtil.isEmpty(content)) return JsonResult.error("公告内容不能为空");
+      if (!ConstantUtil.PHONE_PLATFORM_ALL.equals(platform) && !ConstantUtil.PHONE_PLATFORM_ANDROID.equals(platform) && !ConstantUtil.PHONE_PLATFORM_IOS.equals(platform))
+        return JsonResult.error("请选择推送终端");
+      notice.setPlatform(platform);
+      notice.setTitle(title.trim());
+      notice.setContent(content.trim());
+      if (StringUtil.isEmpty(linkurl)) {
+        notice.setLinkurl(null);
+      } else {
+        notice.setLinkurl(linkurl.trim());
+      }
+
+      if (noticeService.doSaveOrUpdateNotice(notice)) {
+        return JsonResult.ok(prefix + "成功!");
+      } else {
+        return JsonResult.error(prefix + "失败!");
+      }
+    } catch (Exception e) {
+      e.printStackTrace();
+      if (StringUtil.isEmpty(noticeId)) {
+        return JsonResult.error("新增失败,业务异常!").put("exception", e);
+      } else {
+        return JsonResult.error("修改失败,业务异常!").put("exception", e);
+      }
+    }
+  }
+
+  @PostMapping("/delete")
+  @PreAuthorize("hasPermission('/notice/delete','')")
+  @ResponseBody
+  public JsonResult delete(@RequestParam("noticeId") String noticeId) {
+    try {
+      TNotice notice = noticeService.getById(noticeId);
+      if (null == notice) return JsonResult.error("未找到原始记录,请重新查询后在操作。");
+      if (notice.getSendcnt() > 0) return JsonResult.error("公告已发布,不能删除!");
+      if (noticeService.deleteNotice(notice)) {
+        return JsonResult.ok("删除成功!");
+      } else {
+        return JsonResult.error("删除失败!");
+      }
+    } catch (Exception e) {
+      e.printStackTrace();
+      return JsonResult.error("删除失败,业务异常!").put("exception", e);
+    }
+  }
+
+  @GetMapping("/load4publish")
+  @PreAuthorize("hasPermission('/notice/load4publish','')")
+  public String load4Publish() {
+    return "system/notice/publish";
+  }
+
+  @PostMapping("/publish")
+  @PreAuthorize("hasPermission('/notice/publish','')")
+  @ResponseBody
+  public JsonResult publish(@RequestParam("noticeId") String noticeId,
+                            @RequestParam("pushmode") String pushmode,
+                            @RequestParam(value = "settime", required = false) String settime,
+                            @AuthenticationPrincipal UserDetails operUser) {
+    try {
+      TOperator oper = (TOperator) operUser;
+      TNotice notice = noticeService.getById(noticeId);
+      if (null == notice) return JsonResult.error("未找到原始记录,请重新查询后在操作。");
+      if (!ConstantUtil.PHONE_NOTICE_PUSHMODE_ATONCE.equals(pushmode) && !ConstantUtil.PHONE_NOTICE_PUSHMODE_DELAY.equals(pushmode))
+        return JsonResult.error("请选择是立即发布还是定时发布");
+      if (!ConstantUtil.PHONE_NOTICE_PUSHMODE_ATONCE.equals(pushmode) && !DateUtil.checkDatetimeValid(settime, "yyyy-MM-dd HH:mm:ss"))
+        return JsonResult.error("定时发布请设定正确的发布时间");
+      if (!ConstantUtil.PHONE_NOTICE_PUSHMODE_ATONCE.equals(pushmode) && DateUtil.compareDatetime(DateUtil.unParseToDateFormat(settime), systemUtilService.getSysdatetime().getHostdatetime(), DateUtil.DATETIME_FMT) <= 0)
+        return JsonResult.error("设定的发布时间必须比当前时间大");
+
+      TNoticeMsg msg = noticeService.doPublishNotice(notice.getId(), pushmode, settime, oper);
+      if (ConstantUtil.PHONE_NOTICE_PUSHMODE_ATONCE.equals(pushmode)) {
+        kafkaSendMsgService.sendNoticeMessage(msg.getMsgid()); //异步立即推送
+      }
+      return JsonResult.ok("发布成功!");
+    } catch (Exception e) {
+      e.printStackTrace();
+      return JsonResult.error("发布失败,业务异常!").put("exception", e);
+    }
+  }
+
+  @GetMapping("/load4detail")
+  @PreAuthorize("hasPermission('/notice/load4detail','')")
+  public String load4PublishDetails() {
+    return "system/notice/details";
+  }
+
+  @GetMapping("/detaillist")
+  @PreAuthorize("hasPermission('/notice/load4detail','')")
+  @ResponseBody
+  public PageResult<TNoticeMsg> searchNoticePublishDetails(@RequestParam("noticeId") String noticeId) {
+    try {
+      return noticeService.getNoticePublishDetails(noticeId);
+    } catch (Exception e) {
+      e.printStackTrace();
+      return new PageResult<>(99, "系统查询错误");
+    }
+  }
+
+
+
+}
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/system/service/NoticeService.java b/payapi/src/main/java/com/supwisdom/dlpay/system/service/NoticeService.java
new file mode 100644
index 0000000..1bb9425
--- /dev/null
+++ b/payapi/src/main/java/com/supwisdom/dlpay/system/service/NoticeService.java
@@ -0,0 +1,34 @@
+package com.supwisdom.dlpay.system.service;
+
+import com.supwisdom.dlpay.framework.domain.TNotice;
+import com.supwisdom.dlpay.framework.domain.TNoticeMsg;
+import com.supwisdom.dlpay.framework.domain.TOperator;
+import com.supwisdom.dlpay.framework.util.PageResult;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+public interface NoticeService {
+  @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class, readOnly = true)
+  PageResult<TNotice> getNotice(String startdate, String enddate, String searchkey, int pageNo, int pageSize);
+
+  @Transactional(rollbackFor = Exception.class, readOnly = true)
+  TNotice getById(String id);
+
+  @Transactional(rollbackFor = Exception.class)
+  boolean doSaveOrUpdateNotice(TNotice notice);
+
+  @Transactional(rollbackFor = Exception.class)
+  boolean deleteNotice(TNotice notice);
+
+  @Transactional(rollbackFor = Exception.class)
+  TNoticeMsg doPublishNotice(String noticeId, String pushmode, String settime, TOperator oper)throws Exception;
+
+  @Transactional(rollbackFor = Exception.class, readOnly = true)
+  PageResult<TNoticeMsg> getNoticePublishDetails(String noticeId);
+
+  @Transactional(rollbackFor = Exception.class, readOnly = true)
+  List<TNoticeMsg> getDelayNoticeByDatetime(String datetime);
+
+}
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/system/service/impl/NoticeServiceImpl.java b/payapi/src/main/java/com/supwisdom/dlpay/system/service/impl/NoticeServiceImpl.java
new file mode 100644
index 0000000..7f26776
--- /dev/null
+++ b/payapi/src/main/java/com/supwisdom/dlpay/system/service/impl/NoticeServiceImpl.java
@@ -0,0 +1,128 @@
+package com.supwisdom.dlpay.system.service.impl;
+
+import com.supwisdom.dlpay.framework.dao.NoticeDao;
+import com.supwisdom.dlpay.framework.dao.NoticeMsgDao;
+import com.supwisdom.dlpay.framework.domain.TNotice;
+import com.supwisdom.dlpay.framework.domain.TNoticeMsg;
+import com.supwisdom.dlpay.framework.domain.TOperator;
+import com.supwisdom.dlpay.framework.service.SystemUtilService;
+import com.supwisdom.dlpay.framework.util.DateUtil;
+import com.supwisdom.dlpay.framework.util.PageResult;
+import com.supwisdom.dlpay.framework.util.StringUtil;
+import com.supwisdom.dlpay.framework.util.TradeDict;
+import com.supwisdom.dlpay.system.service.NoticeService;
+import com.supwisdom.dlpay.util.ConstantUtil;
+import com.supwisdom.dlpay.util.WebCheckException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Service;
+
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.Predicate;
+import javax.persistence.criteria.Root;
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+public class NoticeServiceImpl implements NoticeService {
+  @Autowired
+  private NoticeDao noticeDao;
+  @Autowired
+  private NoticeMsgDao noticeMsgDao;
+  @Autowired
+  private SystemUtilService systemUtilService;
+
+  @Override
+  public PageResult<TNotice> getNotice(String startdate, String enddate, String searchkey, int pageNo, int pageSize) {
+    Pageable pageable = PageRequest.of(pageNo - 1, pageSize, Sort.by(Sort.Direction.DESC, "createdate"));
+    Page<TNotice> page = noticeDao.findAll(new Specification<TNotice>() {
+      @Override
+      public Predicate toPredicate(Root<TNotice> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
+        List<Predicate> predicates = new ArrayList<>();
+        if (!StringUtil.isEmpty(startdate)) {
+          predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.get("createdate").as(String.class), DateUtil.unParseToDateFormat(startdate)));
+        }
+        if (!StringUtil.isEmpty(enddate)) {
+          predicates.add(criteriaBuilder.lessThanOrEqualTo(root.get("createdate").as(String.class), DateUtil.unParseToDateFormat(enddate)));
+        }
+        if (!StringUtil.isEmpty(searchkey)) {
+          predicates.add(criteriaBuilder.like(root.get("title").as(String.class), "%" + searchkey.trim() + "%"));
+        }
+        return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
+      }
+    }, pageable);
+    return new PageResult<>(page);
+  }
+
+  @Override
+  public TNotice getById(String id) {
+    if (!StringUtil.isEmpty(id)) return noticeDao.getById(id.trim());
+    return null;
+  }
+
+  @Override
+  public boolean doSaveOrUpdateNotice(TNotice notice) {
+    if (null != notice) {
+      notice.setLastsaved(systemUtilService.getSysdatetime().getHostdatetime());
+      noticeDao.save(notice);
+      return true;
+    }
+    return false;
+  }
+
+  @Override
+  public boolean deleteNotice(TNotice notice) {
+    if (null != notice) {
+      noticeDao.delete(notice);
+      return true;
+    }
+    return false;
+  }
+
+  @Override
+  public TNoticeMsg doPublishNotice(String noticeId, String pushmode, String settime, TOperator oper) throws Exception {
+    TNotice notice = noticeDao.getByIdForUpdate(noticeId);
+    if (null == notice) throw new WebCheckException("未找到原始记录,请重新查询后在操作。");
+
+    TNoticeMsg msg = new TNoticeMsg();
+    msg.setNoticeId(notice.getId());
+    msg.setAlltarget(true);
+    msg.setPushmode(pushmode);
+    if (!ConstantUtil.PHONE_NOTICE_PUSHMODE_ATONCE.equals(pushmode)) {
+      msg.setPushSettime(DateUtil.unParseToDateFormat(settime));
+    }
+    msg.setPublisher(oper.getOpername());
+    msg.setOperid(oper.getOperid());
+    msg.setStatus(TradeDict.STATUS_NORMAL);
+    msg.setSendkafka(false);
+    msg.setCreatetime(systemUtilService.getSysdatetime().getHostdatetime());
+    msg.setTenantId(notice.getTenantId());
+    msg.setPushresult("待推送");
+
+    notice.setSendcnt(notice.getSendcnt() + 1);
+    notice.setLastsaved(systemUtilService.getSysdatetime().getHostdatetime());
+    noticeDao.save(notice);
+    return noticeMsgDao.save(msg);
+  }
+
+  @Override
+  public PageResult<TNoticeMsg> getNoticePublishDetails(String noticeId) {
+    if (!StringUtil.isEmpty(noticeId)) {
+      List<TNoticeMsg> list = noticeMsgDao.findAllByNoticeId(noticeId);
+      return new PageResult<>(list);
+    }
+    return new PageResult<>(99, "无数据");
+  }
+
+  @Override
+  public List<TNoticeMsg> getDelayNoticeByDatetime(String datetime) {
+    List<TNoticeMsg> list = noticeMsgDao.findAllDelayNoticeByDatetime(datetime);
+    if (!StringUtil.isEmpty(list)) return list;
+    return new ArrayList<>(0);
+  }
+}
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/util/ConstantUtil.java b/payapi/src/main/java/com/supwisdom/dlpay/util/ConstantUtil.java
index ba129e6..4f6d410 100644
--- a/payapi/src/main/java/com/supwisdom/dlpay/util/ConstantUtil.java
+++ b/payapi/src/main/java/com/supwisdom/dlpay/util/ConstantUtil.java
@@ -91,4 +91,17 @@
   public static final String OPERCHK_CHKMODE_DELETE = "删除";
 
   public static final String CHKFILE_DELIMITER = "|";
+
+  public static final String PHONE_PLATFORM_ALL = "all";
+  public static final String PHONE_PLATFORM_ANDROID = "android";
+  public static final String PHONE_PLATFORM_IOS = "ios";
+
+  public static final String PHONE_NOTICE_PUSHMODE_ATONCE = "atonce"; //立即发布
+  public static final String PHONE_NOTICE_PUSHMODE_DELAY = "delay"; //延时发布
+
+  /**
+   * kafka消息类型
+   * */
+  public static final String KAFKA_MAGTYPE_NOTICE = "dlsmk_phone_notice";
+  public static final String KAFKA_MAGTYPE_CONSUME = "dlsmk_card_consume";
 }
diff --git a/payapi/src/main/kotlin/com/supwisdom/dlpay/PayApiApplication.kt b/payapi/src/main/kotlin/com/supwisdom/dlpay/PayApiApplication.kt
index d993aea..f489996 100644
--- a/payapi/src/main/kotlin/com/supwisdom/dlpay/PayApiApplication.kt
+++ b/payapi/src/main/kotlin/com/supwisdom/dlpay/PayApiApplication.kt
@@ -4,6 +4,7 @@
 import io.lettuce.core.ReadFrom
 import net.javacrumbs.shedlock.core.LockProvider
 import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider
+import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.beans.factory.annotation.Value
 import org.springframework.boot.SpringApplication
@@ -148,7 +149,7 @@
     }
 }
 
-
+@EnableSchedulerLock(defaultLockAtMostFor = "PT30M")
 @SpringBootApplication
 @EnableDiscoveryClient
 @EnableScheduling
diff --git a/payapi/src/main/kotlin/com/supwisdom/dlpay/agent/service/citizencard_service.kt b/payapi/src/main/kotlin/com/supwisdom/dlpay/agent/service/citizencard_service.kt
index 4278bf0..b601151 100644
--- a/payapi/src/main/kotlin/com/supwisdom/dlpay/agent/service/citizencard_service.kt
+++ b/payapi/src/main/kotlin/com/supwisdom/dlpay/agent/service/citizencard_service.kt
@@ -14,11 +14,12 @@
 import org.springframework.stereotype.Component
 
 interface CitizencardPayService {
-    fun bindCard(bankcardno: String, username: String, idtype: String, idno: String, phone: String): DlpayResp
+    fun bindCard(bankcardno: String, username: String, idtype: String, idno: String,
+            phone: String): DlpayResp
 
-    fun signCard(bankcardno: String, username: String, idtype: String, idno: String, phone: String, transtype: String): DlpayResp
+    fun signCard(bankcardno: String, username: String, idtype: String, idno: String, phone: String, transtype: String,captcha:String): DlpayResp
 
-    fun cardPay(shopaccno: String, userid: String, accdate: String, amount: Int, refno: String): DlpayResp
+    fun cardPay(shopaccno: String, userid: String, accdate: String, amount: Int, scenario: String, refno: String): DlpayResp
 
     fun cardPayRefund(refno: String, accdate: String, orignRefno: String, amount: Int): DlpayResp
 
@@ -52,7 +53,7 @@
 
     override fun pay(transaction: TTransactionMain): AgentResponse<DtlStatus> {
         val resp = citizencardPayService.cardPay(transaction.shopDtl.shopaccno, transaction.personDtl.userid, transaction.accdate,
-                Math.abs(MoneyUtil.YuanToFen(transaction.personDtl.amount)), transaction.refno)
+                Math.abs(MoneyUtil.YuanToFen(transaction.personDtl.amount)), transaction.dtltype, transaction.refno)
 
         return AgentResponse<DtlStatus>().also {
             val code = agentCode(resp.code, resp.message)
diff --git a/payapi/src/main/kotlin/com/supwisdom/dlpay/agent/service/impl/citizencard_service_impl.kt b/payapi/src/main/kotlin/com/supwisdom/dlpay/agent/service/impl/citizencard_service_impl.kt
index 0601599..aa5c0ae 100644
--- a/payapi/src/main/kotlin/com/supwisdom/dlpay/agent/service/impl/citizencard_service_impl.kt
+++ b/payapi/src/main/kotlin/com/supwisdom/dlpay/agent/service/impl/citizencard_service_impl.kt
@@ -101,7 +101,7 @@
         }
     }
 
-    override fun signCard(bankcardno: String, username: String, idtype: String, idno: String, phone: String, transtype: String): DlpayResp {
+    override fun signCard(bankcardno: String, username: String, idtype: String, idno: String, phone: String, transtype: String,captcha:String): DlpayResp {
         var resp = DlpayResp()
         val config = sourceTypeService.getChargePaytypeConfig(TradeDict.PAYTYPE_CITIZEN_CARD, true)
         if (!checkCitizencardConfig(config, resp)) {
@@ -135,6 +135,7 @@
         params["idtype"] = idType.toString()
         params["idno"] = idno
         params["phone"] = phone
+        params["captcha"] = captcha
         params["sign_type"] = "MD5"
         val sign = MD5.encodeByMD5(StringUtil.createLinkString(StringUtil.paraFilter(params)) + config[YnrccUtil.YNRCC_SIGNKEY]!!.trim())
         params["sign"] = sign
@@ -161,7 +162,7 @@
         }
     }
 
-    override fun cardPay(shopaccno: String, userid: String, accdate: String, amount: Int, refno: String): DlpayResp {
+    override fun cardPay(shopaccno: String, userid: String, accdate: String, amount: Int, scenario: String, refno: String): DlpayResp {
         var resp = DlpayResp()
         val config = sourceTypeService.getConsumePaytypeConfig(TradeDict.PAYTYPE_CITIZEN_CARD, shopaccno, false, false)
         if (!checkCitizencardConfig(config, resp)) {
@@ -216,6 +217,7 @@
         params["merchant_bankcardno"] = merchantBankcardno!!
         params["merchant_bankaccname"] = merchantBankaccname!!
         params["amount"] = amount.toString()
+        params["scenario"] = scenario
         params["description"] = "市民卡代扣消费"
         params["sign_type"] = "MD5"
         val sign = MD5.encodeByMD5(StringUtil.createLinkString(StringUtil.paraFilter(params)) + config[YnrccUtil.YNRCC_SIGNKEY]!!.trim())
diff --git a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/async_tasks.kt b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/async_tasks.kt
index e771add..f01d870 100644
--- a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/async_tasks.kt
+++ b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/async_tasks.kt
@@ -71,6 +71,15 @@
         }
     }
 
+    @Bean(name = ["kafkaSendMessageAsyncTask"])
+    fun kafkaSendMessageAsyncTaskExecutor(): Executor {
+        return ThreadPoolTaskExecutor().apply {
+            corePoolSize = 20
+            maxPoolSize = 100
+            setWaitForTasksToCompleteOnShutdown(true)
+        }
+    }
+
     override fun getAsyncUncaughtExceptionHandler(): AsyncUncaughtExceptionHandler? {
         return MyAsyncUncaughtExceptionHandler()
     }
diff --git a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/scheduler_task.kt b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/scheduler_task.kt
index 1d8e2fc..e7caa77 100644
--- a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/scheduler_task.kt
+++ b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/scheduler_task.kt
@@ -7,10 +7,12 @@
 import com.supwisdom.dlpay.api.repositories.ShopaccService
 import com.supwisdom.dlpay.api.service.ConsumePayService
 import com.supwisdom.dlpay.api.service.DtlQueryResultService
+import com.supwisdom.dlpay.api.service.KafkaSendMsgService
 import com.supwisdom.dlpay.api.service.TransactionServiceProxy
 import com.supwisdom.dlpay.framework.service.SystemUtilService
 import com.supwisdom.dlpay.framework.util.ApplicationUtil
 import com.supwisdom.dlpay.framework.util.TradeDict
+import com.supwisdom.dlpay.system.service.NoticeService
 import com.supwisdom.dlpay.util.ConstantUtil
 import net.javacrumbs.shedlock.core.SchedulerLock
 import org.springframework.beans.factory.annotation.Autowired
@@ -150,4 +152,34 @@
             }
         }
     }
+}
+
+/**
+ * 定时通知公告的推送任务
+ * */
+@Component
+class NoticePushMessageTask {
+    @Autowired
+    private lateinit var noticeService: NoticeService
+    @Autowired
+    private lateinit var systemUtilService: SystemUtilService
+    @Autowired
+    private lateinit var kafkaSendMsgService: KafkaSendMsgService
+
+    @Scheduled(cron = "\${send.delay.notice.task.cron:-}")
+    @SchedulerLock(name = "SendDelayNoticeTask", lockAtMostForString = "PT10M")
+    fun doSendDelayNotice() {
+        try {
+            val hostdatetime = systemUtilService.sysdatetime.hostdatetime
+            noticeService.getDelayNoticeByDatetime(hostdatetime).forEach {
+                try {
+                    kafkaSendMsgService.sendNoticeMessage(it.msgid)
+                } catch (e: Exception) {
+                    e.printStackTrace()
+                }
+            }
+        } catch (ex: Exception) {
+            ex.printStackTrace()
+        }
+    }
 }
\ No newline at end of file
diff --git a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/service/impl/kafka_service_impl.kt b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/service/impl/kafka_service_impl.kt
index 290bfaa..c913448 100644
--- a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/service/impl/kafka_service_impl.kt
+++ b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/service/impl/kafka_service_impl.kt
@@ -3,11 +3,17 @@
 import com.google.gson.Gson
 import com.supwisdom.dlpay.api.bean.KafkaXgMessage
 import com.supwisdom.dlpay.api.service.KafkaSendMsgService
+import com.supwisdom.dlpay.framework.dao.NoticeDao
+import com.supwisdom.dlpay.framework.dao.NoticeMsgDao
+import com.supwisdom.dlpay.framework.service.SystemUtilService
 import com.supwisdom.dlpay.framework.util.DateUtil
+import com.supwisdom.dlpay.framework.util.StringUtil
 import com.supwisdom.dlpay.framework.util.TradeDict
 import com.supwisdom.dlpay.mobile.dao.MsgDao
 import com.supwisdom.dlpay.mobile.domain.TBMsg
 import com.supwisdom.dlpay.mobile.service.MobileApiService
+import com.supwisdom.dlpay.util.ConstantUtil
+import com.supwisdom.dlpay.util.WebCheckException
 import mu.KotlinLogging
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.kafka.core.KafkaTemplate
@@ -15,19 +21,26 @@
 import org.springframework.stereotype.Service
 
 @Service
-class KafkaSendMsgServiceImpl:KafkaSendMsgService{
+class KafkaSendMsgServiceImpl : KafkaSendMsgService {
     val logger = KotlinLogging.logger { }
     @Autowired
     private lateinit var kafkaTemplate: KafkaTemplate<String, String>
     @Autowired
     private lateinit var msgDao: MsgDao
     @Autowired
+    private lateinit var noticeMsgDao: NoticeMsgDao
+    @Autowired
+    private lateinit var noticeDao: NoticeDao
+    @Autowired
     private lateinit var mobileApiService: MobileApiService
+    @Autowired
+    private lateinit var systemUtilService: SystemUtilService
+
     val gson = Gson()
 
     val topic = "jpush-messages"
 
-    @Async
+    @Async("kafkaSendMessageAsyncTask")
     override fun sendJpushMessage(userid: String, title: String, content: String, refno: String, extras: MutableMap<String, String>, tenantId: String?) {
         val musers = mobileApiService.findByUseridAndStatus(userid, TradeDict.STATUS_NORMAL)
         var msg = TBMsg().apply {
@@ -63,16 +76,60 @@
             message.custom = gson.toJson(extras)
             message.expiretime = DateUtil.getNewTime(DateUtil.getNow(), 300)
             message.gids = it.uid
-            if(it.lastloginplatform.isNullOrEmpty()){
-                message.platform="ios"
+            if (it.lastloginplatform.isNullOrEmpty()) {
+                message.platform = "all"
                 kafkaTemplate.send(topic, msg.msgid, gson.toJson(message))
-                message.platform="android"
-                kafkaTemplate.send(topic, msg.msgid, gson.toJson(message))
-            }else{
+//                message.platform="ios"
+//                kafkaTemplate.send(topic, msg.msgid, gson.toJson(message))
+//                message.platform="android"
+//                kafkaTemplate.send(topic, msg.msgid, gson.toJson(message))
+            } else {
                 kafkaTemplate.send(topic, msg.msgid, gson.toJson(message))
             }
         }
         msg.pusheduids = uids
         msgDao.save(msg)
     }
+
+    @Async("kafkaSendMessageAsyncTask")
+    override fun sendNoticeMessage(noticeMsgid: String) {
+        val noticeMsg = noticeMsgDao.getByMsgidForUpdateNowait(noticeMsgid) ?: return
+        try {
+            if (noticeMsg.sendkafka) return
+            val notice = noticeDao.getById(noticeMsg.noticeId) ?: throw WebCheckException("未找到对应的通知公告")
+
+            val extras = mutableMapOf<String, String>("msgid" to noticeMsg.msgid, "msg_type" to ConstantUtil.KAFKA_MAGTYPE_NOTICE)
+            if (!StringUtil.isEmpty(notice.linkurl)) extras["url"] = notice.linkurl
+            val message = KafkaXgMessage().apply {
+                this.title = notice.title
+                this.content = notice.content
+                this.alltarget = true //gids不需要传递
+                this.platform = notice.platform
+                this.type = 1 //TYPE_NOTIFICATION = 1,TYPE_MESSAGE = 2
+                this.callback = true
+                this.retries = 3
+                this.custom = gson.toJson(extras)
+            }
+            println(gson.toJson(message))
+            kafkaTemplate.send(topic, noticeMsg.msgid, gson.toJson(message))
+
+            noticeMsg.sendkafka = true
+            noticeMsg.pushtime = systemUtilService.sysdatetime.hostdatetime
+            noticeMsg.pushresult = "已推送"
+            noticeMsgDao.save(noticeMsg)
+        } catch (wex: WebCheckException) {
+            noticeMsg.sendkafka = false
+            noticeMsg.pushtime = systemUtilService.sysdatetime.hostdatetime
+            noticeMsg.pushresult = "推送失败! ${wex.message}"
+            noticeMsgDao.save(noticeMsg)
+            return
+        } catch (e: Exception) {
+            e.printStackTrace()
+            noticeMsg.sendkafka = false
+            noticeMsg.pushtime = systemUtilService.sysdatetime.hostdatetime
+            noticeMsg.pushresult = "推送失败"
+            noticeMsgDao.save(noticeMsg)
+            return
+        }
+    }
 }
diff --git a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/service/kafka_service.kt b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/service/kafka_service.kt
index a5cc0e8..efd45f7 100644
--- a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/service/kafka_service.kt
+++ b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/service/kafka_service.kt
@@ -1,5 +1,6 @@
 package com.supwisdom.dlpay.api.service
 
+import com.supwisdom.dlpay.framework.dao.NoticeMsgDao
 import com.supwisdom.dlpay.mobile.dao.MsgDao
 import mu.KotlinLogging
 import org.apache.kafka.clients.consumer.ConsumerRecord
@@ -9,22 +10,38 @@
 
 interface KafkaSendMsgService {
     fun sendJpushMessage(userid: String, title: String, content: String, refno: String, extras: MutableMap<String, String>, tenantId: String?)
+
+    fun sendNoticeMessage(noticeMsgid: String)
 }
+
 @Component
-class  KafkaMsgListener{
+class KafkaMsgListener {
     val logger = KotlinLogging.logger { }
     @Autowired
     private lateinit var msgDao: MsgDao
+    @Autowired
+    private lateinit var noticeMsgDao: NoticeMsgDao
 
-    @KafkaListener(topics = ["jpush-messages-result"],autoStartup = "\${spring.kafka.listen.auto.start:true}")
-    fun listen(record :ConsumerRecord<String, String>) {
+    @KafkaListener(topics = ["jpush-messages-result"],
+            autoStartup = "\${spring.kafka.listen.auto.start:true}",
+            groupId = "0")
+    fun listen(record: ConsumerRecord<String, String>) {
         logger.debug { "${record.key()},${record.value()}" }
-        if(!record.key().isNullOrEmpty()){
-           val opt =  msgDao.findById(record.key())
-            if(opt.isPresent){
+        if (!record.key().isNullOrEmpty()) {
+            val opt = msgDao.findById(record.key())
+            if (opt.isPresent) {
                 var msg = opt.get()
                 msg.pushresult = record.value()
                 msgDao.save(msg)
+                return
+            }
+
+            val noticemsg = noticeMsgDao.findById(record.key())
+            if (noticemsg.isPresent) {
+                var nmsg = noticemsg.get()
+                nmsg.pushresult = record.value()
+                noticeMsgDao.save(nmsg)
+                return
             }
         }
     }
diff --git a/payapi/src/main/kotlin/com/supwisdom/dlpay/mobile/MobileApi.kt b/payapi/src/main/kotlin/com/supwisdom/dlpay/mobile/MobileApi.kt
index 0f6f5d6..8854b19 100644
--- a/payapi/src/main/kotlin/com/supwisdom/dlpay/mobile/MobileApi.kt
+++ b/payapi/src/main/kotlin/com/supwisdom/dlpay/mobile/MobileApi.kt
@@ -36,6 +36,7 @@
 import org.springframework.web.multipart.MultipartFile
 import java.time.Duration
 import java.util.*
+import kotlin.collections.HashMap
 
 
 @RestController
@@ -236,6 +237,14 @@
                 ?.put("signed", signed)
                 ?.put("tenantid", "mobile")!!
     }
+
+    /**
+     *
+     * 小程序认证接口
+     *
+     * */
+
+
 }
 
 
@@ -403,27 +412,32 @@
         if (exsitUser != null) {
             return JsonResult.error("该银行卡号已被绑定,若您本人绑定,请先解除绑定,若非本人,请联系客服")
         }
+        var signed = ""
         //call api
         var resp = citizencardPayService.bindCard(cardno, name, idtype, idno, phone)
         if (resp.code != "0000") {
             return JsonResult.error(resp.message)
         }
-        var signed = ""
+        var needupdate = false
         if (resp.sinstatus == "1") {
             signed = TradeDict.STATUS_YES
-            card.signed = true
+            if(!card.signed){
+                card.signed = true
+                mobileApiService.saveCard(card)
+            }
             user.signedtime = DateUtil.getNow();
-            mobileApiService.saveCard(card)
+            mobileApiService.saveUser(user)
+            needupdate = true;
+
+        }
+        if( user.userid.isNullOrEmpty()){
+            user.userid = person.userid
+            user.bindtime = DateUtil.getNow()
+            needupdate=true
+        }
+        if(needupdate){
             mobileApiService.saveUser(user)
         }
-        logger.error { resp.captcha }
-        var code = if (resp.captcha.isNullOrEmpty()) {
-            RandomUtils.randomNumber(6)
-        } else {
-            resp.captcha
-        }
-        logger.error { code }
-        redisTemplate.opsForValue().set(phone, code, Duration.ofMinutes(10))
         var payseted = false
         if (!user.paypwd.isNullOrEmpty()) {
             payseted = true
@@ -436,6 +450,42 @@
 
 
     }
+    /**
+     * 绑卡
+     * */
+    @RequestMapping("/bindcardcode")
+    fun bindcardcode(): JsonResult {
+        val p = SecurityContextHolder.getContext().authentication
+        val user = mobileApiService.findUserById(p.name)
+                ?: return JsonResult.error("用户不存在,请注册")
+        if (user.phone.isNullOrEmpty()) {
+            return JsonResult.error("手机号不存在,请注册")
+        }
+        var card = mobileApiService.findCardByUserid(user.userid!!)
+                ?: return JsonResult.error("卡片不存在,请重新绑定")
+        //call sign api
+        val person = userService.findOnePersonByUserid(card.userid)
+        var signed=""
+        //call api
+        var resp = citizencardPayService.bindCard(card.cardno, person.name, person.idtype, person.idno, user.phone!!)
+        if (resp.code != "0000") {
+            return JsonResult.error(resp.message)
+        }
+
+        if (resp.sinstatus == "1") {
+            signed = TradeDict.STATUS_YES
+            if(!card.signed){
+                card.signed = true
+                mobileApiService.saveCard(card)
+            }
+            user.signedtime = DateUtil.getNow();
+            mobileApiService.saveUser(user)
+        }
+
+        return JsonResult.ok("OK")
+                ?.put("signed", signed)!!
+
+    }
 
     /**
      * 支付密码
@@ -550,7 +600,8 @@
                     ?: return JsonResult.error("卡片不存在,请重新绑定")
             //call sign api
             val person = userService.findOnePersonByUserid(card.userid)
-            var resp = citizencardPayService.signCard(card.cardno, person.name, person.idtype, person.idno, user.phone!!, YnrccUtil.TRANSTYPE_SIGNCARD)
+            val captcha = agree//此处为验证码,暂由此参数代替
+            var resp = citizencardPayService.signCard(card.cardno, person.name, person.idtype, person.idno, user.phone!!, YnrccUtil.TRANSTYPE_SIGNCARD,captcha)
             if (resp.code != "0000") {
                 return JsonResult.error(resp.message)
             }
@@ -570,7 +621,7 @@
      * 查询账单
      * */
     @RequestMapping("/bills")
-    fun bills(pageno: Int): JsonResult {
+    fun bills(pageno: Int,platform: String?): JsonResult {
         val p = SecurityContextHolder.getContext().authentication
         val user = mobileApiService.findUserById(p.name)
                 ?: return JsonResult.error("用户不存在,请注册")
@@ -615,6 +666,46 @@
                 signed = TradeDict.STATUS_YES
             }
         }
+        var version:String?=""
+        var minversion:String?=""
+        var versionmsg:String?=""
+        var versionurl:String?=""
+        if(!platform.isNullOrEmpty()){
+            var map= mutableMapOf<String,Any>()
+            if(platform.toLowerCase().contains("android")){
+                dictionaryProxy.refreshDictionary("androidapp")
+                map = dictionaryProxy.getDictionaryAsMap("androidapp")
+                if(map["androidversion"]!=null){
+                    version = map["androidversion"] as String
+                }
+                if(map["androidminversion"]!=null){
+                    minversion = map["androidminversion"] as String
+                }
+                if(map["androidversionmsg"]!=null){
+                    versionmsg = map["androidversionmsg"] as String
+                }
+                if(map["androidversionurl"]!=null){
+                    versionurl = map["androidversionurl"] as String
+                }
+
+            }else if(platform.toLowerCase().contains("iphone")){
+                map = dictionaryProxy.getDictionaryAsMap("iosapp")
+                if(map["iosversion"]!=null){
+                    version = map["iosversion"] as String
+                }
+                if(map["iosminversion"]!=null){
+                    minversion = map["iosminversion"] as String
+                }
+                if(map["iosversionmsg"]!=null){
+                    versionmsg = map["iosversionmsg"] as String
+                }
+                if(map["iosversionurl"]!=null){
+                    versionurl = map["iosversionurl"] as String
+                }
+            }
+
+        }
+
         var name = person.name
         val page = userService.findPersondtlByUserid(user.userid!!, no)
         return JsonResult.ok("OK").put("page", page)
@@ -626,9 +717,10 @@
                 ?.put("name", name)
                 ?.put("needrebind", needrebind)
                 ?.put("signed", signed)
-                ?.put("version","1")
-                ?.put("minversion","1")
-                ?.put("versionmsg","1")
+                ?.put("version",version)
+                ?.put("minversion",minversion)
+                ?.put("versionmsg",versionmsg)
+                ?.put("versionurl",versionurl)
                 ?.put("userid", if (user.userid.isNullOrEmpty()) "" else user.userid)!!.put("t", t)!!
     }
 
@@ -783,7 +875,8 @@
                     ?: return JsonResult.error("银行卡不存在,不能解除代扣协议")
             //call sign api
             val person = userService.findOnePersonByUserid(card.userid)
-            var resp = citizencardPayService.signCard(card.cardno, person.name, person.idtype, person.idno, user.phone!!, YnrccUtil.TRANSTYPE_UNSIGNCARD)
+            val captcha = ""//此处为验证码,暂由此参数代替
+            var resp = citizencardPayService.signCard(card.cardno, person.name, person.idtype, person.idno, user.phone!!, YnrccUtil.TRANSTYPE_UNSIGNCARD,captcha)
             if (resp.code != "0000") {
                 return JsonResult.error(resp.message)
             }
diff --git a/payapi/src/main/resources/application.properties b/payapi/src/main/resources/application.properties
index 0a2b535..091b5cf 100644
--- a/payapi/src/main/resources/application.properties
+++ b/payapi/src/main/resources/application.properties
@@ -35,8 +35,10 @@
 query.third.transdtl.result.cron=7 0/1 * * * ?
 payapi.sourcetype.checker.scheduler=7 3/10 * * * ?
 citizencard.dolosstask.cron=-
+send.delay.notice.task.cron=29 0/1 * * * ?
 ################################################
 # user password
 auth.password.bcrypt.length=10
 ###################################################
-spring.redis.database=0
\ No newline at end of file
+spring.redis.database=0
+multi-tenant.datasource.base-package=com.supwisdom.dlpay
\ No newline at end of file
diff --git a/payapi/src/main/resources/data.sql b/payapi/src/main/resources/data.sql
index 7244bf3..e2c3fac 100644
--- a/payapi/src/main/resources/data.sql
+++ b/payapi/src/main/resources/data.sql
@@ -77,6 +77,8 @@
 VALUES (37, NULL, 1, NULL, '', '/user/card', '市民卡查询', 1, 19, '{tenantid}');
 INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
 VALUES (38, NULL, 1, NULL, '', '/report/shoptodaybusiness', '商户当天统计表', 4, 20, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (39, NULL, 1, NULL, '', '/notice/index', '手机通知公告', 6, 3, '{tenantid}');
 
 
 INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
@@ -145,6 +147,8 @@
 VALUES ('ff8080816db87e27016db88be41f0015', 37, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
 INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
 VALUES ('4028ee9f6e5d95d8016e5d99e8d50012', 38, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816f8d8258016f8d85e4d70005', 39, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
 
 
 INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
@@ -311,6 +315,21 @@
 VALUES (94, '', 37, '修改跳转', '/user/load4modifycard', '{tenantid}');
 INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
 VALUES (95, '', 37, '修改', '/user/cardupdate', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (96, '', 39, '查询', '/notice/index', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (97, '', 39, '新增修改跳转', '/notice/load4add', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (98, '', 39, '新增修改', '/notice/edit', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (99, '', 39, '删除', '/notice/delete', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (100, '', 39, '发布跳转', '/notice/load4publish', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (101, '', 39, '发布', '/notice/publish', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (102, '', 39, '发布明细', '/notice/load4detail', '{tenantid}');
+
 
 INSERT INTO "tb_permission" ("id", "resid", "role_func_id", "roleid", tenantid)
 VALUES ('ff8080816b7947ed016b795577300036', 16, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
@@ -476,7 +495,20 @@
 VALUES ('ff8080816ecaebf8016ecaeedcec0004', 94, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
 INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
 VALUES ('ff8080816ecafb4e016ecafd58400005', 95, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
-
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816f8d8258016f8d85e4e20006', 96, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816fa2c2b7016fa2d809780030', 97, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816fa2c2b7016fa2d80978002e', 98, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816fa2c2b7016fa2d809780031', 99, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816fa2c2b7016fa2d809780032', 100, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816fa2c2b7016fa2d80978002f', 101, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816fa2c2b7016fa2d80977002d', 102, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
 
 
 INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
diff --git a/payapi/src/main/resources/static/libs/custom.js b/payapi/src/main/resources/static/libs/custom.js
index d034401..0307a2d 100644
--- a/payapi/src/main/resources/static/libs/custom.js
+++ b/payapi/src/main/resources/static/libs/custom.js
@@ -104,8 +104,9 @@
     }
 
     root.isempty = function (s) {
-        if (s == null || s.length == 0)
+        if (undefined == s || null == s || '' == s || s.length == 0) {
             return true;
-        return /\s/.test(s);
+        }
+        return false;
     }
 }(window));
\ No newline at end of file
diff --git a/payapi/src/main/resources/templates/system/notice/add.html b/payapi/src/main/resources/templates/system/notice/add.html
new file mode 100644
index 0000000..1247eda
--- /dev/null
+++ b/payapi/src/main/resources/templates/system/notice/add.html
@@ -0,0 +1,85 @@
+<form id="notice-add-form" lay-filter="notice-add-form" class="layui-form model-form">
+    <input name="noticeId" id="hidden-notice-add-msgid" type="hidden"/>
+    <div class="layui-form-item">
+        <label class="layui-form-label"><span style="color: red;">*</span>推送终端</label>
+        <div class="layui-input-block">
+            <input type="radio" name="platform" value="all" title="全部" checked/>
+            <input type="radio" name="platform" value="android" title="安卓手机"/>
+            <input type="radio" name="platform" value="ios" title="苹果手机"/>
+        </div>
+    </div>
+    <div class="layui-form-item">
+        <label class="layui-form-label"><span style="color: red;">*</span>公告标题</label>
+        <div class="layui-input-block">
+            <input name="title" placeholder="请填写标题,不能超过16个字" type="text" class="layui-input" maxlength="16"
+                   autocomplete="off" lay-verify="required" required/>
+        </div>
+    </div>
+    <div class="layui-form-item">
+        <label class="layui-form-label"><span style="color: red;">*</span>公告内容</label>
+        <div class="layui-input-block">
+            <textarea name="content" placeholder="请输入内容,不能超过200个字" autocomplete="off" maxlength="200" class="layui-textarea" rows="6" lay-verify="required" required></textarea>
+        </div>
+    </div>
+    <div class="layui-form-item">
+        <label class="layui-form-label">链接地址</label>
+        <div class="layui-input-block">
+            <textarea name="linkurl" placeholder="https://www.baidu.com" autocomplete="off" class="layui-textarea" lay-verify="linkurl"></textarea>
+        </div>
+    </div>
+
+    <div class="layui-form-item model-form-footer">
+        <button class="layui-btn layui-btn-primary" type="button" ew-event="closeDialog">取消</button>
+        <button class="layui-btn" lay-filter="notice-add-form-submit" lay-submit id="notice-add-submit-btn">保存</button>
+    </div>
+</form>
+
+<script>
+    layui.use(['layer', 'admin', 'form'], function () {
+        var layer = layui.layer;
+        var admin = layui.admin;
+        var form = layui.form;
+
+        form.render('radio');
+        form.verify({
+            linkurl: function (value, item) {
+                if ("" != value) {
+                    if (!/(^#)|(^http(s*):\/\/[^\s]+\.[^\s]+)/.test(value)) {
+                        return "链接格式不正确";
+                    }
+                }
+            }
+        });
+
+        var bean = admin.getTempData('t_noticeTmp');
+        if (bean) {
+            form.val('notice-add-form', bean);
+        }
+        // 表单提交事件
+        form.on('submit(notice-add-form-submit)', function (data) {
+            layer.load(2);
+            var token = $("meta[name='_csrf_token']").attr("value");
+            var param = data.field;
+            param["_csrf"] = token;
+            admin.go('[[@{/notice/edit}]]', param, function (result) {
+                console.log(result);
+                layer.closeAll('loading');
+                if (result.code == 200) {
+                    layer.msg(result.msg, {icon: 1});
+                    admin.finishPopupCenter();
+                } else if (result.code == 401) {
+                    layer.msg(result.msg, {icon: 2, time: 1500}, function () {
+                        location.replace('[[@{/login}]]');
+                    }, 1000);
+                    return;
+                } else {
+                    layer.msg(result.msg, {icon: 2});
+                }
+            }, function (ret) {
+                console.log(ret);
+                admin.errorBack(ret);
+            });
+            return false;
+        });
+    });
+</script>
\ No newline at end of file
diff --git a/payapi/src/main/resources/templates/system/notice/details.html b/payapi/src/main/resources/templates/system/notice/details.html
new file mode 100644
index 0000000..e2de243
--- /dev/null
+++ b/payapi/src/main/resources/templates/system/notice/details.html
@@ -0,0 +1,86 @@
+<form id="notice-publish-details" lay-filter="notice-publish-details" class="layui-form model-form" style="padding-top: 20px;">
+    <div class="layui-form-item">
+        <label class="layui-form-label">标题</label>
+        <div class="layui-input-block">
+            <input name="title" type="text" class="layui-input" readonly="readonly"/>
+        </div>
+    </div>
+    <div class="layui-form-item">
+        <label class="layui-form-label">内容</label>
+        <div class="layui-input-block">
+            <textarea name="content" class="layui-textarea" readonly="readonly" rows="5"></textarea>
+        </div>
+    </div>
+
+    <div class="layui-form-item">
+        <input type="hidden" name="noticeId" id="notice-publish-details-noticeId"/>
+        <div style="margin-left: 45px;" id="noticePublishDetailTableDiv">
+            <table class="layui-table" id="noticePublishDetailTable"
+                   lay-filter="noticePublishDetailTable-filter"></table>
+        </div>
+    </div>
+
+    <div class="layui-form-item model-form-footer">
+        <button class="layui-btn layui-btn-primary" type="button" ew-event="closeDialog">关闭</button>
+    </div>
+</form>
+
+<script>
+    layui.use(['layer', 'admin', 'form', 'table'], function () {
+        var layer = layui.layer;
+        var admin = layui.admin;
+        var form = layui.form;
+        var table = layui.table;
+
+        var bean = admin.getTempData('t_noticePublishDetail');
+        if (bean) {
+            if (isempty(bean.linkurl)) {
+                form.val('notice-publish-details', {
+                    noticeId: bean.id,
+                    title: bean.title,
+                    content: bean.content
+                });
+            } else {
+                form.val('notice-publish-details', {
+                    noticeId: bean.id,
+                    title: bean.title,
+                    content: bean.content + '\n\n链接:' + bean.linkurl
+                });
+            }
+        }
+
+        table.render({
+            elem: '#noticePublishDetailTable',
+            url: '[[@{/notice/detaillist}]]',
+            where: {
+                noticeId: $("#notice-publish-details-noticeId").val()
+            },
+            page: false,
+            size: 'sm',
+            height: 275,
+            cols: [
+                [
+                    {type: 'numbers', title: '编号', align: 'center', width: 65},
+                    {field: 'publisher', title: '发布者', align: 'center', width: 100},
+                    {
+                        field: 'createtime', title: '发布时间', align: 'right', width: 150, templet: function (e) {
+                            return admin.formatDate(e.createtime);
+                        }
+                    },
+                    {
+                        field: 'pushtime', title: '推送时间', align: 'right', width: 175, templet: function (e) {
+                            if (null != e.pushtime) {
+                                return admin.formatDate(e.pushtime);
+                            } else if (null != e.pushSettime) {
+                                return '<span style=\"color:red;\">预计</span> ' + admin.formatDate(e.pushSettime);
+                            } else {
+                                return '-';
+                            }
+                        }
+                    },
+                    {field: 'pushresult', title: '备注', align: 'center'},
+                ]
+            ]
+        });
+    });
+</script>
\ No newline at end of file
diff --git a/payapi/src/main/resources/templates/system/notice/index.html b/payapi/src/main/resources/templates/system/notice/index.html
new file mode 100644
index 0000000..02c000c
--- /dev/null
+++ b/payapi/src/main/resources/templates/system/notice/index.html
@@ -0,0 +1,250 @@
+<div class="layui-card">
+    <div class="layui-card-header">
+        <h2 class="header-title">手机通知公告</h2>
+        <span class="layui-breadcrumb pull-right">
+          <a href="#">系统中心</a>
+          <a><cite>手机通知公告</cite></a>
+        </span>
+    </div>
+    <div class="layui-card-body">
+        <div class="layui-form" lay-filter="notice-search-form">
+            <div class="layui-form-item" style="margin-bottom: 0;">
+                <div class="layui-inline">
+                    <label class="layui-form-label">创建日期</label>
+                    <div class="layui-input-inline">
+                        <input type="text" name="startdate" id="notice-search-startdate" placeholder="起始日期"
+                               autocomplete="off" class="layui-input"/>
+                    </div>
+                    <div class="layui-form-mid">-</div>
+                    <div class="layui-input-inline">
+                        <input type="text" name="enddate" id="notice-search-enddate" placeholder="截止日期"
+                               autocomplete="off" class="layui-input"/>
+                    </div>
+                </div>
+                <div class="layui-inline" style="margin-right: 20px;">
+                    <label class="layui-form-label">标题</label>
+                    <div class="layui-input-block" style="width: 265px;">
+                        <input type="text" name="searchkey" id="notice-search-searchkey" maxlength="20"
+                               autocomplete="off" class="layui-input"/>
+                    </div>
+                </div>
+                <div class="layui-inline" style="margin-right: 20px;margin-top: 10px;">
+                    <div class="layui-input-block" style="width: 200px;margin-left: 10px;">
+                        <button id="notice-search-btn" class="layui-btn icon-btn"><i class="layui-icon">&#xe615;</i>搜 索
+                        </button>
+                        <button id="notice-search-btn-reset" class="layui-btn layui-btn-primary">清 空</button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="layui-card-body">
+        <div class="layui-form toolbar">
+            <button class="layui-btn layui-btn-sm" id="btn-notice-add"><i
+                    class="layui-icon">&#xe654;</i>新 增
+            </button>
+        </div>
+        <table class="layui-table" id="noticeSearchTable" lay-filter="noticeSearchTable-filter"></table>
+    </div>
+</div>
+
+<!-- 表格操作列 -->
+<script type="text/html" id="notice-table-bar">
+    <a class="layui-btn layui-btn-xs layui-btn-warm" lay-event="publish">发布</a>
+    {{# if(d.sendcnt == 0){ }}
+    <a class="layui-btn layui-btn-xs" lay-event="modify">修改</a>
+    <a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="delete">删除</a>
+    {{# } else{ }}
+    <a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="detail">发布详情</a>
+    {{# } }}
+</script>
+
+<script>
+    layui.use(['form', 'table', 'layer', 'admin', 'laydate'], function () {
+        var form = layui.form;
+        var table = layui.table;
+        var admin = layui.admin;
+        var laydate = layui.laydate;
+
+        laydate.render({
+            elem: '#notice-search-startdate',
+            trigger: 'click'
+        });
+        laydate.render({
+            elem: '#notice-search-enddate',
+            trigger: 'click'
+        });
+
+        table.render({
+            elem: '#noticeSearchTable',
+            url: '[[@{/notice/list}]]',
+            page: true,
+            cols: [
+                [
+                    {align: 'left', title: '操作', width: 170, toolbar: '#notice-table-bar', fixed: 'left'},
+                    {field: 'title', title: '消息标题', align: 'left', width: 160, fixed: 'left', sort: true},
+                    {field: 'content', title: '消息内容', align: 'left'},
+                    {
+                        field: 'platform',
+                        title: '推送终端',
+                        align: 'center',
+                        width: 120,
+                        sort: true,
+                        templet: function (d) {
+                            if ('all' == d.platform) {
+                                return '全部手机';
+                            } else if ('ios' == d.platform) {
+                                return '苹果手机';
+                            } else if ('android' == d.platform) {
+                                return '安卓手机';
+                            } else {
+                                return d.platform;
+                            }
+                        }
+                    },
+                    {field: 'sendcnt', title: '发布次数', align: 'center', width: 120, sort: true},
+                    {field: 'creator', title: '创建者', align: 'center', width: 140, sort: true},
+                    {
+                        field: 'createdate',
+                        title: '创建时间',
+                        align: 'center',
+                        width: 170,
+                        sort: true,
+                        templet: function (d) {
+                            return admin.formatDate(d.createdate + '' + d.createtime);
+                        }
+                    },
+                    {
+                        field: 'lastsaved',
+                        title: '最后更新时间',
+                        align: 'center',
+                        width: 170,
+                        sort: true,
+                        templet: function (d) {
+                            return admin.formatDate(d.lastsaved);
+                        }
+                    }
+                ]
+            ]
+        });
+
+        $("#notice-search-btn").click(function () {
+            table.reload('noticeSearchTable', {
+                where: {
+                    startdate: $("#notice-search-startdate").val(),
+                    enddate: $("#notice-search-enddate").val(),
+                    searchkey: $("#notice-search-searchkey").val()
+                }, page: {curr: 1}
+            });
+        });
+
+        $("#notice-search-btn-reset").click(function () {
+            $("#notice-search-startdate").val("");
+            $("#notice-search-enddate").val("");
+            $("#notice-search-searchkey").val("");
+        });
+
+        $("#btn-notice-add").click(function () {
+            showNoticeModel("新增公告", {
+                noticeId: '',
+                platform: 'all',
+                title: '',
+                content: '',
+                linkurl: ''
+            })
+        });
+
+        function showNoticeModel(title, data) {
+            admin.putTempData('t_noticeTmp', data);
+            admin.popupCenter({
+                title: title,
+                area: '650px',
+                path: '[[@{/notice/load4add}]]',
+                finish: function () {
+                    table.reload('noticeSearchTable', {});
+                }
+            });
+        }
+
+        function showNoticePublishModel(data) {
+            admin.putTempData('t_noticePubidTmp', {
+                noticeId: data.id,
+                pushmode: 'atonce',
+                settime: ''
+            });
+            admin.popupCenter({
+                title: '发布通知公告【' + data.title + '】',
+                area: '550px',
+                path: '[[@{/notice/load4publish}]]',
+                finish: function () {
+                    table.reload('noticeSearchTable', {});
+                }
+            });
+        }
+
+        function deleteNotice(data) {
+            layer.confirm('确定删除公告【' + data.title + '】吗?', function (i) {
+                layer.close(i);
+                layer.load(2);
+                admin.go('[[@{/notice/delete}]]', {
+                    noticeId: data.id,
+                    _csrf: $("meta[name='_csrf_token']").attr("value")
+                }, function (data) {
+                    layer.closeAll('loading');
+                    if (data.code == 200) {
+                        layer.msg(data.msg, {icon: 1});
+                    } else if (data.code == 401) {
+                        layer.msg(data.msg, {icon: 2, time: 1500}, function () {
+                            location.replace('[[@{/login}]]');
+                        }, 1000);
+                        return;
+                    } else {
+                        layer.msg(data.msg, {icon: 2});
+                    }
+                    table.reload('noticeSearchTable');
+                }, function (ret) {
+                    console.log(ret);
+                    admin.errorBack(ret);
+                });
+            })
+        }
+
+        function showNoticePublishDetailsModel(data) {
+            admin.putTempData('t_noticePublishDetail', data);
+            admin.popupCenter({
+                title: '通知公告【' + data.title + '】发布详情',
+                area: '750px',
+                path: '[[@{/notice/load4detail}]]',
+                finish: function () {
+                    table.reload('noticeSearchTable', {});
+                }
+            });
+        }
+
+
+        //监听单元格
+        table.on('tool(noticeSearchTable-filter)', function (obj) {
+            var data = obj.data;
+            switch (obj.event) {
+                case "publish":
+                    showNoticePublishModel(data);
+                    break;
+                case "modify":
+                    showNoticeModel("修改公告", {
+                        noticeId: data.id,
+                        platform: data.platform,
+                        title: data.title,
+                        content: data.content,
+                        linkurl: data.linkurl
+                    });
+                    break;
+                case "delete":
+                    deleteNotice(data);
+                    break;
+                case "detail":
+                    showNoticePublishDetailsModel(data);
+                    break;
+            }
+        });
+    });
+</script>
diff --git a/payapi/src/main/resources/templates/system/notice/publish.html b/payapi/src/main/resources/templates/system/notice/publish.html
new file mode 100644
index 0000000..621749b
--- /dev/null
+++ b/payapi/src/main/resources/templates/system/notice/publish.html
@@ -0,0 +1,85 @@
+<form id="notice-publish-form" lay-filter="notice-publish-form" class="layui-form model-form">
+    <input name="noticeId" id="hidden-notice-publish-noticeid" type="hidden"/>
+    <div class="layui-form-item">
+        <label class="layui-form-label"><span style="color: red;">*</span>发布时间</label>
+        <div class="layui-input-block">
+            <input type="radio" name="pushmode" value="atonce" title="立即发布" lay-filter="notice-publish-form-pushmode-filter"/>
+            <input type="radio" name="pushmode" value="delay" title="定时发布" lay-filter="notice-publish-form-pushmode-filter"/>
+        </div>
+    </div>
+    <div class="layui-form-item" id="hidden-div-set-delaytime" style="display: none;">
+        <label class="layui-form-label"><span style="color: red;">*</span>设定时间</label>
+        <div class="layui-input-block">
+            <input name="settime" type="text" id="notice-publish-form-settime" style="width: 195px;" class="layui-input" autocomplete="off" lay-verify="pushmode"/>
+        </div>
+    </div>
+
+    <div class="layui-form-item model-form-footer">
+        <button class="layui-btn layui-btn-primary" type="button" ew-event="closeDialog">取消</button>
+        <button class="layui-btn" lay-filter="notice-publish-form-submit" lay-submit id="notice-publish-submit-btn">保存</button>
+    </div>
+</form>
+
+<script>
+    layui.use(['layer', 'admin', 'form', 'laydate'], function () {
+        var layer = layui.layer;
+        var admin = layui.admin;
+        var form = layui.form;
+        var laydate = layui.laydate;
+
+        form.render('radio');
+        form.verify({
+            pushmode: function (value, item) {
+                var pushmode = $("#notice-publish-form").find("input[name='pushmode']:checked").val();
+                if('atonce' != pushmode && isempty(value)){
+                    return "定时发布请设定时间";
+                }
+            }
+        });
+        form.on('radio(notice-publish-form-pushmode-filter)', function (data) {
+            if('atonce'==data.value){
+                $("#notice-publish-form-settime").val("");
+                $("#hidden-div-set-delaytime").hide();
+            }else{
+                $("#hidden-div-set-delaytime").show();
+            }
+        });
+        laydate.render({
+            elem: '#notice-publish-form-settime',
+            type: 'datetime',
+            min: moment().locale('zh-cn').format('YYYY-MM-DD HH:mm:ss'),
+            trigger: 'click'
+        });
+        var bean = admin.getTempData('t_noticePubidTmp');
+        if (bean) {
+            form.val('notice-publish-form', bean);
+        }
+
+        // 表单提交事件
+        form.on('submit(notice-publish-form-submit)', function (data) {
+            layer.load(2);
+            var param = data.field;
+            var token = $("meta[name='_csrf_token']").attr("value");
+            param["_csrf"] = token;
+            admin.go('[[@{/notice/publish}]]', param, function (result) {
+                console.log(result);
+                layer.closeAll('loading');
+                if (result.code == 200) {
+                    layer.msg(result.msg, {icon: 1});
+                    admin.finishPopupCenter();
+                } else if (result.code == 401) {
+                    layer.msg(result.msg, {icon: 2, time: 1500}, function () {
+                        location.replace('[[@{/login}]]');
+                    }, 1000);
+                    return;
+                } else {
+                    layer.msg(result.msg, {icon: 2});
+                }
+            }, function (ret) {
+                console.log(ret);
+                admin.errorBack(ret);
+            });
+            return false;
+        });
+    });
+</script>
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 35b2274..a1a579b 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,3 +1,3 @@
 rootProject.name = 'payapi'
-include 'payapi', 'payapi-sdk', 'payapi-common', 'ynrcc-agent','oauth'
+include 'payapi', 'payapi-sdk', 'payapi-common', 'ynrcc-agent','oauth','bus-qrcode'
 
diff --git a/ynrcc-agent/src/main/java/com/supwisdom/agent/api/bean/DlpayReq.java b/ynrcc-agent/src/main/java/com/supwisdom/agent/api/bean/DlpayReq.java
index 7fae31a..bc5954f 100644
--- a/ynrcc-agent/src/main/java/com/supwisdom/agent/api/bean/DlpayReq.java
+++ b/ynrcc-agent/src/main/java/com/supwisdom/agent/api/bean/DlpayReq.java
@@ -35,6 +35,8 @@
 
   private String oriSn; //原应用系统唯一流水号
   private String chkdate; //对账日期
+  private String captcha; //签约验证码
+  private String scenario;//消费场景
 
   /**
    * 市民卡绑定请求XML
@@ -49,7 +51,7 @@
         .append("<CATEGORIE>").append(categorie).append("</CATEGORIE>")
         .append("<SN>").append(sn).append("</SN>")
         .append("<BC_NO>").append(bcNo).append("</BC_NO>")
-        .append("<NAME>").append(name).append("</NAME>")
+       .append("<NAME>").append(name).append("</NAME>")
         .append("<ID_TYPE>").append(idType).append("</ID_TYPE>")
         .append("<ID_NO>").append(idNo).append("</ID_NO>")
         .append("<PHONE>").append(phone).append("</PHONE>")
@@ -75,7 +77,8 @@
         .append("<ID_NO>").append(idNo).append("</ID_NO>")
         .append("<PHONE>").append(phone).append("</PHONE>")
         .append("<TRANS_TYPE>").append(transType).append("</TRANS_TYPE>")
-        .append("</root>");
+        .append("<CAPTCHA>").append(captcha).append("</CAPTCHA>")
+            .append("</root>");
     return String.format("%08d", xml.toString().getBytes("GBK").length) + xml.toString();
   }
 
@@ -290,4 +293,20 @@
   public void setTranscode(String transcode) {
     this.transcode = transcode;
   }
+
+  public String getCaptcha() {
+    return captcha;
+  }
+
+  public void setCaptcha(String captcha) {
+    this.captcha = captcha;
+  }
+
+  public String getScenario() {
+    return scenario;
+  }
+
+  public void setScenario(String scenario) {
+    this.scenario = scenario;
+  }
 }
diff --git a/ynrcc-agent/src/main/java/com/supwisdom/agent/api/controller/YnrccApiController.java b/ynrcc-agent/src/main/java/com/supwisdom/agent/api/controller/YnrccApiController.java
index 56e4d60..d7dfcb4 100644
--- a/ynrcc-agent/src/main/java/com/supwisdom/agent/api/controller/YnrccApiController.java
+++ b/ynrcc-agent/src/main/java/com/supwisdom/agent/api/controller/YnrccApiController.java
@@ -77,10 +77,12 @@
                                 @FormParam("transtime") String transtime, @FormParam("categorie") String categorie,
                                 @FormParam("refno") String refno, @FormParam("bankcardno") String bankcardno,
                                 @FormParam("username") String username, @FormParam("idtype") String idtype,
-                                @FormParam("idno") String idno, @FormParam("phone") String phone,
+                                @FormParam("idno") String idno,
+                                @FormParam("phone") String phone,
                                 @FormParam("sign_type") String sign_type, @FormParam("sign") String sign) {
     DlpayResp resp = new DlpayResp();
-    if (!ynrccParamCheckService.checkBindCardParam(transcode, transdate, transtime, refno, bankcardno, username, idtype, idno, phone, categorie, sign_type, sign, resp)) {
+    if (!ynrccParamCheckService.checkBindCardParam(transcode, transdate, transtime, refno, bankcardno, username, idtype, idno,
+            phone, categorie, sign_type, sign, resp)) {
       logger.error(resp.errPrint());
       return resp;
     }
@@ -136,10 +138,11 @@
                                 @FormParam("bankcardno") String bankcardno, @FormParam("username") String username,
                                 @FormParam("idtype") String idtype, @FormParam("idno") String idno,
                                 @FormParam("phone") String phone,
+                                @FormParam("captcha")String captcha,
                                 @FormParam("sign_type") String sign_type, @FormParam("sign") String sign) {
     DlpayResp resp = new DlpayResp();
     if (!ynrccParamCheckService.checkSignCardParam(transcode, transdate, transtime, refno, categorie, bankcardno,
-        username, idtype, idno, phone, transtype, sign_type, sign, resp)) {
+        username, idtype, idno, phone, captcha,transtype, sign_type, sign, resp)) {
       logger.error(resp.errPrint());
       return resp;
     }
@@ -156,6 +159,7 @@
     params.put("idno", idno);
     params.put("phone", phone);
     params.put("transtype", transtype);
+    params.put("captcha",captcha);
     params.put("sign_type", sign_type);
     params.put("sign", sign);
     if (!checkYnrccSign(params, resp)) {
@@ -175,6 +179,7 @@
       req.setIdNo(idno);
       req.setPhone(phone);
       req.setTransType(transtype);
+      req.setCaptcha(captcha);
       return ynrccApiService.sendToYnrcc(DlpayUtil.OPTYPE_SIGNCARD, req);
     } catch (BussinessException be) {
       resp.setCode(ErrorCode.BUSSINESS_ERROR);
@@ -199,11 +204,12 @@
                            @FormParam("merchant_bankcardno") String merchant_bankcardno,
                            @FormParam("merchant_bankaccname") String merchant_bankaccname,
                            @FormParam("amount") Integer amount,
+                           @FormParam("scenario") String scenario,
                            @FormParam("description") String description,
                            @FormParam("sign_type") String sign_type, @FormParam("sign") String sign) {
     DlpayResp resp = new DlpayResp();
     if (!ynrccParamCheckService.checkCardPayParam(transcode, transdate, transtime, refno, categorie, bankcardno,
-        username, idtype, idno, merchant_bankcardno, merchant_bankaccname, amount, description, sign_type, sign, resp)) {
+        username, idtype, idno, merchant_bankcardno, merchant_bankaccname, amount,scenario, description, sign_type, sign, resp)) {
       logger.error(resp.errPrint());
       return resp;
     }
@@ -216,11 +222,12 @@
     params.put("categorie", categorie);
     params.put("bankcardno", bankcardno);
     params.put("username", username);
-    params.put("idtype", idtype);
+    //params.put("idtype", idtype);
     params.put("idno", idno);
     params.put("merchant_bankcardno", merchant_bankcardno);
     params.put("merchant_bankaccname", merchant_bankaccname);
     params.put("amount", String.valueOf(amount));
+    params.put("scenario",scenario);
     params.put("description", description);
     params.put("sign_type", sign_type);
     params.put("sign", sign);
@@ -242,6 +249,7 @@
       req.setMerchantBcno(merchant_bankcardno);
       req.setMerchantName(merchant_bankaccname);
       req.setAmount(amount);
+      req.setScenario(scenario);
       req.setDescription(description);
       return ynrccApiService.sendToYnrcc(DlpayUtil.OPTYPE_CARDPAY, req);
     } catch (BussinessException be) {
diff --git a/ynrcc-agent/src/main/java/com/supwisdom/agent/api/service/YnrccParamCheckService.java b/ynrcc-agent/src/main/java/com/supwisdom/agent/api/service/YnrccParamCheckService.java
index 4eb674b..9673aff 100644
--- a/ynrcc-agent/src/main/java/com/supwisdom/agent/api/service/YnrccParamCheckService.java
+++ b/ynrcc-agent/src/main/java/com/supwisdom/agent/api/service/YnrccParamCheckService.java
@@ -12,10 +12,10 @@
   boolean checkSign(Map<String, String> param);
 
   boolean checkSignCardParam(String transcode, String transdate, String transtime, String refno, String categorie, String bankcardno, String username,
-                             String idtype, String idno, String phone, String transtype, String sign_type, String sign, DlpayResp resp);
+                             String idtype, String idno, String phone, String captcha,String transtype, String sign_type, String sign, DlpayResp resp);
 
   boolean checkCardPayParam(String transcode, String transdate, String transtime, String refno, String categorie, String bankcardno, String username,
-                            String idtype, String idno, String merchant_bankcardno, String merchant_bankaccname, Integer amount, String description,
+                            String idtype, String idno, String merchant_bankcardno, String merchant_bankaccname, Integer amount,String scenario, String description,
                             String sign_type, String sign, DlpayResp resp);
 
   boolean checkPayRefundParam(String transcode, String transdate, String transtime, String refno, String refundRefno, Integer amount, String description,
diff --git a/ynrcc-agent/src/main/java/com/supwisdom/agent/api/service/impl/YnrccParamCheckServiceImpl.java b/ynrcc-agent/src/main/java/com/supwisdom/agent/api/service/impl/YnrccParamCheckServiceImpl.java
index 069bf8f..bea60e8 100644
--- a/ynrcc-agent/src/main/java/com/supwisdom/agent/api/service/impl/YnrccParamCheckServiceImpl.java
+++ b/ynrcc-agent/src/main/java/com/supwisdom/agent/api/service/impl/YnrccParamCheckServiceImpl.java
@@ -58,7 +58,8 @@
   }
 
   @Override
-  public boolean checkBindCardParam(String transcode, String transdate, String transtime, String refno, String bankcardno, String username, String idtype, String idno, String phone, String categorie, String sign_type, String sign, DlpayResp resp) {
+  public boolean checkBindCardParam(String transcode, String transdate, String transtime, String refno, String bankcardno, String username, String idtype, String idno,
+                                    String phone, String categorie, String sign_type, String sign, DlpayResp resp) {
     if (!checkYnrccBaseParam(transcode, transdate, transtime, refno, sign_type, sign, resp)) {
       return false;
     }
@@ -120,8 +121,9 @@
 
   @Override
   public boolean checkSignCardParam(String transcode, String transdate, String transtime, String refno, String categorie, String bankcardno, String username,
-                                    String idtype, String idno, String phone, String transtype, String sign_type, String sign, DlpayResp resp) {
-    if (!checkBindCardParam(transcode, transdate, transtime, refno, bankcardno, username, idtype, idno, phone, categorie, sign_type, sign, resp)) {
+                                    String idtype, String idno, String phone, String captcha,String transtype, String sign_type, String sign, DlpayResp resp) {
+    if (!checkBindCardParam(transcode, transdate, transtime, refno, bankcardno, username, idtype, idno,
+             phone, categorie, sign_type, sign, resp)) {
       return false;
     }
     if (StringUtil.isEmpty(transtype)) {
@@ -129,12 +131,17 @@
       resp.setMessage("请求参数错误[签约标志]");
       return false;
     }
+    if (StringUtil.isEmpty(captcha)) {
+      resp.setCode(ErrorCode.REQ_PARAM_ERROR);
+      resp.setMessage("请求参数错误[验证码为空]");
+      return false;
+    }
 
     return true;
   }
 
   @Override
-  public boolean checkCardPayParam(String transcode, String transdate, String transtime, String refno, String categorie, String bankcardno, String username, String idtype, String idno, String merchant_bankcardno, String merchant_bankaccname, Integer amount, String description, String sign_type, String sign, DlpayResp resp) {
+  public boolean checkCardPayParam(String transcode, String transdate, String transtime, String refno, String categorie, String bankcardno, String username, String idtype, String idno, String merchant_bankcardno, String merchant_bankaccname, Integer amount, String scenario,String description, String sign_type, String sign, DlpayResp resp) {
     if (!checkYnrccBaseParam(transcode, transdate, transtime, refno, sign_type, sign, resp)) {
       return false;
     }
@@ -179,6 +186,11 @@
       resp.setMessage("请求参数错误[交易金额为空]");
       return false;
     }
+    if (null == scenario) {
+      resp.setCode(ErrorCode.REQ_PARAM_ERROR);
+      resp.setMessage("请求参数错误[交易场景为空]");
+      return false;
+    }
 
     return true;
   }