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.
- package/core/chain-signer.js +15 -5
- package/oracle/c5-glue.js +60 -3
- package/package.json +1 -1
- package/payments/wechat/profitsharing.js +2 -0
- package/proto/trustchain/reward/v1/params.proto +44 -0
- package/proto/trustchain/reward/v1/tx.proto +62 -0
- package/routes/onboard.html +97 -19
- package/routes/onboard.js +65 -16
- package/routes/sp-applyment.js +42 -15
- package/run-unified.js +11 -0
- package/test-chain-signer.js +61 -0
- package/test-onboard-applyment.js +523 -387
- package/test-oracle-glue.js +69 -0
- package/test-oracle-pool-refill.js +8 -0
- package/test-sp-applyment.js +59 -5
- package/test-sp-gateway.js +38 -0
package/core/chain-signer.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
* 抛
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
@@ -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 {}
|
package/routes/onboard.html
CHANGED
|
@@ -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?('
|
|
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='
|
|
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{
|
|
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
|
-
|
|
599
|
-
|
|
600
|
-
|
|
661
|
+
// 服务商名片卡 (2026-10-03): /api/onboard/sp-info 代理 SP 网关公开名片; 离线降级灰色态
|
|
662
|
+
function escHtml(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
|
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/
|
|
603
|
-
if(j.
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
// 省市编码选项 (国标区划码, 两处下拉共用; 微信官方对照表口径)
|