xiongzhu пре 2 година
родитељ
комит
58701c5d1d
6 измењених фајлова са 331 додато и 53 уклоњено
  1. 0 34
      certs/6888806043057.crt
  2. 1 1
      certs/6888806043057.key
  3. 19 0
      certs/sand.crt
  4. 143 8
      pem.mjs
  5. 156 4
      src/utils/sand-pay.ts
  6. 12 6
      src/withdraw/withdraw.service.ts

+ 0 - 34
src/cert/key.pem → certs/6888806043057.crt

@@ -1,37 +1,3 @@
-Bag Attributes
-    localKeyID: 01 00 00 00 
-    friendlyName: {AC31643A-66C7-430E-9B6C-BB09EC235B3C}
-    Microsoft CSP Name: Microsoft Enhanced Cryptographic Provider v1.0
-Key Attributes
-    X509v3 Key Usage: 10 
------BEGIN PRIVATE KEY-----
-MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDHhJT9hzZEYbUF
-J+evgd5jxgcAeKO5LjNItXQ1uFKR1bQ3faMUroCnWq/1zSGVJFp/Ht9x4D6qGsM2
-hSWxN3POmHzedhhx3c2meAXPYZsGz9ENcEzF5sDaJEvv5s2Z35TCpgphoPmVNKBl
-1A3BCRMuugY1ojWWP7vejMSrnhHbVQSn7l62C/4FpL5ppwBoYCUtxQ/QAfS7unSr
-Lx5kHxxs2cGUBLr2tosQduCScIV3peg+IJ4PtmeKJRtdBW3p67Qksk6wv2NaNMLu
-CoSTJNuEZA/BbUHx3q6cPoxmP6leQf3BFSgxgYdVeF/vzLTiBQJDvM1XMwNgkhbs
-Onne+KJJAgMBAAECggEAIdKiB1CyrGj77QHUzvXvI27gpP3Ok4f3fa31I1458SsP
-QBD2Yn0qpjRDfCHoPYZKKR6stntTZDbhPQ7ZUu+ZnD5RfMz8W+FECaJzKbLcOw1j
-3Slaenoh21kn3mV8GUfDyjOnsuE9LkQbpOeL5VuKevNiNhlniC8JxDKM7KMBV/MG
-485X0kvVYXC+vdjzRihBS6DXJ7ON5cbimXtCEM8NlHiyaKrmzkFvb8zY/gZobsA5
-OdxDNyASciYaxhb5Jo7fRNtawFl6D4sX50Yh6UE8jrDczc3DAYjfX8A/Sw+iWKaU
-bjf8tW3yqKa4xQLUnejjF6UwzRKVyl/L9hSBTUU2GQKBgQDSlABWYyc3mBAHX+nr
-tEZ3jhmjrfPEkWlSfQb4Z2mFnt1R4P+BjZ+kPCrU0VoYaNg0SlxjdBvVd9JP4Vvg
-WnM0n16iGT92cN1m2TbU2z6/iFYGprOwEYUyJ1Om9xWm/jHZCNWX1/E8iRue5NfF
-AI90Sq8Nyca6uV5evSnQxVZWCwKBgQDyjddeEpFG+Yl3VeCh4Zi/ABNTPB96p0fF
-COx6sGnc9NQ68nLfJnIQakvASyvArLhpkdk3/r+KUwN2RKSjsRynzhSAT7HTW2uO
-uEnGISz95Jxy2mx/aaNuVyfXlCYPRbo3gPu0uVvA8gcYnBYgZAdAQw8e2W4+3HBz
-hUdUxT3BewKBgEfUSkUtb0wsZ5NMaUuIY90WMLK/fH5Zp5aGpWEITVtR9GagnCdB
-+umXyHhtrRDUeqNjlh4qle/7/lZXJfLih5e0oSAkApzRJQSXJXf4DXBh/g4owPRX
-FZslBQjYEZ+z56cDt3AjLdubXTuXDe7aIRTnAM1wPYSmBIXGesQG/Es/AoGAPTFX
-wl4A1N0jtz2tPGoD9Tv5pCgo8PK/ApCryJA1RQ9nc6+zF2VFpaqLjenpEeUBaXDo
-Ul4syKME3pOWzc1jI1q0hWKVxHLuAcWwpxG7nhpM9hTxBnwKRU4ruRGhQH0g69vY
-pNGWumjVLXpDgsx5n9bLO8czLxzCKgLX6iUQ3X0CgYEAz/kvI9qznmcgUFf2i09k
-BiY9tZUBKv1QcNdRwjLhCpp24Ru/YjBU6UWN7LwbK3I91qHT1bTILIaAJYx2HwAc
-j40pBQrS9YVQo6NArlq8Cifd3/JELP1Jkr2OId5RHMP2SQUbX76g2W6P0Vqs/Zmn
-ad7Hduy/OzdMQD5r8ZsFC0Q=
------END PRIVATE KEY-----
 Bag Attributes
     localKeyID: 01 00 00 00 
 subject=/C=CN/O=OCA1RSA/OU=sand/OU=Organizational-1/CN=sand@\xE5\x8D\x8E\xE5\x82\xA8\xE8\x89\xBA\xE6\x9C\xAF\xE5\x93\x81\xE4\xB8\xAD\xE5\xBF\x83\xEF\xBC\x88\xE6\xB7\xB1\xE5\x9C\xB3\xEF\xBC\x89\xE6\x9C\x89\xE9\x99\x90\xE5\x85\xAC\xE5\x8F\xB8@6888806043057@N91440300MA5GX6XG8K@1

