定义InApp 支付接口,实现记账业务
diff --git a/payapi-common/src/main/java/com/supwisdom/dlpay/api/bean/InAppPayParam.java b/payapi-common/src/main/java/com/supwisdom/dlpay/api/bean/InAppPayParam.java
index 9b7ecf9..6564025 100644
--- a/payapi-common/src/main/java/com/supwisdom/dlpay/api/bean/InAppPayParam.java
+++ b/payapi-common/src/main/java/com/supwisdom/dlpay/api/bean/InAppPayParam.java
@@ -24,8 +24,13 @@
   private String recipientType;
 
   @NotNull
+  private String summary; // 交易类型,目前支持 pay(消费),deposit(充值)
+
+  @NotNull
   private String sourceType;
 
+  private boolean anonymous = false;
+
   private String userid;
 
   private String merchno;
@@ -33,7 +38,7 @@
   @Positive(message = "金额不合法")
   private Integer totalAmount;
 
-  private String describe;
+  private String description;
 
   @TransDate
   private String transDate;
diff --git a/payapi-common/src/main/java/com/supwisdom/dlpay/api/bean/InAppPayResponse.java b/payapi-common/src/main/java/com/supwisdom/dlpay/api/bean/InAppPayResponse.java
index 8858860..053fe03 100644
--- a/payapi-common/src/main/java/com/supwisdom/dlpay/api/bean/InAppPayResponse.java
+++ b/payapi-common/src/main/java/com/supwisdom/dlpay/api/bean/InAppPayResponse.java
@@ -15,4 +15,7 @@
   private String message;
 
   private String status;
+
+  private String callbackUrl;
+
 }
diff --git a/payapi/src/main/kotlin/com/supwisdom/dlpay/PayApiApplication.kt b/payapi/src/main/kotlin/com/supwisdom/dlpay/PayApiApplication.kt
index 27b9c1c..2eda4b3 100644
--- a/payapi/src/main/kotlin/com/supwisdom/dlpay/PayApiApplication.kt
+++ b/payapi/src/main/kotlin/com/supwisdom/dlpay/PayApiApplication.kt
@@ -237,12 +237,10 @@
 @SpringBootApplication
 @EnableDiscoveryClient
 @EnableScheduling
-@EnableSchedulerLock(defaultLockAtMostFor = "PT15m")
+@EnableSchedulerLock(defaultLockAtMostFor = "PT15M")
 @EnableCaching
-@EnableHttpHeaderTenantInterceptor
-@EnableSessionTenantInterceptor
 @EnableJwtTenantInterceptor
-@EnableTenantDetailsStorage
+@EnableTenantDetailsStorage(enableHttpHeader = true, enableHttpSession = true)
 @EnableMultiTenantTaskWorker(propertyName = "payapi.multitenant")
 @ServletComponentScan
 @EnableAsync
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 d9c159e..db1aaf3 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
@@ -4,6 +4,7 @@
 import com.supwisdom.dlpay.agent.AgentPayServiceContext
 import com.supwisdom.dlpay.agent.DtlStatus
 import com.supwisdom.dlpay.agent.citizencard.YnrccUtil
+import com.supwisdom.dlpay.api.controller.TransactionContainer
 import com.supwisdom.dlpay.api.domain.TDtlQuery
 import com.supwisdom.dlpay.api.domain.TShopdtl
 import com.supwisdom.dlpay.api.domain.TTransactionMain
@@ -23,6 +24,7 @@
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.context.annotation.Bean
 import org.springframework.context.annotation.Configuration
+import org.springframework.data.redis.connection.RedisConnectionFactory
 import org.springframework.retry.annotation.Backoff
 import org.springframework.retry.annotation.Recover
 import org.springframework.retry.annotation.Retryable
@@ -100,20 +102,15 @@
 
 
 @Component
