xiongzhu 3 лет назад
Родитель
Сommit
0fc7cb73b6

+ 1 - 0
src/main/java/com/izouma/nineth/config/AdapayProperties.java

@@ -6,6 +6,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
 @Data
 @ConfigurationProperties(prefix = "adapay")
 public class AdapayProperties {
+    private String  merchant;
     private String  appId;
     private boolean debug;
     private boolean prod;

+ 40 - 0
src/main/java/com/izouma/nineth/domain/AdapayMerchant.java

@@ -0,0 +1,40 @@
+package com.izouma.nineth.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+
+@Data
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class AdapayMerchant extends BaseEntity {
+
+    @Column(unique = true, length = 72)
+    private String name;
+
+    @Column(unique = true, length = 72)
+    private String appId;
+
+    private String apiKey;
+
+    private String mockKey;
+
+    @Column(columnDefinition = "TEXT")
+    private String publicKey;
+
+    @Column(columnDefinition = "TEXT")
+    private String privKey;
+
+    private String wxAppId;
+
+    private String status;
+
+    private boolean selected;
+
+}

+ 4 - 0
src/main/java/com/izouma/nineth/dto/UserBankCard.java

@@ -28,4 +28,8 @@ public class UserBankCard extends BaseEntity {
     private String bankNo;
 
     private String phone;
+
+    private String realName;
+
+    private String idNo;
 }

+ 116 - 0
src/main/java/com/izouma/nineth/dto/adapay/AccountInfo.java

@@ -0,0 +1,116 @@
+package com.izouma.nineth.dto.adapay;
+
+import com.alibaba.fastjson.annotation.JSONField;
+
+public class AccountInfo{
+
+	@JSONField(name="bank_code")
+	private String bankCode;
+
+	@JSONField(name="cert_type")
+	private String certType;
+
+	@JSONField(name="tel_no")
+	private String telNo;
+
+	@JSONField(name="area_code")
+	private String areaCode;
+
+	@JSONField(name="bank_name")
+	private String bankName;
+
+	@JSONField(name="card_name")
+	private String cardName;
+
+	@JSONField(name="cert_id")
+	private String certId;
+
+	@JSONField(name="bank_acct_type")
+	private String bankAcctType;
+
+	@JSONField(name="prov_code")
+	private String provCode;
+
+	@JSONField(name="card_id")
+	private String cardId;
+
+	public void setBankCode(String bankCode){
+		this.bankCode = bankCode;
+	}
+
+	public String getBankCode(){
+		return bankCode;
+	}
+
+	public void setCertType(String certType){
+		this.certType = certType;
+	}
+
+	public String getCertType(){
+		return certType;
+	}
+
+	public void setTelNo(String telNo){
+		this.telNo = telNo;
+	}
+
+	public String getTelNo(){
+		return telNo;
+	}
+
+	public void setAreaCode(String areaCode){
+		this.areaCode = areaCode;
+	}
+
+	public String getAreaCode(){
+		return areaCode;
+	}
+
+	public void setBankName(String bankName){
+		this.bankName = bankName;
+	}
+
+	public String getBankName(){
+		return bankName;
+	}
+
+	public void setCardName(String cardName){
+		this.cardName = cardName;
+	}
+
+	public String getCardName(){
+		return cardName;
+	}
+
+	public void setCertId(String certId){
+		this.certId = certId;
+	}
+
+	public String getCertId(){
+		return certId;
+	}
+
+	public void setBankAcctType(String bankAcctType){
+		this.bankAcctType = bankAcctType;
+	}
+
+	public String getBankAcctType(){
+		return bankAcctType;
+	}
+
+	public void setProvCode(String provCode){
+		this.provCode = provCode;
+	}
+
+	public String getProvCode(){
+		return provCode;
+	}
+
+	public void setCardId(String cardId){
+		this.cardId = cardId;
+	}
+
+	public String getCardId(){
+		return cardId;
+	}
+}

+ 206 - 0
src/main/java/com/izouma/nineth/dto/adapay/MemberInfo.java

@@ -0,0 +1,206 @@
+package com.izouma.nineth.dto.adapay;
+
+import com.alibaba.fastjson.annotation.JSONField;
+
+import java.util.List;
+
+public class MemberInfo {
+
+    @JSONField(name = "member_id")
+    private String memberId;
+
+    @JSONField(name = "created_time")
+    private String createdTime;
+
+    @JSONField(name = "cert_type")
+    private String certType;
+
+    @JSONField(name = "gender")
+    private String gender;
+
+    @JSONField(name = "identified")
+    private String identified;
+
+    @JSONField(name = "tel_no")
+    private String telNo;
+
+    @JSONField(name = "prod_mode")
+    private String prodMode;
+
+    @JSONField(name = "nickname")
+    private String nickname;
+
+    @JSONField(name = "disabled")
+    private String disabled;
+
+    @JSONField(name = "location")
+    private String location;
+
+    @JSONField(name = "app_id")
+    private String appId;
+
+    @JSONField(name = "settle_accounts")
+    private List<SettleAccountsItem> settleAccounts;
+
+    @JSONField(name = "email")
+    private String email;
+
+    @JSONField(name = "object")
+    private String object;
+
+    @JSONField(name = "status")
+    private String status;
+
+    @JSONField(name = "error_type")
+    private String errorType;
+
+    @JSONField(name = "error_code")
+    private String errorCode;
+
+    @JSONField(name = "error_msg")
+    private String errorMsg;
+
+    public void setMemberId(String memberId) {
+        this.memberId = memberId;
+    }
+
+    public String getMemberId() {
+        return memberId;
+    }
+
+    public void setCreatedTime(String createdTime) {
+        this.createdTime = createdTime;
+    }
+
+    public String getCreatedTime() {
+        return createdTime;
+    }
+
+    public void setCertType(String certType) {
+        this.certType = certType;
+    }
+
+    public String getCertType() {
+        return certType;
+    }
+
+    public void setGender(String gender) {
+        this.gender = gender;
+    }
+
+    public String getGender() {
+        return gender;
+    }
+
+    public void setIdentified(String identified) {
+        this.identified = identified;
+    }
+
+    public String getIdentified() {
+        return identified;
+    }
+
+    public void setTelNo(String telNo) {
+        this.telNo = telNo;
+    }
+
+    public String getTelNo() {
+        return telNo;
+    }
+
+    public void setProdMode(String prodMode) {
+        this.prodMode = prodMode;
+    }
+
+    public String getProdMode() {
+        return prodMode;
+    }
+
+    public void setNickname(String nickname) {
+        this.nickname = nickname;
+    }
+
+    public String getNickname() {
+        return nickname;
+    }
+
+    public void setDisabled(String disabled) {
+        this.disabled = disabled;
+    }
+
+    public String getDisabled() {
+        return disabled;
+    }
+
+    public void setLocation(String location) {
+        this.location = location;
+    }
+
+    public String getLocation() {
+        return location;
+    }
+
+    public void setAppId(String appId) {
+        this.appId = appId;
+    }
+
+    public String getAppId() {
+        return appId;
+    }
+
+    public void setSettleAccounts(List<SettleAccountsItem> settleAccounts) {
+        this.settleAccounts = settleAccounts;
+    }
+
+    public List<SettleAccountsItem> getSettleAccounts() {
+        return settleAccounts;
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setObject(String object) {
+        this.object = object;
+    }
+
+    public String getObject() {
+        return object;
+    }
+
+    public void setStatus(String status) {
+        this.status = status;
+    }
+
+    public String getStatus() {
+        return status;
+    }
+
+    public String getErrorType() {
+        return errorType;
+    }
+
+    public void setErrorType(String errorType) {
+        this.errorType = errorType;
+    }
+
+    public String getErrorCode() {
+        return errorCode;
+    }
+
+    public void setErrorCode(String errorCode) {
+        this.errorCode = errorCode;
+    }
+
+    public String getErrorMsg() {
+        return errorMsg;
+    }
+
+    public void setErrorMsg(String errorMsg) {
+        this.errorMsg = errorMsg;
+    }
+}

+ 94 - 0
src/main/java/com/izouma/nineth/dto/adapay/SettleAccountsItem.java

@@ -0,0 +1,94 @@
+package com.izouma.nineth.dto.adapay;
+
+import com.alibaba.fastjson.annotation.JSONField;
+
+public class SettleAccountsItem{
+
+	@JSONField(name="member_id")
+	private String memberId;
+
+	@JSONField(name="upd_ts")
+	private long updTs;
+
+	@JSONField(name="account_info")
+	private AccountInfo accountInfo;
+
+	@JSONField(name="channel")
+	private String channel;
+
+	@JSONField(name="id")
+	private String id;
+
+	@JSONField(name="type")
+	private String type;
+
+	@JSONField(name="app_id")
+	private String appId;
+
+	@JSONField(name="cre_ts")
+	private long creTs;
+
+	public void setMemberId(String memberId){
+		this.memberId = memberId;
+	}
+
+	public String getMemberId(){
+		return memberId;
+	}
+
+	public void setUpdTs(long updTs){
+		this.updTs = updTs;
+	}
+
+	public long getUpdTs(){
+		return updTs;
+	}
+
+	public void setAccountInfo(AccountInfo accountInfo){
+		this.accountInfo = accountInfo;
+	}
+
+	public AccountInfo getAccountInfo(){
+		return accountInfo;
+	}
+
+	public void setChannel(String channel){
+		this.channel = channel;
+	}
+
+	public String getChannel(){
+		return channel;
+	}
+
+	public void setId(String id){
+		this.id = id;
+	}
+
+	public String getId(){
+		return id;
+	}
+
+	public void setType(String type){
+		this.type = type;
+	}
+
+	public String getType(){
+		return type;
+	}
+
+	public void setAppId(String appId){
+		this.appId = appId;
+	}
+
+	public String getAppId(){
+		return appId;
+	}
+
+	public void setCreTs(long creTs){
+		this.creTs = creTs;
+	}
+
+	public long getCreTs(){
+		return creTs;
+	}
+}

+ 16 - 0
src/main/java/com/izouma/nineth/repo/AdapayMerchantRepo.java

@@ -0,0 +1,16 @@
+package com.izouma.nineth.repo;
+
+import com.izouma.nineth.domain.AdapayMerchant;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+
+import javax.transaction.Transactional;
+
+public interface AdapayMerchantRepo extends JpaRepository<AdapayMerchant, Long>, JpaSpecificationExecutor<AdapayMerchant> {
+    @Query("update AdapayMerchant t set t.del = true where t.id = ?1")
+    @Modifying
+    @Transactional
+    void softDelete(Long id);
+}

+ 259 - 0
src/main/java/com/izouma/nineth/service/AdapayMerchantService.java

@@ -0,0 +1,259 @@
+package com.izouma.nineth.service;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.huifu.adapay.Adapay;
+import com.huifu.adapay.core.exception.BaseAdaPayException;
+import com.huifu.adapay.model.*;
+import com.izouma.nineth.config.AdapayProperties;
+import com.izouma.nineth.domain.AdapayMerchant;
+import com.izouma.nineth.dto.PageQuery;
+import com.izouma.nineth.dto.adapay.MemberInfo;
+import com.izouma.nineth.dto.adapay.SettleAccountsItem;
+import com.izouma.nineth.exception.BusinessException;
+import com.izouma.nineth.repo.AdapayMerchantRepo;
+import com.izouma.nineth.utils.JpaUtils;
+import com.izouma.nineth.utils.ObjUtils;
+import com.izouma.nineth.utils.SnowflakeIdWorker;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections.MapUtils;
+import org.springframework.data.domain.Page;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+@Service
+@AllArgsConstructor
+@Slf4j
+public class AdapayMerchantService {
+
+    private final AdapayMerchantRepo adapayMerchantRepo;
+    private final AdapayProperties   adapayProperties;
+
+    @PostConstruct
+    public void init() {
+        log.info("init AdapayMerchantService");
+        for (AdapayMerchant merchant : adapayMerchantRepo.findAll()) {
+            addMerchant(merchant);
+            if (merchant.isSelected()) {
+                try {
+                    select(merchant.getId());
+                    log.info("select merchant success={}", merchant.getName());
+                } catch (Exception e) {
+                    log.error("select merchant error", e);
+                }
+            }
+        }
+    }
+
+    public Page<AdapayMerchant> all(PageQuery pageQuery) {
+        return adapayMerchantRepo.findAll(JpaUtils.toSpecification(pageQuery, AdapayMerchant.class), JpaUtils.toPageRequest(pageQuery));
+    }
+
+    public AdapayMerchant save(AdapayMerchant record) {
+        record = adapayMerchantRepo.save(record);
+        addMerchant(record);
+        return record;
+    }
+
+    public AdapayMerchant update(AdapayMerchant record) {
+        if (record.getId() == null) {
+            throw new BusinessException("id不能为空");
+        }
+        AdapayMerchant orig = adapayMerchantRepo.findById(record.getId()).orElseThrow(new BusinessException("无记录"));
+        ObjUtils.merge(orig, record);
+        record = adapayMerchantRepo.save(orig);
+        addMerchant(record);
+        return record;
+    }
+
+    public void select(Long id) throws Exception {
+        AdapayMerchant merchant = adapayMerchantRepo.findById(id).orElseThrow(new BusinessException("商户不存在"));
+
+        MerConfig merConfig = new MerConfig();
+        merConfig.setApiKey(merchant.getApiKey());
+        merConfig.setApiMockKey(merchant.getMockKey());
+        merConfig.setRSAPrivateKey(merchant.getPrivKey());
+        merConfig.setRSAPublicKey(merchant.getPublicKey());
+        Adapay.initWithMerConfig(merConfig);
+
+        adapayProperties.setApiKey(merchant.getApiKey());
+        adapayProperties.setMockKey(merchant.getMockKey());
+        adapayProperties.setPrivKey(merchant.getPrivKey());
+        adapayProperties.setPublicKey(merchant.getPublicKey());
+        adapayProperties.setMerchant(merchant.getName());
+        adapayProperties.setAppId(merchant.getAppId());
+
+        List<AdapayMerchant> list = adapayMerchantRepo.findAll();
+        list.forEach(m -> m.setSelected(m.getId().equals(id)));
+        adapayMerchantRepo.saveAll(list);
+    }
+
+    public void addMerchant(AdapayMerchant merchant) {
+        try {
+            MerConfig merConfig = new MerConfig();
+            merConfig.setApiKey(merchant.getApiKey());
+            merConfig.setApiMockKey(merchant.getMockKey());
+            merConfig.setRSAPrivateKey(merchant.getPrivKey());
+            merConfig.setRSAPublicKey(merchant.getPublicKey());
+            Adapay.addMerConfig(merConfig, merchant.getName());
+        } catch (Exception e) {
+            log.error("add Merchant Error", e);
+        }
+    }
+
+    public void createMemberForAll(String memberId, String tel, String realName, String idno) throws BaseAdaPayException {
+        AtomicReference<BaseAdaPayException> ex = new AtomicReference<>();
+        adapayMerchantRepo.findAll().parallelStream().forEach(merchant -> {
+            try {
+                createMember(merchant.getName(), merchant.getAppId(), memberId, tel, realName, idno);
+            } catch (BaseAdaPayException e) {
+                log.error("createMemberForAll", e);
+                ex.set(e);
+            }
+        });
+        if (ex.get() != null) {
+            throw ex.get();
+        }
+    }
+
+    public void createMember(String merchant, String appId, String memberId, String tel, String realName, String idno) throws BaseAdaPayException {
+        Map<String, Object> memberParams = new HashMap<>();
+        memberParams.put("adapay_func_code", "members.realname");
+        memberParams.put("member_id", memberId);
+        memberParams.put("app_id", appId);
+        memberParams.put("tel_no", tel);
+        memberParams.put("user_name", realName);
+        memberParams.put("cert_type", "00");
+        memberParams.put("cert_id", idno);
+        Map<String, Object> res = AdapayCommon.requestAdapay(memberParams, merchant);
+        log.info("createMember merchant={} response={}", merchant, JSON.toJSONString(res, SerializerFeature.PrettyFormat));
+        if (!("succeeded".equals(MapUtils.getString(res, "status"))
+                || "member_id_exists".equals(MapUtils.getString(res, "error_code")))) {
+            String errMsg = MapUtils.getString(res, "error_msg");
+            String errCode = MapUtils.getString(res, "error_code");
+            log.error("createMember error merchant={} memberId={}", merchant, memberId);
+            throw new BusinessException(errMsg + "(" + errCode + ")");
+        }
+    }
+
+    public String createSettleAccountForAll(String memberId, String realName, String idNo, String phone, String bankNo) throws BaseAdaPayException {
+        String id = null;
+        for (AdapayMerchant merchant : adapayMerchantRepo.findAll()) {
+            id = createSettleAccount(merchant.getName(), merchant.getAppId(), memberId, realName, idNo, phone, bankNo);
+        }
+        return id;
+    }
+
+    public String createSettleAccount(String merchant, String appId, String memberId, String realName, String idNo, String phone, String bankNo) throws BaseAdaPayException {
+        Map<String, Object> settleCountParams = new HashMap<>();
+        Map<String, Object> accountInfo = new HashMap<>();
+        accountInfo.put("card_id", bankNo);
+        accountInfo.put("card_name", realName);
+        accountInfo.put("cert_id", idNo);
+        accountInfo.put("cert_type", "00");
+        accountInfo.put("tel_no", phone);
+        accountInfo.put("bank_acct_type", "2");
+        settleCountParams.put("member_id", memberId);
+        settleCountParams.put("app_id", appId);
+        settleCountParams.put("channel", "bank_account");
+        settleCountParams.put("account_info", accountInfo);
+        Map<String, Object> res = SettleAccount.create(settleCountParams, merchant);
+        log.info("createSettleAccount merchant={} response={}", merchant, JSON.toJSONString(res, SerializerFeature.PrettyFormat));
+        if (!("succeeded".equals(MapUtils.getString(res, "status"))
+                || "account_exists".equals(MapUtils.getString(res, "error_code")))) {
+            String errMsg = MapUtils.getString(res, "error_msg");
+            String errCode = MapUtils.getString(res, "error_code");
+            log.error("createSettleAccount error merchant={} memberId={}", merchant, memberId);
+            throw new BusinessException(errMsg + "(" + errCode + ")");
+        }
+        return MapUtils.getString(res, "id");
+    }
+
+    public void delSettleAccountForAll(String memberId) throws BaseAdaPayException {
+        for (AdapayMerchant merchant : adapayMerchantRepo.findAll()) {
+            delSettleAccount(merchant.getName(), merchant.getAppId(), memberId);
+        }
+    }
+
+    public void delSettleAccount(String merchant, String appId, String memberId) throws BaseAdaPayException {
+        Map<String, Object> memberParams = new HashMap<>();
+        memberParams.put("member_id", memberId);
+        memberParams.put("app_id", appId);
+        Map<String, Object> member = Member.query(memberParams, merchant);
+        log.info("query member, merchant={} member={} res={}",
+                merchant, memberId, JSON.toJSONString(member, SerializerFeature.PrettyFormat));
+        MemberInfo memberInfo = JSON.parseObject(JSON.toJSONString(member), MemberInfo.class);
+        if ("succeeded".equals(memberInfo.getStatus())) {
+            if (memberInfo.getSettleAccounts() != null && memberInfo.getSettleAccounts().size() > 0) {
+                SettleAccountsItem settleAccountsItem = memberInfo.getSettleAccounts().get(0);
+                Map<String, Object> settleCountParams = new HashMap<>();
+                settleCountParams.put("settle_account_id", settleAccountsItem.getId());
+                settleCountParams.put("member_id", memberId);
+                settleCountParams.put("app_id", appId);
+                Map<String, Object> settleCount = SettleAccount.delete(settleCountParams, merchant);
+                log.info("delSettleAccount, merchant={} member={} res={}",
+                        merchant, memberId, JSON.toJSONString(settleCount, SerializerFeature.PrettyFormat));
+                checkSuccess(settleCount);
+            }
+        } else if ("member_not_exists".equals(memberInfo.getErrorCode())) {
+            log.info("delSettleAccount member_not_exists, merchant={} member={}", merchant, memberId);
+        } else {
+            throw new BusinessException(memberInfo.getErrorMsg() + memberInfo.getErrorCode());
+        }
+    }
+
+    public void queryMembers(String merchant, String appId) {
+        boolean hasMore = true;
+        while (hasMore) {
+            try {
+                Map<String, Object> memberParams = new HashMap<>(2);
+                memberParams.put("page_index", "1");
+                memberParams.put("app_id", appId);
+                memberParams.put("page_size", "20");
+                memberParams.put("created_gte", String.valueOf(System.currentTimeMillis() - 5 * 60 * 1000));
+                memberParams.put("created_lte", String.valueOf(System.currentTimeMillis()));
+                Map<String, Object> member = Member.queryList(memberParams);
+                MapUtils.getBoolean(member, "has_more", false);
+            } catch (Exception e) {
+                log.error("queryMembers error", e);
+                hasMore = false;
+            }
+        }
+    }
+
+    public Object query(Long merchantId, String id) throws BaseAdaPayException {
+        AdapayMerchant merchant = adapayMerchantRepo.findById(merchantId).orElseThrow(new BusinessException("商户不存在"));
+        Map<String, Object> map = Payment.query(id, merchant.getName());
+        log.info(JSON.toJSONString(map, SerializerFeature.PrettyFormat));
+        return map;
+    }
+
+    public Object refund(Long merchantId, String id) throws BaseAdaPayException {
+        AdapayMerchant merchant = adapayMerchantRepo.findById(merchantId).orElseThrow(new BusinessException("商户不存在"));
+        Map<String, Object> map = Payment.query(id, merchant.getName());
+        if (!"succeeded".equals(MapUtils.getString(map, "status"))) {
+            return map;
+        }
+        String amt = MapUtils.getString(map, "pay_amt");
+        Map<String, Object> refundParams = new HashMap<>();
+        refundParams.put("refund_amt", amt);
+        refundParams.put("refund_order_no", new SnowflakeIdWorker(0, 0).nextId() + "");
+        Map<String, Object> response = Refund.create(id, refundParams, merchant.getName());
+        log.info(JSON.toJSONString(response, SerializerFeature.PrettyFormat));
+        return response;
+    }
+
+    public static void checkSuccess(Map<String, Object> map) {
+        if (!"succeeded".equals(MapUtils.getString(map, "status"))) {
+            String errMsg = MapUtils.getString(map, "error_msg");
+            String errCode = MapUtils.getString(map, "error_code");
+            throw new BusinessException(errMsg + "(" + errCode + ")");
+        }
+    }
+}

+ 18 - 6
src/main/java/com/izouma/nineth/service/OrderService.java

@@ -13,6 +13,7 @@ import com.github.binarywang.wxpay.constant.WxPayConstants;
 import com.github.binarywang.wxpay.exception.WxPayException;
 import com.github.binarywang.wxpay.service.WxPayService;
 import com.google.common.base.Splitter;
+import com.huifu.adapay.Adapay;
 import com.huifu.adapay.core.exception.BaseAdaPayException;
 import com.huifu.adapay.model.AdapayCommon;
 import com.huifu.adapay.model.Payment;
@@ -447,7 +448,7 @@ public class OrderService {
 
             // 保存adapay的订单id,用于后续取消订单时的查询
             BoundSetOperations<String, Object> ops = redisTemplate.boundSetOps(RedisKeys.PAY_RECORD + order.getId());
-            ops.add(MapUtils.getString(response, "id"));
+            ops.add(adapayProperties.getMerchant() + "#" + MapUtils.getString(response, "id"));
             ops.expire(7, TimeUnit.DAYS);
         }
 
@@ -642,18 +643,29 @@ public class OrderService {
             if (transactionIds != null && transactionIds.size() > 0) {
                 AtomicInteger succeeded = new AtomicInteger();
                 AtomicInteger pending = new AtomicInteger();
-                transactionIds.parallelStream().forEach(transactionId -> {
+                transactionIds.parallelStream().forEach(s -> {
+                    String transactionIdStr = Optional.ofNullable(s).map(Object::toString).orElse("");
+                    String transactionId = null;
+                    String merchant = null;
+                    if (transactionIdStr.contains("#")) {
+                        String[] arr = transactionIdStr.split("#");
+                        merchant = arr[0];
+                        transactionId = arr[1];
+                    } else {
+                        merchant = Adapay.defaultMerchantKey;
+                        transactionId = transactionIdStr;
+                    }
                     try {
-                        Map<String, Object> map = Payment.query(transactionId.toString());
+                        Map<String, Object> map = Payment.query(transactionId, merchant);
                         if ("succeeded".equalsIgnoreCase(MapUtils.getString(map, "status"))) {
                             succeeded.getAndIncrement();
                         }
                         if ("pending".equalsIgnoreCase(MapUtils.getString(map, "status"))) {
                             pending.getAndIncrement();
                             // 未支付的订单调用关单接口
-                            Payment.close(new HashMap<>() {{
-                                put("payment_id", transactionId);
-                            }});
+                            Map<String, Object> closeParams = new HashMap<>();
+                            closeParams.put("payment_id", transactionId);
+                            Payment.close(closeParams, merchant);
                         }
                     } catch (BaseAdaPayException e) {
                         log.error("adapay error", e);

+ 32 - 26
src/main/java/com/izouma/nineth/service/UserService.java

@@ -62,26 +62,27 @@ import java.util.stream.Collectors;
 @Slf4j
 @AllArgsConstructor
 public class UserService {
-    private UserRepo           userRepo;
-    private WxMaService        wxMaService;
-    private WxMpService        wxMpService;
-    private SmsService         smsService;
-    private StorageService     storageService;
-    private JwtTokenUtil       jwtTokenUtil;
-    private CaptchaService     captchaService;
-    private FollowService      followService;
-    private FollowRepo         followRepo;
-    private IdentityAuthRepo   identityAuthRepo;
-    private SysConfigService   sysConfigService;
-    private AdapayService      adapayService;
-    private UserBankCardRepo   userBankCardRepo;
-    private InviteRepo         inviteRepo;
-    private NFTService         nftService;
-    private CacheService       cacheService;
-    private ApplicationContext context;
-    private TokenHistoryRepo   tokenHistoryRepo;
-    private CollectionRepo     collectionRepo;
-    private AdapayProperties   adapayProperties;
+    private UserRepo              userRepo;
+    private WxMaService           wxMaService;
+    private WxMpService           wxMpService;
+    private SmsService            smsService;
+    private StorageService        storageService;
+    private JwtTokenUtil          jwtTokenUtil;
+    private CaptchaService        captchaService;
+    private FollowService         followService;
+    private FollowRepo            followRepo;
+    private IdentityAuthRepo      identityAuthRepo;
+    private SysConfigService      sysConfigService;
+    private AdapayService         adapayService;
+    private UserBankCardRepo      userBankCardRepo;
+    private InviteRepo            inviteRepo;
+    private NFTService            nftService;
+    private CacheService          cacheService;
+    private ApplicationContext    context;
+    private TokenHistoryRepo      tokenHistoryRepo;
+    private CollectionRepo        collectionRepo;
+    private AdapayProperties      adapayProperties;
+    private AdapayMerchantService adapayMerchantService;
 
     public User update(User user) {
         User orig = userRepo.findById(user.getId()).orElseThrow(new BusinessException("无记录"));
@@ -491,12 +492,15 @@ public class UserService {
         if (!bankValidate.isValidated()) {
             throw new BusinessException("暂不支持此卡");
         }
-        user.setMemberId(adapayService.createMember(userId, user.getPhone(), identityAuth.getRealName(),
-                identityAuth.getIdNo()));
-        userRepo.saveAndFlush(user);
         smsService.verify(phone, code);
-        String accountId = adapayService.createSettleAccount(user.getMemberId(), identityAuth.getRealName(),
-                identityAuth.getIdNo(), phone, bankNo);
+
+        adapayMerchantService.createMemberForAll(userId.toString(), user.getPhone(), identityAuth.getRealName(), identityAuth.getIdNo());
+        user.setMemberId(user.getId().toString());
+        userRepo.save(user);
+
+        String accountId = adapayMerchantService.createSettleAccountForAll
+                (user.getMemberId(), identityAuth.getRealName(),
+                        identityAuth.getIdNo(), phone, bankNo);
         user.setSettleAccountId(accountId);
         userRepo.save(user);
 
@@ -508,13 +512,15 @@ public class UserService {
                 .cardTypeDesc(bankValidate.getCardTypeDesc())
                 .userId(userId)
                 .phone(phone)
+                .realName(identityAuth.getRealName())
+                .idNo(identityAuth.getIdNo())
                 .build());
     }
 
     public void removeBankCard(Long userId) throws BaseAdaPayException {
         User user = userRepo.findById(userId).orElseThrow(new BusinessException("用户不存在"));
         if (StringUtils.isNotBlank(user.getSettleAccountId()) && StringUtils.isNotBlank(user.getMemberId())) {
-            adapayService.delSettleAccount(user.getMemberId(), user.getSettleAccountId());
+            adapayMerchantService.delSettleAccountForAll(user.getMemberId());
             user.setSettleAccountId(null);
             userRepo.save(user);
             userBankCardRepo.deleteByUserId(userId);

+ 72 - 0
src/main/java/com/izouma/nineth/web/AdapayMerchantController.java

@@ -0,0 +1,72 @@
+package com.izouma.nineth.web;
+
+import com.huifu.adapay.core.exception.BaseAdaPayException;
+import com.izouma.nineth.domain.AdapayMerchant;
+import com.izouma.nineth.dto.PageQuery;
+import com.izouma.nineth.exception.BusinessException;
+import com.izouma.nineth.repo.AdapayMerchantRepo;
+import com.izouma.nineth.service.AdapayMerchantService;
+import com.izouma.nineth.utils.excel.ExcelUtils;
+import lombok.AllArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.List;
+
+@RestController
+@RequestMapping("/adapayMerchant")
+@AllArgsConstructor
+@PreAuthorize("hasRole('ADMIN')")
+public class AdapayMerchantController extends BaseController {
+    private AdapayMerchantService adapayMerchantService;
+    private AdapayMerchantRepo    adapayMerchantRepo;
+
+    @PostMapping("/save")
+    public AdapayMerchant save(@RequestBody AdapayMerchant record) {
+        if (record.getId() != null) {
+            return adapayMerchantService.update(record);
+        }
+        return adapayMerchantService.save(record);
+    }
+
+    @PostMapping("/all")
+    public Page<AdapayMerchant> all(@RequestBody PageQuery pageQuery) {
+        return adapayMerchantService.all(pageQuery);
+    }
+
+    @GetMapping("/get/{id}")
+    public AdapayMerchant get(@PathVariable Long id) {
+        return adapayMerchantRepo.findById(id).orElseThrow(new BusinessException("无记录"));
+    }
+
+    @PostMapping("/del/{id}")
+    public void del(@PathVariable Long id) {
+        adapayMerchantRepo.softDelete(id);
+    }
+
+    @GetMapping("/excel")
+    @ResponseBody
+    public void excel(HttpServletResponse response, PageQuery pageQuery) throws IOException {
+        List<AdapayMerchant> data = all(pageQuery).getContent();
+        ExcelUtils.export(response, data);
+    }
+
+    @PostMapping("/select")
+    public void select(@RequestParam Long id) throws Exception {
+        adapayMerchantService.select(id);
+    }
+
+    @PostMapping("/query")
+    public Object query(@RequestParam Long id, @RequestParam String orderId) throws BaseAdaPayException {
+        return adapayMerchantService.query(id, orderId);
+    }
+
+    @PostMapping("/refund")
+    public Object refund(@RequestParam Long id, @RequestParam String orderId) throws BaseAdaPayException {
+        return adapayMerchantService.refund(id, orderId);
+    }
+}
+

+ 8 - 1
src/main/java/com/izouma/nineth/web/OrderNotifyController.java

@@ -9,6 +9,7 @@ import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
 import com.github.binarywang.wxpay.exception.WxPayException;
 import com.github.binarywang.wxpay.service.WxPayService;
 import com.github.kevinsawicki.http.HttpRequest;
+import com.huifu.adapay.Adapay;
 import com.huifu.adapay.core.AdapayCore;
 import com.huifu.adapay.core.util.AdapaySign;
 import com.izouma.nineth.config.AlipayProperties;
@@ -185,6 +186,12 @@ public class OrderNotifyController {
     @PostMapping("/adapay/order/{orderId}")
     @ResponseBody
     public void adapayNotify(@PathVariable Long orderId, HttpServletRequest request) throws Exception {
+        adapayNotify(Adapay.defaultMerchantKey, orderId, request);
+    }
+
+    @PostMapping("/adapay/order/{merchant}/{orderId}")
+    @ResponseBody
+    public void adapayNotify(@PathVariable String merchant, @PathVariable Long orderId, HttpServletRequest request) throws Exception {
         log.info("adapay notify: \n{}", JSON.toJSONString(request.getParameterMap(), PrettyFormat));
         String data = request.getParameter("data");
         String sign = request.getParameter("sign");
@@ -199,7 +206,7 @@ public class OrderNotifyController {
                 PayMethod payMethod = channel.startsWith("wx") ? PayMethod.WEIXIN : PayMethod.ALIPAY;
 
                 BoundSetOperations<String, Object> listOps = redisTemplate.boundSetOps(RedisKeys.PAY_RECORD + orderId);
-                listOps.add(id);
+                listOps.add(merchant + "#" + id);
                 listOps.expire(7, TimeUnit.DAYS);
 
                 rocketMQTemplate.syncSend(generalProperties.getOrderNotifyTopic(),

+ 18 - 2
src/main/vue/src/router.js

@@ -502,7 +502,23 @@ const router = new Router({
                     meta: {
                        title: '新闻管理',
                     },
-               }
+               },
+                {
+                    path: '/adapayMerchantEdit',
+                    name: 'AdapayMerchantEdit',
+                    component: () => import(/* webpackChunkName: "adapayMerchantEdit" */ '@/views/AdapayMerchantEdit.vue'),
+                    meta: {
+                        title: 'ada编辑',
+                    },
+                },
+                {
+                    path: '/adapayMerchantList',
+                    name: 'AdapayMerchantList',
+                    component: () => import(/* webpackChunkName: "adapayMerchantList" */ '@/views/AdapayMerchantList.vue'),
+                    meta: {
+                        title: 'ada',
+                    },
+                }
                 /**INSERT_LOCATION**/
             ]
         },
@@ -562,4 +578,4 @@ router.beforeEach((to, from, next) => {
     }
 });
 
-export default router;
+export default router;

+ 133 - 0
src/main/vue/src/views/AdapayMerchantEdit.vue

@@ -0,0 +1,133 @@
+<template>
+    <div class="edit-view">
+        <page-title>
+            <el-button @click="$router.go(-1)" :disabled="saving">取消</el-button>
+            <el-button @click="onDelete" :disabled="saving" type="danger" v-if="formData.id"> 删除 </el-button>
+            <el-button @click="onSave" :loading="saving" type="primary">保存</el-button>
+        </page-title>
+        <div class="edit-view__content-wrapper">
+            <div class="edit-view__content-section">
+                <el-form
+                    :model="formData"
+                    :rules="rules"
+                    ref="form"
+                    label-width="89px"
+                    label-position="right"
+                    size="small"
+                    style="max-width: 500px"
+                >
+                    <el-form-item prop="name" label="name">
+                        <el-input v-model="formData.name"></el-input>
+                    </el-form-item>
+                    <el-form-item prop="appId" label="appId">
+                        <el-input v-model="formData.appId"></el-input>
+                    </el-form-item>
+                    <el-form-item prop="apiKey" label="apiKey">
+                        <el-input v-model="formData.apiKey"></el-input>
+                    </el-form-item>
+                    <el-form-item prop="mockKey" label="mockKey">
+                        <el-input v-model="formData.mockKey"></el-input>
+                    </el-form-item>
+                    <el-form-item prop="publicKey" label="publicKey">
+                        <el-input v-model="formData.publicKey" type="textarea"></el-input>
+                    </el-form-item>
+                    <el-form-item prop="privKey" label="privKey">
+                        <el-input v-model="formData.privKey" type="textarea"></el-input>
+                    </el-form-item>
+                    <el-form-item prop="wxAppId" label="wxAppId">
+                        <el-input v-model="formData.wxAppId"></el-input>
+                    </el-form-item>
+                    <el-form-item prop="status" label="status">
+                        <el-input v-model="formData.status"></el-input>
+                    </el-form-item>
+                    <el-form-item class="form-submit">
+                        <el-button @click="onSave" :loading="saving" type="primary"> 保存 </el-button>
+                        <el-button @click="onDelete" :disabled="saving" type="danger" v-if="formData.id">
+                            删除
+                        </el-button>
+                        <el-button @click="$router.go(-1)" :disabled="saving">取消</el-button>
+                    </el-form-item>
+                </el-form>
+            </div>
+        </div>
+    </div>
+</template>
+<script>
+export default {
+    name: 'AdapayMerchantEdit',
+    created() {
+        if (this.$route.query.id) {
+            this.$http
+                .get('adapayMerchant/get/' + this.$route.query.id)
+                .then(res => {
+                    this.formData = res;
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.$message.error(e.error);
+                });
+        }
+    },
+    data() {
+        return {
+            saving: false,
+            formData: {},
+            rules: {
+                name: [
+                    {
+                        pattern: /^[a-z0-9-]+$/,
+                        message: '只能包含小写字母和数字-',
+                        trigger: 'blur'
+                    }
+                ]
+            }
+        };
+    },
+    methods: {
+        onSave() {
+            this.$refs.form.validate(valid => {
+                if (valid) {
+                    this.submit();
+                } else {
+                    return false;
+                }
+            });
+        },
+        submit() {
+            let data = { ...this.formData };
+            data.name = data.name.toLowerCase();
+
+            this.saving = true;
+            this.$http
+                .post('/adapayMerchant/save', data, { body: 'json' })
+                .then(res => {
+                    this.saving = false;
+                    this.$message.success('成功');
+                    this.$router.go(-1);
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.saving = false;
+                    this.$message.error(e.error);
+                });
+        },
+        onDelete() {
+            this.$confirm('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/adapayMerchant/del/${this.formData.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.$router.go(-1);
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        console.log(e);
+                        this.$message.error((e || {}).error || '删除失败');
+                    }
+                });
+        }
+    }
+};
+</script>
+<style lang="less" scoped></style>

+ 298 - 0
src/main/vue/src/views/AdapayMerchantList.vue

@@ -0,0 +1,298 @@
+<template>
+    <div class="list-view">
+        <page-title>
+            <el-button
+                @click="addRow"
+                type="primary"
+                icon="el-icon-plus"
+                :disabled="fetchingData || downloading"
+                class="filter-item"
+            >
+                新增
+            </el-button>
+            <el-button
+                @click="download"
+                icon="el-icon-upload2"
+                :loading="downloading"
+                :disabled="fetchingData"
+                class="filter-item"
+            >
+                导出
+            </el-button>
+        </page-title>
+        <div class="filters-container">
+            <el-button class="filter-item" @click="showQueryDialog = true">查询</el-button>
+            <el-button class="filter-item" @click="showRefundDialog = true">退款</el-button>
+            <el-input
+                placeholder="搜索..."
+                v-model="search"
+                clearable
+                class="filter-item search"
+                @keyup.enter.native="getData"
+            >
+                <el-button @click="getData" slot="append" icon="el-icon-search"> </el-button>
+            </el-input>
+        </div>
+        <el-table
+            :data="tableData"
+            row-key="id"
+            ref="table"
+            header-row-class-name="table-header-row"
+            header-cell-class-name="table-header-cell"
+            row-class-name="table-row"
+            cell-class-name="table-cell"
+            :height="tableHeight"
+            v-loading="fetchingData"
+        >
+            <el-table-column v-if="multipleMode" align="center" type="selection" width="50"> </el-table-column>
+            <el-table-column prop="id" label="ID" width="100"> </el-table-column>
+            <el-table-column prop="name" label="name"> </el-table-column>
+            <el-table-column prop="appId" label="appId" width="330"> </el-table-column>
+            <el-table-column prop="status" label="status"> </el-table-column>
+            <el-table-column prop="selected" label="selected" align="center">
+                <template v-slot="{ row }">
+                    <el-tag :type="row.selected ? 'success' : 'info'">{{ row.selected ? '是' : '否' }}</el-tag>
+                </template>
+            </el-table-column>
+            <el-table-column label="操作" align="right" fixed="right" width="150">
+                <template slot-scope="{ row }">
+                    <el-button @click="select(row)" type="primary" size="mini" plain v-if="!row.selected"
+                        >切换</el-button
+                    >
+                    <el-button @click="editRow(row)" type="primary" size="mini" plain>编辑</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div class="pagination-wrapper">
+            <!-- <div class="multiple-mode-wrapper">
+                <el-button v-if="!multipleMode" @click="toggleMultipleMode(true)">批量编辑</el-button>
+                <el-button-group v-else>
+                    <el-button @click="operation1">批量操作1</el-button>
+                    <el-button @click="operation2">批量操作2</el-button>
+                    <el-button @click="toggleMultipleMode(false)">取消</el-button>
+                </el-button-group>
+            </div> -->
+            <el-pagination
+                background
+                @size-change="onSizeChange"
+                @current-change="onCurrentChange"
+                :current-page="page"
+                :page-sizes="[10, 20, 30, 40, 50]"
+                :page-size="pageSize"
+                layout="total, sizes, prev, pager, next, jumper"
+                :total="totalElements"
+            >
+            </el-pagination>
+        </div>
+        <el-dialog title="查询订单" :visible.sync="showQueryDialog" width="650px">
+            <el-form label-position="right" label-width="100px">
+                <el-form-item label="商户">
+                    <el-select v-model="merchantId">
+                        <el-option
+                            v-for="item in tableData"
+                            :label="item.name"
+                            :value="item.id"
+                            :key="item.id"
+                        ></el-option>
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="订单流水号">
+                    <el-input v-model="orderId"></el-input>
+                </el-form-item>
+                <el-form-item v-if="queryResult" label="结果">
+                    <pre v-html="queryResult" style="line-height: 1.5"></pre>
+                </el-form-item>
+            </el-form>
+            <div slot="footer">
+                <el-button @click="query" :loading="querying">查询</el-button>
+            </div>
+        </el-dialog>
+
+        <el-dialog title="订单退款" :visible.sync="showRefundDialog" width="650px">
+            <el-form label-position="right" label-width="100px">
+                <el-form-item label="商户">
+                    <el-select v-model="merchantId">
+                        <el-option
+                            v-for="item in tableData"
+                            :label="item.name"
+                            :value="item.id"
+                            :key="item.id"
+                        ></el-option>
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="订单流水号">
+                    <el-input v-model="orderId"></el-input>
+                </el-form-item>
+                <el-form-item v-if="refundResult" label="结果">
+                    <pre v-html="refundResult" style="line-height: 1.5"></pre>
+                </el-form-item>
+            </el-form>
+            <div slot="footer">
+                <el-button @click="refund" :loading="querying">退款</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+import pageableTable from '@/mixins/pageableTable';
+
+export default {
+    name: 'AdapayMerchantList',
+    mixins: [pageableTable],
+    data() {
+        return {
+            multipleMode: false,
+            search: '',
+            url: '/adapayMerchant/all',
+            downloading: false,
+            merchantId: null,
+            orderId: null,
+            showQueryDialog: false,
+            showRefundDialog: false,
+            querying: false,
+            queryResult: null,
+            refundResult: null
+        };
+    },
+    computed: {
+        selection() {
+            return this.$refs.table.selection.map(i => i.id);
+        }
+    },
+    methods: {
+        beforeGetData() {
+            return { search: this.search, query: { del: false } };
+        },
+        toggleMultipleMode(multipleMode) {
+            this.multipleMode = multipleMode;
+            if (!multipleMode) {
+                this.$refs.table.clearSelection();
+            }
+        },
+        addRow() {
+            this.$router.push({
+                path: '/adapayMerchantEdit',
+                query: {
+                    ...this.$route.query
+                }
+            });
+        },
+        editRow(row) {
+            this.$router.push({
+                path: '/adapayMerchantEdit',
+                query: {
+                    id: row.id
+                }
+            });
+        },
+        download() {
+            this.downloading = true;
+            this.$axios
+                .get('/adapayMerchant/excel', {
+                    responseType: 'blob',
+                    params: { size: 10000 }
+                })
+                .then(res => {
+                    console.log(res);
+                    this.downloading = false;
+                    const downloadUrl = window.URL.createObjectURL(new Blob([res.data]));
+                    const link = document.createElement('a');
+                    link.href = downloadUrl;
+                    link.setAttribute('download', res.headers['content-disposition'].split('filename=')[1]);
+                    document.body.appendChild(link);
+                    link.click();
+                    link.remove();
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.downloading = false;
+                    this.$message.error(e.error);
+                });
+        },
+        operation1() {
+            this.$notify({
+                title: '提示',
+                message: this.selection
+            });
+        },
+        operation2() {
+            this.$message('操作2');
+        },
+        deleteRow(row) {
+            this.$alert('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/adapayMerchant/del/${row.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.getData();
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        this.$message.error(e.error);
+                    }
+                });
+        },
+        select(row) {
+            this.$alert('确认切换?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/adapayMerchant/select`, { id: row.id });
+                })
+                .then(() => {
+                    this.$message.success('切换成功');
+                    this.getData();
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        this.$message.error(e.error);
+                    }
+                });
+        },
+        query() {
+            this.querying = true;
+            this.$http
+                .post('/adapayMerchant/query', {
+                    id: this.merchantId,
+                    orderId: this.orderId
+                })
+                .then(res => {
+                    this.querying = false;
+                    console.log(res);
+                    this.queryResult = JSON.stringify(res, null, 4);
+                })
+                .catch(e => {
+                    this.querying = false;
+                    this.$message.error(e.error || '失败');
+                });
+        },
+        refund() {
+            this.$alert('确认退款?', '警告', { type: 'error' })
+                .then(() => {
+                    this.querying = true;
+                    this.$http
+                        .post('/adapayMerchant/refund', {
+                            id: this.merchantId,
+                            orderId: this.orderId
+                        })
+                        .then(res => {
+                            this.querying = false;
+                            console.log(res);
+                            this.refundResult = JSON.stringify(res, null, 4);
+                        })
+                        .catch(e => {
+                            this.querying = false;
+                            this.$message.error(e.error || '失败');
+                        });
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        this.$message.error(e.error);
+                    }
+                });
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+</style>