+ 1 - 1
src/cert/6888806043057.priv.pem → certs/6888806043057.key

@@ -25,4 +25,4 @@ pNGWumjVLXpDgsx5n9bLO8czLxzCKgLX6iUQ3X0CgYEAz/kvI9qznmcgUFf2i09k
 BiY9tZUBKv1QcNdRwjLhCpp24Ru/YjBU6UWN7LwbK3I91qHT1bTILIaAJYx2HwAc
 j40pBQrS9YVQo6NArlq8Cifd3/JELP1Jkr2OId5RHMP2SQUbX76g2W6P0Vqs/Zmn
 ad7Hduy/OzdMQD5r8ZsFC0Q=
------END PRIVATE KEY-----
+-----END PRIVATE KEY-----

+ 19 - 0
certs/sand.crt

@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDJjCCAg6gAwIBAgIGAViVUCVLMA0GCSqGSIb3DQEBCwUAMFQxDTALBgNVBAMT
+BFNBTkQxDTALBgNVBAsTBFNBTkQxDTALBgNVBAoTBFNBTkQxCzAJBgNVBAcTAlNI
+MQswCQYDVQQIEwJTSDELMAkGA1UEBhMCQ04wHhcNMTYxMTIzMDc1MDA3WhcNMjYx
+MTIyMDc1MDA3WjBUMQ0wCwYDVQQDEwRzYW5kMQ0wCwYDVQQLEwRTQU5EMQ0wCwYD
+VQQKEwRTQU5EMQswCQYDVQQHEwJTSDELMAkGA1UECBMCU0gxCzAJBgNVBAYTAkNO
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyIwo8Jq6XiUSY8cMrDfT
+Rb65QaWcPH2hITZrei3jgLIdHP3kTQjZueWhp2nQ7H9s6nD99MYSydB4YKZ5qVAo
+VxuwRE1fnNKOx8M3npIcr/JKtvCN5TrE1XIUyxWG3F7sPbsafN+7Gwxqh5gT4/u/
+zq5busBztvXh+/woiqi3EGQ1WO9+P4AtYA6nr3KoVU7hdO8Aj+6aXMjQQTtDrgH/
+oiAHkEMJfrQmZ6irdnxzRwQ53D/GzVieAqME/sUMeIBWiy/Uj7d2TVJZkLLlC76l
+g6AVo/z9Wl26T0wyttxlCzjfZt1naT3B5IIp8k6lYrOdj3SX1gMD3ej0NGnnrQuu
+vwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCd6W65I4SJ2BkH8RsBnZNDpQ7fMTYt
+VfQtBctptmEtbSlNz7WKrBFcjsZ6KvsMoUy0ftvaddHKu7t8UhW29C0vWGW00Ihf
+dkjGYMWxrNcicX/4KJPjFWwPXw11Vc3NuSlVJrr+eh5OSLHPnpvxoKs4I+55hXS4
+4Ch1x2LZ4rLsQ6vrVJz2mnygg2JEeredh74XAMAgAWGZ/Tqn4/QWpjFDggHOF8I9
+eXddK5yiD+cJ3EcZDYr4LGaaG95XQfvdKNl0igAfFmGd3Sxg5MrFnFJbDsqE0HAF
+crEaCK5rqJVMQdvZEWO4j6c6ZX9WCMfjcXRbZonE3b1DSXCrh1uildk2
+-----END CERTIFICATE-----

