trustbase-seller-backend 0.3.2 → 0.3.4

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.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "TrustBase Seller Backend (merchant edition) — 商品上链/订单/支付/质押 write API for TrustBase 保障链生态. 单卖家自托管形态: 签名 key 即卖家身份, 私钥不出门.",
5
5
  "main": "run-unified.js",
6
6
  "bin": {
@@ -201,13 +201,10 @@ img.auprev{display:none;width:100%;max-height:140px;object-fit:cover;border-radi
201
201
  <div class="afield"><label for="a_service_phone">客服电话</label><input id="a_service_phone" placeholder="交易记录向买家展示"></div>
202
202
  <div class="afield"><label>结算规则 (按主体自动确定, 不用选)</label>
203
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>
205
- <input id="a_settlement_filter" placeholder="搜索: 编码或名称 (如 5732 / 电子)" style="margin-bottom:6px">
206
- <select id="a_settlement_id">
207
- <option value="">请选择行业类目</option>
208
- <option value="__MANUAL__">手动输入编码</option>
209
- </select>
210
- <input id="a_settlement_manual" class="amanual" placeholder="4 位 MCC 编码, 如: 5311" style="display:none"></div>
204
+ <div class="afield"><label for="a_industry_cat">所属行业·大类</label>
205
+ <select id="a_industry_cat"></select></div>
206
+ <div class="afield"><label for="a_settlement_id">所属行业·子类 (微信行业名称)</label>
207
+ <select id="a_settlement_id"></select></div>
211
208
  </div>
212
209
  </div>
213
210
 
@@ -218,7 +215,9 @@ img.auprev{display:none;width:100%;max-height:140px;object-fit:cover;border-radi
218
215
  <div class="afield"><label for="a_legal_id_number">法人身份证号</label><input id="a_legal_id_number" placeholder="17 位数字 + 数字或 X"></div>
219
216
  <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>
220
217
  <div class="afield afield-full" id="afield_ubo_note" style="display:none">
221
- <div class="ahint" style="margin-top:0">企业主体:若最终受益人只有法定代表人本人,无需填写受益人信息 (微信自动回填);否则暂不支持,请联系服务商协助进件</div>
218
+ <label for="a_beneficiary">最终受益人</label>
219
+ <select id="a_beneficiary"><option value="legal">法定代表人本人 (默认, 微信自动回填受益人信息)</option><option value="other">他人</option></select>
220
+ <div class="ahint" id="a_beneficiary_hint" style="display:none;margin-top:8px">⚠️ 暂不支持受益人为他人的进件, 请选"法定代表人本人"或联系服务商协助</div>
222
221
  </div>
223
222
  <div class="afield"><label for="a_period_begin">身份证有效期开始</label><input id="a_period_begin" placeholder="YYYY-MM-DD"></div>
224
223
  <div class="afield"><label for="a_period_end">有效期结束</label><input id="a_period_end" placeholder="YYYY-MM-DD 或 长期"></div>
@@ -822,6 +821,8 @@ function restoreApplyDraft(f){
822
821
  var e=document.getElementById(PREFILL_MAP[k]);
823
822
  if(e&&!e.value) e.value=v;
824
823
  }
824
+ // 所属行业 (二级联动定位)
825
+ syncIndustrySelection(f.qualification_type);
825
826
  // 照片 media_id 复用 → 槽位已传双态
826
827
  PREFILL_MEDIA.forEach(function(k){
827
828
  if(f[k]&&_aSlots[k]) _aSlots[k].prefill(f[k]);
@@ -833,6 +834,18 @@ function restoreApplyDraft(f){
833
834
  // 草稿恢复了编辑态 → 展开表单
834
835
  document.getElementById('apply-form').style.display='block';
835
836
  }
837
+ // 所属行业二级联动选中 (预填共用): 合法行业名称 → 定位大类 + 选中子类; 非法/旧 MCC → 保持默认重选
838
+ function syncIndustrySelection(ind){
839
+ if(!ind||String(ind).indexOf('***')===0||/^\d+$/.test(String(ind))||ind==='无') return;
840
+ if(typeof industrySubsFor!=='function') return; // 增强脚本未就绪 (理论不到这)
841
+ var type2=(document.getElementById('a_subject_type')||{}).value||'SUBJECT_TYPE_INDIVIDUAL';
842
+ var hit=industrySubsFor(type2).filter(function(s){return s.n===ind;})[0];
843
+ if(!hit) return;
844
+ var ce=document.getElementById('a_industry_cat');
845
+ if(ce){ ce.value=hit.cat; if(typeof renderIndustrySubs==='function') renderIndustrySubs(false); }
846
+ var se=document.getElementById('a_settlement_id');
847
+ if(se) se.value=ind;
848
+ }
836
849
  // 任何字段 input/change → 防抖自动保存 (含场景切换时的字段集; file 选择也走 change)
837
850
  (function(){
838
851
  var form=document.getElementById('apply-form');
@@ -849,7 +862,6 @@ var PREFILL_MAP={subject_type:'a_subject_type',merchant_shortname:'a_shortname',
849
862
  legal_id_period_begin:'a_period_begin',legal_id_period_end:'a_period_end',
850
863
  account_type:'a_account_type',account_name:'a_account_name',account_number:'a_account_number',
851
864
  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 兼容段)
853
865
  store_name:'a_store_name',store_address_code:'a_store_address_code',store_address:'a_store_address',
854
866
  website_url:'a_website_url',appid:'a_appid',appid_type:'a_appid_type'};
855
867
  var PREFILL_MEDIA=['license_media_id','legal_id_front_media_id','legal_id_back_media_id',
@@ -874,16 +886,9 @@ function prefillApplyForm(snap){
874
886
  var e=document.getElementById(PREFILL_MAP[k]);
875
887
  if(e&&!e.value) e.value=v;
876
888
  }
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
- }
889
+ // 所属行业预填 (二级联动): 合法行业名称 → 定位所属大类并选中子类;
890
+ // 旧快照 MCC 数字/非法值 → 保持默认让商户重选 (静默不报错)
891
+ syncIndustrySelection(snap.qualification_type);
887
892
  // 照片复用: media_id 还在微信侧有效 → 槽位走"已上传"双态 (占位图+替换/删除, 无第二个空框)
888
893
  var reused=0;
889
894
  PREFILL_MEDIA.forEach(function(k){
@@ -1006,45 +1011,73 @@ async function restoreIdentity(){
1006
1011
  ① 编码字段下拉+联动 ② 状态卡片着色 + 签约二维码占位。
1007
1012
  (证件缩略图预览已于二重框修复时并入主脚本 mountApplyUpload 槽位双态) ===== */
1008
1013
  (function(){
1009
- // ⓪ 编码字段下拉 (2026-10-03 xiaomu: 手填编码易被微信秒拒 → 下拉)
1010
- // 经营类目 ↔ 主体类型联动 — MCC 真实编码 (V3 进件 settlement_id 用《商户行业编码》4 位 MCC,
1011
- // 之前 716/719/727 是 V2 结算规则 ID, 被微信拒 "所属行业不存在"。编码主体通用,
1012
- // 联动只影响默认选中: 企业→5311 百货, 个体户→5331 杂货便利店)
1013
- var MCC=[['5311','百货商店'],['5331','各类杂货店便利店'],['5411','大型仓储式超级市场'],
1014
- ['5611','男装店'],['5621','妇女成衣店'],['5641','婴儿儿童服装店'],['5651','家庭服装商店'],
1015
- ['5655','运动服饰商店'],['5661','鞋店'],['5691','成人成衣店'],['5699','各类服装及饰物店'],
1016
- ['5712','家具家用设备零售商'],['5722','家用电器商店'],['5732','电子设备商店'],['5734','计算机软件商店'],
1017
- ['5921','瓶装酒零售店'],['5942','书店'],['5943','文具办公用品商店'],['5944','珠宝钟表银器店'],
1018
- ['5945','玩具游戏店'],['5948','箱包皮具店'],['5949','纺织品针织品零售'],['5970','工艺美术商店'],
1019
- ['5977','化妆品商店'],['5995','宠物商店及用品'],['5999','其他专门零售店'],
1020
- ['5812','就餐场所和餐馆'],['5814','快餐店'],['5499','各类食品店'],['5441','糖果坚果商店'],
1021
- ['5451','乳制品冷饮店'],['5462','面包房糕点商店'],['5422','各类肉类零售商'],['5912','药店药房'],
1022
- ['4816','计算机网络信息服务'],['7011','住宿服务'],['4722','旅行社'],['4733','票务'],
1023
- ['7230','美容理发店'],['7298','保健美容SPA'],['7295','家政服务'],['7699','各类维修店'],
1024
- ['8249','贸易职业学校(职业技能培训)'],['8299','其他学校和教育服务']];
1014
+ // ⓪ 所属行业二级联动 (2026-10-03 微信官方网页版实证校准):
1015
+ // qualification_type = 二级子类的名称文本 (如 "零售"), 大类只做导航不上送;
1016
+ // 行业结构 大类>子类, 按主体过滤 (企业全套, 个体户/其他组织子集 — 对照 716/719/727 行业子项);
1017
+ // "所属行业不存在"30+ 次失败的真根因是"服务商不能给自己进件"(业务规则), 行业值 "零售" 一直合法。
1018
+ var INDUSTRY_CATS=[
1019
+ {cat:'餐饮/零售', subs:[
1020
+ {n:'餐饮',lic:1},{n:'零售'},{n:'电商平台',ent:1},{n:'食品生鲜',lic:1},{n:'服饰鞋包'},
1021
+ {n:'文物经营/文物复制品销售',lic:1,ent:1}]},
1022
+ {cat:'交通/加油', subs:[
1023
+ {n:'机票/机票代理',lic:1},{n:'旅行社',lic:1,ent:1},{n:'景区酒店',ent:1}]},
1024
+ {cat:'生活/娱乐', subs:[
1025
+ {n:'宠物医院',lic:1},{n:'共享服务',lic:1,ent:1},{n:'休闲娱乐旅游服务',ent:1},
1026
+ {n:'游艺厅/KTV/网吧',lic:1},{n:'院线影城',lic:1,ent:1},{n:'演出赛事',lic:1,ent:1},
1027
+ {n:'居民生活服务',ent:1},{n:'保健美容SPA',lic:1},{n:'美容理发店'}]},
1028
+ {cat:'互联网服务', subs:[
1029
+ {n:'门户论坛/网络广告及推广/软件开发/其他互联网服务'},{n:'电信运营商/宽带收费',lic:1,ent:1}]},
1030
+ {cat:'房产/金融', subs:[
1031
+ {n:'房地产/房产中介',lic:1,ent:1}]},
1032
+ {cat:'医疗/教育', subs:[
1033
+ {n:'私立/民营医院/诊所',lic:1},{n:'保健器械/医疗器械/非处方药品',lic:1},{n:'教育/培训/考试缴费/学费',lic:1}]},
1034
+ {cat:'其他', subs:[
1035
+ {n:'宗教组织'},{n:'拍卖典当',lic:1,ent:1},{n:'咨询/娱乐票务/其他'}]}
1036
+ ];
1037
+ // 其他组织 (727) 子集 (对照表口径)
1038
+ var INDUSTRY_OTHERS_ALLOW=['宗教组织','机票/机票代理','私立/民营医院/诊所','咨询/娱乐票务/其他'];
1039
+ var INDUSTRY_DEFAULT_SUB={SUBJECT_TYPE_ENTERPRISE:'零售',SUBJECT_TYPE_INDIVIDUAL:'零售',SUBJECT_TYPE_OTHERS:'咨询/娱乐票务/其他'};
1025
1040
  var subj=document.getElementById('a_subject_type');
1026
- var stl=document.getElementById('a_settlement_id');
1027
- if(stl){
1028
- // 填充 MCC 选项 (手动输入项保持在最后)
1029
- var manualOpt=stl.querySelector('option[value="__MANUAL__"]');
1030
- MCC.forEach(function(c){
1031
- var o=document.createElement('option');
1032
- o.value=c[0];o.textContent=c[0]+' · '+c[1];
1033
- stl.insertBefore(o,manualOpt);
1034
- });
1035
- stl.value='5311'; // 默认 百货商店 (综合电商最高频)
1036
- // 搜索过滤 (45 个选项手翻太长)
1037
- var filt=document.getElementById('a_settlement_filter');
1038
- if(filt) filt.addEventListener('input',function(){
1039
- var q=filt.value.trim().toLowerCase();
1040
- Array.prototype.forEach.call(stl.options,function(o){
1041
- if(o.value===''||o.value==='__MANUAL__') return;
1042
- o.hidden = !!(q && o.textContent.toLowerCase().indexOf(q)<0);
1041
+ var stl=document.getElementById('a_settlement_id'); // 子类下拉 (提交 id 稳定: value=子类名称 → qualification_type)
1042
+ var catEl=document.getElementById('a_industry_cat');
1043
+ function industrySubsFor(type){
1044
+ var out=[];
1045
+ INDUSTRY_CATS.forEach(function(c){
1046
+ c.subs.forEach(function(s){
1047
+ if(type==='SUBJECT_TYPE_ENTERPRISE'){ out.push({cat:c.cat,n:s.n,lic:s.lic}); }
1048
+ else if(type==='SUBJECT_TYPE_OTHERS'){ if(INDUSTRY_OTHERS_ALLOW.indexOf(s.n)>=0) out.push({cat:c.cat,n:s.n,lic:s.lic}); }
1049
+ else if(!s.ent){ out.push({cat:c.cat,n:s.n,lic:s.lic}); } // 个体户: 非"仅企业"子项
1043
1050
  });
1044
1051
  });
1045
- // 手动输入接管 (沿用 id 移交逻辑)
1046
- linkSelectManual('a_settlement_id','a_settlement_manual','__MANUAL__');
1052
+ return out;
1053
+ }
1054
+ function renderIndustryCats(keepSub){
1055
+ if(!catEl||!stl) return;
1056
+ var type=(subj&&subj.value)||'SUBJECT_TYPE_INDIVIDUAL';
1057
+ var subs=industrySubsFor(type);
1058
+ var cats=[]; subs.forEach(function(s){ if(cats.indexOf(s.cat)<0) cats.push(s.cat); });
1059
+ catEl.innerHTML='';
1060
+ cats.forEach(function(c){ var o=document.createElement('option'); o.value=c; o.textContent=c; catEl.appendChild(o); });
1061
+ catEl.value=cats.indexOf('餐饮/零售')>=0?'餐饮/零售':cats[0];
1062
+ renderIndustrySubs(keepSub);
1047
1063
  }
1064
+ function renderIndustrySubs(keepSub){
1065
+ var type=(subj&&subj.value)||'SUBJECT_TYPE_INDIVIDUAL';
1066
+ var cat=catEl.value;
1067
+ var subs=industrySubsFor(type).filter(function(s){ return s.cat===cat; });
1068
+ var cur=keepSub?stl.value:'';
1069
+ stl.innerHTML='';
1070
+ subs.forEach(function(s){
1071
+ var o=document.createElement('option');
1072
+ o.value=s.n; // 提交直接带子类名称文本 (qualification_type)
1073
+ o.textContent=s.n+(s.lic?' ⚠️需许可证':'');
1074
+ stl.appendChild(o);
1075
+ });
1076
+ var def=INDUSTRY_DEFAULT_SUB[type];
1077
+ stl.value=(subs.some(function(s){return s.n===cur;})&&cur)?cur
1078
+ :(subs.some(function(s){return s.n===def;})?def:(subs[0]?subs[0].n:''));
1079
+ }
1080
+ if(catEl){ catEl.addEventListener('change',function(){ renderIndustrySubs(false); }); }
1048
1081
  // 结算规则只读展示 (settlement_id 按主体推导: 企业 716 / 个体户 719 / 其他组织 727; 商户不用选)
1049
1082
  var SETTLE_RULES={SUBJECT_TYPE_INDIVIDUAL:'719 · 个体户通用(0.6% T+1)',
1050
1083
  SUBJECT_TYPE_ENTERPRISE:'716 · 企业通用(0.6% T+1)',SUBJECT_TYPE_OTHERS:'727 · 其他组织通用(0.6% T+1)'};
@@ -1055,14 +1088,44 @@ async function restoreIdentity(){
1055
1088
  paintSettleRule();
1056
1089
  if(subj&&stl){
1057
1090
  subj.addEventListener('change',function(){
1058
- stl.value = subj.value==='SUBJECT_TYPE_ENTERPRISE' ? '5311' : '5331';
1091
+ renderIndustryCats(false); // 主体切换 → 大类/子类选项集联动刷新 + 重选默认
1059
1092
  paintSettleRule();
1093
+ paintAccountLock(); // 修正2: 企业强制对公
1060
1094
  // F4: 企业主体显示身份证居住地址(必填) + UBO 说明; 个体户隐藏
1061
1095
  var ent = subj.value==='SUBJECT_TYPE_ENTERPRISE';
1062
1096
  var ia=document.getElementById('afield_idaddr'); if(ia) ia.style.display=ent?'':'none';
1063
1097
  var un=document.getElementById('afield_ubo_note'); if(un) un.style.display=ent?'':'none';
1064
1098
  });
1065
1099
  }
1100
+ renderIndustryCats(false); // 初始: 大类 餐饮/零售 > 子类 零售
1101
+
1102
+ // 修正2 (微信网页版实证): 企业主体强制对公账户 — 账户类型锁死对公, 户名=商户全称 (自动带出不可改)
1103
+ function paintAccountLock(){
1104
+ var ent = subj && subj.value==='SUBJECT_TYPE_ENTERPRISE';
1105
+ var at=document.getElementById('a_account_type');
1106
+ var an=document.getElementById('a_account_name');
1107
+ if(at){
1108
+ at.value = ent ? 'BANK_ACCOUNT_TYPE_CORPORATE' : at.value;
1109
+ at.disabled = !!ent;
1110
+ }
1111
+ if(an){
1112
+ if(ent){ an.value=document.getElementById('a_merchant_name').value; an.readOnly=true; }
1113
+ else an.readOnly=false;
1114
+ }
1115
+ }
1116
+ paintAccountLock();
1117
+ // 修正3 受益人 (网页版同款): 默认"法定代表人本人"(平台自动回填, 页面不重复填); 选"他人"只给提示
1118
+ var bf=document.getElementById('a_beneficiary');
1119
+ if(bf) bf.addEventListener('change',function(){
1120
+ var h=document.getElementById('a_beneficiary_hint');
1121
+ if(h) h.style.display=bf.value==='other'?'':'none';
1122
+ });
1123
+ var mnEl=document.getElementById('a_merchant_name');
1124
+ if(mnEl) mnEl.addEventListener('input',function(){
1125
+ if(subj&&subj.value==='SUBJECT_TYPE_ENTERPRISE'){
1126
+ var an=document.getElementById('a_account_name'); if(an) an.value=mnEl.value;
1127
+ }
1128
+ });
1066
1129
  // 省市编码选项 (国标区划码, 两处下拉共用; 微信官方对照表口径)
1067
1130
  var CITY_CODES=[['110000','北京市'],['120000','天津市'],['310000','上海市'],['500000','重庆市'],
1068
1131
  ['130100','石家庄'],['140100','太原'],['150100','呼和浩特'],['210100','沈阳'],['210200','大连'],
package/routes/onboard.js CHANGED
@@ -606,7 +606,7 @@ 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: "客服电话", qualification_type: "行业类目 (MCC 编码)",
609
+ service_phone: "客服电话", qualification_type: "所属行业 (微信行业名称)",
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: "公众号/小程序页面截图",
@@ -819,6 +819,11 @@ CREATE TABLE IF NOT EXISTS onboard_draft (
819
819
  // settlement_id = 结算规则 ID, 按主体推导 (商户不用选; 前端不传也兜底)
820
820
  payload.settlement_id = payload.subject_type === "SUBJECT_TYPE_ENTERPRISE" ? "716"
821
821
  : (payload.subject_type === "SUBJECT_TYPE_OTHERS" ? "727" : "719");
822
+ // 修正2 (微信网页版实证): 企业主体强制对公账户 — account_type 锁死对公, 户名=商户全称
823
+ if (payload.subject_type === "SUBJECT_TYPE_ENTERPRISE") {
824
+ payload.account_type = "BANK_ACCOUNT_TYPE_CORPORATE";
825
+ payload.account_name = payload.merchant_name;
826
+ }
822
827
  // M2 断点 c: tb_address 贯通 — SP 台账落列, FINISHED 后 oracle 引导金才知道发给谁
823
828
  const identity = await ensureSellerIdentity(basePaths.baseDir());
824
829
  payload.tb_address = identity.address;
@@ -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", qualification_type: "5331", // 行业类目 MCC (settlement_id 结算规则由后端按主体推导)
114
+ service_phone: "0755-12345678", qualification_type: "零售", // 所属行业=二级子类名称文本 (settlement_id 结算规则由后端按主体推导)
115
115
  ...media,
116
116
  };
117
117
  }
@@ -287,14 +287,19 @@ test("onboard 页面含进件表单 (btn-apply-submit) 且第③步状态机元
287
287
  assert.ok(!html.includes("slot.insertBefore(img"), "旧增强脚本预览补丁已删 (二重框根因)");
288
288
  assert.ok(html.includes("prefill:function(mediaId)"), "预填走双态 (无第二个空框)");
289
289
  // 2026-10-03 MCC 修正: 经营类目下拉是真实 MCC 编码 (716/719/727 是 V2 结算规则 ID, 被微信拒)
290
- assert.ok(html.includes("5311") && html.includes("5732"), "MCC 编码选项");
291
- assert.ok(html.includes("百货商店") && html.includes("就餐场所和餐馆"), "MCC 名称");
292
- assert.ok(!html.includes('value="719"') && !html.includes('value="716"') && !html.includes('value="727"'), "V2 错误编码已清除");
293
- assert.ok(html.includes("a_settlement_filter"), "类目搜索过滤");
294
- assert.ok(html.includes("a_settlement_manual") && html.includes("__MANUAL__"), "手动输入接管保留");
295
- // 2026-10-03 字段语义最终修正: 双字段 (结算规则只读展示 + 行业类目 MCC 下拉)
290
+ // 2026-10-03 网页版实证校准: 行业二级联动 (大类导航 + 子类名称文本提交)
291
+ assert.ok(html.includes("a_industry_cat"), "行业大类下拉");
292
+ assert.ok(html.includes("INDUSTRY_CATS") && html.includes("renderIndustrySubs"), "二级联动数据+渲染");
293
+ assert.ok(html.includes("{n:'零售'}"), "默认子类 零售");
294
+ assert.ok(html.includes("餐饮/零售") && html.includes("门户论坛/网络广告及推广/软件开发/其他互联网服务"), "对照表子项");
295
+ assert.ok(html.includes("⚠️需许可证"), "特殊行业许可证标记");
296
+ assert.ok(!html.includes("a_settlement_manual"), "行业手输框不存在 (封闭集合)");
297
+ // 修正2 企业强制对公 + 修正3 受益人
298
+ assert.ok(html.includes("paintAccountLock"), "企业对公锁逻辑");
299
+ assert.ok(html.includes("a_beneficiary"), "受益人选择");
300
+ // 双字段 (结算规则只读展示 + 所属行业下拉)
296
301
  assert.ok(html.includes("a_settlement_rule"), "结算规则只读展示框");
297
- assert.ok(html.includes("结算规则") && html.includes("行业类目"), "双字段标签");
302
+ assert.ok(html.includes("结算规则") && html.includes("所属行业"), "双字段标签");
298
303
  assert.ok(!html.includes("a_qualification_type"), "旧资质描述字段已移除");
299
304
  assert.ok(html.includes("maybeAutoKyc"), "M2: 进件受理后自动 KYC 提交");
300
305
  assert.ok(!html.includes("btn-verify"), "手动实名按钮已删 (自动触发)");
@@ -357,17 +362,18 @@ test("字段语义最终修正: settlement_id=结算规则按主体推导 + qual
357
362
  let j = await r.json();
358
363
  assert.strictEqual(r.status, 400);
359
364
  assert.strictEqual(j.field, "qualification_type");
360
- assert.ok(j.message.includes("行业类目"), j.message);
365
+ assert.ok(j.message.includes("所属行业"), j.message);
361
366
  // 个体户 → settlement_id=719 (前端没传, 后端推导) + qualification_type=5331 透传
362
367
  const before = spCapture.submits.length;
368
+ // 补回行业名称再提交
369
+ form.qualification_type = "零售";
363
370
  r = await fetch(base + "/api/onboard/applyment", {
364
- method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(form2ok(form) ),
371
+ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(form),
365
372
  });
366
- function form2ok(f){ const c={...f, qualification_type:"5331"}; return c; }
367
373
  j = await r.json();
368
374
  assert.strictEqual(r.status, 200, JSON.stringify(j));
369
375
  assert.strictEqual(spCapture.submits[before].settlement_id, "719", "个体户 → 结算规则 719 (后端推导)");
370
- assert.strictEqual(spCapture.submits[before].qualification_type, "5331", "行业类目 MCC 透传");
376
+ assert.strictEqual(spCapture.submits[before].qualification_type, "零售", "所属行业=二级子类名称文本透传");
371
377
  } finally { srv.close(); }
372
378
  });
373
379
 
@@ -376,7 +382,7 @@ test("字段语义: 企业主体 → settlement_id=716 + qualification_type=MCC
376
382
  try {
377
383
  const form = await fillForm(base);
378
384
  form.subject_type = "SUBJECT_TYPE_ENTERPRISE";
379
- form.qualification_type = "5311";
385
+ form.qualification_type = "零售";
380
386
  form.legal_id_address = "广东省深圳市南山区xx路1号101室"; // 企业必填 (F4)
381
387
  const before = spCapture.submits.length;
382
388
  const r = await fetch(base + "/api/onboard/applyment", {
@@ -386,11 +392,19 @@ test("字段语义: 企业主体 → settlement_id=716 + qualification_type=MCC
386
392
  assert.strictEqual(r.status, 200, JSON.stringify(j));
387
393
  const fwd = spCapture.submits[before];
388
394
  assert.strictEqual(fwd.settlement_id, "716", "企业 → 结算规则 716");
389
- assert.strictEqual(fwd.qualification_type, "5311", "行业类目 MCC");
395
+ assert.strictEqual(fwd.qualification_type, "零售", "所属行业=二级子类名称文本");
396
+ // 修正2 (网页版实证): 企业强制对公 — account_type 锁死 + 户名=公司名 (密文可还原验证)
397
+ assert.strictEqual(fwd.account_type, "BANK_ACCOUNT_TYPE_CORPORATE", "企业强制对公");
398
+ const decName = crypto.privateDecrypt(
399
+ { key: wxPub.privateKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha1" }, // 加密侧已锁 OAEP-SHA1 (2026-09-26 微信实测)
400
+ Buffer.from(fwd.account_name, "base64")
401
+ ).toString("utf8");
402
+ assert.strictEqual(decName, form.merchant_name, "对公户名=商户全称 (自动带出)");
390
403
  // SP 侧组装: settlement_info 两字段正确分工
391
404
  const wxBody = spApplymentLib.buildApplymentBody({ ...fwd, business_code: "TBTEST" }, (s) => s, { alreadyEncrypted: true });
392
405
  assert.strictEqual(wxBody.settlement_info.settlement_id, "716");
393
- assert.strictEqual(wxBody.settlement_info.qualification_type, "5311");
406
+ assert.strictEqual(wxBody.settlement_info.qualification_type, "零售");
407
+ assert.strictEqual(wxBody.bank_account_info.bank_account_type, "BANK_ACCOUNT_TYPE_CORPORATE");
394
408
  } finally { srv.close(); }
395
409
  });
396
410