chengjie 2 週間 前
コミット
97345c00c7
共有4 個のファイルを変更した42 個の追加7 個の削除を含む
  1. 2 0
      doc/miaoguo_virtual_payment.md
  2. 15 6
      src/api/virtualPay/virtualPayController.js
  3. 2 1
      src/config/index.js
  4. 23 0
      test/virtualPayFulfillment.test.js

+ 2 - 0
doc/miaoguo_virtual_payment.md

@@ -14,6 +14,7 @@ WX_VIRTUAL_PAY_APP_KEY=正式环境AppKey
14 14
 WX_VIRTUAL_PAY_SANDBOX_APP_KEY=沙箱环境AppKey
15 15
 WX_VIRTUAL_PAY_ENV=1
16 16
 WX_MIAOGUO_MESSAGE_TOKEN=秒过小程序消息推送Token
17
+WX_VIRTUAL_PAY_POST_ACTION_TIMEOUT_MS=60000
17 18
 WX_VIRTUAL_PAY_PRODUCTS_JSON='[
18 19
   {"productId":"后台正式商品ID","payType":7,"price":19900,"goodsPrice":36500,"env":0},
19 20
   {"productId":"后台正式试用商品ID","payType":9,"price":100,"env":0},
@@ -29,6 +30,7 @@ WX_VIRTUAL_PAY_PRODUCTS_JSON='[
29 30
 - 如果微信后台商品原价与实际支付价不同,需要在配置表中加入对应售价,服务端才能正确传入 `activitySellingPrice`。
30 31
 - `WX_VIRTUAL_PAY_ENV=1` 为沙箱,`0` 为正式环境。iOS 没有沙箱,服务端会强制使用正式环境商品和正式 AppKey。
31 32
 - AppKey 只放在服务端环境变量中,不能写入小程序代码或接口响应。
33
+- `WX_VIRTUAL_PAY_POST_ACTION_TIMEOUT_MS` 只控制支付事务提交后的统计刷新等非关键任务,默认 60 秒;该任务失败不会回滚已经完成的订单和会员权益。
32 34
 
33 35
 ## 新接口
34 36
 

+ 15 - 6
src/api/virtualPay/virtualPayController.js

@@ -66,6 +66,17 @@ function normalizePlatform(value) {
66 66
     return platform || 'unknown';
67 67
 }
68 68
 
69
+function parseMemberEndTime(value) {
70
+    if (!value || value === 'Invalid date') return null;
71
+    const normalized = String(value).trim().replaceAll('/', '-');
72
+    const parsed = moment(normalized, [
73
+        'YYYY-MM-DD',
74
+        'YYYY-MM-DD HH:mm:ss',
75
+        moment.ISO_8601
76
+    ], true);
77
+    return parsed.isValid() ? parsed : null;
78
+}
79
+
69 80
 function resolveEnvironment(platform) {
70 81
     if (platform === 'ios') return 0;
71 82
     const env = Number(config.virtualPay.env);
@@ -159,10 +170,8 @@ function calculateMemberUpdate(user, order, detail) {
159 170
 
160 171
     let productServiceTime;
161 172
     const requestedEndTime = detail?.EndTime;
162
-    const requestedMoment = requestedEndTime && requestedEndTime !== 'Invalid date'
163
-        ? moment(requestedEndTime)
164
-        : null;
165
-    if (requestedMoment?.isValid()) {
173
+    const requestedMoment = parseMemberEndTime(requestedEndTime);
174
+    if (requestedMoment) {
166 175
         productServiceTime = requestedMoment.format('YYYY-MM-DD HH:mm:ss');
167 176
     } else {
168 177
         const oldEndTime = moment(user.ProductServiceTime);
@@ -319,10 +328,10 @@ async function runCompatiblePostPayActions({ order, user, detail, memberUpdate }
319 328
 
320 329
     try {
321 330
         await axios.get(`http://localhost:${config.port}/api/BuildStatisticsShareUserPay`, {
322
-            timeout: config.virtualPay.requestTimeoutMs
331
+            timeout: config.virtualPay.postActionTimeoutMs
323 332
         });
324 333
     } catch (error) {
325
-        console.error('Miaoguo virtual pay statistics action failed:', error.message);
334
+        console.warn('Miaoguo virtual pay statistics refresh delayed after committed payment:', error.message);
326 335
     }
327 336
 }
328 337
 

+ 2 - 1
src/config/index.js

@@ -50,7 +50,8 @@ const commonConfig = {
50 50
         env: toNumber(process.env.WX_VIRTUAL_PAY_ENV, runtimeEnv === 'production' ? 0 : 1),
51 51
         products: parseJsonArray(process.env.WX_VIRTUAL_PAY_PRODUCTS_JSON, 'WX_VIRTUAL_PAY_PRODUCTS_JSON'),
52 52
         messageToken: process.env.WX_MIAOGUO_MESSAGE_TOKEN || '',
53
-        requestTimeoutMs: toNumber(process.env.WX_VIRTUAL_PAY_TIMEOUT_MS, 10000)
53
+        requestTimeoutMs: toNumber(process.env.WX_VIRTUAL_PAY_TIMEOUT_MS, 10000),
54
+        postActionTimeoutMs: toNumber(process.env.WX_VIRTUAL_PAY_POST_ACTION_TIMEOUT_MS, 60000)
54 55
     },
55 56
     CDNUrl: 'https://cdn.example.com/',
56 57
     database: {

+ 23 - 0
test/virtualPayFulfillment.test.js

@@ -115,6 +115,29 @@ test('测试用户的 1 元购买执行与正式购买相同的会员完成逻
115 115
     assert.ok(state.queries.some(item => item.sql.includes('Status=1')));
116 116
 });
117 117
 
118
+test('会员有效期兼容小程序提交的斜杠日期且不触发 Moment 回退解析', async () => {
119
+    const slashDateOrder = paidMemberOrder({
120
+        TradeNo: 'VP_TEST_SLASH_DATE',
121
+        Money: 100,
122
+        XMLPre: orderXml({
123
+            productId: '166_1',
124
+            goodsPrice: 100,
125
+            detail: { EndTime: '2032/05/14' }
126
+        })
127
+    });
128
+    const { connection } = createFakeConnection({ order: slashDateOrder, user: memberUser });
129
+
130
+    const result = await fulfillOrder('VP_TEST_SLASH_DATE', {
131
+        openid: 'openid-test',
132
+        env: 0,
133
+        productId: '166_1',
134
+        originalPrice: 100,
135
+        actualPrice: 100
136
+    }, dependencies(connection));
137
+
138
+    assert.equal(result.memberUpdate.ProductServiceTime, '2032-05-14 00:00:00');
139
+});
140
+
118 141
 test('重复发货通知不会重复增加会员有效期', async () => {
119 142
     const { connection, state } = createFakeConnection({
120 143
         order: paidMemberOrder({ Status: 1 }),