+ 143 - 8
pem.mjs

@@ -1,14 +1,149 @@
 import pem from 'pem'
 import { readFileSync } from 'fs'
-import { createPrivateKey, createSign } from 'crypto'
+import crypto from 'crypto'
+import axios from 'axios'
+import { format, addSeconds } from 'date-fns'
+import qs from 'querystring'
 
-const pfx = readFileSync('src/cert/6888806043057.pfx')
-pem.readPkcs12(pfx, { p12Password: '3edc#EDC' }, (err, cert) => {
-    console.log(cert)
+const privateKey = crypto.createPrivateKey({ key: readFileSync('certs/6888806043057.key') })
+const publicKey = crypto.createPublicKey({ key: readFileSync('certs/sand.crt') })
 
-    const privateKey = createPrivateKey({ key: cert.key })
-    const signer = createSign('SHA1')
+function privSign(str) {
+    const signer = crypto.createSign('SHA1')
+    signer.update(str)
+    signer.end()
     const signature = signer.sign(privateKey)
+    return signature.toString('base64')
+}
 
-    console.log(signature.toString('base64'))
-})
+function verifySign(str, sign) {
+    const verifier = crypto.createVerify('SHA1')
+    verifier.update(str)
+    verifier.end()
+    return verifier.verify(publicKey, Buffer.from(sign.replace(/\s/g, '+'), 'base64'))
+}
+
+async function request(header, body, url) {
+    const data = {
+        head: header,
+        body
+    }
+    const dataStr = JSON.stringify(data)
+    console.log(JSON.stringify(data, null, 4))
+    const sign = await privSign(dataStr)
+    const postBody = {
+        charset: 'UTF-8',
+        data: dataStr,
+        signType: '01',
+        sign,
+        extend: ''
+    }
+    console.log(qs.stringify(postBody))
+    return await axios.post(url, qs.stringify(postBody), {
+        headers: {
+            'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
+        },
+        timeout: 30000,
+        timeoutErrorMessage: '请求超时'
+    })
+}
+const algorithm = 'aes-128-ecb'
+async function pay() {
+    const data = {
+        cityNo: '',
+        productId: '00000004',
+        bankType: '',
+        payMode: '',
+        accNo: '6222024301070380165',
+        accName: '熊竹',
+        bankName: '',
+        remark: '消费',
+        channelType: '',
+        accAttr: '0',
+        version: '01',
+        timeOut: '20230506150701',
+        extend: '',
+        extendParams: '',
+        tranTime: '20230506150401',
+        provNo: '',
+        phone: '',
+        tranAmt: '000000000100',
+        reqReserved: '',
+        orderCode: new Date().getTime() + '',
+        accType: '4',
+        currencyCode: '156'
+    }
+    const dataStr = JSON.stringify(data)
+
+    // 加密密钥,必须是 16、24 或 32 个字符长(分别对应 AES-128、AES-192 或 AES-256)
+    const key = crypto.randomBytes(16)
+
+    // 加密算法使用 AES-256-ECB
+
+    // 将要加密的数据转换为一个 Buffer 对象
+    const dataBuffer = Buffer.from(dataStr, 'utf8')
+
+    // 创建一个加密器对象,使用 PKCS5Padding 进行补位
+    const cipher = crypto.createCipheriv(algorithm, key, null)
+    cipher.setAutoPadding(true)
+
+    // 加密数据,并返回加密后的 Buffer 对象
+    const encryptedBuffer = Buffer.concat([cipher.update(dataBuffer), cipher.final()])
+
+    // 将加密后的 Buffer 对象转换为十六进制字符串
+    const encrypted = encryptedBuffer.toString('base64')
+
+    const sign = await privSign(dataStr)
+
+    console.log(encrypted)
+
+    const encryptKey = crypto
+        .publicEncrypt(
+            {
+                key: publicKey,
+                padding: crypto.constants.RSA_PKCS1_PADDING
+            },
+            key
+        )
+        .toString('base64')
+    console.log(encryptKey)
+
+    const body = {
+        transCode: 'RTPM',
+        accessType: '0',
+        merId: '6888806043057',
+        encryptKey: encryptKey,
+        encryptData: encrypted,
+        sign,
+        extend: ''
+    }
+    const res = await axios.post('http://120.78.171.194:11223/agent-main/openapi/agentpay', qs.stringify(body))
+
+    const retData = qs.parse(qs.unescape(res.data))
+    console.log(retData)
+
+    const decryptKey = crypto.privateDecrypt(
+        {
+            key: privateKey,
+            padding: crypto.constants.RSA_PKCS1_PADDING
+        },
+        Buffer.from(retData.encryptKey.replace(/\s/g, '+'), 'base64')
+    )
+    console.log(decryptKey.toString())
+
+    const decipher = crypto.createDecipheriv(algorithm, decryptKey, null)
+    decipher.setAutoPadding(true)
+
+    const decryptedBuffer = Buffer.concat([
+        decipher.update(Buffer.from(retData.encryptData.replace(/\s/g, '+'), 'base64')),
+        decipher.final()
+    ])
+    const decrypted = decryptedBuffer.toString('utf8')
+    console.log(decrypted)
+
+    const verify = verifySign(decrypted, retData.sign)
+    console.log(verify)
+
+
+}
+await pay()

