trustbase-seller-backend 0.3.4 → 0.3.5
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 +47 -1
- package/routes/onboard.js +24 -2
- package/routes/sp-applyment.js +5 -0
- package/test-onboard-applyment.js +49 -0
package/package.json
CHANGED
package/routes/onboard.html
CHANGED
|
@@ -292,6 +292,7 @@ img.auprev{display:none;width:100%;max-height:140px;object-fit:cover;border-radi
|
|
|
292
292
|
<div id="a_up_idback"></div>
|
|
293
293
|
<div id="a_up_s1"></div>
|
|
294
294
|
<div id="a_up_s2"></div>
|
|
295
|
+
<div id="a_up_qual_wrap" style="display:none;grid-column:1/-1"><div id="a_up_qual"></div></div>
|
|
295
296
|
</div>
|
|
296
297
|
</div>
|
|
297
298
|
|
|
@@ -653,6 +654,7 @@ function applyFormBody(){
|
|
|
653
654
|
account_type:aval('a_account_type'),account_name:aval('a_account_name'),account_number:aval('a_account_number'),
|
|
654
655
|
account_bank:aval('a_account_bank'),bank_name:aval('a_bank_name'),bank_address_code:aval('a_bank_address_code'),
|
|
655
656
|
legal_id_address:aval('a_legal_id_address'), // 企业主体必填 (F4), 个体户留空
|
|
657
|
+
qual_media_id:_aMedia.qual_media_id||'', // 特殊资质 (需资质行业必传 → 后端组装 qualifications 数组)
|
|
656
658
|
sales_scene:scene,
|
|
657
659
|
license_media_id:_aMedia.license_media_id||'',legal_id_front_media_id:_aMedia.legal_id_front_media_id||'',
|
|
658
660
|
legal_id_back_media_id:_aMedia.legal_id_back_media_id||''
|
|
@@ -865,7 +867,7 @@ var PREFILL_MAP={subject_type:'a_subject_type',merchant_shortname:'a_shortname',
|
|
|
865
867
|
store_name:'a_store_name',store_address_code:'a_store_address_code',store_address:'a_store_address',
|
|
866
868
|
website_url:'a_website_url',appid:'a_appid',appid_type:'a_appid_type'};
|
|
867
869
|
var PREFILL_MEDIA=['license_media_id','legal_id_front_media_id','legal_id_back_media_id',
|
|
868
|
-
'store_entrance_media_id','store_indoor_media_id','web_home_media_id','web_product_media_id','mp_media_id'];
|
|
870
|
+
'store_entrance_media_id','store_indoor_media_id','web_home_media_id','web_product_media_id','mp_media_id','qual_media_id'];
|
|
869
871
|
var _prefilled=false;
|
|
870
872
|
function prefillApplyForm(snap){
|
|
871
873
|
if(_prefilled||!snap||typeof snap!=='object') return;
|
|
@@ -1076,7 +1078,51 @@ async function restoreIdentity(){
|
|
|
1076
1078
|
var def=INDUSTRY_DEFAULT_SUB[type];
|
|
1077
1079
|
stl.value=(subs.some(function(s){return s.n===cur;})&&cur)?cur
|
|
1078
1080
|
:(subs.some(function(s){return s.n===def;})?def:(subs[0]?subs[0].n:''));
|
|
1081
|
+
paintQualSlot(); // 行业变化 → 资质槽显隐
|
|
1079
1082
|
}
|
|
1083
|
+
// 行业 → 特殊资质映射 (微信 qualifications: 对照表"特殊资质"列有值的行业, 选需资质行业必传许可证)
|
|
1084
|
+
var QUAL_REQUIRED={
|
|
1085
|
+
'餐饮':'食品经营许可证 / 餐饮服务许可证 (三选一, 或门头+店内+收银台三张照片)',
|
|
1086
|
+
'食品生鲜':'食品经营许可证',
|
|
1087
|
+
'私立/民营医院/诊所':'医疗机构执业许可证',
|
|
1088
|
+
'保健器械/医疗器械/非处方药品':'药品经营许可证 / 医疗器械经营企业许可证',
|
|
1089
|
+
'教育/培训/考试缴费/学费':'办学许可证',
|
|
1090
|
+
'游艺厅/KTV/网吧':'娱乐场所经营许可证 / 网络文化经营许可证 (网吧)',
|
|
1091
|
+
'院线影城':'电影放映经营许可证',
|
|
1092
|
+
'演出赛事':'营业性演出许可证',
|
|
1093
|
+
'机票/机票代理':'航空公司营业执照 或 代理资格证',
|
|
1094
|
+
'宠物医院':'动物诊疗许可证',
|
|
1095
|
+
'旅行社':'旅行社业务经营许可证',
|
|
1096
|
+
'房地产/房产中介':'房地产五证 (建设用地规划/建设工程规划/施工/预售/产权)',
|
|
1097
|
+
'文物经营/文物复制品销售':'文物经营许可证',
|
|
1098
|
+
'拍卖典当':'拍卖经营批准证书 / 典当经营许可证',
|
|
1099
|
+
'电信运营商/宽带收费':'基础电信业务经营许可证',
|
|
1100
|
+
'共享服务':'对应行业经营许可/备案',
|
|
1101
|
+
'保健美容SPA':'卫生许可证'
|
|
1102
|
+
};
|
|
1103
|
+
var _qualIndustry='';
|
|
1104
|
+
// 选中需资质行业 → 第⑤卡动态出现"特殊资质 (必传)"上传槽 (复用上传槽双态机);
|
|
1105
|
+
// 切回不需资质行业 → 槽隐藏且清空 (不留 media_id 污染)
|
|
1106
|
+
function paintQualSlot(){
|
|
1107
|
+
var wrap=document.getElementById('a_up_qual_wrap');
|
|
1108
|
+
if(!wrap||!stl) return;
|
|
1109
|
+
var ind=stl.value;
|
|
1110
|
+
var doc=QUAL_REQUIRED[ind]||'';
|
|
1111
|
+
if(!doc){
|
|
1112
|
+
wrap.style.display='none';
|
|
1113
|
+
if(_qualIndustry&&_aSlots.qual_media_id) _aSlots.qual_media_id.clear();
|
|
1114
|
+
delete _aMedia.qual_media_id;
|
|
1115
|
+
_qualIndustry='';
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
wrap.style.display='';
|
|
1119
|
+
if(_qualIndustry!==ind){
|
|
1120
|
+
delete _aMedia.qual_media_id; // 换行业=换资质, 清旧 media
|
|
1121
|
+
mountApplyUpload('a_up_qual','qual_media_id','特殊资质 (必传): '+doc);
|
|
1122
|
+
_qualIndustry=ind;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
if(stl){ stl.addEventListener('change',paintQualSlot); }
|
|
1080
1126
|
if(catEl){ catEl.addEventListener('change',function(){ renderIndustrySubs(false); }); }
|
|
1081
1127
|
// 结算规则只读展示 (settlement_id 按主体推导: 企业 716 / 个体户 719 / 其他组织 727; 商户不用选)
|
|
1082
1128
|
var SETTLE_RULES={SUBJECT_TYPE_INDIVIDUAL:'719 · 个体户通用(0.6% T+1)',
|
package/routes/onboard.js
CHANGED
|
@@ -624,7 +624,18 @@ function createOnboardRouter(deps) {
|
|
|
624
624
|
SALES_SCENES_WEB: { fields: ["website_url"], media: [] },
|
|
625
625
|
SALES_SCENES_MP: { fields: ["appid"], media: ["mp_media_id"] },
|
|
626
626
|
};
|
|
627
|
-
const APPLY_MEDIA_COMMON = ["license_media_id", "legal_id_front_media_id", "legal_id_back_media_id"];
|
|
627
|
+
const APPLY_MEDIA_COMMON = ["license_media_id", "legal_id_front_media_id", "legal_id_back_media_id"];
|
|
628
|
+
// 行业 → 特殊资质映射 (微信 qualifications: 需资质行业缺证必被拒; 与页面 QUAL_REQUIRED 同口径)
|
|
629
|
+
const APPLY_QUAL_REQUIRED = {
|
|
630
|
+
"餐饮": "食品经营许可证/餐饮服务许可证", "食品生鲜": "食品经营许可证",
|
|
631
|
+
"私立/民营医院/诊所": "医疗机构执业许可证", "保健器械/医疗器械/非处方药品": "药品经营许可证/医疗器械经营企业许可证",
|
|
632
|
+
"教育/培训/考试缴费/学费": "办学许可证", "游艺厅/KTV/网吧": "娱乐场所经营许可证/网络文化经营许可证",
|
|
633
|
+
"院线影城": "电影放映经营许可证", "演出赛事": "营业性演出许可证",
|
|
634
|
+
"机票/机票代理": "航空公司营业执照或代理资格证", "宠物医院": "动物诊疗许可证",
|
|
635
|
+
"旅行社": "旅行社业务经营许可证", "房地产/房产中介": "房地产五证",
|
|
636
|
+
"文物经营/文物复制品销售": "文物经营许可证", "拍卖典当": "拍卖经营批准证书/典当经营许可证",
|
|
637
|
+
"电信运营商/宽带收费": "基础电信业务经营许可证", "共享服务": "对应行业经营许可/备案", "保健美容SPA": "卫生许可证",
|
|
638
|
+
};
|
|
628
639
|
|
|
629
640
|
/** SP 网关配置 (懒读 env, 测试可注入 deps.fetcher) */
|
|
630
641
|
function spGatewayUrl() {
|
|
@@ -796,6 +807,12 @@ CREATE TABLE IF NOT EXISTS onboard_draft (
|
|
|
796
807
|
return res.status(400).json({ ok: false, error: "invalid_param", field: k, message: `缺少必填字段: ${label} (${k})` });
|
|
797
808
|
}
|
|
798
809
|
}
|
|
810
|
+
// 需资质行业 → 特殊资质图片必传 (微信 qualifications, 缺证必被拒)
|
|
811
|
+
const qualNeed = APPLY_QUAL_REQUIRED[String(b.qualification_type || "")];
|
|
812
|
+
if (qualNeed && !b.qual_media_id) {
|
|
813
|
+
return res.status(400).json({ ok: false, error: "media_missing", field: "qual_media_id",
|
|
814
|
+
message: `请上传: ${b.qualification_type} 所需资质 (${qualNeed})` });
|
|
815
|
+
}
|
|
799
816
|
for (const k of APPLY_MEDIA_COMMON.concat(sceneCfg.media)) {
|
|
800
817
|
if (!b[k]) {
|
|
801
818
|
const label = APPLY_LABELS[k] || k;
|
|
@@ -814,7 +831,12 @@ CREATE TABLE IF NOT EXISTS onboard_draft (
|
|
|
814
831
|
for (const k of Object.keys(b)) {
|
|
815
832
|
if (typeof b[k] === "string" && b[k].trim()) payload[k] = b[k].trim();
|
|
816
833
|
}
|
|
817
|
-
payload.sales_scene = scene; // 场景必带 (SP 侧按它组装 biz_store_info/web_info/mp_info)
|
|
834
|
+
payload.sales_scene = scene; // 场景必带 (SP 侧按它组装 biz_store_info/web_info/mp_info)
|
|
835
|
+
// 特殊资质: qual_media_id → qualifications 数组 (没有则不传该字段)
|
|
836
|
+
if (payload.qual_media_id) {
|
|
837
|
+
payload.qualifications = [payload.qual_media_id];
|
|
838
|
+
delete payload.qual_media_id;
|
|
839
|
+
}
|
|
818
840
|
// 通用类目资质留空 → 默认 "无" (微信通用场景接受, 避免留空被 SP 拒)
|
|
819
841
|
// settlement_id = 结算规则 ID, 按主体推导 (商户不用选; 前端不传也兜底)
|
|
820
842
|
payload.settlement_id = payload.subject_type === "SUBJECT_TYPE_ENTERPRISE" ? "716"
|
package/routes/sp-applyment.js
CHANGED
|
@@ -260,6 +260,11 @@ function buildApplymentBody(form, enc, opts) {
|
|
|
260
260
|
if (f.bank_name) body.bank_account_info.bank_name = String(f.bank_name);
|
|
261
261
|
if (f.bank_branch_id) body.bank_account_info.bank_branch_id = String(f.bank_branch_id);
|
|
262
262
|
if (f.bank_address_code) body.bank_account_info.bank_address_code = String(f.bank_address_code);
|
|
263
|
+
// 特殊资质图片数组 (微信 qualifications: 需资质行业必传, 最多 5 张; already_encrypted 直通)
|
|
264
|
+
if (Array.isArray(f.qualifications) && f.qualifications.length) {
|
|
265
|
+
body.qualifications = f.qualifications.map((x) => String(x)).filter(Boolean).slice(0, 5);
|
|
266
|
+
if (!body.qualifications.length) delete body.qualifications;
|
|
267
|
+
}
|
|
263
268
|
return body;
|
|
264
269
|
}
|
|
265
270
|
|
|
@@ -685,3 +685,52 @@ test("草稿页面逻辑: 自动保存/恢复优先级/FINISHED 清除/提交保
|
|
|
685
685
|
assert.ok(html.indexOf("applyment/draft") < html.indexOf("form_snapshot") || html.includes("恢复优先级"), "草稿优先于快照");
|
|
686
686
|
} finally { srv.close(); }
|
|
687
687
|
});
|
|
688
|
+
|
|
689
|
+
test("特殊资质: 餐饮无许可证 → 400 中文提示; 有证 → 转发体 qualifications 数组", async () => {
|
|
690
|
+
const { srv, base } = await startSeller();
|
|
691
|
+
try {
|
|
692
|
+
const form = await fillForm(base);
|
|
693
|
+
form.qualification_type = "餐饮"; // 需资质行业
|
|
694
|
+
// 无资质 → 400 中文提示含资质名
|
|
695
|
+
let r = await fetch(base + "/api/onboard/applyment", {
|
|
696
|
+
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(form),
|
|
697
|
+
});
|
|
698
|
+
let j = await r.json();
|
|
699
|
+
assert.strictEqual(r.status, 400);
|
|
700
|
+
assert.strictEqual(j.error, "media_missing");
|
|
701
|
+
assert.strictEqual(j.field, "qual_media_id");
|
|
702
|
+
assert.ok(j.message.includes("餐饮") && j.message.includes("食品经营"), j.message);
|
|
703
|
+
// 上传资质 → 转发体 qualifications=["media_id"], SP 侧顶层直通
|
|
704
|
+
const up = await fetch(base + "/api/onboard/applyment/upload", {
|
|
705
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
706
|
+
body: JSON.stringify({ filename: "qual.jpg", content_b64: Buffer.from("fake-qual").toString("base64") }),
|
|
707
|
+
});
|
|
708
|
+
form.qual_media_id = (await up.json()).media_id;
|
|
709
|
+
const before = spCapture.submits.length;
|
|
710
|
+
r = await fetch(base + "/api/onboard/applyment", {
|
|
711
|
+
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(form),
|
|
712
|
+
});
|
|
713
|
+
j = await r.json();
|
|
714
|
+
assert.strictEqual(r.status, 200, JSON.stringify(j));
|
|
715
|
+
const fwd = spCapture.submits[before];
|
|
716
|
+
assert.deepStrictEqual(fwd.qualifications, [form.qual_media_id], "qualifications 数组");
|
|
717
|
+
assert.ok(!("qual_media_id" in fwd), "中间字段已转换");
|
|
718
|
+
const wxBody = spApplymentLib.buildApplymentBody({ ...fwd, business_code: "TBTEST" }, (s) => s, { alreadyEncrypted: true });
|
|
719
|
+
assert.deepStrictEqual(wxBody.qualifications, [form.qual_media_id], "SP 顶层 qualifications 直通");
|
|
720
|
+
} finally { srv.close(); }
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
test("特殊资质页面逻辑: QUAL_REQUIRED 映射 + 动态槽位 + 双态复用", async () => {
|
|
724
|
+
const { srv, base } = await startSeller();
|
|
725
|
+
try {
|
|
726
|
+
const html = await (await fetch(base + "/onboard/")).text();
|
|
727
|
+
assert.ok(html.includes("QUAL_REQUIRED"), "行业-资质映射表");
|
|
728
|
+
assert.ok(html.includes("a_up_qual"), "资质槽位容器");
|
|
729
|
+
assert.ok(html.includes("paintQualSlot"), "槽位显隐逻辑");
|
|
730
|
+
assert.ok(html.includes("食品经营许可证"), "餐饮资质名");
|
|
731
|
+
assert.ok(html.includes("医疗机构执业许可证"), "医疗资质名");
|
|
732
|
+
assert.ok(html.includes("办学许可证"), "教育资质名");
|
|
733
|
+
assert.ok(html.includes("qual_media_id"), "资质 media_id 字段");
|
|
734
|
+
assert.ok(html.includes("特殊资质 (必传)"), "槽位必传标注");
|
|
735
|
+
} finally { srv.close(); }
|
|
736
|
+
});
|