|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+import axios from 'axios';
|
|
|
2
|
+import moment from 'moment';
|
|
|
3
|
+import config from '../../config/index.js';
|
|
|
4
|
+import { getConnection, query } from '../../util/db.js';
|
|
|
5
|
+import {
|
|
|
6
|
+ buildV2Sign,
|
|
|
7
|
+ buildV2Xml,
|
|
|
8
|
+ createNonce,
|
|
|
9
|
+ createTradeNo,
|
|
|
10
|
+ normalizeV2Fields,
|
|
|
11
|
+ parseV2Xml,
|
|
|
12
|
+ verifyV2Sign
|
|
|
13
|
+} from './wechatPayV2.js';
|
|
|
14
|
+
|
|
|
15
|
+export const WECHAT_SERVICE_PRODUCT_ID = 167;
|
|
|
16
|
+export const WECHAT_SERVICE_PAY_TYPE = 8;
|
|
|
17
|
+export const WECHAT_SERVICE_BUY_TYPE = 131;
|
|
|
18
|
+export const WECHAT_SERVICE_TRIAL_AMOUNT_FEN = 100;
|
|
|
19
|
+const TRIAL_DAYS = 16;
|
|
|
20
|
+const ORDER_BODY = '秒过-试用(服务号)';
|
|
|
21
|
+const UNIFIED_ORDER_URL = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
|
|
|
22
|
+const ORDER_QUERY_URL = 'https://api.mch.weixin.qq.com/pay/orderquery';
|
|
|
23
|
+
|
|
|
24
|
+function dependenciesOf(value) {
|
|
|
25
|
+ return value && typeof value === 'object' ? value : {};
|
|
|
26
|
+}
|
|
|
27
|
+
|
|
|
28
|
+function sendSuccess(ctx, result) {
|
|
|
29
|
+ ctx.body = { errcode: 10000, result };
|
|
|
30
|
+}
|
|
|
31
|
+
|
|
|
32
|
+function sendFailure(ctx, error) {
|
|
|
33
|
+ const message = error instanceof Error ? error.message : String(error || '服务号支付处理失败');
|
|
|
34
|
+ ctx.body = { errcode: 101, errStr: message, result: { errorMessage: message } };
|
|
|
35
|
+}
|
|
|
36
|
+
|
|
|
37
|
+function assertPaymentConfig() {
|
|
|
38
|
+ if (!config.wx.wechatservice_appid || !config.wx.wechatservice_appsecret)
|
|
|
39
|
+ throw new Error('缺少服务号 AppID 或 AppSecret 配置');
|
|
|
40
|
+ if (!config.wx.mch_id || !config.wx.payapisecret)
|
|
|
41
|
+ throw new Error('缺少微信商户号或 APIv2 密钥配置');
|
|
|
42
|
+}
|
|
|
43
|
+
|
|
|
44
|
+function assertWechatIdentity(fields) {
|
|
|
45
|
+ if (fields.appid !== String(config.wx.wechatservice_appid))
|
|
|
46
|
+ throw new Error('微信支付 AppID 与服务号配置不一致');
|
|
|
47
|
+ if (fields.mch_id !== String(config.wx.mch_id))
|
|
|
48
|
+ throw new Error('微信支付商户号与本地配置不一致');
|
|
|
49
|
+}
|
|
|
50
|
+
|
|
|
51
|
+function assertSignedWechatResponse(fields) {
|
|
|
52
|
+ if (!verifyV2Sign(fields, config.wx.payapisecret))
|
|
|
53
|
+ throw new Error('微信支付响应签名校验失败');
|
|
|
54
|
+ assertWechatIdentity(fields);
|
|
|
55
|
+}
|
|
|
56
|
+
|
|
|
57
|
+function v2Error(fields, fallback) {
|
|
|
58
|
+ return fields.err_code_des || fields.return_msg || fields.err_code || fallback;
|
|
|
59
|
+}
|
|
|
60
|
+
|
|
|
61
|
+function requestIp(ctx) {
|
|
|
62
|
+ const forwarded = String(ctx.request.headers['x-forwarded-for'] || '').split(',')[0].trim();
|
|
|
63
|
+ const candidate = forwarded || ctx.req.socket?.remoteAddress || '';
|
|
|
64
|
+ const normalized = candidate.replace(/^::ffff:/, '');
|
|
|
65
|
+ return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(normalized) ? normalized : '127.0.0.1';
|
|
|
66
|
+}
|
|
|
67
|
+
|
|
|
68
|
+function parsePaidTime(value) {
|
|
|
69
|
+ const parsed = moment(String(value || ''), 'YYYYMMDDHHmmss', true);
|
|
|
70
|
+ return parsed.isValid() ? parsed : moment();
|
|
|
71
|
+}
|
|
|
72
|
+
|
|
|
73
|
+async function postWechatXml(httpClient, url, fields) {
|
|
|
74
|
+ const xml = buildV2Xml(fields);
|
|
|
75
|
+ const { data } = await httpClient.post(url, xml, {
|
|
|
76
|
+ headers: { 'Content-Type': 'text/xml; charset=utf-8' },
|
|
|
77
|
+ timeout: config.wechatServicePay.requestTimeoutMs,
|
|
|
78
|
+ responseType: 'text',
|
|
|
79
|
+ transformRequest: [value => value]
|
|
|
80
|
+ });
|
|
|
81
|
+ return { xml, fields: parseV2Xml(data) };
|
|
|
82
|
+}
|
|
|
83
|
+
|
|
|
84
|
+function buildJsapiParameters(prepayId) {
|
|
|
85
|
+ const result = {
|
|
|
86
|
+ appId: String(config.wx.wechatservice_appid),
|
|
|
87
|
+ timeStamp: String(Math.floor(Date.now() / 1000)),
|
|
|
88
|
+ nonceStr: createNonce(),
|
|
|
89
|
+ package: `prepay_id=${prepayId}`,
|
|
|
90
|
+ signType: 'MD5'
|
|
|
91
|
+ };
|
|
|
92
|
+ result.paySign = buildV2Sign(result, config.wx.payapisecret);
|
|
|
93
|
+ return result;
|
|
|
94
|
+}
|
|
|
95
|
+
|
|
|
96
|
+async function runPostPayMessages({ serviceUser }, dependencies = {}) {
|
|
|
97
|
+ const httpClient = dependencies.httpClient || axios;
|
|
|
98
|
+ const wait = dependencies.wait || (ms => new Promise(resolve => setTimeout(resolve, ms)));
|
|
|
99
|
+ for (const messageID of [4, 5]) {
|
|
|
100
|
+ await httpClient.get(`http://localhost:${config.port}/api/SendWXServiceMessage`, {
|
|
|
101
|
+ params: { UserID: serviceUser.UserID, MessageID: messageID },
|
|
|
102
|
+ timeout: config.wechatServicePay.requestTimeoutMs
|
|
|
103
|
+ });
|
|
|
104
|
+ if (messageID === 4) await wait(500);
|
|
|
105
|
+ }
|
|
|
106
|
+}
|
|
|
107
|
+
|
|
|
108
|
+export async function fulfillWechatServiceOrder(tradeNo, payment, dependencies = {}) {
|
|
|
109
|
+ dependencies = dependenciesOf(dependencies);
|
|
|
110
|
+ const connectionFactory = dependencies.getConnection || getConnection;
|
|
|
111
|
+ const postPayActions = dependencies.runPostPayActions || runPostPayMessages;
|
|
|
112
|
+ const schedulePostCommit = dependencies.schedulePostCommit || (callback => setTimeout(callback, 0));
|
|
|
113
|
+ const conn = await connectionFactory();
|
|
|
114
|
+ let postCommit = null;
|
|
|
115
|
+
|
|
|
116
|
+ try {
|
|
|
117
|
+ await conn.beginTransaction();
|
|
|
118
|
+ const [orders] = await conn.query(
|
|
|
119
|
+ 'SELECT * FROM ProductPayInfo WHERE TradeNo=? AND ProductID=? FOR UPDATE',
|
|
|
120
|
+ [tradeNo, WECHAT_SERVICE_PRODUCT_ID]
|
|
|
121
|
+ );
|
|
|
122
|
+ if (orders.length !== 1) throw new Error('未找到唯一的服务号支付订单');
|
|
|
123
|
+
|
|
|
124
|
+ const order = orders[0];
|
|
|
125
|
+ if (Number(order.Status) === 1) {
|
|
|
126
|
+ await conn.commit();
|
|
|
127
|
+ return { alreadyFulfilled: true, order };
|
|
|
128
|
+ }
|
|
|
129
|
+ if (Number(order.PayType) !== WECHAT_SERVICE_PAY_TYPE
|
|
|
130
|
+ || Number(order.BuyType) !== WECHAT_SERVICE_BUY_TYPE)
|
|
|
131
|
+ throw new Error('本地订单类型不符合服务号 1 元试用规则');
|
|
|
132
|
+ if (Number(order.Money) !== WECHAT_SERVICE_TRIAL_AMOUNT_FEN)
|
|
|
133
|
+ throw new Error('本地服务号支付订单金额不是 1 元');
|
|
|
134
|
+ if (!payment.transactionId)
|
|
|
135
|
+ throw new Error('微信支付结果缺少交易单号');
|
|
|
136
|
+ if (payment.openid !== order.OpenID)
|
|
|
137
|
+ throw new Error('微信支付用户与本地订单不一致');
|
|
|
138
|
+ if (Number(payment.totalFee) !== Number(order.Money))
|
|
|
139
|
+ throw new Error('微信支付金额与本地订单不一致');
|
|
|
140
|
+
|
|
|
141
|
+ const [serviceUsers] = await conn.query(
|
|
|
142
|
+ 'SELECT * FROM WechatServiceWXUsers WHERE OpenID=? FOR UPDATE',
|
|
|
143
|
+ [order.OpenID]
|
|
|
144
|
+ );
|
|
|
145
|
+ if (serviceUsers.length !== 1) throw new Error('未找到唯一的服务号用户');
|
|
|
146
|
+ const serviceUser = serviceUsers[0];
|
|
|
147
|
+
|
|
|
148
|
+ let miaoguoUsers = [];
|
|
|
149
|
+ if (serviceUser.UnionID) {
|
|
|
150
|
+ [miaoguoUsers] = await conn.query(
|
|
|
151
|
+ 'SELECT * FROM MiaoguoWXUsers WHERE UnionID=? FOR UPDATE',
|
|
|
152
|
+ [serviceUser.UnionID]
|
|
|
153
|
+ );
|
|
|
154
|
+ }
|
|
|
155
|
+
|
|
|
156
|
+ const paidMoment = parsePaidTime(payment.timeEnd);
|
|
|
157
|
+ const paidAt = paidMoment.format('YYYY-MM-DD HH:mm:ss');
|
|
|
158
|
+ const trialEnd = paidMoment.clone().add(TRIAL_DAYS, 'days').format('YYYY-MM-DD HH:mm:ss');
|
|
|
159
|
+
|
|
|
160
|
+ await conn.query(
|
|
|
161
|
+ 'UPDATE WechatServiceWXUsers SET IsProbation=1,ProbationPayTime=? WHERE OpenID=?',
|
|
|
162
|
+ [paidAt, order.OpenID]
|
|
|
163
|
+ );
|
|
|
164
|
+ if (serviceUser.UnionID && miaoguoUsers.length) {
|
|
|
165
|
+ await conn.query(
|
|
|
166
|
+ 'UPDATE MiaoguoWXUsers SET ProductServiceTime=? WHERE UnionID=?',
|
|
|
167
|
+ [trialEnd, serviceUser.UnionID]
|
|
|
168
|
+ );
|
|
|
169
|
+ }
|
|
|
170
|
+ await conn.query(
|
|
|
171
|
+ `UPDATE ProductPayInfo
|
|
|
172
|
+ SET PayEndTime=?,XMLPay=?,Status=1,ProductServiceTime=?,UserID=?
|
|
|
173
|
+ WHERE ID=?`,
|
|
|
174
|
+ [paidAt, JSON.stringify(payment.raw || payment), trialEnd, serviceUser.UserID, order.ID]
|
|
|
175
|
+ );
|
|
|
176
|
+ await conn.commit();
|
|
|
177
|
+
|
|
|
178
|
+ postCommit = { order, serviceUser, miaoguoUsers, trialEnd };
|
|
|
179
|
+ return {
|
|
|
180
|
+ alreadyFulfilled: false,
|
|
|
181
|
+ order,
|
|
|
182
|
+ serviceUserID: serviceUser.UserID,
|
|
|
183
|
+ miaoguoUserIDs: miaoguoUsers.map(user => user.UserID),
|
|
|
184
|
+ trialEnd
|
|
|
185
|
+ };
|
|
|
186
|
+ } catch (error) {
|
|
|
187
|
+ await conn.rollback();
|
|
|
188
|
+ throw error;
|
|
|
189
|
+ } finally {
|
|
|
190
|
+ conn.release();
|
|
|
191
|
+ if (postCommit) {
|
|
|
192
|
+ schedulePostCommit(() => {
|
|
|
193
|
+ Promise.resolve(postPayActions(postCommit, dependencies)).catch(error =>
|
|
|
194
|
+ console.error('Wechat service pay post action failed:', error.message)
|
|
|
195
|
+ );
|
|
|
196
|
+ });
|
|
|
197
|
+ }
|
|
|
198
|
+ }
|
|
|
199
|
+}
|
|
|
200
|
+
|
|
|
201
|
+export async function MiaoguoWechatServicePayLogin500(ctx, injected = {}) {
|
|
|
202
|
+ const dependencies = dependenciesOf(injected);
|
|
|
203
|
+ const httpClient = dependencies.httpClient || axios;
|
|
|
204
|
+ const dbQuery = dependencies.query || query;
|
|
|
205
|
+ const param = ctx.request.body || {};
|
|
|
206
|
+ const code = String(param.code || '').trim();
|
|
|
207
|
+
|
|
|
208
|
+ try {
|
|
|
209
|
+ assertPaymentConfig();
|
|
|
210
|
+ if (!code || code.length > 512) throw new Error('微信网页授权 code 无效,请刷新页面重试');
|
|
|
211
|
+
|
|
|
212
|
+ const { data: oauthData } = await httpClient.get('https://api.weixin.qq.com/sns/oauth2/access_token', {
|
|
|
213
|
+ params: {
|
|
|
214
|
+ appid: config.wx.wechatservice_appid,
|
|
|
215
|
+ secret: config.wx.wechatservice_appsecret,
|
|
|
216
|
+ code,
|
|
|
217
|
+ grant_type: 'authorization_code'
|
|
|
218
|
+ },
|
|
|
219
|
+ timeout: config.wechatServicePay.requestTimeoutMs
|
|
|
220
|
+ });
|
|
|
221
|
+ const oauth = typeof oauthData === 'string' ? JSON.parse(oauthData) : oauthData;
|
|
|
222
|
+ if (!oauth?.openid)
|
|
|
223
|
+ throw new Error(`微信网页授权失败${oauth?.errcode ? `(${oauth.errcode})` : ''}`);
|
|
|
224
|
+
|
|
|
225
|
+ const serviceUsers = await dbQuery(
|
|
|
226
|
+ 'SELECT UserID,OpenID,Subscribe,IsProbation FROM WechatServiceWXUsers WHERE OpenID=?',
|
|
|
227
|
+ [oauth.openid]
|
|
|
228
|
+ );
|
|
|
229
|
+ if (serviceUsers.length !== 1 || Number(serviceUsers[0].Subscribe) !== 1)
|
|
|
230
|
+ throw new Error('未找到已关注的服务号用户,请先关注公众号后重试');
|
|
|
231
|
+ if (Number(serviceUsers[0].IsProbation) === 1)
|
|
|
232
|
+ throw new Error('您已经开通过新手包,无需重复支付');
|
|
|
233
|
+
|
|
|
234
|
+ const tradeNo = createTradeNo();
|
|
|
235
|
+ const payFields = {
|
|
|
236
|
+ appid: String(config.wx.wechatservice_appid),
|
|
|
237
|
+ mch_id: String(config.wx.mch_id),
|
|
|
238
|
+ nonce_str: createNonce(),
|
|
|
239
|
+ sign_type: 'MD5',
|
|
|
240
|
+ body: ORDER_BODY,
|
|
|
241
|
+ detail: JSON.stringify({ ProductID: 166 }),
|
|
|
242
|
+ attach: JSON.stringify({ ProductID: WECHAT_SERVICE_PRODUCT_ID, PayType: WECHAT_SERVICE_PAY_TYPE }),
|
|
|
243
|
+ device_info: 'WEB',
|
|
|
244
|
+ out_trade_no: tradeNo,
|
|
|
245
|
+ total_fee: String(WECHAT_SERVICE_TRIAL_AMOUNT_FEN),
|
|
|
246
|
+ spbill_create_ip: requestIp(ctx),
|
|
|
247
|
+ time_start: moment().format('YYYYMMDDHHmmss'),
|
|
|
248
|
+ time_expire: moment().add(15, 'minutes').format('YYYYMMDDHHmmss'),
|
|
|
249
|
+ notify_url: config.wechatServicePay.notifyUrl,
|
|
|
250
|
+ trade_type: 'JSAPI',
|
|
|
251
|
+ openid: oauth.openid,
|
|
|
252
|
+ fee_type: 'CNY',
|
|
|
253
|
+ limit_pay: 'no_credit'
|
|
|
254
|
+ };
|
|
|
255
|
+ payFields.sign = buildV2Sign(payFields, config.wx.payapisecret);
|
|
|
256
|
+
|
|
|
257
|
+ const unifiedOrder = await postWechatXml(httpClient, UNIFIED_ORDER_URL, payFields);
|
|
|
258
|
+ const response = normalizeV2Fields(unifiedOrder.fields);
|
|
|
259
|
+ if (!Object.keys(response).length) throw new Error('微信统一下单返回了空响应');
|
|
|
260
|
+ if (response.return_code !== 'SUCCESS')
|
|
|
261
|
+ throw new Error(`微信统一下单失败:${v2Error(response, '未知错误')}`);
|
|
|
262
|
+ assertSignedWechatResponse(response);
|
|
|
263
|
+ if (response.result_code !== 'SUCCESS' || !response.prepay_id)
|
|
|
264
|
+ throw new Error(`微信统一下单失败:${v2Error(response, '未知错误')}`);
|
|
|
265
|
+ if (response.trade_type && response.trade_type !== 'JSAPI')
|
|
|
266
|
+ throw new Error('微信统一下单结果不是 JSAPI 支付');
|
|
|
267
|
+
|
|
|
268
|
+ await dbQuery(
|
|
|
269
|
+ `INSERT INTO ProductPayInfo
|
|
|
270
|
+ (TradeNo,PayType,BuyType,CreateTime,OpenID,Body,Status,Money,XMLPre,ProductID,Remark)
|
|
|
271
|
+ VALUES (?,?,?,?,?,?,?,?,?,?,?)`,
|
|
|
272
|
+ [
|
|
|
273
|
+ tradeNo,
|
|
|
274
|
+ WECHAT_SERVICE_PAY_TYPE,
|
|
|
275
|
+ WECHAT_SERVICE_BUY_TYPE,
|
|
|
276
|
+ new Date(),
|
|
|
277
|
+ oauth.openid,
|
|
|
278
|
+ ORDER_BODY,
|
|
|
279
|
+ 0,
|
|
|
280
|
+ WECHAT_SERVICE_TRIAL_AMOUNT_FEN,
|
|
|
281
|
+ unifiedOrder.xml,
|
|
|
282
|
+ WECHAT_SERVICE_PRODUCT_ID,
|
|
|
283
|
+ 'wechatServicePay-v2'
|
|
|
284
|
+ ]
|
|
|
285
|
+ );
|
|
|
286
|
+
|
|
|
287
|
+ sendSuccess(ctx, { ...buildJsapiParameters(response.prepay_id), TradeNo: tradeNo });
|
|
|
288
|
+ } catch (error) {
|
|
|
289
|
+ sendFailure(ctx, error);
|
|
|
290
|
+ }
|
|
|
291
|
+}
|
|
|
292
|
+
|
|
|
293
|
+async function queryWechatOrder(order, dependencies = {}) {
|
|
|
294
|
+ const httpClient = dependencies.httpClient || axios;
|
|
|
295
|
+ const fields = {
|
|
|
296
|
+ appid: String(config.wx.wechatservice_appid),
|
|
|
297
|
+ mch_id: String(config.wx.mch_id),
|
|
|
298
|
+ out_trade_no: String(order.TradeNo),
|
|
|
299
|
+ nonce_str: createNonce(),
|
|
|
300
|
+ sign_type: 'MD5'
|
|
|
301
|
+ };
|
|
|
302
|
+ fields.sign = buildV2Sign(fields, config.wx.payapisecret);
|
|
|
303
|
+ const response = normalizeV2Fields((await postWechatXml(httpClient, ORDER_QUERY_URL, fields)).fields);
|
|
|
304
|
+ if (!Object.keys(response).length) throw new Error('微信查单返回了空响应');
|
|
|
305
|
+ if (response.return_code !== 'SUCCESS')
|
|
|
306
|
+ throw new Error(`微信查单失败:${v2Error(response, '未知错误')}`);
|
|
|
307
|
+ assertSignedWechatResponse(response);
|
|
|
308
|
+ if (response.result_code !== 'SUCCESS')
|
|
|
309
|
+ throw new Error(`微信查单失败:${v2Error(response, '未知错误')}`);
|
|
|
310
|
+ if (response.out_trade_no !== String(order.TradeNo))
|
|
|
311
|
+ throw new Error('微信查单结果与本地订单号不一致');
|
|
|
312
|
+ if (response.trade_state === 'SUCCESS' && response.trade_type !== 'JSAPI')
|
|
|
313
|
+ throw new Error('微信查单结果不是 JSAPI 支付');
|
|
|
314
|
+ return response;
|
|
|
315
|
+}
|
|
|
316
|
+
|
|
|
317
|
+export async function MiaoguoWechatServicePayOrderStatus500(ctx, injected = {}) {
|
|
|
318
|
+ const dependencies = dependenciesOf(injected);
|
|
|
319
|
+ const dbQuery = dependencies.query || query;
|
|
|
320
|
+ const fulfill = dependencies.fulfillOrder || fulfillWechatServiceOrder;
|
|
|
321
|
+ const tradeNo = String(ctx.request.body?.TradeNo || '').trim();
|
|
|
322
|
+
|
|
|
323
|
+ try {
|
|
|
324
|
+ assertPaymentConfig();
|
|
|
325
|
+ if (!/^WSP[A-Za-z0-9_-]{10,29}$/.test(tradeNo)) throw new Error('服务号支付订单号无效');
|
|
|
326
|
+ const orders = await dbQuery(
|
|
|
327
|
+ 'SELECT * FROM ProductPayInfo WHERE TradeNo=? AND ProductID=? LIMIT 2',
|
|
|
328
|
+ [tradeNo, WECHAT_SERVICE_PRODUCT_ID]
|
|
|
329
|
+ );
|
|
|
330
|
+ if (orders.length !== 1) throw new Error('未找到唯一的服务号支付订单');
|
|
|
331
|
+ const order = orders[0];
|
|
|
332
|
+ if (Number(order.Status) === 1) {
|
|
|
333
|
+ sendSuccess(ctx, { TradeNo: tradeNo, Status: 1, TradeState: 'SUCCESS' });
|
|
|
334
|
+ return;
|
|
|
335
|
+ }
|
|
|
336
|
+
|
|
|
337
|
+ const response = await queryWechatOrder(order, dependencies);
|
|
|
338
|
+ if (response.trade_state !== 'SUCCESS') {
|
|
|
339
|
+ sendSuccess(ctx, {
|
|
|
340
|
+ TradeNo: tradeNo,
|
|
|
341
|
+ Status: 0,
|
|
|
342
|
+ TradeState: response.trade_state || 'NOTPAY'
|
|
|
343
|
+ });
|
|
|
344
|
+ return;
|
|
|
345
|
+ }
|
|
|
346
|
+ const result = await fulfill(tradeNo, {
|
|
|
347
|
+ openid: response.openid,
|
|
|
348
|
+ totalFee: Number(response.total_fee),
|
|
|
349
|
+ transactionId: response.transaction_id,
|
|
|
350
|
+ timeEnd: response.time_end,
|
|
|
351
|
+ raw: response
|
|
|
352
|
+ }, dependencies);
|
|
|
353
|
+ sendSuccess(ctx, {
|
|
|
354
|
+ TradeNo: tradeNo,
|
|
|
355
|
+ Status: 1,
|
|
|
356
|
+ TradeState: 'SUCCESS',
|
|
|
357
|
+ alreadyFulfilled: result.alreadyFulfilled,
|
|
|
358
|
+ trialEnd: result.trialEnd
|
|
|
359
|
+ });
|
|
|
360
|
+ } catch (error) {
|
|
|
361
|
+ sendFailure(ctx, error);
|
|
|
362
|
+ }
|
|
|
363
|
+}
|
|
|
364
|
+
|
|
|
365
|
+function replyV2(ctx, returnCode, returnMessage) {
|
|
|
366
|
+ ctx.status = 200;
|
|
|
367
|
+ ctx.type = 'text/xml; charset=utf-8';
|
|
|
368
|
+ ctx.body = buildV2Xml({ return_code: returnCode, return_msg: returnMessage });
|
|
|
369
|
+}
|
|
|
370
|
+
|
|
|
371
|
+export async function MiaoguoWechatServicePayNotify500(ctx, injected = {}) {
|
|
|
372
|
+ const dependencies = dependenciesOf(injected);
|
|
|
373
|
+ const fulfill = dependencies.fulfillOrder || fulfillWechatServiceOrder;
|
|
|
374
|
+ const logger = dependencies.logger || console;
|
|
|
375
|
+ try {
|
|
|
376
|
+ assertPaymentConfig();
|
|
|
377
|
+ const body = ctx.request.body || {};
|
|
|
378
|
+ const callbackFields = body.rawBody ? parseV2Xml(body.rawBody) : (body.xml || body);
|
|
|
379
|
+ const fields = normalizeV2Fields(callbackFields);
|
|
|
380
|
+ if (!Object.keys(fields).length) throw new Error('微信支付通知为空');
|
|
|
381
|
+ if (fields.return_code !== 'SUCCESS' || fields.result_code !== 'SUCCESS')
|
|
|
382
|
+ throw new Error(`微信支付通知失败:${v2Error(fields, '未知错误')}`);
|
|
|
383
|
+ assertSignedWechatResponse(fields);
|
|
|
384
|
+ if (!fields.out_trade_no || !fields.openid || !fields.transaction_id)
|
|
|
385
|
+ throw new Error('微信支付通知缺少订单、用户或交易单号');
|
|
|
386
|
+ if (fields.trade_type && fields.trade_type !== 'JSAPI')
|
|
|
387
|
+ throw new Error('微信支付通知类型不是 JSAPI');
|
|
|
388
|
+ if (fields.fee_type && fields.fee_type !== 'CNY')
|
|
|
389
|
+ throw new Error('微信支付通知币种不是人民币');
|
|
|
390
|
+
|
|
|
391
|
+ await fulfill(fields.out_trade_no, {
|
|
|
392
|
+ openid: fields.openid,
|
|
|
393
|
+ totalFee: Number(fields.total_fee),
|
|
|
394
|
+ transactionId: fields.transaction_id,
|
|
|
395
|
+ timeEnd: fields.time_end,
|
|
|
396
|
+ raw: fields
|
|
|
397
|
+ }, dependencies);
|
|
|
398
|
+ replyV2(ctx, 'SUCCESS', 'OK');
|
|
|
399
|
+ } catch (error) {
|
|
|
400
|
+ logger.error('Wechat service pay notify failed:', error.message);
|
|
|
401
|
+ replyV2(ctx, 'FAIL', 'PAYMENT_VALIDATION_FAILED');
|
|
|
402
|
+ }
|
|
|
403
|
+}
|