trustbase-seller-backend 0.1.12 → 0.2.1

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.
@@ -47,6 +47,8 @@ const CUSTOM_TYPE_URLS = [
47
47
  "/trustchain.economy.v1.MsgGrantCredits",
48
48
  // N13 (2026-09-23): trustcoin 账户自助注册 (上架/下单前置; signer=creator, 商户本人签)
49
49
  "/trustchain.trustcoin.v1.MsgRegisterAccount",
50
+ // N17 (2026-10): 引导金池自动回补 (oracle c5-glue stepPoolRefill, authority 签)
51
+ "/trustchain.reward.v1.MsgRewardPoolTransfer",
50
52
  ];
51
53
 
52
54
  let _root = null;
@@ -68,6 +70,8 @@ function protoRoot() {
68
70
  "trustchain/economy/v1/tx.proto",
69
71
  // N13 (2026-09-23): trustcoin 账户注册
70
72
  "trustchain/trustcoin/v1/tx.proto",
73
+ // N17 (2026-10): reward 池划转 (MsgRewardPoolTransfer)
74
+ "trustchain/reward/v1/tx.proto",
71
75
  ], { keepCase: true });
72
76
  _root = root;
73
77
  return root;
@@ -192,7 +196,12 @@ async function pollFinalTx(lcdUrl, txhash) {
192
196
  return null;
193
197
  }
194
198
 
195
- class SequenceMismatchErrorJs extends Error {}
199
+ /** P4: sequence mismatch 统一抛 routes/chain-writes 的 SequenceMismatchError —
200
+ * c5-glue/sp-notify/chain-writes 三处 "重查序号重试一次" 的 instanceof 判断对 JS 通道同样生效。
201
+ * 延迟 require 规避循环依赖 (chain-writes.js 顶部即 require 本模块, 加载期取不到其 class)。 */
202
+ function newSequenceMismatchError(msg) {
203
+ return new (require("../routes/chain-writes").SequenceMismatchError)(msg);
204
+ }
196
205
 
197
206
  /**
198
207
  * JS 直签 + 广播完整流程。入参/返回与 chain-writes.js 的 CLI 路径对齐:
@@ -201,7 +210,8 @@ class SequenceMismatchErrorJs extends Error {}
201
210
  * account: {accountNumber, sequence} (字符串)
202
211
  * 返回 {txhash, code, raw_log} (code!=0 也返回, 由调用方判 502)
203
212
  * memo: 可选; 下单 (MsgCreateOrder) 走 "tb-auth:<sha256>" 意愿摘要, 其余写路径留空
204
- * 抛 SequenceMismatchErrorJs (message 含 "account sequence mismatch")
213
+ * 抛 chain-writes.SequenceMismatchError (message 含 "account sequence mismatch") — P4 统一后
214
+ * 调用方 instanceof chainWrites.SequenceMismatchError 的重试逻辑对 JS 通道同样生效
205
215
  * 抛 Error("tx_delivery_timeout: ...") 当 15s 未入块
206
216
  */