+ 156 - 4
src/utils/sand-pay.ts

@@ -1,7 +1,159 @@
-import { createSign } from 'crypto'
-function requestServer(header: any, body: any, url: string) {
-    const req = {
+import { readFileSync } from 'fs'
+import * as crypto from 'crypto'
+import axios from 'axios'
+import { format, addSeconds } from 'date-fns'
+import * as qs from 'querystring'
+import * as path from 'path'
+import { InternalServerErrorException, Logger } from '@nestjs/common'
+
+const TAG = 'SandPay'
+
+const privateKey = crypto.createPrivateKey({
+    key: readFileSync(path.join(__dirname, '..', '..', 'certs', '6888806043057.key'))
+})
+
+const publicKey = crypto.createPublicKey({
+    key: readFileSync(path.join(__dirname, '..', '..', 'certs', 'sand.crt'))
+})
+
+function privSign(str) {
+    const signer = crypto.createSign('SHA1')
+    signer.update(str)
+    signer.end()
+    const signature = signer.sign(privateKey)
+    return signature.toString('base64')
+}
+
+function verifySign(str, sign) {
+    const verifier = crypto.createVerify('SHA1')
+    verifier.update(str)
+    verifier.end()
+    return verifier.verify(publicKey, Buffer.from(sign.replace(/\s/g, '+'), 'base64'))
+}
+
+async function request(header, body, url) {
+    const data = {
         head: header,
-        body: body
+        body
+    }
+    const dataStr = JSON.stringify(data)
+    console.log(JSON.stringify(data, null, 4))
+    const sign = await privSign(dataStr)
+    const postBody = {
+        charset: 'UTF-8',
+        data: dataStr,
+        signType: '01',
+        sign,
+        extend: ''
+    }
+    console.log(qs.stringify(postBody))
+    return await axios.post(url, qs.stringify(postBody), {
+        headers: {
+            'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
+        },
+        timeout: 30000,
+        timeoutErrorMessage: '请求超时'
+    })
+}
+const algorithm = 'aes-128-ecb'
+async function withdraw(name, bank, amount) {
+    const data = {
+        cityNo: '',
+        productId: '00000004',
+        bankType: '',
+        payMode: '',
+        accNo: bank,
+        accName: name,
+        bankName: '',
+        remark: '消费',
+        channelType: '',
+        accAttr: '0',
+        version: '01',
+        timeOut: format(addSeconds(new Date(), 180), 'yyyyMMddHHmmss'),
+        extend: '',
+        extendParams: '',
+        tranTime: format(new Date(), 'yyyyMMddHHmmss'),
+        provNo: '',
+        phone: '',
+        tranAmt: (amount * 100).toFixed(0).padStart(12, '0'),
+        reqReserved: '',
+        orderCode: new Date().getTime() + '',
+        accType: '4',
+        currencyCode: '156'
+    }
+    const dataStr = JSON.stringify(data)
+
+    // 加密密钥,必须是 16、24 或 32 个字符长(分别对应 AES-128、AES-192 或 AES-256)
+    const key = crypto.randomBytes(16)
+
+    // 加密算法使用 AES-256-ECB
+
+    // 将要加密的数据转换为一个 Buffer 对象
+    const dataBuffer = Buffer.from(dataStr, 'utf8')
+
+    // 创建一个加密器对象,使用 PKCS5Padding 进行补位
+    const cipher = crypto.createCipheriv(algorithm, key, null)
+    cipher.setAutoPadding(true)
+
+    // 加密数据,并返回加密后的 Buffer 对象
+    const encryptedBuffer = Buffer.concat([cipher.update(dataBuffer), cipher.final()])
+
+    // 将加密后的 Buffer 对象转换为十六进制字符串
+    const encrypted = encryptedBuffer.toString('base64')
+
+    const sign = await privSign(dataStr)
+
+    const encryptKey = crypto
+        .publicEncrypt(
+            {
+                key: publicKey,
+                padding: crypto.constants.RSA_PKCS1_PADDING
+            },
+            key
+        )
+        .toString('base64')
+
+    const body = {
+        transCode: 'RTPM',
+        accessType: '0',
+        merId: '6888806043057',
+        encryptKey: encryptKey,
+        encryptData: encrypted,
+        sign,
+        extend: ''
+    }
+    const res = await axios.post('http://120.78.171.194:11223/agent-main/openapi/agentpay', qs.stringify(body))
+
+    const retData = qs.parse(qs.unescape(res.data))
+
+    const decryptKey = crypto.privateDecrypt(
+        {
+            key: privateKey,
+            padding: crypto.constants.RSA_PKCS1_PADDING
+        },
+        Buffer.from((retData.encryptKey as string).replace(/\s/g, '+'), 'base64')
+    )
+
+    const decipher = crypto.createDecipheriv(algorithm, decryptKey, null)
+    decipher.setAutoPadding(true)
+
+    const decryptedBuffer = Buffer.concat([
+        decipher.update(Buffer.from((retData.encryptData as string).replace(/\s/g, '+'), 'base64')),
+        decipher.final()
+    ])
+    const decrypted = decryptedBuffer.toString('utf8')
+
+    const verify = verifySign(decrypted, retData.sign)
+
+    if (!verify) {
+        throw new InternalServerErrorException('验签失败')
+    }
+
+    const ret = JSON.parse(decrypted)
+
+    Logger.log(`withdraw resp=${JSON.stringify(ret, null, 4)}`, TAG)
+    if (ret.respCode !== '0000') {
+        throw new InternalServerErrorException(ret.respDesc)
     }
 }
+export { withdraw }

+ 12 - 6
src/withdraw/withdraw.service.ts

@@ -1,4 +1,4 @@
-import { BadRequestException, Injectable } from '@nestjs/common'
+import { BadRequestException, Injectable, InternalServerErrorException } from '@nestjs/common'
 import { InjectRepository } from '@nestjs/typeorm'
 import { UserBalanceService } from '../user-balance/user-balance.service'
 import { Withdraw, WithdrawStatus } from './entities/withdraw.entity'
@@ -7,6 +7,7 @@ import BigNumber from 'bignumber.js'
 import { BalanceType } from '../user-balance/entities/balance-record.entity'
 import { IPaginationOptions, Pagination, paginate } from 'nestjs-typeorm-paginate'
 import { PageRequest } from '../common/dto/page-request'
+import { withdraw } from '../utils/sand-pay'
 
 @Injectable()
 export class WithdrawService {
@@ -53,14 +54,19 @@ export class WithdrawService {
     }
 
     async finishWithdraw(id: number) {
-        const withdraw = await this.withdrawRepository.findOneBy({ id })
-        if (!withdraw) {
+        const apply = await this.withdrawRepository.findOneBy({ id })
+        if (!apply) {
             throw new BadRequestException('提现记录不存在')
         }
-        if (withdraw.status !== WithdrawStatus.PENDING) {
+        if (apply.status !== WithdrawStatus.PENDING) {
             throw new BadRequestException('提现记录不可完成')
         }
-        withdraw.status = WithdrawStatus.SUCCESS
-        return await this.withdrawRepository.save(withdraw)
+        try {
+            await withdraw(apply.name, apply.account, apply.amount.toNumber())
+            apply.status = WithdrawStatus.SUCCESS
+            return await this.withdrawRepository.save(apply)
+        } catch (error) {
+            throw new InternalServerErrorException(error.message)
+        }
     }
 }