-class AgentQueryResultTask {
+class AgentQueryResultTask(private val transactionService: TransactionServiceProxy,
+                           private val agentPayServiceContext: AgentPayServiceContext,
+                           private val dtlQueryResultService: DtlQueryResultService,
+                           private val systemUtilService: SystemUtilService,
+                           redisConnectionFactory: RedisConnectionFactory) {
+
     private val logger = KotlinLogging.logger { }
 
-    @Autowired
-    lateinit var transactionService: TransactionServiceProxy
-
-    @Autowired
-    private lateinit var agentPayServiceContext: AgentPayServiceContext
-
-    @Autowired
-    private lateinit var dtlQueryResultService: DtlQueryResultService
-
-    @Autowired
-    private lateinit var systemUtilService: SystemUtilService
+    private val transactionContainer = TransactionContainer("qrcode.pay", redisConnectionFactory)
 
     @Recover
     fun quertRecover(ex: TaskRetryException, details: TenantDetails,
@@ -146,6 +143,7 @@
 
         when (resp.code) {
             AgentCode.SUCCESS -> {
+                transactionContainer.remove(transaction.refno)
                 //查询成功
                 when (resp.dtlStatus) {
                     DtlStatus.SUCCESS ->
@@ -166,12 +164,15 @@
                     }
                 }
             }
-            AgentCode.REFNO_NOT_EXISTS ->
+            AgentCode.REFNO_NOT_EXISTS -> {
+                transactionContainer.remove(transaction.refno)
                 transactionService.fail(transaction.refno, "银行流水不存在") //银行返回流水不存在
+            }
             AgentCode.REQUIRE_QUERY -> {
                 throw TaskRetryException()
             }
             else -> {
+                transactionContainer.remove(transaction.refno)
                 //其他明确错误,查询失败
                 logger.error("查询refno=[${transaction.refno}]流水结果返回失败:code=[${resp.agentCode}],message=[${resp.agentMsg}]")
             }
diff --git a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/controller/consume_api_controller.kt b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/controller/consume_api_controller.kt
index 52f0529..93e305d 100644
--- a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/controller/consume_api_controller.kt
+++ b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/controller/consume_api_controller.kt
@@ -17,11 +17,10 @@
 import com.supwisdom.dlpay.exception.TransactionCheckException
 import com.supwisdom.dlpay.framework.ResponseBodyBuilder
 import com.supwisdom.dlpay.framework.service.SystemUtilService
-import com.supwisdom.dlpay.framework.task.TaskExecuteTimes
 import com.supwisdom.dlpay.framework.util.*
 import com.supwisdom.multitenant.TenantContextHolder
 import org.apache.commons.lang3.StringUtils
-import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.data.redis.connection.RedisConnectionFactory
 import org.springframework.data.redis.core.RedisTemplate
 import org.springframework.http.ResponseEntity
 import org.springframework.validation.annotation.Validated
@@ -31,35 +30,25 @@
 
 @RestController
 @RequestMapping("/api/consume")
-class ConsumeAPIController {
-    @Autowired
-    lateinit var accountUtilServcie: AccountUtilServcie
-    @Autowired
-    lateinit var userService: UserService
-    @Autowired
-    lateinit var systemUtilService: SystemUtilService
-    @Autowired
-    lateinit var consumePayService: ConsumePayService
-    @Autowired
-    lateinit var transactionService: TransactionServiceProxy
-    @Autowired
-    lateinit var cardService: CardService
-    @Autowired
-    private lateinit var agentPayServiceContext: AgentPayServiceContext
+class ConsumeAPIController(private val qrCodeService: QRCodeService,
+                           private val accountUtilServcie: AccountUtilServcie,
+                           private val userService: UserService,
+                           private val systemUtilService: SystemUtilService,
+                           private val consumePayService: ConsumePayService,
+                           private val transactionService: TransactionServiceProxy,
+                           private val cardService: CardService,
+                           private val agentPayServiceContext: AgentPayServiceContext,
+                           private val sourceTypeService: SourceTypeService,
+                           private val agentQueryResultTask: AgentQueryResultTask,
+                           private val agentServiceProxy: AgentServiceProxy,
+                           redisConnectionFactory: RedisConnectionFactory) {
 
-    @Autowired
-    private lateinit var sourceTypeService: SourceTypeService
-    @Autowired
-    lateinit var agentQueryResultTask: AgentQueryResultTask
+    private val redisTemplate = RedisTemplate<String, String>()
 
-    @Autowired
-    private lateinit var agentServiceProxy: AgentServiceProxy
-
-    @Autowired
-    lateinit var redisTemplate: RedisTemplate<String, String>
-
-    @Autowired
-    private lateinit var qrCodeService: QRCodeService
+    init {
+        redisTemplate.setConnectionFactory(redisConnectionFactory)
+        redisTemplate.afterPropertiesSet()
+    }
 
     /**
      * ============================================================================
diff --git a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/controller/transaction_controller.kt b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/controller/transaction_controller.kt
index f0846af..63312ad 100644
--- a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/controller/transaction_controller.kt
+++ b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/controller/transaction_controller.kt
@@ -1,33 +1,55 @@
 package com.supwisdom.dlpay.api.controller
 
+import com.supwisdom.dlpay.api.TransactionBuilder
 import com.supwisdom.dlpay.api.bean.InAppPayParam
 import com.supwisdom.dlpay.api.bean.InAppPayResponse
 import com.supwisdom.dlpay.api.bean.TransactionQueryResponse
+import com.supwisdom.dlpay.api.domain.TSourceType
+import com.supwisdom.dlpay.api.service.AccountUtilServcie
+import com.supwisdom.dlpay.api.service.SourceTypeService
 import com.supwisdom.dlpay.api.service.TransactionServiceProxy
+import com.supwisdom.dlpay.exception.TransactionCheckException
 import com.supwisdom.dlpay.framework.ResponseBodyBuilder
 import com.supwisdom.dlpay.framework.service.SystemUtilService
+import com.supwisdom.dlpay.framework.util.TradeCode
+import com.supwisdom.dlpay.framework.util.TradeDict
+import com.supwisdom.dlpay.framework.util.TradeErrorCode
 import com.supwisdom.multitenant.TenantContextHolder
-import org.springframework.http.HttpStatus
+import com.supwisdom.multitenant.storage.TenantDetailsRegistrar
+import org.springframework.context.annotation.Lazy
+import org.springframework.data.redis.connection.RedisConnectionFactory
+import org.springframework.data.redis.core.RedisTemplate
 import org.springframework.http.ResponseEntity
 import org.springframework.web.bind.annotation.GetMapping
 import org.springframework.web.bind.annotation.PathVariable
 import org.springframework.web.bind.annotation.PostMapping
 import org.springframework.web.bind.annotation.RestController
 import javax.validation.Valid
+import kotlin.math.roundToInt
 
 
 @RestController("/api/transaction")
 class TransactionController(private val transactionServiceProxy: TransactionServiceProxy,
-                            private val systemUtilService: SystemUtilService) {
+                            private val systemUtilService: SystemUtilService,
+                            private val accountUtilServcie: AccountUtilServcie,
+                            private val sourceTypeService: SourceTypeService,
+                            redisConnectionFactory: RedisConnectionFactory) {
 
-    @GetMapping("/query")
+    private val transactionContainer = TransactionContainer("inapp.pay", redisConnectionFactory)
+
+    @GetMapping("/inapp/query")
     fun queryTransaction(refno: String): ResponseEntity<*> {
         val tranaction = transactionServiceProxy.findTransactionByRefno(refno)
-                ?: return ResponseBodyBuilder.fail(HttpStatus.NOT_FOUND, "未找到流水")
+                ?: return ResponseBodyBuilder.notFound("未找到流水")
         if (tranaction.tenantid != TenantContextHolder.getContext().tenant.id) {
-            return ResponseBodyBuilder.fail(HttpStatus.CONFLICT, "未找到流水")
+            return ResponseBodyBuilder.conflict("未找到流水")
         }
 
+        if (tranaction.status != TradeDict.DTL_STATUS_SUCCESS) {
+            transactionContainer.add(refno)
+            // 查询第三方
+            transactionContainer.remove(refno)
+        }
         return ResponseBodyBuilder.successEntity(TransactionQueryResponse().apply {
             this.refno = tranaction.refno
             accdate = tranaction.accdate
@@ -37,18 +59,138 @@
         }, "成功")
     }
 
+    private fun transSummary(sourceType: TSourceType, param: InAppPayParam): String {
+        return when (param.summary) {
+            "pay" -> "${sourceType.paydesc}消费"
+            "deposit" -> "${sourceType.paydesc}充值"
+            else -> throw TransactionCheckException(TradeErrorCode.INPUT_DATA_ERROR, "交易摘要类型错误")
+        }
+    }
+
     @PostMapping("/inapp/payinit")
-    fun inAppPayInit(@Valid param: InAppPayParam): ResponseEntity<InAppPayResponse> {
+    fun inAppPayInit(@Valid param: InAppPayParam): ResponseEntity<*> {
         val response = InAppPayResponse()
+        if (transactionContainer.count() > 100) {
+            return ResponseBodyBuilder.serviceUnavailable("第三方请求异常")
+        }
+        val sourceType = sourceTypeService.getBySourceType(param.sourceType)
+                ?: return ResponseBodyBuilder.badRequest("source type <${param.sourceType}> 不存在")
+
+        val subject = accountUtilServcie.readSubject(sourceType.depositeSubjno)
+
+        val tradeCode = when (param.inAppType) {
+            "h5" -> TradeCode.TRANSCODE_H5PAY
+            "native" -> TradeCode.TRANSCODE_APPPAY
+            else -> return ResponseBodyBuilder.badRequest("app typ <${param.inAppType}> 不支持")
+        }
+
+        val builder = TransactionBuilder()
+                .setTransInfo(param.transDate, param.transTime, tradeCode, sourceType.sourceType)
+                .setOutTransInfo(TenantContextHolder.getContext().tenant.id, param.billno)
+                .apply {
+                    description = param.description
+                }
+        val amount = param.totalAmount / 100.0
+        when (param.recipientType) {
+            "shop" -> {
+                // check shop acc
+                val shopacc = accountUtilServcie.readShopbyShopaccno(param.merchno)
+                builder.shop(shopacc)
+                        .setAmount(amount, TradeDict.TRADE_FLAG_IN)
+                        .setOpposite(subject.subjno, subject.subjname)
+                if (!param.isAnonymous) {
+                    // 支付给商户,并且不是匿名支付
+                    val account = accountUtilServcie.readAccount(param.userid)
+                    builder.person(account)
+                            .setAmount(amount, TradeDict.TRADE_FLAG_OUT)
+                            .setOpposite(subject.subjno, subject.subjname)
+                            .and()
+                            .addDebitCreditRecord(subject.subjno, subject.subjno,
+                                    account.accno, account.subjno, amount,
+                                    transSummary(sourceType, param))
+                            .addDebitCreditRecord(account.accno, account.subjno,
+                                    shopacc.shopaccno, shopacc.subjno, amount,
+                                    transSummary(sourceType, param))
+                } else {
+                    // 匿名消费,直接到商户账户
+                    builder.addDebitCreditRecord(subject.subjno, subject.subjno,
+                            shopacc.shopaccno, shopacc.subjno, amount, transSummary(sourceType, param))
+                }
+            }
+            "person" -> {
+                // check person
+                val account = accountUtilServcie.readAccount(param.userid)
+                builder.person(account)
+                        .setAmount(amount, TradeDict.TRADE_FLAG_IN)
+                        .setOpposite(subject.subjno, subject.subjname)
+                        .and()
+                        .addDebitCreditRecord(subject.subjno, subject.subjno,
+                                account.accno, account.subjno, amount, transSummary(sourceType, param))
+            }
+            else -> {
+                return ResponseBodyBuilder.badRequest("recipient type 错误")
+            }
+        }
+        val transaction = transactionServiceProxy.init(builder)
+        // 调用第三方完成交易初始化
+
+        transactionServiceProxy.wip(transaction.refno)
+        transactionContainer.add(transaction.refno)
+        response.apply {
+            when (param.recipientType) {
+                "shop" -> actuallyAmount = (transaction.shopDtl.amount * 100).roundToInt()
+                "person" -> actuallyAmount = (transaction.personDtl.amount * 100).roundToInt()
+            }
+            this.refno = transaction.refno
+            this.status = transaction.status
+            this.message = "初始化成功"
+        }
         return ResponseBodyBuilder.successEntity(response)
     }
 }
 
+class TransactionContainer(private val name: String,
+                           redisConnectionFactory: RedisConnectionFactory) {
+    private val redisTemplate: RedisTemplate<Any, Any> = RedisTemplate<Any, Any>()
+
+    init {
+        redisTemplate.setConnectionFactory(redisConnectionFactory)
+        redisTemplate.afterPropertiesSet()
+    }
+
+    private fun keyName(): String = "TRANS:REFNO:$name"
+
+    fun add(refno: String): Long {
+        redisTemplate.opsForSet().add(keyName(), refno)
+        return redisTemplate.opsForSet().size(keyName()) ?: 0L
+    }
+
+    fun has(refno: String): Boolean {
+        return redisTemplate.opsForSet().isMember(keyName(), refno) ?: false
+    }
+
+    fun remove(refno: String): Long {
+        redisTemplate.opsForSet().remove(keyName(), refno)
+        return redisTemplate.opsForSet().size(keyName()) ?: 0L
+    }
+
+    fun count(): Long {
+        return redisTemplate.opsForSet().size(keyName()) ?: 0L
+    }
+}
+
 @RestController("/api/notify")
-class TransactionNotifyController {
+class TransactionNotifyController(@Lazy private val tenantDetailsRegistrar: TenantDetailsRegistrar,
+                                  private val transactionServiceProxy: TransactionServiceProxy) {
 
     @PostMapping("/inapp/alipay/{tenant}")
-    fun inAppPayCallback(@PathVariable("tenant") tenant: String): ResponseEntity<Any> {
-        TODO("")
+    fun inAppPayCallback(@PathVariable("tenant") tenant: String,
+                         refno: String): ResponseEntity<*> {
+        val tenantDetails = tenantDetailsRegistrar.getTenantDetailsById(tenant)
+                ?: return ResponseBodyBuilder.notFound("请求租户 <$tenant> 不存在")
+        TenantContextHolder.getContext().tenant = tenantDetails.get()
+        val transaction = transactionServiceProxy.success(refno)
+        return ResponseEntity.ok(ResponseBodyBuilder.create()
+                .data("refno", transaction.refno))
     }
 }
\ No newline at end of file
diff --git a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/transaction_builder.kt b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/transaction_builder.kt
index 1b81661..85ed71a 100644
--- a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/transaction_builder.kt
+++ b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/transaction_builder.kt
@@ -42,14 +42,16 @@
         return if (hasOpposite()) this.oppAccName else ""
     }
 
-    fun setOpposite(accno: String, accName: String) {
+    fun setOpposite(accno: String, accName: String): SubTransactionBuilder<T> {
         this.oppAccno = accno
         this.oppAccName = accName
+        return this
     }
 
-    fun setOpposite(accout: AccountProxy<*>) {
+    fun setOpposite(accout: AccountProxy<*>): SubTransactionBuilder<T> {
         this.oppAccno = accout.getAccountNo()
         this.oppAccName = accout.getAccountName()
+        return this
     }
 
     fun and(): TransactionBuilder {
diff --git a/payapi/src/main/kotlin/com/supwisdom/dlpay/framework/framework_util.kt b/payapi/src/main/kotlin/com/supwisdom/dlpay/framework/framework_util.kt
index 8614d23..597dedf 100644
--- a/payapi/src/main/kotlin/com/supwisdom/dlpay/framework/framework_util.kt
+++ b/payapi/src/main/kotlin/com/supwisdom/dlpay/framework/framework_util.kt
@@ -34,6 +34,30 @@
         fun fail(status: HttpStatus, msg: String): ResponseEntity<*> {
             return ResponseEntity.status(status).body(msg)
         }
+
+        fun notFound(msg: String): ResponseEntity<*> {
+            return fail(HttpStatus.NOT_FOUND, msg)
+        }
+
+        fun badRequest(msg: String): ResponseEntity<*> {
+            return fail(HttpStatus.BAD_REQUEST, msg)
+        }
+
+        fun conflict(msg: String): ResponseEntity<*> {
+            return fail(HttpStatus.CONFLICT, msg)
+        }
+
+        fun forbbiden(msg: String): ResponseEntity<*> {
+            return fail(HttpStatus.FORBIDDEN, msg)
+        }
+
+        fun unauthorized(msg: String): ResponseEntity<*> {
+            return fail(HttpStatus.UNAUTHORIZED, msg)
+        }
+
+        fun serviceUnavailable(msg: String): ResponseEntity<*> {
+            return fail(HttpStatus.SERVICE_UNAVAILABLE, msg)
+        }
     }
 
     private var retCode = INVALIDE_RETCODE