207
217
  async function jsSignAndBroadcast(c, messages, account, baseDir, memo) {
@@ -228,7 +238,7 @@ async function jsSignAndBroadcast(c, messages, account, baseDir, memo) {
228
238
  const txBytes = TxRaw.encode(txRaw).finish();
229
239
  const checkTx = await broadcastTxSync(c.rpcUrl, txBytes);
230
240
  if (/account sequence mismatch/i.test(checkTx.raw_log)) {
231
- throw new SequenceMismatchErrorJs(`account sequence mismatch: ${checkTx.raw_log}`);
241
+ throw newSequenceMismatchError(`account sequence mismatch: ${checkTx.raw_log}`);
232
242
  }
233
243
  if (checkTx.code !== 0) {
234
244
  // CheckTx 拒绝 (码/ccodespace/log 原样回传, 语义同 CLI broadcast 的 code!=0)
@@ -237,7 +247,7 @@ async function jsSignAndBroadcast(c, messages, account, baseDir, memo) {
237
247
  const final = await pollFinalTx(c.lcdUrl, checkTx.txhash);
238
248
  if (final) {
239
249
  if (/account sequence mismatch/i.test(final.raw_log)) {
240
- throw new SequenceMismatchErrorJs(`account sequence mismatch: ${final.raw_log}`);
250
+ throw newSequenceMismatchError(`account sequence mismatch: ${final.raw_log}`);
241
251
  }
242
252
  return final;
243
253
  }
@@ -254,5 +264,5 @@ module.exports = {
254
264
  jsSignerAddress,
255
265
  jsSignAndBroadcast,
256
266
  toEncodeObjects,
257
- SequenceMismatchErrorJs,
267
+ newSequenceMismatchError,
258
268
  };
package/oracle/c5-glue.js CHANGED
@@ -80,6 +80,9 @@ const SIGNER_FORBIDDEN_RE = /intended signer does not match|signature verificati
80
80
  // N14: settle 幂等 (已结算/状态已变/订单不存在 → 按完成处理, 不再重试)
81
81
  const SETTLE_DONE_RE = /already settled|not in confirmed status|order not found/i;
82
82
 
83
+ // E6 P2: 链上 reason_kind+ref_id 幂等命中 (上轮 tx 实际已入块但本地未记 done, 如交付超时后重发被链拒)
84
+ const GRANT_DUP_RE = /already granted|duplicate|grant already exists/i;
85
+
83
86
  function sha256Hex(s) {
84
87
  return crypto.createHash("sha256").update(String(s)).digest("hex");
85
88
  }
@@ -566,8 +569,21 @@ function createOracleGlue(deps) {
566
569
  amount: String(g.amount),
567
570
  reason_kind: reasonKind,
568
571
  ref_id: g.refId,
569
- }]);
572
+ }]).catch((e) => {
573
+ // sendTx 异常 (链抖动/超时): 留 pending 等下轮 retryPendingGrants 补发,
574
+ // 不上抛 — 避免中断本批后续 grant (此前异常会丢失同批剩余 grant 的台账行)
575
+ warn(`[oracle-glue] E6 发放异常 ${reasonKind}/${g.refId}: ${e.message} (留 pending 下轮重试)`);
576
+ return null;
577
+ });
578
+ if (!r) continue;
570
579
  if (r.code !== 0) {
580
+ if (GRANT_DUP_RE.test(r.raw_log)) {
581
+ // 链上幂等命中: 该笔实际已发放 (本地没落 done), 按完成处理
582
+ db.prepare("UPDATE oracle_credit_grants SET status='done', note=?, updated_at=? WHERE reason_kind=? AND ref_id=?")
583
+ .run(`链上幂等命中: ${String(r.raw_log).slice(0, 160)}`, nowSec(), reasonKind, g.refId);
584
+ log(`[oracle-glue] E6 ${reasonKind}/${g.refId} 链上已发放 (幂等命中), 按完成处理`);
585
+ continue;
586
+ }
571
587
  const status = MODULE_NOT_LIVE_RE.test(r.raw_log) ? "pending" : "failed";
572
588
  db.prepare("UPDATE oracle_credit_grants SET status=?, note=?, updated_at=? WHERE reason_kind=? AND ref_id=?")
573
589
  .run(status, `code=${r.code}: ${String(r.raw_log).slice(0, 200)}`, nowSec(), reasonKind, g.refId);
@@ -580,8 +596,49 @@ function createOracleGlue(deps) {
580
596
  }
581
597
  }
582
598
 
