trustbase-seller-backend 0.1.6 → 0.1.8

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trustbase-seller-backend",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "TrustBase Seller Backend (merchant edition) — 商品上链/订单/支付/质押 write API for TrustBase 保障链生态. 单卖家自托管形态: 签名 key 即卖家身份, 私钥不出门.",
5
5
  "main": "run-unified.js",
6
6
  "bin": {
@@ -114,6 +114,8 @@ async function wxApiCall(method, urlPath, bodyObj, override) {
114
114
  "Authorization": wxSignAuth(method, urlPath, bodyStr, override),
115
115
  "Content-Type": "application/json",
116
116
  "Accept": "application/json",
117
+ // 微信 APIv3 拒默认 Accept-Language: * (undici 自动带) — 2026-09-25 /v3/certificates 实测 PARAM_ERROR
118
+ "Accept-Language": "zh-CN",
117
119
  "User-Agent": "trustbase-seller-backend/1.0",
118
120
  },
119
121
  body: method === "GET" ? undefined : bodyStr,
@@ -213,10 +215,28 @@ async function refreshPlatformCerts() {
213
215
  console.log(`[agent-pay] 微信平台证书已刷新 (${Object.keys(certs).length} 张)`);
214
216
  }
215
217
 
216
- /** 验签: timestamp\nnonce\nbody 用平台证书公钥 RSA-SHA256 校验 */
218
+ /** 验签: timestamp\nnonce\nbody RSA-SHA256 校验。
219
+ * 双轨 (2026-09-25 真机): serial 以 "PUB_KEY_ID_" 开头 = 微信支付公钥模式 (新政策,
220
+ * /v3/certificates 返回空, 商户平台下载固定公钥); 否则走平台证书缓存 (老模式)。
221
+ * 公钥池 (2026-09-25): merchant + SP 多把公钥并存, 按回调 serial 精确匹配 ID —
222
+ * 直连回调用商户号公钥, SP 特约商户回调用服务商号公钥。 */
217
223
  function wxVerifyNotifySignature(serial, timestamp, nonce, rawBody, signature) {
218
- loadPlatformCertsFromDisk();
219
- const pem = wxPlatformCerts && wxPlatformCerts[serial];
224
+ let pem = null;
225
+ if (String(serial).startsWith("PUB_KEY_ID_")) {
226
+ const pool = [
227
+ [process.env.WXPAY_PUBLIC_KEY_ID, process.env.WXPAY_PUBLIC_KEY_PATH],
228
+ [process.env.WXPAY_SP_PUBLIC_KEY_ID, process.env.WXPAY_SP_PUBLIC_KEY_PATH],
229
+ ];
230
+ for (const [id, p] of pool) {
231
+ if (id && id === serial && p) {
232
+ try { pem = require("fs").readFileSync(p, "utf8"); } catch (_) { pem = null; }
233
+ break;
234
+ }
235
+ }
236
+ } else {
237
+ loadPlatformCertsFromDisk();
238
+ pem = wxPlatformCerts && wxPlatformCerts[serial];
239
+ }
220
240
  if (!pem) return false;
221
241
  const msg = [timestamp, nonce, rawBody].join("\n") + "\n";
222
242
  return crypto.createVerify("RSA-SHA256").update(msg).verify(pem, signature, "base64");
@@ -49,6 +49,45 @@ h1{font-size:22px;margin-bottom:4px}
49
49
  .sbtn.primary:hover{background:#A95230}
50
50
  .hintbox{display:none;font-size:12px;color:#6B655B;background:#F7F3EB;border-radius:8px;padding:10px 12px;margin-top:8px;line-height:1.9}
51
51
  .hintbox code{background:#fff;padding:1px 6px;border-radius:4px;font-family:Consolas,monospace}
52
+ /* ===== ③ 进件表单美化 (2026-10-03): 分组卡片 / 统一控件 / 虚线上传卡 / 状态卡片 ===== */
53
+ #apply-form .acard{background:#FCFAF4;border:1px solid #EAE3D6;border-radius:14px;padding:18px 16px;margin-bottom:14px}
54
+ #apply-form .acard-h{font-size:15px;font-weight:600;color:#33302B;margin-bottom:12px;display:flex;align-items:center;gap:8px}
55
+ #apply-form .anum{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;background:#C4633C;color:#fff;font-size:12px;font-weight:600;flex:none}
56
+ #apply-form .agrid{display:grid;grid-template-columns:1fr 1fr;gap:12px 14px}
57
+ #apply-form .afield-full{grid-column:1/-1}
58
+ #apply-form .afield label{display:block;font-size:13px;color:#6B655B;margin:0 0 5px}
59
+ #apply-form .aopt{font-size:11px;color:#BDB5A6;font-weight:400;margin-left:4px}
60
+ #apply-form .afield input,#apply-form .afield select{width:100%;padding:10px 12px;border:1px solid #D4CDBF;border-radius:8px;font-size:15px;background:#fff;transition:border-color .15s,box-shadow .15s}
61
+ #apply-form .afield input::placeholder{color:#BDB5A6}
62
+ #apply-form .afield input:focus,#apply-form .afield select:focus{outline:none;border-color:#C4633C;box-shadow:0 0 0 3px rgba(196,99,60,.15)}
63
+ /* 上传卡片 (结构由 mountApplyUpload 生成: 标题label + label.sbtn>input + span#st_*) */
64
+ #apply-form .auploads{display:grid;grid-template-columns:1fr 1fr;gap:12px 14px}
65
+ #apply-form div[id^="a_up_"]>label:first-child{display:block;font-size:13px;font-weight:600;color:#33302B;margin-bottom:6px}
66
+ #apply-form div[id^="a_up_"]>label:first-child::after{content:' *';color:#DC2626}
67
+ #apply-form div[id^="a_up_"] label.sbtn{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;border:2px dashed #D4CDBF;border-radius:10px;background:#FFFDF9;padding:18px 10px;margin:0;text-align:center;cursor:pointer;color:#6B655B;transition:border-color .15s,background .15s}
68
+ #apply-form div[id^="a_up_"] label.sbtn::before{content:'📷';font-size:22px;line-height:1}
69
+ #apply-form div[id^="a_up_"] label.sbtn::after{content:'支持 jpg/png, 本机压缩后上传';font-size:11px;color:#BDB5A6;font-weight:400;border:none;background:none;padding:0}
70
+ #apply-form div[id^="a_up_"] label.sbtn:hover{border-color:#C4633C;background:#FBF1EA}
71
+ #apply-form div[id^="a_up_"] span[id^="st_"]{display:block;font-size:12px;margin-top:6px}
72
+ img.auprev{display:none;width:100%;max-height:140px;object-fit:cover;border-radius:8px;margin-bottom:8px;border:1px solid #EAE3D6}
73
+ .auname{font-size:11px;color:#8A857B;margin:-2px 0 6px;word-break:break-all}
74
+ /* 提交按钮 + 隐私行 */
75
+ #btn-apply-submit{display:block;width:100%;background:#C4633C;color:#fff;border:none;border-radius:10px;padding:15px;font-size:17px;font-weight:600;cursor:pointer;margin-top:4px;transition:background .15s}
76
+ #btn-apply-submit:hover{background:#A95230}
77
+ #btn-apply-submit:disabled{background:#D8D2C8;cursor:not-allowed}
78
+ .aprivacy{font-size:12px;color:#8A857B;text-align:center;margin-top:10px;line-height:1.8}
79
+ /* 状态卡片 (按 data-state 着色, 由追加脚本按内容标) */
80
+ #apply-status{border-radius:12px;padding:14px 16px;font-size:14px;line-height:2;border:1px solid #EAE3D6;background:#FCFAF4;word-break:break-all}
81
+ #apply-status[data-state="done"]{background:#F0FDF4;border-color:#BBF7D0}
82
+ #apply-status[data-state="checking"]{background:#FFFBEB;border-color:#FDE68A}
83
+ #apply-status[data-state="sign"]{background:#EFF6FF;border-color:#BFDBFE}
84
+ #apply-status[data-state="pending"]{background:#FFF7ED;border-color:#FED7AA}
85
+ #apply-status a{display:inline-block;background:#C4633C;color:#fff !important;border-radius:8px;padding:10px 18px;font-size:15px;font-weight:600;text-decoration:none;margin-top:8px}
86
+ #apply-status .aqr{display:flex;align-items:center;justify-content:center;width:120px;height:120px;border:2px dashed #93C5FD;border-radius:10px;background:#fff;color:#8A857B;font-size:12px;text-align:center;margin:10px 0 2px;line-height:1.6}
87
+ @media (max-width:640px){
88
+ #apply-form .agrid{grid-template-columns:1fr}
89
+ #apply-form .auploads{grid-template-columns:1fr}
90
+ }
52
91
  @media print{
53
92
  body *{visibility:hidden}
54
93
  #print-area,#print-area *{visibility:visible}
@@ -82,29 +121,74 @@ h1{font-size:22px;margin-bottom:4px}
82
121
  <div class="ops"><button class="sbtn" id="btn-verify" onclick="submitVerification()">提交实名认证</button></div>
83
122
  <div class="desc" id="verify-desc">提交后由服务商审核微信特约商户收款账户; 审核通过后自动发放 100 TCT 引导金 (无需重复提交)</div>
84
123
  <div class="hintbox" id="verify-msg"></div>
85
- <div class="ops noprint"><button class="sbtn" id="btn-materials" onclick="toggleMaterials()">填写进件资料并上传证件</button></div>
86
- <div class="hintbox" id="materials-msg"></div>
87
- <div id="materials-form" style="display:none;margin-top:10px">
88
- <div class="desc" style="margin-bottom:6px">以下资料用于服务商代你向微信申请特约商户收款账户。<b>资料只保存在你的电脑上</b>,并交给服务商用于进件。</div>
89
- <label>主体类型</label>
90
- <select id="m_subject_type"><option>个体户</option><option>企业</option><option>小微</option></select>
91
- <label>商户全称 (与执照一致)</label><input id="m_merchant_name" placeholder="如: 无锡嘟嘟百货商行">
92
- <label>统一社会信用代码 / 执照编号</label><input id="m_license_no" placeholder="18 位">
93
- <label>法人/经营者姓名</label><input id="m_legal_name">
94
- <label>法人身份证号</label><input id="m_legal_id_no" placeholder="18 位">
95
- <label>超级管理员姓名</label><input id="m_admin_name">
96
- <label>超级管理员手机号</label><input id="m_admin_mobile" placeholder="需与微信实名一致">
97
- <label>门店名称 (选填)</label><input id="m_store_name">
98
- <label>门店地址 (选填)</label><input id="m_store_address">
99
- <label>结算账户类型 (选填)</label><select id="m_settle_type"><option></option><option>对公</option><option>对私</option></select>
100
- <label>开户行 (选填)</label><input id="m_bank_name">
101
- <label>结算账号 (选填)</label><input id="m_bank_account">
102
- <label>营业执照照片 (必传)</label><input type="file" id="m_file_license" accept="image/*">
103
- <label>法人身份证·正面 (必传)</label><input type="file" id="m_file_legal_id_front" accept="image/*">
104
- <label>法人身份证·反面 (必传)</label><input type="file" id="m_file_legal_id_back" accept="image/*">
105
- <label>门店照片 (选填)</label><input type="file" id="m_file_store_photo" accept="image/*">
106
- <div class="ops"><button class="sbtn" id="btn-materials-submit" onclick="submitMaterials()">提交资料</button></div>
107
- <div class="desc">单个文件不超过 4MB;资料含证件照片,请勿转发到聊天工具或网盘。</div>
124
+ <div class="ops noprint"><button class="sbtn" id="btn-apply-toggle" onclick="toggleApplyForm()">填写进件资料并上传证件</button></div>
125
+ <div class="hintbox" id="apply-msg"></div>
126
+ <div id="apply-status" style="display:none;margin-top:12px"></div>
127
+ <div id="apply-form" style="display:none;margin-top:12px">
128
+ <div class="desc" style="margin-bottom:12px">以下资料由你的电脑<b>本地加密</b>后交给服务商,向微信申请特约商户收款账户。身份证号/银行卡号在你电脑上就已加密,任何人 (含服务商) 都看不到明文。</div>
129
+
130
+ <div class="acard">
131
+ <div class="acard-h"><span class="anum">1</span>主体资料</div>
132
+ <div class="agrid">
133
+ <div class="afield"><label for="a_subject_type">主体类型</label>
134
+ <select id="a_subject_type"><option value="SUBJECT_TYPE_INDIVIDUAL">个体工商户</option><option value="SUBJECT_TYPE_ENTERPRISE">企业</option></select></div>
135
+ <div class="afield"><label for="a_shortname">商户简称</label><input id="a_shortname" placeholder="支付完成页展示, 如: 张三餐饮店"></div>
136
+ <div class="afield"><label for="a_merchant_name">营业执照名称</label><input id="a_merchant_name" placeholder="全称, 与执照一致"></div>
137
+ <div class="afield"><label for="a_license_number">统一社会信用代码</label><input id="a_license_number" placeholder="18 位, 9 开头"></div>
138
+ <div class="afield"><label for="a_service_phone">客服电话</label><input id="a_service_phone" placeholder="交易记录向买家展示"></div>
139
+ <div class="afield"><label for="a_settlement_id">经营类目编码</label><input id="a_settlement_id" placeholder="如: 719 (服务商提供对照表)"></div>
140
+ <div class="afield afield-full"><label for="a_qualification_type">类目资质描述</label><input id="a_qualification_type" placeholder="如: 零售"></div>
141
+ </div>
142
+ </div>
143
+
144
+ <div class="acard">
145
+ <div class="acard-h"><span class="anum">2</span>法人身份 / 超级管理员</div>
146
+ <div class="agrid">
147
+ <div class="afield"><label for="a_legal_name">法人/经营者姓名</label><input id="a_legal_name" placeholder="与身份证一致"></div>
148
+ <div class="afield"><label for="a_legal_id_number">法人身份证号</label><input id="a_legal_id_number" placeholder="17 位数字 + 数字或 X"></div>
149
+ <div class="afield"><label for="a_period_begin">身份证有效期开始</label><input id="a_period_begin" placeholder="YYYY-MM-DD"></div>
150
+ <div class="afield"><label for="a_period_end">有效期结束</label><input id="a_period_end" placeholder="YYYY-MM-DD 或 长期"></div>
151
+ <div class="afield"><label for="a_contact_name">超级管理员姓名</label><input id="a_contact_name" placeholder="与微信实名一致"></div>
152
+ <div class="afield"><label for="a_contact_mobile">超管手机号</label><input id="a_contact_mobile" placeholder="11 位, 接收微信验证码"></div>
153
+ <div class="afield afield-full"><label for="a_contact_email">联系邮箱</label><input id="a_contact_email" placeholder="接收微信开户邮件"></div>
154
+ </div>
155
+ </div>
156
+
157
+ <div class="acard">
158
+ <div class="acard-h"><span class="anum">3</span>结算账户</div>
159
+ <div class="agrid">
160
+ <div class="afield"><label for="a_account_type">账户类型</label>
161
+ <select id="a_account_type"><option value="BANK_ACCOUNT_TYPE_PERSONAL">经营者个人银行卡 (个体户)</option><option value="BANK_ACCOUNT_TYPE_CORPORATE">对公银行账户</option></select></div>
162
+ <div class="afield"><label for="a_account_bank">开户银行</label><input id="a_account_bank" placeholder="如: 招商银行"></div>
163
+ <div class="afield"><label for="a_account_name">开户名称</label><input id="a_account_name" placeholder="个人卡=经营者姓名 / 对公=执照名称"></div>
164
+ <div class="afield"><label for="a_account_number">银行账号</label><input id="a_account_number" placeholder="卡号"></div>
165
+ <div class="afield"><label for="a_bank_name">开户支行 <span class="aopt">选填</span></label><input id="a_bank_name" placeholder="如: 招商银行深圳分行营业部"></div>
166
+ <div class="afield"><label for="a_bank_address_code">开户行省市编码 <span class="aopt">选填</span></label><input id="a_bank_address_code" placeholder="如: 440300"></div>
167
+ </div>
168
+ </div>
169
+
170
+ <div class="acard">
171
+ <div class="acard-h"><span class="anum">4</span>门店信息</div>
172
+ <div class="agrid">
173
+ <div class="afield"><label for="a_store_name">门店名称</label><input id="a_store_name" placeholder="线下场所名称"></div>
174
+ <div class="afield"><label for="a_store_address_code">门店省市编码</label><input id="a_store_address_code" placeholder="如: 440300"></div>
175
+ <div class="afield afield-full"><label for="a_store_address">门店详细地址</label><input id="a_store_address" placeholder="xx 路 xx 号"></div>
176
+ </div>
177
+ </div>
178
+
179
+ <div class="acard">
180
+ <div class="acard-h"><span class="anum">5</span>证件上传</div>
181
+ <div class="auploads">
182
+ <div id="a_up_license"></div>
183
+ <div id="a_up_idfront"></div>
184
+ <div id="a_up_idback"></div>
185
+ <div id="a_up_entrance"></div>
186
+ <div id="a_up_indoor"></div>
187
+ </div>
188
+ </div>
189
+
190
+ <button class="abtn" id="btn-apply-submit" onclick="submitApplyment()">加密并提交进件</button>
191
+ <div class="aprivacy">🔒 资料只保存在这台电脑; 敏感字段本机加密后交服务商; 服务商暂时不可达时加密暂存本机, 可重试。</div>
108
192
  </div>
109
193
  </div>
110
194
  </div>
@@ -235,67 +319,130 @@ function reprintKey(){
235
319
  h.style.display=h.style.display==='block'?'none':'block';
236
320
  }
237
321
  }
238
- function toggleMaterials(){
239
- var el=document.getElementById('materials-form');
322
+ // ---- 进件 (2026-10-03 架构纠偏: 卖家本机提交 → 本机加密 → SP 网关 → 微信) ----
323
+ var _aMedia={}; // 字段名 → 微信 media_id (上传成功后才提交得动)
324
+ function toggleApplyForm(){
325
+ var el=document.getElementById('apply-form');
240
326
  el.style.display = el.style.display==='none' ? 'block' : 'none';
241
327
  }
242
- function readFileBase64(input){
243
- return new Promise(function(resolve,reject){
244
- var f=input.files && input.files[0];
245
- if(!f) return resolve(null);
246
- if(f.size > 4*1024*1024) return reject(new Error('文件超过 4MB: '+f.name));
247
- var r=new FileReader();
248
- r.onload=function(){resolve({name:f.name,data:String(r.result).replace(/^data:[^;]+;base64,/,'')})};
249
- r.onerror=function(){reject(new Error('读取失败: '+f.name))};
250
- r.readAsDataURL(f);
328
+ // 照片本机压缩 (≤1600px JPEG; 全局 JSON 上限 1MB, 超了降质再压)
329
+ function shrinkImage(file,cb){
330
+ var rd=new FileReader();
331
+ rd.onload=function(){
332
+ var img=new Image();
333
+ img.onload=function(){
334
+ var max=1600,w=img.width,h=img.height;
335
+ if(Math.max(w,h)>max){var k=max/Math.max(w,h);w=Math.round(w*k);h=Math.round(h*k);}
336
+ var cv=document.createElement('canvas');cv.width=w;cv.height=h;
337
+ cv.getContext('2d').drawImage(img,0,0,w,h);
338
+ var b64=cv.toDataURL('image/jpeg',0.85).split(',')[1];
339
+ if(b64.length*0.75>950*1024) b64=cv.toDataURL('image/jpeg',0.6).split(',')[1];
340
+ if(b64.length*0.75>950*1024) return cb(new Error('图片压缩后仍超服务器上限, 请换更小的图'));
341
+ cb(null,{filename:(file.name||'photo').replace(/\.[^.]*$/,'')+'.jpg',content_b64:b64});
342
+ };
343
+ img.onerror=function(){cb(new Error('图片读取失败'))};
344
+ img.src=rd.result;
345
+ };
346
+ rd.onerror=function(){cb(new Error('文件读取失败'))};
347
+ rd.readAsDataURL(file);
348
+ }
349
+ // 照片上传位: 选图 → 本机压缩 → 卖家后端转发 SP 网关 → media_id
350
+ function mountApplyUpload(slotId,field,label){
351
+ var slot=document.getElementById(slotId);
352
+ slot.innerHTML='<label>'+label+'</label>'
353
+ +'<label class="sbtn" style="cursor:pointer">拍照/选图<input type="file" accept="image/*" style="display:none"></label>'
354
+ +' <span id="st_'+field+'" style="font-size:12px"></span>';
355
+ var input=slot.querySelector('input[type=file]');
356
+ var stat=document.getElementById('st_'+field);
357
+ input.addEventListener('change',function(){
358
+ if(!input.files||!input.files[0]) return;
359
+ stat.style.color='';stat.textContent='压缩中...';
360
+ shrinkImage(input.files[0],function(err,payload){
361
+ if(err){stat.style.color='#DC2626';stat.textContent=err.message;return;}
362
+ stat.textContent='上传中...';
363
+ fetch('/api/onboard/applyment/upload',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})
364
+ .then(readJson).then(function(j){
365
+ if(j.ok&&j.media_id){_aMedia[field]=j.media_id;stat.style.color='#16A34A';stat.textContent='✓ 已上传';}
366
+ else{stat.style.color='#DC2626';stat.textContent='上传失败: '+(j.message||j.error||'未知错误');}
367
+ }).catch(function(e){stat.style.color='#DC2626';stat.textContent='网络错误: '+e.message;});
368
+ });
251
369
  });
252
370
  }
253
- function mval(id){var e=document.getElementById(id);return e?e.value.trim():''}
254
- async function submitMaterials(){
255
- var btn=document.getElementById('btn-materials-submit');
256
- var msg=document.getElementById('materials-msg');
257
- btn.disabled=true;btn.textContent='提交中...';
371
+ mountApplyUpload('a_up_license','license_media_id','营业执照照片 (必传)');
372
+ mountApplyUpload('a_up_idfront','legal_id_front_media_id','身份证人像面 (必传)');
373
+ mountApplyUpload('a_up_idback','legal_id_back_media_id','身份证国徽面 (必传)');
374
+ mountApplyUpload('a_up_entrance','store_entrance_media_id','门店门头照片 (必传)');
375
+ mountApplyUpload('a_up_indoor','store_indoor_media_id','门店内部照片 (必传)');
376
+ function aval(id){var e=document.getElementById(id);return e?e.value.trim():''}
377
+ function applyFormBody(){
378
+ return {
379
+ subject_type:aval('a_subject_type'),merchant_shortname:aval('a_shortname'),merchant_name:aval('a_merchant_name'),
380
+ license_number:aval('a_license_number'),service_phone:aval('a_service_phone'),
381
+ settlement_id:aval('a_settlement_id'),qualification_type:aval('a_qualification_type'),
382
+ contact_name:aval('a_contact_name'),contact_mobile:aval('a_contact_mobile'),contact_email:aval('a_contact_email'),
383
+ legal_name:aval('a_legal_name'),legal_id_number:aval('a_legal_id_number'),
384
+ legal_id_period_begin:aval('a_period_begin'),legal_id_period_end:aval('a_period_end'),
385
+ account_type:aval('a_account_type'),account_name:aval('a_account_name'),account_number:aval('a_account_number'),
386
+ account_bank:aval('a_account_bank'),bank_name:aval('a_bank_name'),bank_address_code:aval('a_bank_address_code'),
387
+ store_name:aval('a_store_name'),store_address_code:aval('a_store_address_code'),store_address:aval('a_store_address'),
388
+ license_media_id:_aMedia.license_media_id||'',legal_id_front_media_id:_aMedia.legal_id_front_media_id||'',
389
+ legal_id_back_media_id:_aMedia.legal_id_back_media_id||'',store_entrance_media_id:_aMedia.store_entrance_media_id||'',
390
+ store_indoor_media_id:_aMedia.store_indoor_media_id||''
391
+ };
392
+ }
393
+ // 状态机渲染: 未提交(表单) → 已提交(business_code+轮询) → TO_BE_SIGNED(签约链接) → FINISHED(sub_mchid+引导金)
394
+ function renderApplyment(j){
395
+ var box=document.getElementById('apply-status');
396
+ if(!j||!j.status||j.status==='NOT_SUBMITTED'){box.style.display='none';return;}
397
+ box.style.display='block';
398
+ var html='';
399
+ if(j.status==='PENDING_PUSH'){
400
+ html='📦 进件资料已加密暂存本机'+(j.push_error?' (上次提交: '+j.push_error+')':'')
401
+ +' <button class="sbtn" onclick="retryApplyment()">重新提交</button>';
402
+ } else {
403
+ html='业务申请编号: <b>'+(j.business_code||'')+'</b>'
404
+ +'<br>申请状态: <b>'+j.status+'</b>'+(j.state_msg?' ('+j.state_msg+')':'')+(j.cached?' (本机缓存)':'');
405
+ if(j.reject_reason) html+='<br><span style="color:#DC2626">驳回原因: '+j.reject_reason+'</span> — 点"填写进件资料"修改后重新提交';
406
+ if(j.sub_mchid) html+='<br>特约商户号 sub_mchid: <b>'+j.sub_mchid+'</b>';
407
+ if(j.sign_url) html+='<br><a href="'+String(j.sign_url).replace(/"/g,'')+'" target="_blank" style="color:#C4633C;font-weight:600">👉 法人/超管用微信打开签约链接完成签约</a>';
408
+ if(j.status==='APPLYMENT_STATE_FINISHED') html+='<br>✅ 微信收款账户已开通, 引导金将自动发放, 可进入下一步上架商品';
409
+ html+=' <button class="sbtn" onclick="queryApplyment()">刷新状态</button>';
410
+ }
411
+ box.innerHTML=html;
412
+ if(j.status==='APPLYMENT_STATE_FINISHED') markStepDone('st-verification');
413
+ }
414
+ async function submitApplyment(){
415
+ var btn=document.getElementById('btn-apply-submit');
416
+ var msg=document.getElementById('apply-msg');
417
+ btn.disabled=true;btn.textContent='加密并提交中...';
258
418
  try{
259
- var files={};
260
- var names={license:'m_file_license',legal_id_front:'m_file_legal_id_front',legal_id_back:'m_file_legal_id_back',store_photo:'m_file_store_photo'};
261
- for(var k in names){
262
- var f=await readFileBase64(document.getElementById(names[k]));
263
- if(f) files[k]=f;
419
+ var j=await readJson(await fetch('/api/onboard/applyment',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(applyFormBody())}));
420
+ if(j.ok){
421
+ msg.style.display='block';
422
+ msg.textContent='✅ 已提交微信审核 (敏感字段已在本机加密)'+(j.idempotent?' — 此前已提交过, 幂等忽略':'');
423
+ document.getElementById('apply-form').style.display='none';
424
+ queryApplyment();
425
+ } else {
426
+ msg.style.display='block';
427
+ msg.textContent='提交失败: '+(j.message||j.error||'未知错误');
428
+ loadApplymentStatus(); // PENDING_PUSH 时状态框给出重试按钮
264
429
  }
265
- var body={subject_type:mval('m_subject_type'),merchant_name:mval('m_merchant_name'),license_no:mval('m_license_no'),
266
- legal_name:mval('m_legal_name'),legal_id_no:mval('m_legal_id_no'),admin_name:mval('m_admin_name'),admin_mobile:mval('m_admin_mobile'),
267
- store_name:mval('m_store_name'),store_address:mval('m_store_address'),settle_type:mval('m_settle_type'),bank_name:mval('m_bank_name'),bank_account:mval('m_bank_account'),
268
- files:files};
269
- var r=await fetch('/onboard/materials',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
270
- var j=await readJson(r);
271
- if(!j.ok){msg.style.display='block';msg.textContent='提交失败: '+(j.message||j.error||'未知错误');btn.disabled=false;btn.textContent='提交资料';return}
272
- msg.style.display='block';
273
- msg.textContent='✅ '+(j.message||'资料已提交')+' — 服务商审核通过后自动发放引导金 (凭据 '+String(j.bundle_hash||'').slice(0,16)+'…)';
274
- document.getElementById('materials-form').style.display='none';
275
- markStepDone('st-verification');
276
- loadStatus(); // 资料提交后立刻拉一次链上状态
277
- btn.disabled=false;btn.textContent='提交资料';
278
- }catch(e){
279
- msg.style.display='block';msg.textContent='提交失败: '+e.message;
280
- btn.disabled=false;btn.textContent='提交资料';
281
- }
430
+ }catch(e){msg.style.display='block';msg.textContent='提交失败: 服务暂时不可用,请刷新重试 ('+e.message+')';}
431
+ btn.disabled=false;btn.textContent='加密并提交进件';
282
432
  }
433
+ async function retryApplyment(){
434
+ var j=await readJson(await fetch('/api/onboard/applyment',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'}));
435
+ if(j.ok){queryApplyment();}
436
+ else{var msg=document.getElementById('apply-msg');msg.style.display='block';msg.textContent=j.message||'重试失败, 请稍后再试';}
437
+ }
438
+ async function queryApplyment(){
439
+ try{renderApplyment(await readJson(await fetch('/api/onboard/applyment/status')));}catch(e){}
440
+ }
441
+ async function loadApplymentStatus(){queryApplyment();}
283
442
  function markStepDone(id){
284
443
  var el=document.getElementById(id);
285
444
  if(el){el.className='step done';var m=el.querySelector('.mark');if(m)m.textContent='✅'}
286
445
  }
287
- async function loadMaterialsStatus(){
288
- try{
289
- var j=await readJson(await fetch('/onboard/materials'));
290
- if(!j.ok) return;
291
- if(j.submitted){
292
- markStepDone('st-verification');
293
- var msg=document.getElementById('materials-msg');
294
- msg.style.display='block';
295
- msg.textContent='资料已提交 (待审核)'+(j.pushed?' — 已送达服务商':' — '+ (j.push_error||'未能送达服务商'));
296
- }
297
- }catch(e){}
298
- }
299
446
  async function submitVerification(){
300
447
  var btn=document.getElementById('btn-verify');btn.disabled=true;btn.textContent='提交中...';
301
448
  var msg=document.getElementById('verify-msg');
@@ -324,7 +471,7 @@ async function loadStatus(){
324
471
  }
325
472
  window.addEventListener('load',loadStatus);
326
473
  setInterval(loadStatus,15000); // 链上实名审批/引导金发放会变, 页面自动刷新 (不必手点)
327
- window.addEventListener('load',loadMaterialsStatus);
474
+ window.addEventListener('load',loadApplymentStatus);
328
475
  async function submit(){
329
476
  var btn=document.getElementById('btn');btn.disabled=true;btn.textContent='生成中...';
330
477
  var d={merchant_shortname:document.getElementById('m_name').value,mobile_phone:document.getElementById('m_phone').value,category:document.getElementById('m_category').value};
@@ -348,5 +495,57 @@ async function submit(){
348
495
  }catch(e){alert('网络错误: '+e.message);btn.disabled=false}
349
496
  }
350
497
  </script>
498
+ <script>
499
+ /* ===== ③ 进件表单 美化增强 (2026-10-03) —— 纯追加脚本: 不修改/不覆盖任何现有函数,
500
+ 只做两件视觉事: ① 证件选中后显示缩略图+文件名 ② 状态卡片按内容着色 + 签约二维码占位。
501
+ 所有字段 id / 上传提交函数 / 状态机 DOM 目标均沿用现有逻辑。 ===== */
502
+ (function(){
503
+ // ① 证件缩略图预览 (事件委托, 与现有上传 handler 并存, 只读文件不干预流程)
504
+ var form=document.getElementById('apply-form');
505
+ if(form){
506
+ form.addEventListener('change',function(ev){
507
+ var input=ev.target;
508
+ if(!input||input.type!=='file'||!input.files||!input.files[0]) return;
509
+ var slot=input.closest('div[id^="a_up_"]');
510
+ if(!slot) return;
511
+ var rd=new FileReader();
512
+ rd.onload=function(){
513
+ var img=slot.querySelector('img.auprev');
514
+ if(!img){
515
+ img=document.createElement('img');
516
+ img.className='auprev';img.alt='预览';
517
+ slot.insertBefore(img,slot.children[1]||null); // 标题label之后
518
+ }
519
+ img.src=rd.result;img.style.display='block';
520
+ var nm=slot.querySelector('.auname');
521
+ if(!nm){nm=document.createElement('div');nm.className='auname';img.insertAdjacentElement('afterend',nm);}
522
+ nm.textContent=input.files[0].name+' · 点卡片可重新选择';
523
+ };
524
+ rd.readAsDataURL(input.files[0]);
525
+ },true);
526
+ }
527
+ // ② 状态卡片着色 + 签约二维码占位 (监听现有状态机的 DOM 更新, 不改其内容逻辑)
528
+ var box=document.getElementById('apply-status');
529
+ if(box){
530
+ var paint=function(){
531
+ var t=box.textContent||'';
532
+ box.setAttribute('data-state',
533
+ t.indexOf('FINISHED')>=0?'done':
534
+ (t.indexOf('TO_BE_SIGNED')>=0||t.indexOf('签约')>=0)?'sign':
535
+ t.indexOf('PENDING_PUSH')>=0?'pending':'checking');
536
+ // 有签约链接 → 补一个二维码占位块 (真二维码后续可接 qrcode 库, 链接本身可点)
537
+ var link=box.querySelector('a');
538
+ if(link&&!box.querySelector('.aqr')){
539
+ var qr=document.createElement('div');
540
+ qr.className='aqr';
541
+ qr.textContent='签约二维码 (用法人微信扫)';
542
+ link.insertAdjacentElement('beforebegin',qr);
543
+ }
544
+ };
545
+ new MutationObserver(paint).observe(box,{childList:true,subtree:true,characterData:true});
546
+ paint();
547
+ }
548
+ })();
549
+ </script>
351
550
  </body>
352
551
  </html>