trustbase-seller-backend 0.3.3 → 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.3",
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,9 +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">所属行业 (微信行业名称)</label>
205
- <input id="a_settlement_filter" placeholder="搜索行业 (如 零售 / 餐饮)" style="margin-bottom:6px">
206
- <select id="a_settlement_id"><option value="">请选择所属行业</option></select></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>
207
208
  </div>
208
209
  </div>
209
210
 
@@ -214,7 +215,9 @@ img.auprev{display:none;width:100%;max-height:140px;object-fit:cover;border-radi
214
215
  <div class="afield"><label for="a_legal_id_number">法人身份证号</label><input id="a_legal_id_number" placeholder="17 位数字 + 数字或 X"></div>
215
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>
216
217
  <div class="afield afield-full" id="afield_ubo_note" style="display:none">
217
- <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>
218
221
  </div>
219
222
  <div class="afield"><label for="a_period_begin">身份证有效期开始</label><input id="a_period_begin" placeholder="YYYY-MM-DD"></div>
220
223
  <div class="afield"><label for="a_period_end">有效期结束</label><input id="a_period_end" placeholder="YYYY-MM-DD 或 长期"></div>
@@ -818,6 +821,8 @@ function restoreApplyDraft(f){
818
821
  var e=document.getElementById(PREFILL_MAP[k]);
819
822
  if(e&&!e.value) e.value=v;
820
823
  }
824
+ // 所属行业 (二级联动定位)
825
+ syncIndustrySelection(f.qualification_type);
821
826
  // 照片 media_id 复用 → 槽位已传双态
822
827
  PREFILL_MEDIA.forEach(function(k){
823
828
  if(f[k]&&_aSlots[k]) _aSlots[k].prefill(f[k]);
@@ -829,6 +834,18 @@ function restoreApplyDraft(f){
829
834
  // 草稿恢复了编辑态 → 展开表单
830
835
  document.getElementById('apply-form').style.display='block';
831
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
+ }
832
849
  // 任何字段 input/change → 防抖自动保存 (含场景切换时的字段集; file 选择也走 change)
833
850
  (function(){
834
851
  var form=document.getElementById('apply-form');
@@ -845,7 +862,6 @@ var PREFILL_MAP={subject_type:'a_subject_type',merchant_shortname:'a_shortname',
845
862
  legal_id_period_begin:'a_period_begin',legal_id_period_end:'a_period_end',
846
863
  account_type:'a_account_type',account_name:'a_account_name',account_number:'a_account_number',
847
864
  account_bank:'a_account_bank',bank_name:'a_bank_name',bank_address_code:'a_bank_address_code',
848
- qualification_type:'a_settlement_id', // 行业类目 MCC (draft 路径用; snapshot 路径走 mcc 兼容段)
849
865
  store_name:'a_store_name',store_address_code:'a_store_address_code',store_address:'a_store_address',
850
866
  website_url:'a_website_url',appid:'a_appid',appid_type:'a_appid_type'};
851
867
  var PREFILL_MEDIA=['license_media_id','legal_id_front_media_id','legal_id_back_media_id',
@@ -870,14 +886,9 @@ function prefillApplyForm(snap){
870
886
  var e=document.getElementById(PREFILL_MAP[k]);
871
887
  if(e&&!e.value) e.value=v;
872
888
  }
873
- // 所属行业预填 (第三次字段修正口径): qualification_type=中文行业名称 (封闭集合);
874
- // 旧快照若是 MCC 数字 (5722 等) 或 "无"/"零售" → 识别为非法, 清空让商户重选 (不报错);
875
- // 合法行业名称则回填 (不在当前主体选项集时 select 置空, 商户重选)
876
- var ind=snap.qualification_type;
877
- if(ind && String(ind).indexOf('***')!==0 && !/^\d+$/.test(String(ind)) && ind!=='无' && ind!=='零售'){
878
- var se=document.getElementById('a_settlement_id');
879
- if(se) se.value=ind; // renderIndustries 已按主体建好选项集; 不在集合内 value 置空即"未选"
880
- }
889
+ // 所属行业预填 (二级联动): 合法行业名称 → 定位所属大类并选中子类;
890
+ // 旧快照 MCC 数字/非法值 → 保持默认让商户重选 (静默不报错)
891
+ syncIndustrySelection(snap.qualification_type);
881
892
  // 照片复用: media_id 还在微信侧有效 → 槽位走"已上传"双态 (占位图+替换/删除, 无第二个空框)
882
893
  var reused=0;
883
894
  PREFILL_MEDIA.forEach(function(k){
@@ -1000,51 +1011,73 @@ async function restoreIdentity(){
1000
1011
  ① 编码字段下拉+联动 ② 状态卡片着色 + 签约二维码占位。
1001
1012
  (证件缩略图预览已于二重框修复时并入主脚本 mountApplyUpload 槽位双态) ===== */
1002
1013
  (function(){
1003
- // ⓪ 所属行业下拉 (2026-10-03 第三次字段修正, 微信文档实锤):
1004
- // qualification_type【所属行业】=《费率结算规则对照表》"行业"列的**中文行业名称** (封闭集合),
1005
- // 不是 MCC 数字编码 (填 5722 被微信拒 "所属行业不存在")。行业子项按主体联动 (716/719/727 各自的表)。
1006
- var INDUSTRIES={
1007
- SUBJECT_TYPE_ENTERPRISE:['餐饮','食品生鲜','电信运营商/宽带收费','私立/民营医院/诊所','保健器械/医疗器械/非处方药品',
1008
- '游艺厅/KTV/网吧','机票/机票代理','宠物医院','旅行社','宗教组织','房地产/房产中介','共享服务',
1009
- '文物经营/文物复制品销售','拍卖典当','教育/培训/考试缴费/学费','零售批发/生活娱乐/网上商城/其他'],
1010
- SUBJECT_TYPE_INDIVIDUAL:['餐饮','食品生鲜','私立/民营医院/诊所','保健器械/医疗器械/非处方药品',
1011
- '游艺厅/KTV/网吧','机票/机票代理','宠物医院','教育/培训/考试缴费/学费','零售批发/生活娱乐/其他'],
1012
- SUBJECT_TYPE_OTHERS:['宗教组织','机票/机票代理','私立/民营医院/诊所','咨询/娱乐票务/其他']
1013
- };
1014
- var INDUSTRY_DEFAULT={SUBJECT_TYPE_ENTERPRISE:'零售批发/生活娱乐/网上商城/其他',
1015
- SUBJECT_TYPE_INDIVIDUAL:'零售批发/生活娱乐/其他',SUBJECT_TYPE_OTHERS:'咨询/娱乐票务/其他'};
1016
- // 需要许可证的特殊行业 (选项后加 ⚠️需许可证 提示; value 仍是行业名称原文)
1017
- var INDUSTRY_LICENSE=['餐饮','食品生鲜','私立/民营医院/诊所','保健器械/医疗器械/非处方药品',
1018
- '游艺厅/KTV/网吧','教育/培训/考试缴费/学费','宠物医院','旅行社','拍卖典当','文物经营/文物复制品销售',
1019
- '房地产/房产中介','电信运营商/宽带收费','共享服务'];
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:'咨询/娱乐票务/其他'};
1020
1040
  var subj=document.getElementById('a_subject_type');
1021
- var stl=document.getElementById('a_settlement_id');
1022
- function renderIndustries(){
1023
- if(!stl) return;
1024
- var type=(subj&&INDUSTRIES[subj.value])?subj.value:'SUBJECT_TYPE_INDIVIDUAL';
1025
- var list=INDUSTRIES[type];
1026
- var cur=stl.value; // 尽量保留已选 (主体没换集合内同名行业时)
1027
- stl.innerHTML='<option value="">请选择所属行业</option>';
1028
- list.forEach(function(name){
1029
- var o=document.createElement('option');
1030
- o.value=name; // 提交直接带行业名称原文 (含斜杠)
1031
- o.textContent=name+(INDUSTRY_LICENSE.indexOf(name)>=0?' ⚠️需许可证':'');
1032
- stl.appendChild(o);
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}); } // 个体户: 非"仅企业"子项
1050
+ });
1033
1051
  });
1034
- stl.value=(list.indexOf(cur)>=0&&cur)?cur:(INDUSTRY_DEFAULT[type]||list[0]);
1052
+ return out;
1035
1053
  }
1036
- if(stl){
1037
- renderIndustries();
1038
- // 搜索过滤 (行业多, 按名称子串过滤)
1039
- var filt=document.getElementById('a_settlement_filter');
1040
- if(filt) filt.addEventListener('input',function(){
1041
- var q=filt.value.trim().toLowerCase();
1042
- Array.prototype.forEach.call(stl.options,function(o){
1043
- if(o.value==='') return;
1044
- o.hidden = !!(q && o.textContent.toLowerCase().indexOf(q)<0);
1045
- });
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);
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);
1046
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:''));
1047
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
- renderIndustries(); // 主体切换 → 行业选项集联动刷新 + 重选默认
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
@@ -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: "零售批发/生活娱乐/其他", // 所属行业=中文行业名称 (settlement_id 结算规则由后端按主体推导)
114
+ service_phone: "0755-12345678", qualification_type: "零售", // 所属行业=二级子类名称文本 (settlement_id 结算规则由后端按主体推导)
115
115
  ...media,
116
116
  };
117
117
  }
@@ -287,14 +287,16 @@ 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
- // 2026-10-03 第三次字段修正: qualification_type = 中文行业名称 (封闭集合, 非 MCC)
291
- assert.ok(html.includes("零售批发/生活娱乐/网上商城/其他"), "企业行业子项");
292
- assert.ok(html.includes("零售批发/生活娱乐/其他") && html.includes("咨询/娱乐票务/其他"), "个体户/其他组织行业子项");
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("门户论坛/网络广告及推广/软件开发/其他互联网服务"), "对照表子项");
293
295
  assert.ok(html.includes("⚠️需许可证"), "特殊行业许可证标记");
294
- assert.ok(html.includes("INDUSTRIES"), "主体联动选项集");
295
- assert.ok(html.includes("a_settlement_filter"), "行业搜索过滤");
296
- assert.ok(!html.includes("a_settlement_manual"), "行业手输框已删 (封闭集合不允许手输; 省市编码的 __MANUAL__ 保留)");
297
- assert.ok(!/['"]5311['"]/.test(html) && !/['"]5722['"]/.test(html), "MCC 数字编码已清除");
296
+ assert.ok(!html.includes("a_settlement_manual"), "行业手输框不存在 (封闭集合)");
297
+ // 修正2 企业强制对公 + 修正3 受益人
298
+ assert.ok(html.includes("paintAccountLock"), "企业对公锁逻辑");
299
+ assert.ok(html.includes("a_beneficiary"), "受益人选择");
298
300
  // 双字段 (结算规则只读展示 + 所属行业下拉)
299
301
  assert.ok(html.includes("a_settlement_rule"), "结算规则只读展示框");
300
302
  assert.ok(html.includes("结算规则") && html.includes("所属行业"), "双字段标签");
@@ -364,14 +366,14 @@ test("字段语义最终修正: settlement_id=结算规则按主体推导 + qual
364
366
  // 个体户 → settlement_id=719 (前端没传, 后端推导) + qualification_type=5331 透传
365
367
  const before = spCapture.submits.length;
366
368
  // 补回行业名称再提交
367
- form.qualification_type = "零售批发/生活娱乐/其他";
369
+ form.qualification_type = "零售";
368
370
  r = await fetch(base + "/api/onboard/applyment", {
369
371
  method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(form),
370
372
  });
371
373
  j = await r.json();
372
374
  assert.strictEqual(r.status, 200, JSON.stringify(j));
373
375
  assert.strictEqual(spCapture.submits[before].settlement_id, "719", "个体户 → 结算规则 719 (后端推导)");
374
- assert.strictEqual(spCapture.submits[before].qualification_type, "零售批发/生活娱乐/其他", "所属行业=中文行业名称透传");
376
+ assert.strictEqual(spCapture.submits[before].qualification_type, "零售", "所属行业=二级子类名称文本透传");
375
377
  } finally { srv.close(); }
376
378
  });
377
379
 
@@ -380,7 +382,7 @@ test("字段语义: 企业主体 → settlement_id=716 + qualification_type=MCC
380
382
  try {
381
383
  const form = await fillForm(base);
382
384
  form.subject_type = "SUBJECT_TYPE_ENTERPRISE";
383
- form.qualification_type = "零售批发/生活娱乐/网上商城/其他";
385
+ form.qualification_type = "零售";
384
386
  form.legal_id_address = "广东省深圳市南山区xx路1号101室"; // 企业必填 (F4)
385
387
  const before = spCapture.submits.length;
386
388
  const r = await fetch(base + "/api/onboard/applyment", {
@@ -390,11 +392,19 @@ test("字段语义: 企业主体 → settlement_id=716 + qualification_type=MCC
390
392
  assert.strictEqual(r.status, 200, JSON.stringify(j));
391
393
  const fwd = spCapture.submits[before];
392
394
  assert.strictEqual(fwd.settlement_id, "716", "企业 → 结算规则 716");
393
- assert.strictEqual(fwd.qualification_type, "零售批发/生活娱乐/网上商城/其他", "所属行业=中文行业名称");
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, "对公户名=商户全称 (自动带出)");
394
403
  // SP 侧组装: settlement_info 两字段正确分工
395
404
  const wxBody = spApplymentLib.buildApplymentBody({ ...fwd, business_code: "TBTEST" }, (s) => s, { alreadyEncrypted: true });
396
405
  assert.strictEqual(wxBody.settlement_info.settlement_id, "716");
397
- assert.strictEqual(wxBody.settlement_info.qualification_type, "零售批发/生活娱乐/网上商城/其他");
406
+ assert.strictEqual(wxBody.settlement_info.qualification_type, "零售");
407
+ assert.strictEqual(wxBody.bank_account_info.bank_account_type, "BANK_ACCOUNT_TYPE_CORPORATE");
398
408
  } finally { srv.close(); }
399
409
  });
400
410