583
- /** E6 一轮: 拉 CONFIRMED 事实 → 逐笔结算 */
599
+ /** P2: 存量 pending 行补发 (上轮 economy 未上线/sendTx 异常遗留 — 此前永不再试)。
600
+ * 每轮结算开头先扫台账重发; 链上 reason_kind+ref_id 幂等是第二道保险:
601
+ * 重复发放被链拒 (GRANT_DUP_RE) 按完成处理。返回本轮处理的 pending 行数。 */
602
+ async function retryPendingGrants(oracleAddr) {
603
+ const rows = db.prepare("SELECT * FROM oracle_credit_grants WHERE status='pending'").all();
604
+ for (const row of rows) {
605
+ let r;
606
+ try {
607
+ r = await sendTx([{
608
+ "@type": "/trustchain.economy.v1.MsgGrantCredits",
609
+ issuer: oracleAddr,
610
+ recipient: row.recipient,
611
+ amount: String(row.amount),
612
+ reason_kind: row.reason_kind,
613
+ ref_id: row.ref_id,
614
+ }]);
615
+ } catch (e) {
616
+ warn(`[oracle-glue] E6 pending 补发 ${row.reason_kind}/${row.ref_id} 异常: ${e.message} (留 pending 下轮重试)`);
617
+ continue;
618
+ }
619
+ if (r.code !== 0) {
620
+ if (GRANT_DUP_RE.test(r.raw_log)) {
621
+ db.prepare("UPDATE oracle_credit_grants SET status='done', note=?, updated_at=? WHERE reason_kind=? AND ref_id=?")
622
+ .run(`链上幂等命中: ${String(r.raw_log).slice(0, 160)}`, nowSec(), row.reason_kind, row.ref_id);
623
+ log(`[oracle-glue] E6 pending 补发 ${row.reason_kind}/${row.ref_id} 链上已发放 (幂等命中), 按完成处理`);
624
+ } else if (!MODULE_NOT_LIVE_RE.test(r.raw_log)) {
625
+ db.prepare("UPDATE oracle_credit_grants SET status='failed', note=?, updated_at=? WHERE reason_kind=? AND ref_id=?")
626
+ .run(`补发 code=${r.code}: ${String(r.raw_log).slice(0, 200)}`, nowSec(), row.reason_kind, row.ref_id);
627
+ warn(`[oracle-glue] E6 pending 补发失败 ${row.reason_kind}/${row.ref_id} code=${r.code}: ${String(r.raw_log).slice(0, 200)}`);
628
+ }
629
+ // 模块未上线 → 留 pending 下轮再试
630
+ continue;
631
+ }
632
+ db.prepare("UPDATE oracle_credit_grants SET status='done', txhash=?, updated_at=? WHERE reason_kind=? AND ref_id=?")
633
+ .run(r.txhash, nowSec(), row.reason_kind, row.ref_id);
634
+ log(`[oracle-glue] E6 pending 已补发 ${row.reason_kind}/${row.ref_id} → ${row.recipient} +${row.amount} (tx=${r.txhash})`);
635
+ }
636
+ return rows.length;
637
+ }
638
+
639
+ /** E6 一轮: 先补发存量 pending (P2) → 拉 CONFIRMED 事实 → 逐笔结算 */
584
640
  async function settleConfirmedFacts(oracleAddr) {
641
+ await retryPendingGrants(oracleAddr); // 不依赖链上 params, 先清存量
585
642
  const params = await getEconomyParams();
586
643
  if (!params) { warn("[oracle-glue] economy params 查询失败 (模块未上线?), 本轮 E6 跳过"); return; }
587
644
  const pj = await lcdGet("/oxiaom/trustchain/serviceprovider/v1/providers");
@@ -732,7 +789,7 @@ function createOracleGlue(deps) {
732
789
  await settleExpiredOrders(oracleAddr);
733
790
  }
734
791
 
735
- return { tick, enqueueBootstrapGrant, settleConfirmedFacts, settleExpiredOrders, stepBootstrapGrant, getEconomyParams, monthlyUsed, resolveOrderParties, settleFact, grantFeegrant, grantFeegrantFor, findFeegrant, stepPoolRefill, refillEnabled, poolBalanceUtct, refillWindow };
792
+ return { tick, enqueueBootstrapGrant, settleConfirmedFacts, settleExpiredOrders, stepBootstrapGrant, getEconomyParams, monthlyUsed, resolveOrderParties, settleFact, retryPendingGrants, grantFeegrant, grantFeegrantFor, findFeegrant, stepPoolRefill, refillEnabled, poolBalanceUtct, refillWindow };
736
793
  }
737
794
 
738
795
  module.exports = { createOracleGlue, oracleCfg, sha256Hex, MODULE_NOT_LIVE_RE };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trustbase-seller-backend",
3
- "version": "0.1.12",
3
+ "version": "0.2.1",
4
4
  "description": "TrustBase Seller Backend (merchant edition) — 商品上链/订单/支付/质押 write API for TrustBase 保障链生态. 单卖家自托管形态: 签名 key 即卖家身份, 私钥不出门.",
5
5
  "main": "run-unified.js",
