trustbase-seller-backend 0.3.0 → 0.3.2
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 +1 -1
- package/routes/onboard.html +95 -12
- package/routes/onboard.js +57 -4
- package/routes/sp-applyment.js +5 -3
- package/test-onboard-applyment.js +94 -4
package/package.json
CHANGED
package/routes/onboard.html
CHANGED
|
@@ -60,6 +60,9 @@ h1{font-size:22px;margin-bottom:4px}
|
|
|
60
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
61
|
#apply-form .afield input::placeholder{color:#BDB5A6}
|
|
62
62
|
#apply-form .afield input.amanual{margin-top:6px;border-style:dashed}
|
|
63
|
+
/* 结算规则只读展示框 (按主体推导, 商户不用选; settlement_id=结算规则 ID 716/719/727,
|
|
64
|
+
qualification_type=行业类目 MCC — 两次微信实测错误交叉锁定后的正确口径) */
|
|
65
|
+
#apply-form .arule{padding:10px 12px;border:1px dashed #D4CDBF;border-radius:8px;background:#F7F3EB;color:#6B655B;font-size:14px}
|
|
63
66
|
#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)}
|
|
64
67
|
/* 上传卡片 (结构由 mountApplyUpload 生成: 标题label + label.sbtn>input + span#st_*) */
|
|
65
68
|
#apply-form .auploads{display:grid;grid-template-columns:1fr 1fr;gap:12px 14px}
|
|
@@ -196,14 +199,15 @@ img.auprev{display:none;width:100%;max-height:140px;object-fit:cover;border-radi
|
|
|
196
199
|
<div class="afield"><label for="a_merchant_name">营业执照名称</label><input id="a_merchant_name" placeholder="全称, 与执照一致"></div>
|
|
197
200
|
<div class="afield"><label for="a_license_number">统一社会信用代码</label><input id="a_license_number" placeholder="18 位, 9 开头"></div>
|
|
198
201
|
<div class="afield"><label for="a_service_phone">客服电话</label><input id="a_service_phone" placeholder="交易记录向买家展示"></div>
|
|
199
|
-
<div class="afield"><label
|
|
202
|
+
<div class="afield"><label>结算规则 (按主体自动确定, 不用选)</label>
|
|
203
|
+
<div class="arule" id="a_settlement_rule">719 · 个体户通用(0.6% T+1)</div></div>
|
|
204
|
+
<div class="afield"><label for="a_settlement_id">行业类目 (微信 MCC 编码)</label>
|
|
200
205
|
<input id="a_settlement_filter" placeholder="搜索: 编码或名称 (如 5732 / 电子)" style="margin-bottom:6px">
|
|
201
206
|
<select id="a_settlement_id">
|
|
202
|
-
<option value=""
|
|
207
|
+
<option value="">请选择行业类目</option>
|
|
203
208
|
<option value="__MANUAL__">手动输入编码</option>
|
|
204
209
|
</select>
|
|
205
210
|
<input id="a_settlement_manual" class="amanual" placeholder="4 位 MCC 编码, 如: 5311" style="display:none"></div>
|
|
206
|
-
<div class="afield afield-full"><label for="a_qualification_type">类目资质描述 <span class="aopt">通用类目留空</span></label><input id="a_qualification_type" placeholder="通用零售/餐饮留空即可;特殊行业(医疗/教育/食品)需对应资质"></div>
|
|
207
211
|
</div>
|
|
208
212
|
</div>
|
|
209
213
|
|
|
@@ -586,7 +590,7 @@ function mountApplyUpload(slotId,field,label){
|
|
|
586
590
|
stat.textContent=o.stat||'';
|
|
587
591
|
}
|
|
588
592
|
_aSlots[field]={
|
|
589
|
-
prefill:function(mediaId){ _aMedia[field]=mediaId; toFilled({stat:'✓ 已上传过, 无需重传'}); },
|
|
593
|
+
prefill:function(mediaId){ _aMedia[field]=mediaId; toFilled({stat:'✓ 已上传过, 无需重传'}); saveApplyDraft(); },
|
|
590
594
|
clear:toEmpty
|
|
591
595
|
};
|
|
592
596
|
function onFile(input){
|
|
@@ -599,14 +603,14 @@ function mountApplyUpload(slotId,field,label){
|
|
|
599
603
|
toFilled({previewUrl:previewUrl,name:file.name,stat:'上传中...'});
|
|
600
604
|
fetch('/api/onboard/applyment/upload',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})
|
|
601
605
|
.then(readJson).then(function(j){
|
|
602
|
-
if(j.ok&&j.media_id){ _aMedia[field]=j.media_id; toFilled({previewUrl:previewUrl,name:file.name,stat:'✓ 已上传'}); }
|
|
606
|
+
if(j.ok&&j.media_id){ _aMedia[field]=j.media_id; toFilled({previewUrl:previewUrl,name:file.name,stat:'✓ 已上传'}); saveApplyDraft(); }
|
|
603
607
|
else toFilled({previewUrl:previewUrl,name:file.name,stat:'上传失败: '+(j.message||j.error||'未知错误'),err:true});
|
|
604
608
|
}).catch(function(e){ toFilled({previewUrl:previewUrl,name:file.name,stat:'网络错误: '+e.message,err:true}); });
|
|
605
609
|
});
|
|
606
610
|
}
|
|
607
611
|
zone.querySelector('input[type=file]').addEventListener('change',function(){ onFile(this); });
|
|
608
612
|
filled.querySelector('.upops input[type=file]').addEventListener('change',function(){ onFile(this); });
|
|
609
|
-
filled.querySelector('.updel').addEventListener('click',toEmpty);
|
|
613
|
+
filled.querySelector('.updel').addEventListener('click',function(){ toEmpty(); saveApplyDraft(); });
|
|
610
614
|
}
|
|
611
615
|
mountApplyUpload('a_up_license','license_media_id','营业执照照片 (必传)');
|
|
612
616
|
mountApplyUpload('a_up_idfront','legal_id_front_media_id','身份证人像面 (必传)');
|
|
@@ -643,7 +647,7 @@ function applyFormBody(){
|
|
|
643
647
|
var body={
|
|
644
648
|
subject_type:aval('a_subject_type'),merchant_shortname:aval('a_shortname'),merchant_name:aval('a_merchant_name'),
|
|
645
649
|
license_number:aval('a_license_number'),service_phone:aval('a_service_phone'),
|
|
646
|
-
|
|
650
|
+
qualification_type:aval('a_settlement_id'), // 行业类目 MCC (settlement_id=结算规则由后端按主体推导, 前端不发)
|
|
647
651
|
contact_name:aval('a_contact_name'),contact_mobile:aval('a_contact_mobile'),contact_email:aval('a_contact_email'),
|
|
648
652
|
legal_name:aval('a_legal_name'),legal_id_number:aval('a_legal_id_number'),
|
|
649
653
|
legal_id_period_begin:aval('a_period_begin'),legal_id_period_end:aval('a_period_end'),
|
|
@@ -749,7 +753,7 @@ async function submitApplyment(){
|
|
|
749
753
|
clearTimeout(timer);
|
|
750
754
|
if(j.ok){
|
|
751
755
|
msg.style.display='block';
|
|
752
|
-
msg.textContent='✅ 已提交微信审核 (敏感字段已在本机加密)'+(j.idempotent?' — 此前已提交过, 幂等忽略':'');
|
|
756
|
+
msg.textContent='✅ 已提交微信审核 (敏感字段已在本机加密)'+(j.idempotent?' — 此前已提交过, 幂等忽略':'')+'; 填写内容已保留在本机, 修改后可直接重提';
|
|
753
757
|
document.getElementById('apply-form').style.display='none';
|
|
754
758
|
queryApplyment();
|
|
755
759
|
} else {
|
|
@@ -777,15 +781,75 @@ async function queryApplyment(){
|
|
|
777
781
|
try{
|
|
778
782
|
var j=await readJson(await fetch('/api/onboard/applyment/status'));
|
|
779
783
|
renderApplyment(j);
|
|
780
|
-
|
|
784
|
+
// 草稿生命周期: FINISHED (业务完成) → 清草稿, 不再恢复
|
|
785
|
+
if(j&&j.status==='APPLYMENT_STATE_FINISHED'){ clearApplyDraft(); return; }
|
|
786
|
+
// 恢复优先级: 本机草稿 (未提交编辑态, 含敏感明文) > form_snapshot (已提交脱敏快照) > 空表单
|
|
787
|
+
var d=await readJson(await fetch('/api/onboard/applyment/draft'));
|
|
788
|
+
if(d&&d.ok&&d.fields){ restoreApplyDraft(d.fields); return; }
|
|
789
|
+
if(j&&j.form_snapshot) prefillApplyForm(j.form_snapshot);
|
|
781
790
|
}catch(e){}
|
|
782
791
|
}
|
|
792
|
+
// ---- 本机自动草稿 (2026-10-03 xiaomu: 重进重填敏感字段) ----
|
|
793
|
+
// 隐私口径: 明文只存本机 sqlite (证件原件本来就在本机); 发出时照旧本机加密 (草稿不直接外发)。
|
|
794
|
+
var _draftRestored=false, _draftTimer=null;
|
|
795
|
+
function saveApplyDraft(){
|
|
796
|
+
clearTimeout(_draftTimer);
|
|
797
|
+
_draftTimer=setTimeout(async function(){ // 防抖 800ms
|
|
798
|
+
try{
|
|
799
|
+
await fetch('/api/onboard/applyment/draft',{method:'POST',headers:{'Content-Type':'application/json'},
|
|
800
|
+
body:JSON.stringify({fields:applyFormBody()})}); // 场景切换不丢: 按当前场景存全量字段
|
|
801
|
+
}catch(e){ /* 草稿失败静默, 不挡表单 */ }
|
|
802
|
+
},800);
|
|
803
|
+
}
|
|
804
|
+
async function clearApplyDraft(){
|
|
805
|
+
try{ await fetch('/api/onboard/applyment/draft',{method:'DELETE'}); }catch(e){}
|
|
806
|
+
}
|
|
807
|
+
function restoreApplyDraft(f){
|
|
808
|
+
if(_draftRestored||!f||typeof f!=='object') return;
|
|
809
|
+
_draftRestored=true;
|
|
810
|
+
// 场景先切 (setScene 重挂场景字段/上传槽)
|
|
811
|
+
if(f.sales_scene){
|
|
812
|
+
var r=document.querySelector('input[name="sales_scene"][value="'+f.sales_scene+'"]');
|
|
813
|
+
if(r){r.checked=true;setScene(f.sales_scene);}
|
|
814
|
+
}
|
|
815
|
+
// 主体联动先触发 (居住地址/UBO 显示 + 结算规则/类目默认), 再写草稿值
|
|
816
|
+
var subj=document.getElementById('a_subject_type');
|
|
817
|
+
if(subj&&f.subject_type){subj.value=f.subject_type;subj.dispatchEvent(new Event('change'));}
|
|
818
|
+
for(var k in PREFILL_MAP){
|
|
819
|
+
if(k==='subject_type') continue;
|
|
820
|
+
var v=f[k];
|
|
821
|
+
if(!v||String(v).indexOf('***')===0) continue;
|
|
822
|
+
var e=document.getElementById(PREFILL_MAP[k]);
|
|
823
|
+
if(e&&!e.value) e.value=v;
|
|
824
|
+
}
|
|
825
|
+
// 照片 media_id 复用 → 槽位已传双态
|
|
826
|
+
PREFILL_MEDIA.forEach(function(k){
|
|
827
|
+
if(f[k]&&_aSlots[k]) _aSlots[k].prefill(f[k]);
|
|
828
|
+
else if(f[k]) _aMedia[k]=f[k];
|
|
829
|
+
});
|
|
830
|
+
var msg=document.getElementById('apply-msg');
|
|
831
|
+
msg.style.display='block';
|
|
832
|
+
msg.textContent='ℹ️ 已恢复上次填写 (自动保存于本机)';
|
|
833
|
+
// 草稿恢复了编辑态 → 展开表单
|
|
834
|
+
document.getElementById('apply-form').style.display='block';
|
|
835
|
+
}
|
|
836
|
+
// 任何字段 input/change → 防抖自动保存 (含场景切换时的字段集; file 选择也走 change)
|
|
837
|
+
(function(){
|
|
838
|
+
var form=document.getElementById('apply-form');
|
|
839
|
+
if(form){
|
|
840
|
+
form.addEventListener('input',saveApplyDraft);
|
|
841
|
+
form.addEventListener('change',saveApplyDraft);
|
|
842
|
+
}
|
|
843
|
+
})();
|
|
783
844
|
// F1 预填: 台账快照回填非敏感字段 + 照片 media_id 复用 (敏感字段是密文/脱敏, 需重填)
|
|
784
845
|
var PREFILL_MAP={subject_type:'a_subject_type',merchant_shortname:'a_shortname',merchant_name:'a_merchant_name',
|
|
785
|
-
license_number:'a_license_number',service_phone:'a_service_phone',
|
|
786
|
-
|
|
846
|
+
license_number:'a_license_number',service_phone:'a_service_phone',
|
|
847
|
+
contact_name:'a_contact_name',contact_mobile:'a_contact_mobile',contact_email:'a_contact_email',
|
|
848
|
+
legal_name:'a_legal_name',legal_id_number:'a_legal_id_number',legal_id_address:'a_legal_id_address',
|
|
787
849
|
legal_id_period_begin:'a_period_begin',legal_id_period_end:'a_period_end',
|
|
788
|
-
account_type:'a_account_type',
|
|
850
|
+
account_type:'a_account_type',account_name:'a_account_name',account_number:'a_account_number',
|
|
851
|
+
account_bank:'a_account_bank',bank_name:'a_bank_name',bank_address_code:'a_bank_address_code',
|
|
852
|
+
qualification_type:'a_settlement_id', // 行业类目 MCC (draft 路径用; snapshot 路径走 mcc 兼容段)
|
|
789
853
|
store_name:'a_store_name',store_address_code:'a_store_address_code',store_address:'a_store_address',
|
|
790
854
|
website_url:'a_website_url',appid:'a_appid',appid_type:'a_appid_type'};
|
|
791
855
|
var PREFILL_MEDIA=['license_media_id','legal_id_front_media_id','legal_id_back_media_id',
|
|
@@ -810,6 +874,16 @@ function prefillApplyForm(snap){
|
|
|
810
874
|
var e=document.getElementById(PREFILL_MAP[k]);
|
|
811
875
|
if(e&&!e.value) e.value=v;
|
|
812
876
|
}
|
|
877
|
+
// 行业类目 (MCC) 预填: 新口径 qualification_type=MCC; 旧台账曾把 MCC 误存进 settlement_id
|
|
878
|
+
// → 兼容回填 (旧值若是 4 位 MCC 而非 716/719/727, 填回"行业类目"框; 结算规则按主体重新推导)
|
|
879
|
+
var mcc=snap.qualification_type;
|
|
880
|
+
if((!mcc||mcc==='无'||mcc==='零售') && snap.settlement_id && ['716','719','727'].indexOf(String(snap.settlement_id))<0){
|
|
881
|
+
mcc=snap.settlement_id;
|
|
882
|
+
}
|
|
883
|
+
if(mcc && mcc!=='无' && String(mcc).indexOf('***')!==0){
|
|
884
|
+
var se=document.getElementById('a_settlement_id');
|
|
885
|
+
if(se&&!se.value) se.value=mcc; // 不在码表 (手填值) 时 select 置空, 商户重选/手填
|
|
886
|
+
}
|
|
813
887
|
// 照片复用: media_id 还在微信侧有效 → 槽位走"已上传"双态 (占位图+替换/删除, 无第二个空框)
|
|
814
888
|
var reused=0;
|
|
815
889
|
PREFILL_MEDIA.forEach(function(k){
|
|
@@ -971,9 +1045,18 @@ async function restoreIdentity(){
|
|
|
971
1045
|
// 手动输入接管 (沿用 id 移交逻辑)
|
|
972
1046
|
linkSelectManual('a_settlement_id','a_settlement_manual','__MANUAL__');
|
|
973
1047
|
}
|
|
1048
|
+
// 结算规则只读展示 (settlement_id 按主体推导: 企业 716 / 个体户 719 / 其他组织 727; 商户不用选)
|
|
1049
|
+
var SETTLE_RULES={SUBJECT_TYPE_INDIVIDUAL:'719 · 个体户通用(0.6% T+1)',
|
|
1050
|
+
SUBJECT_TYPE_ENTERPRISE:'716 · 企业通用(0.6% T+1)',SUBJECT_TYPE_OTHERS:'727 · 其他组织通用(0.6% T+1)'};
|
|
1051
|
+
function paintSettleRule(){
|
|
1052
|
+
var el=document.getElementById('a_settlement_rule');
|
|
1053
|
+
if(el&&subj) el.textContent=SETTLE_RULES[subj.value]||SETTLE_RULES.SUBJECT_TYPE_INDIVIDUAL;
|
|
1054
|
+
}
|
|
1055
|
+
paintSettleRule();
|
|
974
1056
|
if(subj&&stl){
|
|
975
1057
|
subj.addEventListener('change',function(){
|
|
976
1058
|
stl.value = subj.value==='SUBJECT_TYPE_ENTERPRISE' ? '5311' : '5331';
|
|
1059
|
+
paintSettleRule();
|
|
977
1060
|
// F4: 企业主体显示身份证居住地址(必填) + UBO 说明; 个体户隐藏
|
|
978
1061
|
var ent = subj.value==='SUBJECT_TYPE_ENTERPRISE';
|
|
979
1062
|
var ia=document.getElementById('afield_idaddr'); if(ia) ia.style.display=ent?'':'none';
|
package/routes/onboard.js
CHANGED
|
@@ -606,15 +606,17 @@ function createOnboardRouter(deps) {
|
|
|
606
606
|
account_name: "开户名称", account_number: "银行账号", account_bank: "开户银行",
|
|
607
607
|
store_name: "门店名称", store_address_code: "门店省市编码", store_address: "门店详细地址",
|
|
608
608
|
website_url: "网站网址", appid: "公众号/小程序 AppID",
|
|
609
|
-
service_phone: "客服电话",
|
|
609
|
+
service_phone: "客服电话", qualification_type: "行业类目 (MCC 编码)",
|
|
610
610
|
license_media_id: "营业执照照片", legal_id_front_media_id: "身份证人像面", legal_id_back_media_id: "身份证国徽面",
|
|
611
611
|
store_entrance_media_id: "门店门头照片", store_indoor_media_id: "门店内部照片",
|
|
612
612
|
web_home_media_id: "网站首页截图", web_product_media_id: "商品/服务页截图", mp_media_id: "公众号/小程序页面截图",
|
|
613
613
|
};
|
|
614
|
-
// 通用必填
|
|
614
|
+
// 通用必填 — 字段语义最终修正 (两次微信实测错误交叉锁定):
|
|
615
|
+
// settlement_id = 结算规则 ID (716/719/727, 后端按主体推导, 不依赖前端);
|
|
616
|
+
// qualification_type = 行业类目编码 MCC (表单"行业类目"下拉值, 必填 — "无"不是合法行业编码)
|
|
615
617
|
const APPLY_REQUIRED_COMMON = ["contact_name", "contact_mobile", "contact_email", "merchant_shortname", "merchant_name", "license_number",
|
|
616
618
|
"legal_name", "legal_id_number", "legal_id_period_begin", "legal_id_period_end",
|
|
617
|
-
"account_name", "account_number", "account_bank", "service_phone", "
|
|
619
|
+
"account_name", "account_number", "account_bank", "service_phone", "qualification_type"];
|
|
618
620
|
// 场景化必填 (2026-10-03 xiaomu 拍板三场景: 线下门店 / 互联网网站 / 公众号·小程序)
|
|
619
621
|
// F2 fix: WEB 场景微信端只有 web_info.domain 无 pics — 截图降为可选 (收集进台账备用, 不进微信 body)
|
|
620
622
|
const APPLY_SCENE_CFG = {
|
|
@@ -697,6 +699,55 @@ CREATE TABLE IF NOT EXISTS onboard_applyments (
|
|
|
697
699
|
}
|
|
698
700
|
});
|
|
699
701
|
|
|
702
|
+
// ---------- 进件表单本机草稿 (2026-10-03 xiaomu 实测: 每次重进重填敏感字段) ----------
|
|
703
|
+
// 隐私口径: 商户资料只存本机 (证件原件本来就在本机 materials 目录), 本机明文存草稿合理;
|
|
704
|
+
// 发出时照旧本机加密 (加密边界不变, 草稿明文永不直接外发)。
|
|
705
|
+
db.exec(`
|
|
706
|
+
CREATE TABLE IF NOT EXISTS onboard_draft (
|
|
707
|
+
id INTEGER PRIMARY KEY CHECK(id=1), -- 单行 (一台机器一个卖家)
|
|
708
|
+
fields TEXT NOT NULL DEFAULT '{}', -- 整表单字段 JSON (含敏感字段明文, 刻意)
|
|
709
|
+
updated_at INTEGER NOT NULL
|
|
710
|
+
);
|
|
711
|
+
`);
|
|
712
|
+
/** POST /applyment/draft {fields:{...}} — 保存草稿 (单行 upsert; 敏感字段明文是本机刻意口径) */
|
|
713
|
+
router.post("/applyment/draft", (req, res) => {
|
|
714
|
+
try {
|
|
715
|
+
const b = req.body || {};
|
|
716
|
+
if (!b.fields || typeof b.fields !== "object" || Array.isArray(b.fields)) {
|
|
717
|
+
return res.status(400).json({ ok: false, error: "invalid_param", message: "fields 必填且为对象" });
|
|
718
|
+
}
|
|
719
|
+
const raw = JSON.stringify(b.fields);
|
|
720
|
+
if (raw.length > 200 * 1024) {
|
|
721
|
+
return res.status(413).json({ ok: false, error: "draft_too_large", message: "草稿超过 200KB" });
|
|
722
|
+
}
|
|
723
|
+
const now = Math.floor(Date.now() / 1000);
|
|
724
|
+
db.prepare("INSERT INTO onboard_draft (id, fields, updated_at) VALUES (1, ?, ?) ON CONFLICT(id) DO UPDATE SET fields=excluded.fields, updated_at=excluded.updated_at")
|
|
725
|
+
.run(raw, now);
|
|
726
|
+
res.json({ ok: true, updated_at: now });
|
|
727
|
+
} catch (e) {
|
|
728
|
+
console.error("[onboard] draft save error:", e && e.message);
|
|
729
|
+
res.status(500).json({ ok: false, error: "internal", message: String(e && e.message).slice(0, 200) });
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
/** GET /applyment/draft — 读草稿 (无缓存; 没有草稿返回 fields:null) */
|
|
733
|
+
router.get("/applyment/draft", (req, res) => {
|
|
734
|
+
try {
|
|
735
|
+
const row = db.prepare("SELECT fields, updated_at FROM onboard_draft WHERE id=1").get();
|
|
736
|
+
res.json({ ok: true, fields: row ? JSON.parse(row.fields) : null, updated_at: row ? row.updated_at : null });
|
|
737
|
+
} catch (e) {
|
|
738
|
+
res.status(500).json({ ok: false, error: "internal", message: String(e && e.message).slice(0, 200) });
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
/** DELETE /applyment/draft — 清除草稿 (FINISHED 后业务完成; 或商户想重来) */
|
|
742
|
+
router.delete("/applyment/draft", (req, res) => {
|
|
743
|
+
try {
|
|
744
|
+
db.prepare("DELETE FROM onboard_draft WHERE id=1").run();
|
|
745
|
+
res.json({ ok: true });
|
|
746
|
+
} catch (e) {
|
|
747
|
+
res.status(500).json({ ok: false, error: "internal", message: String(e && e.message).slice(0, 200) });
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
|
|
700
751
|
/** POST /applyment/upload — 证件照片转发 SP 网关 media/upload (base64 JSON 同款) */
|
|
701
752
|
router.post("/applyment/upload", async (req, res) => {
|
|
702
753
|
try {
|
|
@@ -765,7 +816,9 @@ CREATE TABLE IF NOT EXISTS onboard_applyments (
|
|
|
765
816
|
}
|
|
766
817
|
payload.sales_scene = scene; // 场景必带 (SP 侧按它组装 biz_store_info/web_info/mp_info)
|
|
767
818
|
// 通用类目资质留空 → 默认 "无" (微信通用场景接受, 避免留空被 SP 拒)
|
|
768
|
-
|
|
819
|
+
// settlement_id = 结算规则 ID, 按主体推导 (商户不用选; 前端不传也兜底)
|
|
820
|
+
payload.settlement_id = payload.subject_type === "SUBJECT_TYPE_ENTERPRISE" ? "716"
|
|
821
|
+
: (payload.subject_type === "SUBJECT_TYPE_OTHERS" ? "727" : "719");
|
|
769
822
|
// M2 断点 c: tb_address 贯通 — SP 台账落列, FINISHED 后 oracle 引导金才知道发给谁
|
|
770
823
|
const identity = await ensureSellerIdentity(basePaths.baseDir());
|
|
771
824
|
payload.tb_address = identity.address;
|
package/routes/sp-applyment.js
CHANGED
|
@@ -241,9 +241,11 @@ function buildApplymentBody(form, enc, opts) {
|
|
|
241
241
|
sales_info: salesInfo,
|
|
242
242
|
},
|
|
243
243
|
settlement_info: {
|
|
244
|
-
|
|
245
|
-
//
|
|
246
|
-
qualification_type
|
|
244
|
+
// 字段语义最终修正 (两次微信实测错误交叉锁定):
|
|
245
|
+
// settlement_id = 结算规则 ID (716 企业 / 719 个体户 / 727 其他组织, 卖家侧按主体推导)
|
|
246
|
+
// qualification_type = 行业类目编码 MCC (5311 百货商店等; "无"不是合法行业编码)
|
|
247
|
+
settlement_id: reqStr(f.settlement_id, "结算规则 ID", 16),
|
|
248
|
+
qualification_type: reqStr(f.qualification_type, "行业类目编码(MCC)", 128),
|
|
247
249
|
},
|
|
248
250
|
bank_account_info: {
|
|
249
251
|
bank_account_type: f.account_type === "BANK_ACCOUNT_TYPE_PERSONAL" ? "BANK_ACCOUNT_TYPE_PERSONAL" : "BANK_ACCOUNT_TYPE_CORPORATE",
|
|
@@ -111,7 +111,7 @@ async function fillForm(base) {
|
|
|
111
111
|
account_type: "BANK_ACCOUNT_TYPE_PERSONAL",
|
|
112
112
|
account_name: "张三", account_number: PLAINTEXT.card, account_bank: "招商银行",
|
|
113
113
|
store_name: "张三餐饮店南山店", store_address_code: "440300", store_address: "南山区xx路1号",
|
|
114
|
-
service_phone: "0755-12345678",
|
|
114
|
+
service_phone: "0755-12345678", qualification_type: "5331", // 行业类目 MCC (settlement_id 结算规则由后端按主体推导)
|
|
115
115
|
...media,
|
|
116
116
|
};
|
|
117
117
|
}
|
|
@@ -291,7 +291,11 @@ test("onboard 页面含进件表单 (btn-apply-submit) 且第③步状态机元
|
|
|
291
291
|
assert.ok(html.includes("百货商店") && html.includes("就餐场所和餐馆"), "MCC 名称");
|
|
292
292
|
assert.ok(!html.includes('value="719"') && !html.includes('value="716"') && !html.includes('value="727"'), "V2 错误编码已清除");
|
|
293
293
|
assert.ok(html.includes("a_settlement_filter"), "类目搜索过滤");
|
|
294
|
-
assert.ok(html.includes("a_settlement_manual") && html.includes("__MANUAL__"), "手动输入接管保留");
|
|
294
|
+
assert.ok(html.includes("a_settlement_manual") && html.includes("__MANUAL__"), "手动输入接管保留");
|
|
295
|
+
// 2026-10-03 字段语义最终修正: 双字段 (结算规则只读展示 + 行业类目 MCC 下拉)
|
|
296
|
+
assert.ok(html.includes("a_settlement_rule"), "结算规则只读展示框");
|
|
297
|
+
assert.ok(html.includes("结算规则") && html.includes("行业类目"), "双字段标签");
|
|
298
|
+
assert.ok(!html.includes("a_qualification_type"), "旧资质描述字段已移除");
|
|
295
299
|
assert.ok(html.includes("maybeAutoKyc"), "M2: 进件受理后自动 KYC 提交");
|
|
296
300
|
assert.ok(!html.includes("btn-verify"), "手动实名按钮已删 (自动触发)");
|
|
297
301
|
} finally { srv.close(); }
|
|
@@ -341,18 +345,52 @@ test("错误提示: 缺 store_address → 中文 message + field 定位 (2026-10
|
|
|
341
345
|
} finally { srv.close(); }
|
|
342
346
|
});
|
|
343
347
|
|
|
344
|
-
test("
|
|
348
|
+
test("字段语义最终修正: settlement_id=结算规则按主体推导 + qualification_type=MCC 必填", async () => {
|
|
345
349
|
const { srv, base } = await startSeller();
|
|
346
350
|
try {
|
|
351
|
+
// 行业类目缺失 → 400 (两次微信实测: "无"不是合法行业编码)
|
|
347
352
|
const form = await fillForm(base);
|
|
348
353
|
delete form.qualification_type;
|
|
354
|
+
let r = await fetch(base + "/api/onboard/applyment", {
|
|
355
|
+
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(form),
|
|
356
|
+
});
|
|
357
|
+
let j = await r.json();
|
|
358
|
+
assert.strictEqual(r.status, 400);
|
|
359
|
+
assert.strictEqual(j.field, "qualification_type");
|
|
360
|
+
assert.ok(j.message.includes("行业类目"), j.message);
|
|
361
|
+
// 个体户 → settlement_id=719 (前端没传, 后端推导) + qualification_type=5331 透传
|
|
362
|
+
const before = spCapture.submits.length;
|
|
363
|
+
r = await fetch(base + "/api/onboard/applyment", {
|
|
364
|
+
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(form2ok(form) ),
|
|
365
|
+
});
|
|
366
|
+
function form2ok(f){ const c={...f, qualification_type:"5331"}; return c; }
|
|
367
|
+
j = await r.json();
|
|
368
|
+
assert.strictEqual(r.status, 200, JSON.stringify(j));
|
|
369
|
+
assert.strictEqual(spCapture.submits[before].settlement_id, "719", "个体户 → 结算规则 719 (后端推导)");
|
|
370
|
+
assert.strictEqual(spCapture.submits[before].qualification_type, "5331", "行业类目 MCC 透传");
|
|
371
|
+
} finally { srv.close(); }
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("字段语义: 企业主体 → settlement_id=716 + qualification_type=MCC (submit 组装断言)", async () => {
|
|
375
|
+
const { srv, base } = await startSeller();
|
|
376
|
+
try {
|
|
377
|
+
const form = await fillForm(base);
|
|
378
|
+
form.subject_type = "SUBJECT_TYPE_ENTERPRISE";
|
|
379
|
+
form.qualification_type = "5311";
|
|
380
|
+
form.legal_id_address = "广东省深圳市南山区xx路1号101室"; // 企业必填 (F4)
|
|
349
381
|
const before = spCapture.submits.length;
|
|
350
382
|
const r = await fetch(base + "/api/onboard/applyment", {
|
|
351
383
|
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(form),
|
|
352
384
|
});
|
|
353
385
|
const j = await r.json();
|
|
354
386
|
assert.strictEqual(r.status, 200, JSON.stringify(j));
|
|
355
|
-
|
|
387
|
+
const fwd = spCapture.submits[before];
|
|
388
|
+
assert.strictEqual(fwd.settlement_id, "716", "企业 → 结算规则 716");
|
|
389
|
+
assert.strictEqual(fwd.qualification_type, "5311", "行业类目 MCC");
|
|
390
|
+
// SP 侧组装: settlement_info 两字段正确分工
|
|
391
|
+
const wxBody = spApplymentLib.buildApplymentBody({ ...fwd, business_code: "TBTEST" }, (s) => s, { alreadyEncrypted: true });
|
|
392
|
+
assert.strictEqual(wxBody.settlement_info.settlement_id, "716");
|
|
393
|
+
assert.strictEqual(wxBody.settlement_info.qualification_type, "5311");
|
|
356
394
|
} finally { srv.close(); }
|
|
357
395
|
});
|
|
358
396
|
|
|
@@ -581,3 +619,55 @@ test("sp-info 代理: 全新实例 SP 不可达 → 200 降级 offline (不报
|
|
|
581
619
|
assert.ok(html.includes("可随时更换"), "迁移保障卖点文案");
|
|
582
620
|
} finally { srv.close(); process.env.SP_GATEWAY_URL = spBase; }
|
|
583
621
|
});
|
|
622
|
+
|
|
623
|
+
test("草稿: POST 保存 → GET 读取 (含敏感字段明文, 本机刻意口径) → DELETE 清除", async () => {
|
|
624
|
+
const { srv, base } = await startSeller();
|
|
625
|
+
try {
|
|
626
|
+
// 空草稿
|
|
627
|
+
let j = await (await fetch(base + "/api/onboard/applyment/draft")).json();
|
|
628
|
+
assert.strictEqual(j.fields, null);
|
|
629
|
+
// 保存 (含敏感字段明文 — 本机=商户自己电脑, 刻意)
|
|
630
|
+
const fields = { contact_name: "张三", legal_id_number: "440300199001011234", account_number: "6225880212345678", qualification_type: "5311", sales_scene: "SALES_SCENES_STORE" };
|
|
631
|
+
let r = await fetch(base + "/api/onboard/applyment/draft", {
|
|
632
|
+
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fields }),
|
|
633
|
+
});
|
|
634
|
+
assert.strictEqual(r.status, 200);
|
|
635
|
+
// 读回 (无缓存, upsert 覆盖)
|
|
636
|
+
j = await (await fetch(base + "/api/onboard/applyment/draft")).json();
|
|
637
|
+
assert.deepStrictEqual(j.fields.legal_id_number, "440300199001011234");
|
|
638
|
+
assert.ok(j.updated_at > 0);
|
|
639
|
+
// upsert 覆盖 (单行)
|
|
640
|
+
await fetch(base + "/api/onboard/applyment/draft", {
|
|
641
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
642
|
+
body: JSON.stringify({ fields: { contact_name: "李四" } }),
|
|
643
|
+
});
|
|
644
|
+
j = await (await fetch(base + "/api/onboard/applyment/draft")).json();
|
|
645
|
+
assert.strictEqual(j.fields.contact_name, "李四");
|
|
646
|
+
assert.strictEqual(j.fields.legal_id_number, undefined, "upsert 整行覆盖");
|
|
647
|
+
// 删除
|
|
648
|
+
r = await fetch(base + "/api/onboard/applyment/draft", { method: "DELETE" });
|
|
649
|
+
assert.strictEqual((await r.json()).ok, true);
|
|
650
|
+
j = await (await fetch(base + "/api/onboard/applyment/draft")).json();
|
|
651
|
+
assert.strictEqual(j.fields, null, "删除后为空");
|
|
652
|
+
// 参数校验
|
|
653
|
+
r = await fetch(base + "/api/onboard/applyment/draft", {
|
|
654
|
+
method: "POST", headers: { "Content-Type": "application/json" }, body: "{}",
|
|
655
|
+
});
|
|
656
|
+
assert.strictEqual(r.status, 400);
|
|
657
|
+
} finally { srv.close(); }
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test("草稿页面逻辑: 自动保存/恢复优先级/FINISHED 清除/提交保留 元素齐", async () => {
|
|
661
|
+
const { srv, base } = await startSeller();
|
|
662
|
+
try {
|
|
663
|
+
const html = await (await fetch(base + "/onboard/")).text();
|
|
664
|
+
assert.ok(html.includes("/api/onboard/applyment/draft"), "草稿端点调用");
|
|
665
|
+
assert.ok(html.includes("saveApplyDraft") && html.includes("800"), "防抖 800ms 自动保存");
|
|
666
|
+
assert.ok(html.includes("restoreApplyDraft"), "草稿恢复函数");
|
|
667
|
+
assert.ok(html.includes("clearApplyDraft"), "FINISHED 清除草稿");
|
|
668
|
+
assert.ok(html.includes("已恢复上次填写"), "恢复提示文案");
|
|
669
|
+
assert.ok(html.includes("修改后可直接重提"), "提交后保留草稿提示");
|
|
670
|
+
// 恢复优先级注释: 草稿 > form_snapshot > 空表单
|
|
671
|
+
assert.ok(html.indexOf("applyment/draft") < html.indexOf("form_snapshot") || html.includes("恢复优先级"), "草稿优先于快照");
|
|
672
|
+
} finally { srv.close(); }
|
|
673
|
+
});
|