q-koa 13.6.0 → 13.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,15 @@ const fsPromise = require('fs/promises')
13
13
  const WeixinMp = require('../../services/weixinMP')
14
14
  const OSS = require('ali-oss')
15
15
 
16
+ const axios = require('axios')
17
+ const AdmZip = require('adm-zip')
18
+ const fs = require('fs')
19
+
20
+ const XLSX = require('xlsx')
21
+ const _ = require('lodash')
22
+
23
+ const AllinPay = require('../../services/allinpay')
24
+
16
25
  exports.b2b_refund = async ({
17
26
  ctx,
18
27
  id,
@@ -20,20 +29,25 @@ exports.b2b_refund = async ({
20
29
  total_fee,
21
30
  refund_fee,
22
31
  price,
23
- pay_type = 'B2B-WEIXIN',
32
+ pay_type = 'MP-WEIXIN',
24
33
  type = '',
25
34
  config = 'weixin_mp',
26
35
  pay_config = 'weixin_pay',
27
36
  out_trade_no: _out_trade_no,
28
37
  out_refund_no: _out_refund_no,
29
38
  refund_from = 1,
39
+ mch_id = '',
30
40
  ...rest
31
41
  }) => {
32
42
  if (!ctx) throw new Error('?ctx')
33
43
  const { app, appName } = getAppByCtx(ctx)
34
44
  const appConfig = getConfig(app)
35
45
  const { app_id, app_secrect } = await appConfig.getObject(config)
36
- const { mchId: mchid, key, app_key } = await appConfig.getObject(pay_config)
46
+ const { mchId, b2b_mchId, key, app_key } = await appConfig.getObject(
47
+ pay_config
48
+ )
49
+
50
+ const mchid = mch_id || b2b_mchId || mchId
37
51
 
38
52
  const weixinMp = new WeixinMp({
39
53
  appid: app_id,
@@ -54,6 +68,17 @@ exports.b2b_refund = async ({
54
68
 
55
69
  if (!orderDetail) throw new Error('不存在该订单')
56
70
 
71
+ const payResult = await weixinMp.b2bCheckOrder({
72
+ out_trade_no,
73
+ mchid,
74
+ app_key,
75
+ })
76
+
77
+ if (payResult.settle_status !== 2) {
78
+ console.log(payResult)
79
+ throw new Error('系统正在结算,请等待若干分钟后才能退款')
80
+ }
81
+
57
82
  const refundRecords = lodash.get(orderDetail, 'order_refund_records', [])
58
83
  const refundNumber = refundRecords.length + 1
59
84
  const out_refund_no =
@@ -710,17 +735,20 @@ exports.retail_refund_notify = async ({ ctx, app, result }) => {
710
735
  const type = trade_no.includes('_') ? trade_no.split('_')[1] : ''
711
736
 
712
737
  if (app.service[model] && app.service[model].refund_notify) {
713
- if (result.refund_status === 'REFUND_SUCC') {
714
- await app.service[model].refund_notify({
715
- ctx,
716
- app,
717
- order_id,
718
- order: model,
719
- refund_price: result.refund_amount / 100,
720
- type,
721
- mch_id: result.mchid,
722
- })
723
- } else {
738
+ await app.service[model].refund_notify({
739
+ ctx,
740
+ app,
741
+ order_id,
742
+ order: model,
743
+ refund_price: result.refund_amount / 100,
744
+ type,
745
+ mch_id: result.mchid,
746
+ refundid: result.refundid,
747
+ out_trade_no: result.out_trade_no,
748
+ is_success: result.refund_status === 'REFUND_SUCC',
749
+ })
750
+
751
+ if (result.refund_status !== 'REFUND_SUCC') {
724
752
  app.service.log.push({
725
753
  app,
726
754
  message: JSON.stringify(result),
@@ -759,6 +787,7 @@ exports.retail_pay_notify = async ({ ctx, app, result }) => {
759
787
  order_price: result.amount.order_amount / 100,
760
788
  transactionid,
761
789
  mch_id: result.mchid,
790
+ is_b2b: true,
762
791
  })
763
792
  } else {
764
793
  app.service.log.push({
@@ -780,7 +809,7 @@ exports.pc_pay = async ({
780
809
  pay_config = 'weixin_pay',
781
810
  out_trade_no: _out_trade_no,
782
811
  is_admin = false,
783
- type = 'PC-WEIXIN',
812
+ type = 'SCAN-WEIXIN',
784
813
  prefix = '',
785
814
  }) => {
786
815
  if (!ctx) throw new Error('?ctx')
@@ -1016,6 +1045,279 @@ const handleBillData = (result) => {
1016
1045
  }
1017
1046
  }
1018
1047
 
1048
+ async function downloadZip(url) {
1049
+ const response = await axios.get(url, {
1050
+ responseType: 'arraybuffer',
1051
+ timeout: 30000,
1052
+ headers: {
1053
+ 'User-Agent':
1054
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
1055
+ Accept: 'application/zip,application/octet-stream,*/*;q=0.8',
1056
+ Referer: 'https://cus.allinpay.com/',
1057
+ },
1058
+ maxRedirects: 5,
1059
+ })
1060
+ if (response.status !== 200) {
1061
+ throw new Error(`下载失败: HTTP ${response.status}`)
1062
+ }
1063
+ return Buffer.from(response.data)
1064
+ }
1065
+
1066
+ function extractXlsx(zipBuffer, targetDir) {
1067
+ const zip = new AdmZip(zipBuffer)
1068
+ zip.extractAllTo(targetDir, true)
1069
+
1070
+ const xlsxFiles = []
1071
+ const walk = (dir) => {
1072
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1073
+ const fullPath = path.join(dir, entry.name)
1074
+ if (entry.isDirectory()) {
1075
+ walk(fullPath)
1076
+ } else if (entry.name.toLowerCase().endsWith('.xlsx')) {
1077
+ xlsxFiles.push(fullPath)
1078
+ }
1079
+ }
1080
+ }
1081
+ walk(targetDir)
1082
+
1083
+ if (xlsxFiles.length === 0) {
1084
+ throw new Error('ZIP 包中未找到 xlsx 文件')
1085
+ }
1086
+ return xlsxFiles
1087
+ }
1088
+
1089
+ function parseTransactions(xlsxPath) {
1090
+ const workbook = XLSX.readFile(xlsxPath)
1091
+ const sheet = workbook.Sheets[workbook.SheetNames[0]]
1092
+ const rows = XLSX.utils.sheet_to_json(sheet, {
1093
+ header: 1,
1094
+ defval: '',
1095
+ raw: false,
1096
+ })
1097
+
1098
+ // 1. 定位表头行(包含 "终端号" 或 "交易时间")
1099
+ const headerKeywords = ['终端号', '交易时间', '交易类型']
1100
+ let headerIndex = rows.findIndex((row) =>
1101
+ headerKeywords.every((kw) => row.some((cell) => String(cell).trim() === kw))
1102
+ )
1103
+ if (headerIndex === -1) {
1104
+ throw new Error('未找到表头行')
1105
+ }
1106
+
1107
+ const headers = rows[headerIndex].map((h) => String(h || '').trim())
1108
+
1109
+ // 2. 从表头下一行开始取数据,遇到小计/合计/汇总行停止
1110
+ const stopKeywords = ['小计', '合计', '汇总']
1111
+ const records = []
1112
+
1113
+ for (let i = headerIndex + 1; i < rows.length; i++) {
1114
+ const row = rows[i]
1115
+ if (!row || row.every((cell) => String(cell || '').trim() === '')) continue
1116
+
1117
+ const firstNonEmpty = String(
1118
+ row.find((cell) => String(cell || '').trim() !== '') || ''
1119
+ ).trim()
1120
+ if (stopKeywords.some((kw) => firstNonEmpty.includes(kw))) break
1121
+
1122
+ const record = {}
1123
+ headers.forEach((header, idx) => {
1124
+ if (header)
1125
+ record[header] = row[idx] !== undefined ? String(row[idx]).trim() : ''
1126
+ })
1127
+ records.push(record)
1128
+ }
1129
+
1130
+ return records
1131
+ }
1132
+
1133
+ exports.union_downloadBill = async ({
1134
+ ctx,
1135
+ date,
1136
+ pay_config = 'weixin_pay',
1137
+ }) => {
1138
+ const { app, appName } = getAppByCtx(ctx)
1139
+ const appConfig = getConfig(app)
1140
+ const { appId, union_mchId, union_appid, union_privateKey, union_publicKey } =
1141
+ await appConfig.getObject(pay_config)
1142
+
1143
+ const allinpay = new AllinPay({
1144
+ cusid: union_mchId,
1145
+ appid: union_appid,
1146
+ isTest: false,
1147
+ subAppid: appId,
1148
+ privateKey: union_privateKey,
1149
+ publicKey: union_publicKey,
1150
+ })
1151
+
1152
+ const result = await allinpay.downloadBill({
1153
+ date: date || moment().add(-1, 'days').format('YYYYMMDD'),
1154
+ })
1155
+
1156
+ if (result && result.url) {
1157
+ console.log('\n[1/2] 正在下载 ZIP 文件...')
1158
+ const zipBuffer = await downloadZip(result.url)
1159
+ console.log(`下载完成,大小: ${zipBuffer.length} bytes`)
1160
+
1161
+ console.log('\n[2/2] 正在解压...')
1162
+ const xlsxFiles = extractXlsx(
1163
+ zipBuffer,
1164
+ path.resolve(__dirname, `${process.cwd()}/public/upload`)
1165
+ )
1166
+
1167
+ console.log(`\n========== 完成 ==========`)
1168
+ console.log(`解压出 ${xlsxFiles.length} 个 xlsx 文件:`)
1169
+ xlsxFiles.forEach((f) => console.log(' -', f))
1170
+ const res = parseTransactions(
1171
+ path.resolve(
1172
+ __dirname,
1173
+ `${process.cwd()}/public/upload/${union_mchId}.xlsx`
1174
+ )
1175
+ )
1176
+ let formatRes = []
1177
+ let order_list = []
1178
+ let member_order_list = []
1179
+ let invest_order_list = []
1180
+ let returnRes = []
1181
+ for (const item of res) {
1182
+ const is_refund = !(item['交易类型'] === '微信支付')
1183
+ let order_id = 0
1184
+ let member_order_id = 0
1185
+ let invest_order_id = 0
1186
+ let prefix = ''
1187
+ if (!is_refund) {
1188
+ const orderPrefix = item['订单号'].split('_')[1]
1189
+
1190
+ if (orderPrefix.includes('-')) {
1191
+ order_id = Number(orderPrefix.split('-')[1])
1192
+ prefix = orderPrefix.split('-')[0]
1193
+ if (prefix === 'member') {
1194
+ member_order_id = order_id
1195
+ order_id = 0
1196
+ } else if (prefix === 'invest') {
1197
+ invest_order_id = order_id
1198
+ order_id = 0
1199
+ }
1200
+ } else {
1201
+ order_id = Number(orderPrefix)
1202
+ }
1203
+ } else {
1204
+ console.log(item['订单号'])
1205
+ const orderPrefix = item['订单号'].split('_')[0]
1206
+ prefix = orderPrefix.split('-')[0]
1207
+ if (prefix === 'member') {
1208
+ member_order_id = Number(orderPrefix.split('-')[1])
1209
+ order_id = 0
1210
+ invest_order_id = 0
1211
+ } else if (prefix === 'invest') {
1212
+ invest_order_id = Number(orderPrefix.split('-')[1])
1213
+ order_id = 0
1214
+ member_order_id = 0
1215
+ } else if (prefix === '') {
1216
+ order_id = Number(orderPrefix.split('-')[1])
1217
+ invest_order_id = 0
1218
+ member_order_id = 0
1219
+ }
1220
+ }
1221
+ if (order_id) {
1222
+ order_list.push(order_id)
1223
+ }
1224
+ if (member_order_id) {
1225
+ member_order_list.push(member_order_id)
1226
+ }
1227
+ if (invest_order_id) {
1228
+ invest_order_list.push(invest_order_id)
1229
+ }
1230
+ formatRes.push({
1231
+ ...item,
1232
+ mchid: union_mchId,
1233
+ date: item['交易日期'],
1234
+ order_id,
1235
+ invest_order_id,
1236
+ member_order_id,
1237
+ created_at: `${item['交易日期']} ${item['交易时间']}`,
1238
+ transactionid: item['参考号'],
1239
+ ...(is_refund
1240
+ ? {
1241
+ price: 0,
1242
+ refund_price: Math.abs(Number(item['交易金额'])),
1243
+
1244
+ type: 'REFUND',
1245
+ rate_price: Number(item['手续费']),
1246
+ }
1247
+ : {
1248
+ price: Math.abs(Number(item['交易金额'])),
1249
+ refund_price: 0,
1250
+ type: 'SUCCESS',
1251
+ rate_price: Number(item['手续费']),
1252
+ }),
1253
+ })
1254
+ }
1255
+
1256
+ const orderList = await app.model.order.findAll({
1257
+ attributes: ['application_id', 'created_at', 'id', 'orderid'],
1258
+ where: {
1259
+ id: order_list,
1260
+ },
1261
+ include: [
1262
+ {
1263
+ model: app.model.user,
1264
+ attributes: ['mp_openid'],
1265
+ },
1266
+ ],
1267
+ })
1268
+
1269
+ member_order_list = await app.model.member_order.findAll({
1270
+ attributes: ['application_id', 'created_at', 'id'],
1271
+ where: {
1272
+ id: member_order_list,
1273
+ },
1274
+ include: [
1275
+ {
1276
+ model: app.model.user,
1277
+ attributes: ['mp_openid'],
1278
+ },
1279
+ {
1280
+ model: app.model.application,
1281
+ attributes: ['name'],
1282
+ },
1283
+ ],
1284
+ })
1285
+
1286
+ for (const item of formatRes) {
1287
+ let openid
1288
+ let remark
1289
+ let order_date
1290
+ let application_id
1291
+ if (item.order_id) {
1292
+ const orderTarget = orderList.find((o) => o.id === item.order_id)
1293
+ openid = orderTarget.user && orderTarget.user.mp_openid
1294
+ remark = `订单${orderTarget.orderid}`
1295
+ order_date = moment(orderTarget.created_at).format('YYYY-MM-DD')
1296
+ application_id = orderTarget.application_id
1297
+ } else if (item.member_order_id) {
1298
+ const orderTarget = member_order_list.find(
1299
+ (o) => o.id === item.member_order_id
1300
+ )
1301
+ openid = orderTarget.user && orderTarget.user.mp_openid
1302
+ remark = `购买${orderTarget.application.name}会员${item.member_order_id}`
1303
+ order_date = moment(orderTarget.created_at).format('YYYY-MM-DD')
1304
+ application_id = orderTarget.application_id
1305
+ }
1306
+ returnRes.push({
1307
+ ...item,
1308
+ openid,
1309
+ remark,
1310
+ order_date,
1311
+ application_id,
1312
+ })
1313
+ }
1314
+
1315
+ return returnRes
1316
+ } else {
1317
+ throw new Error('没有url')
1318
+ }
1319
+ }
1320
+
1019
1321
  exports.downloadBill = async ({
1020
1322
  ctx,
1021
1323
  pay_config = 'weixin_pay',
@@ -1059,3 +1361,95 @@ exports.uploadBill = async ({ ctx, content }) => {
1059
1361
  return []
1060
1362
  }
1061
1363
  }
1364
+
1365
+ exports.union_refund = async ({
1366
+ ctx,
1367
+ id,
1368
+ prefix = '',
1369
+ total_fee,
1370
+ refund_fee,
1371
+ price,
1372
+ pay_type = 'MP-WEIXIN',
1373
+ type = '',
1374
+ config = 'weixin_mp',
1375
+ pay_config = 'weixin_pay',
1376
+ out_trade_no: _out_trade_no,
1377
+ out_refund_no: _out_refund_no,
1378
+ is_pem = false,
1379
+ mch_id = '',
1380
+ ...rest
1381
+ }) => {
1382
+ if (!ctx) throw new Error('?ctx')
1383
+ const { app, appName } = getAppByCtx(ctx)
1384
+ const isPem = is_pem || lodash.get(app, 'appConfig.is_pem', false)
1385
+ const appConfig = getConfig(app)
1386
+ const {
1387
+ mchId,
1388
+ key,
1389
+ appId,
1390
+ partner_key,
1391
+ union_mchId,
1392
+ union_appid,
1393
+ union_privateKey,
1394
+ union_publicKey,
1395
+ } = await appConfig.getObject(pay_config)
1396
+ const { site_host } = await appConfig.getObject('base')
1397
+ const notify_url = `https://${site_host}/${appName}/weixin/union_notify`
1398
+
1399
+ const allinpay = new AllinPay({
1400
+ cusid: union_mchId,
1401
+ appid: union_appid,
1402
+ isTest: false,
1403
+ subAppid: appId,
1404
+ privateKey: union_privateKey,
1405
+ publicKey: union_publicKey,
1406
+ notifyUrl: notify_url,
1407
+ })
1408
+ const orderModel = prefix ? `${prefix}_order` : 'order'
1409
+ const out_trade_no = _out_trade_no
1410
+ ? _out_trade_no
1411
+ : prefix
1412
+ ? `${key}_${prefix}-${id}_${pay_type}`
1413
+ : `${key}_${id}_${pay_type}`
1414
+ const orderDetail = await app.model[orderModel].findOne({
1415
+ where: {
1416
+ id,
1417
+ },
1418
+ include: app.model.order_refund_record,
1419
+ })
1420
+ if (!orderDetail) throw new Error('不存在该订单')
1421
+
1422
+ // const payResult = await wxpay.queryOrderSync({
1423
+ // out_trade_no,
1424
+ // })
1425
+
1426
+ try {
1427
+ const refundRecords = lodash.get(orderDetail, 'order_refund_records', [])
1428
+ const refundNumber = refundRecords.length + 1
1429
+ const out_refund_no =
1430
+ _out_refund_no || [`${prefix}-${id}`, type, refundNumber].join('_')
1431
+
1432
+ // const data = {
1433
+ // ...rest,
1434
+ // out_trade_no,
1435
+ // out_refund_no,
1436
+ // total_fee: Number(payResult.total_fee),
1437
+ // refund_fee: Math.round((refund_fee || price) * 100),
1438
+ // notify_url: `https://${
1439
+ // site_host || 'api.kuashou.com'
1440
+ // }/${appName}/weixin/refund_notify/${pay_config}`,
1441
+ // }
1442
+
1443
+ const result = await allinpay.refundOrder({
1444
+ oldreqsn: out_trade_no,
1445
+ trxamt: Math.round((refund_fee || price) * 100) + '',
1446
+ reqsn: out_refund_no,
1447
+ remark: rest.refund_desc,
1448
+ })
1449
+
1450
+ console.log(result)
1451
+ return result
1452
+ } catch (e) {
1453
+ throw new Error(e.message)
1454
+ }
1455
+ }