6
6
  "bin": {
@@ -49,6 +49,8 @@ async function wxCall(method, urlPath, bodyObj) {
49
49
  "Authorization": agentPay.wxSignAuth(method, urlPath, bodyStr, spOverride(c)),
50
50
  "Content-Type": "application/json",
51
51
  "Accept": "application/json",
52
+ // 微信 APIv3 拒 undici 默认 Accept-Language: * (2026-09-25 实测 PARAM_ERROR) — 与 agent-pay wxApiCall 对齐
53
+ "Accept-Language": "zh-CN",
52
54
  "User-Agent": "trustbase-seller-backend/1.0",
53
55
  },
54
56
  body: method === "GET" ? undefined : bodyStr,
@@ -0,0 +1,44 @@
1
+ syntax = "proto3";
2
+ package trustchain.reward.v1;
3
+
4
+ import "amino/amino.proto";
5
+ import "gogoproto/gogo.proto";
6
+
7
+ option go_package = "github.com/oxiaom/trustchain/x/reward/types";
8
+
9
+ message Params {
10
+ option (amino.name) = "trustchain/x/reward/Params";
11
+ option (gogoproto.equal) = true;
12
+
13
+ // reward per purchase (microTCT)
14
+ string purchase_reward = 1;
15
+
16
+ // reward per positive review (microTCT)
17
+ string review_reward = 2;
18
+
19
+ // reward per invitation (microTCT)
20
+ string invite_reward = 3;
21
+
22
+ // daily reward cap per user (microTCT)
23
+ string daily_cap = 4;
24
+
25
+ // registration reward, locked until verified (microTCT) — v0.20.0
26
+ string registration_reward = 5;
27
+
28
+ // invite reward when invitee role=buyer/both activates (microTCT) — v0.20.0 (PRD §4.9 default 5 TCT)
29
+ string invite_buyer_reward = 6;
30
+
31
+ // invite reward when invitee role=seller activates (microTCT) — v0.20.0 (PRD §4.9 default 10 TCT)
32
+ string invite_seller_reward = 7;
33
+ }
34
+
35
+ // RewardEvent tracks a single reward distribution.
36
+ message RewardEvent {
37
+ option (amino.name) = "trustchain/x/reward/RewardEvent";
38
+
39
+ string recipient = 1;
40
+ string amount = 2; // microTCT
41
+ string reward_type = 3; // "purchase", "review", "invite", "listing"
42
+ uint64 reference_id = 4; // order_id or invite_id
43
+ uint64 created_at = 5;
44
+ }
@@ -0,0 +1,62 @@
1
+ syntax = "proto3";
2
+ package trustchain.reward.v1;
3
+
4
+ import "amino/amino.proto";
5
+ import "cosmos/msg/v1/msg.proto";
6
+ import "gogoproto/gogo.proto";
7
+ import "trustchain/reward/v1/params.proto";
8
+
9
+ option go_package = "github.com/oxiaom/trustchain/x/reward/types";
10
+
11
+ // Msg defines the Msg service.
12
+ service Msg {
13
+ option (cosmos.msg.v1.service) = true;
14
+
15
+ // UpdateParams defines a (governance) operation for updating the module
16
+ // parameters. The authority defaults to the x/gov module account.
17
+ rpc UpdateParams(MsgUpdateParams) returns (MsgUpdateParamsResponse);
18
+
19
+ // RewardPoolTransfer — 奖励池定向划转 (authority/emergency 双轨, 池间调拨用)
20
+ rpc RewardPoolTransfer(MsgRewardPoolTransfer) returns (MsgRewardPoolTransferResponse);
21
+ }
22
+
23
+ // MsgRewardPoolTransfer 从 x/reward 奖励池向目标地址划转 TCT (池间调拨, 如: 奖励池 → 引导金池)
24
+ message MsgRewardPoolTransfer {
25
+ option (cosmos.msg.v1.signer) = "authority";
26
+ option (amino.name) = "trustchain/x/reward/MsgRewardPoolTransfer";
27
+
28
+ // authority 是划转签名人 (gov 模块地址或应急密钥, 双轨)
29
+ string authority = 1;
30
+
31
+ // recipient 是收款地址 (普通账户或目标模块账户, 如 subchain 引导金池)
32
+ string recipient = 2;
33
+
34
+ // amount 是划转金额 (utct)
35
+ string amount = 3;
36
+ }
37
+
38
+ message MsgRewardPoolTransferResponse {
39
+ // amount 回显实际划转金额 (utct)
40
+ string amount = 1;
41
+ }
42
+
43
+ // MsgUpdateParams is the Msg/UpdateParams request type.
44
+ message MsgUpdateParams {
45
+ option (cosmos.msg.v1.signer) = "authority";
46
+ option (amino.name) = "trustchain/x/reward/MsgUpdateParams";
47
+
48
+ // authority is the address that controls the module (defaults to x/gov unless overwritten).
49
+ string authority = 1;
50
+
51
+ // params defines the module parameters to update.
52
+ //
53
+ // NOTE: All parameters must be supplied.
54
+ Params params = 2 [
55
+ (gogoproto.nullable) = false,
56
+ (amino.dont_omitempty) = true
57
+ ];
58
+ }
59
+
60
+ // MsgUpdateParamsResponse defines the response structure for executing a
61
+ // MsgUpdateParams message.
62
+ message MsgUpdateParamsResponse {}
@@ -103,6 +103,14 @@ img.auprev{display:none;width:100%;max-height:140px;object-fit:cover;border-radi
103
103
  #apply-form .auploads{grid-template-columns:1fr}
104
104
  #apply-form .ascene-row{grid-template-columns:1fr}
105
105
  }
106
+ /* ===== 服务商名片卡 (2026-10-03 xiaomu: 入驻页显示服务商是谁) ===== */
107
+ .spcard{background:#FCFAF4;border:1px solid #EAE3D6;border-radius:14px;padding:16px 18px;margin-bottom:18px}
108
+ .spcard.offline{opacity:.65;background:#F4F1EA}
109
+ .spcard-h{display:flex;align-items:center;gap:8px;font-size:15px;color:#33302B}
110
+ .spbadge{background:#C4633C;color:#fff;font-size:11px;border-radius:999px;padding:2px 10px;margin-left:auto}
111
+ .spcard-desc{font-size:13px;color:#6B655B;margin-top:6px;line-height:1.7}
112
+ .spcard-foot{font-size:12px;color:#8A857B;margin-top:8px;line-height:1.9}
113
+ .spcard-foot a{color:#C4633C;text-decoration:none}
106
114
  @media print{
107
115
  body *{visibility:hidden}
108
116
  #print-area,#print-area *{visibility:visible}
@@ -116,6 +124,12 @@ img.auprev{display:none;width:100%;max-height:140px;object-fit:cover;border-radi
116
124
  <h1>TrustBase 商家入驻</h1>
117
125
  <p class="sub">在你的电脑上完成入驻,密钥永不出门</p>
118
126
 
127
+ <div class="spcard" id="spcard" style="display:none">
128
+ <div class="spcard-h"><span>🏪</span><span id="sp-name" style="font-weight:600">服务商</span><span class="spbadge" id="sp-rate" style="display:none"></span></div>
129
+ <div class="spcard-desc" id="sp-desc"></div>
130
+ <div class="spcard-foot" id="sp-foot"></div>
131
+ </div>
132
+
119
133
  <div class="progress" id="progress">
120
134
  <h3>入驻进度 <span id="status-meta" style="font-size:12px;color:#8A857B;font-weight:400"></span><button class="sbtn noprint" style="float:right;margin-top:-2px" onclick="loadStatus()">刷新状态</button></h3>
121
135
  <div class="step done" id="st-identity">
@@ -133,7 +147,6 @@ img.auprev{display:none;width:100%;max-height:140px;object-fit:cover;border-radi
133
147
  <div class="step" id="st-verification">
134
148
  <span class="mark">⬜</span>
135
149
  <div class="body">③ 微信收款认证
136
- <div class="ops"><button class="sbtn" id="btn-verify" onclick="submitVerification()" title="链上身份认证由服务商在进件审核通过后自动完成, 一般无需手动">提交实名认证 (一般无需手动)</button></div>
137
150
  <div class="desc" id="verify-desc">提交后由服务商审核微信特约商户收款账户; 审核通过后自动发放 100 TCT 引导金 (无需重复提交)</div>
138
151
  <div class="hintbox" id="verify-msg"></div>
139
152
  <div class="ops noprint"><button class="sbtn" id="btn-apply-toggle" onclick="toggleApplyForm()">填写进件资料并上传证件</button></div>
@@ -178,6 +191,10 @@ img.auprev{display:none;width:100%;max-height:140px;object-fit:cover;border-radi
178
191
  <div class="agrid">
179
192
  <div class="afield"><label for="a_legal_name">法人/经营者姓名</label><input id="a_legal_name" placeholder="与身份证一致"></div>
180
193
  <div class="afield"><label for="a_legal_id_number">法人身份证号</label><input id="a_legal_id_number" placeholder="17 位数字 + 数字或 X"></div>
194
+ <div class="afield afield-full" id="afield_idaddr" style="display:none"><label for="a_legal_id_address">身份证居住地址 <span class="aopt">企业主体必填, 本机加密</span></label><input id="a_legal_id_address" placeholder="与身份证住址一致, 如: 广东省深圳市南山区xx路xx号"></div>
195
+ <div class="afield afield-full" id="afield_ubo_note" style="display:none">
196
+ <div class="ahint" style="margin-top:0">企业主体:若最终受益人只有法定代表人本人,无需填写受益人信息 (微信自动回填);否则暂不支持,请联系服务商协助进件</div>
197
+ </div>
181
198
  <div class="afield"><label for="a_period_begin">身份证有效期开始</label><input id="a_period_begin" placeholder="YYYY-MM-DD"></div>
182
199
  <div class="afield"><label for="a_period_end">有效期结束</label><input id="a_period_end" placeholder="YYYY-MM-DD 或 长期"></div>
183
200
  <div class="afield"><label for="a_contact_name">超级管理员姓名</label><input id="a_contact_name" placeholder="与微信实名一致"></div>
@@ -347,7 +364,7 @@ function renderProgress(s){
347
364
  // 其余 → 引导提交
348
365
  var vs=s.verification||{submitted:false,status:'none'};
349
366
  var qat=vs.queried_at?(' · 查询于 '+new Date(vs.queried_at*1000).toLocaleTimeString()):'';
350
- var chainTxt=vs.raw_status?('链上状态 '+vs.raw_status):'链上状态未知';
367
+ var chainTxt=vs.raw_status?('审核状态 '+vs.raw_status):'审核状态未知';
351
368
  var vStep=document.getElementById('st-verification');
352
369
  var ap=s.applyment||null;
353
370
  var apDone=!!(ap&&ap.done);
@@ -355,11 +372,10 @@ function renderProgress(s){
355
372
  var applyForm=document.getElementById('apply-form');
356
373
  if(apDone){
357
374
  vStep.className='step done'; vStep.querySelector('.mark').textContent='✅';
358
- document.getElementById('btn-verify').style.display='none';
359
375
  if(applyForm) applyForm.style.display='none';
360
376
  document.getElementById('verify-desc').textContent='微信收款进件已完成 (sub_mchid='+(ap.sub_mchid||'')+'), 收款通道就绪'+qat;
361
377
  } else if(kycOk){
362
- document.getElementById('verify-desc').textContent='链上身份认证已过 ✅ · 微信收款进件未完成 — 请填写下方进件资料, 微信审核通过后收款通道才开通'+qat;
378
+ document.getElementById('verify-desc').textContent='身份认证已通过 ✅ · 微信收款进件未完成 — 请填写下方进件资料, 微信审核通过后收款通道才开通'+qat;
363
379
  } else if(vs.submitted){
364
380
  document.getElementById('verify-desc').textContent='实名已提交, 等待服务商审核 ('+chainTxt+qat+')';
365
381
  } else {
@@ -487,6 +503,7 @@ function applyFormBody(){
487
503
  legal_id_period_begin:aval('a_period_begin'),legal_id_period_end:aval('a_period_end'),
488
504
  account_type:aval('a_account_type'),account_name:aval('a_account_name'),account_number:aval('a_account_number'),
489
505
  account_bank:aval('a_account_bank'),bank_name:aval('a_bank_name'),bank_address_code:aval('a_bank_address_code'),
506
+ legal_id_address:aval('a_legal_id_address'), // 企业主体必填 (F4), 个体户留空
490
507
  sales_scene:scene,
491
508
  license_media_id:_aMedia.license_media_id||'',legal_id_front_media_id:_aMedia.legal_id_front_media_id||'',
492
509
  legal_id_back_media_id:_aMedia.legal_id_back_media_id||''
@@ -588,29 +605,86 @@ async function retryApplyment(){
588
605
  else{var msg=document.getElementById('apply-msg');msg.style.display='block';msg.textContent=j.message||'重试失败, 请稍后再试';}
589
606
  }
590
607
  async function queryApplyment(){
591
- try{renderApplyment(await readJson(await fetch('/api/onboard/applyment/status')));}catch(e){}
608
+ try{
609
+ var j=await readJson(await fetch('/api/onboard/applyment/status'));
610
+ renderApplyment(j);
611
+ if(j && j.form_snapshot) prefillApplyForm(j.form_snapshot); // F1: 驳回/暂存后预填
612
+ }catch(e){}
613
+ }
614
+ // F1 预填: 台账快照回填非敏感字段 + 照片 media_id 复用 (敏感字段是密文/脱敏, 需重填)
615
+ var PREFILL_MAP={subject_type:'a_subject_type',merchant_shortname:'a_shortname',merchant_name:'a_merchant_name',
616
+ license_number:'a_license_number',service_phone:'a_service_phone',settlement_id:'a_settlement_id',
617
+ qualification_type:'a_qualification_type',contact_email:'a_contact_email',legal_name:'a_legal_name',
618
+ legal_id_period_begin:'a_period_begin',legal_id_period_end:'a_period_end',
619
+ account_type:'a_account_type',account_bank:'a_account_bank',bank_name:'a_bank_name',bank_address_code:'a_bank_address_code',
620
+ store_name:'a_store_name',store_address_code:'a_store_address_code',store_address:'a_store_address',
621
+ website_url:'a_website_url',appid:'a_appid',appid_type:'a_appid_type'};
622
+ var PREFILL_MEDIA=['license_media_id','legal_id_front_media_id','legal_id_back_media_id',
623
+ 'store_entrance_media_id','store_indoor_media_id','web_home_media_id','web_product_media_id','mp_media_id'];
624
+ var _prefilled=false;
625
+ function prefillApplyForm(snap){
626
+ if(_prefilled||!snap||typeof snap!=='object') return;
627
+ _prefilled=true;
628
+ // 场景先切 (setScene 会重挂上传槽, 必须在写媒体状态前)
629
+ if(snap.sales_scene){
630
+ var r=document.querySelector('input[name="sales_scene"][value="'+snap.sales_scene+'"]');
631
+ if(r){r.checked=true;setScene(snap.sales_scene);}
632
+ }
633
+ // 主体类型联动先触发 (居住地址/UBO 说明显示 + 类目默认), 再写快照值 (避免类目被联动覆盖)
634
+ var subj=document.getElementById('a_subject_type');
635
+ if(subj&&snap.subject_type){subj.value=snap.subject_type;subj.dispatchEvent(new Event('change'));}
636
+ for(var k in PREFILL_MAP){
637
+ if(k==='subject_type') continue; // 上面已处理
638
+ var v=snap[k];
639
+ if(!v||v==='无') continue; // 敏感字段快照是脱敏的 ***后4位, 不会命中这里 (值带 *** 前缀也跳过)
640
+ if(String(v).indexOf('***')===0) continue;
641
+ var e=document.getElementById(PREFILL_MAP[k]);
642
+ if(e&&!e.value) e.value=v;
643
+ }
644
+ // 照片复用: media_id 还在微信侧有效, 标已传
645
+ var reused=0;
646
+ PREFILL_MEDIA.forEach(function(k){
647
+ if(!snap[k]) return;
648
+ _aMedia[k]=snap[k]; reused++;
649
+ var st=document.getElementById('st_'+k);
650
+ if(st){st.style.color='#16A34A';st.textContent='✓ 已上传过, 无需重传';}
651
+ });
652
+ var msg=document.getElementById('apply-msg');
653
+ msg.style.display='block';
654
+ msg.textContent='ℹ️ 已预填上次提交的资料; 身份证号/手机号/卡号等敏感字段需重新填写'+(reused?' (照片已传过, 无需重传)':'');
592
655
  }
593
656
  async function loadApplymentStatus(){queryApplyment();}
594
657
  function markStepDone(id){
595
658
  var el=document.getElementById(id);
596
659
  if(el){el.className='step done';var m=el.querySelector('.mark');if(m)m.textContent='✅'}
597
660
  }
598
- async function submitVerification(){
599
- var btn=document.getElementById('btn-verify');btn.disabled=true;btn.textContent='提交中...';
600
- var msg=document.getElementById('verify-msg');
661
+ // 服务商名片卡 (2026-10-03): /api/onboard/sp-info 代理 SP 网关公开名片; 离线降级灰色态
662
+ function escHtml(s){return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')}
663
+ async function loadSpInfo(){
664
+ var card=document.getElementById('spcard');
665
+ if(!card) return;
601
666
  try{
602
- var j=await readJson(await fetch('/onboard/submit-verification',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'}));
603
- if(j.ok){
604
- msg.style.display='block';msg.textContent='✅ 实名已提交, 等待服务商审核, 通过后自动发放引导金'+(j.duplicate?' (此前已提交过, 幂等忽略'+(j.txhash?', tx '+String(j.txhash).slice(0,12)+'…':'')+')':'');
605
- loadStatus(); // 立刻拉一次链上状态, 不等 15s 轮询
606
- } else if(j.error==='module_not_live'){
607
- msg.style.display='block';msg.textContent='ℹ️ '+(j.message||'链上实名模块未上线')+' — 自动通道将在集群升级后开放, 当前请由服务商协助办理微信特约商户认证';
608
- } else {
609
- msg.style.display='block';msg.textContent='提交失败: '+(j.message||j.error||'未知错误');
610
- }
611
- }catch(e){msg.style.display='block';msg.textContent='提交失败: 服务暂时不可用,请刷新重试 ('+e.message+')'}
612
- btn.disabled=false;btn.textContent='提交实名认证';
667
+ var j=await readJson(await fetch('/api/onboard/sp-info'));
668
+ if(!j||!j.name) return;
669
+ card.style.display='block';
670
+ card.classList.toggle('offline', !!j.offline);
671
+ document.getElementById('sp-name').textContent=j.offline&&!j.stale?'服务商暂时不可达':j.name;
672
+ var rb=document.getElementById('sp-rate');
673
+ if(j.rate_pct){rb.style.display='';rb.textContent='抽佣 '+j.rate_pct;}else{rb.style.display='none';}
674
+ document.getElementById('sp-desc').textContent=j.offline&&!j.stale
675
+ ?'资料仍可填写,提交时会加密暂存本机,服务商恢复后可重试'
676
+ :(j.desc||'');
677
+ var foot=[];
678
+ if(j.official_url) foot.push('<a href="'+escHtml(j.official_url)+'" target="_blank">官网</a>');
679
+ if(j.contact) foot.push('客服: '+escHtml(j.contact));
680
+ if(j.chain_address) foot.push('服务商账号: <span style="font-family:Consolas,monospace;font-size:11px">'+escHtml(j.chain_address)+'</span>');
681
+ // 迁移保障 (核心卖点, 必须写; 面向商户禁用"链上"术语, 用大白话)
682
+ foot.push('服务商由你安装时选定,可随时更换 (你的资产保存在你自己的电脑上,不被锁定)');
683
+ if(j.offline&&j.stale) foot.push('当前离线, 展示缓存信息');
684
+ document.getElementById('sp-foot').innerHTML=foot.join(' · ');
685
+ }catch(e){ /* 名片接口挂了就保持隐藏, 不挡入驻主流程 */ }
613
686
  }
687
+ window.addEventListener('load',loadSpInfo);
614
688
  async function loadStatus(){
615
689
  var meta=document.getElementById('status-meta');
616
690
  try{
@@ -659,6 +733,10 @@ async function submit(){
659
733
  if(subj&&stl){
660
734
  subj.addEventListener('change',function(){
661
735
  stl.value = subj.value==='SUBJECT_TYPE_ENTERPRISE' ? '716' : '719';
736
+ // F4: 企业主体显示身份证居住地址(必填) + UBO 说明; 个体户隐藏
737
+ var ent = subj.value==='SUBJECT_TYPE_ENTERPRISE';
738
+ var ia=document.getElementById('afield_idaddr'); if(ia) ia.style.display=ent?'':'none';
739
+ var un=document.getElementById('afield_ubo_note'); if(un) un.style.display=ent?'':'none';
662
740
  });
663
741
  }
664
742
  // 省市编码选项 (国标区划码, 两处下拉共用; 微信官方对照表口径)