@steedos-labs/content-compliance 0.8.36 → 1.0.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/dist/index.js +1 -1
- package/lib/actions/contentHubMaterialGroups.d.ts +19 -0
- package/lib/actions/contentHubMaterialGroups.js +70 -0
- package/lib/actions/contentHubMaterialGroups.js.map +1 -0
- package/lib/actions/contentHubSyncPreview.d.ts +21 -0
- package/lib/actions/contentHubSyncPreview.js +51 -0
- package/lib/actions/contentHubSyncPreview.js.map +1 -0
- package/lib/actions/contentHubTagValues.d.ts +12 -0
- package/lib/actions/contentHubTagValues.js +70 -0
- package/lib/actions/contentHubTagValues.js.map +1 -0
- package/lib/actions/index.d.ts +3 -0
- package/lib/actions/index.js +3 -0
- package/lib/actions/index.js.map +1 -1
- package/lib/actions/sendToContentHub.js +1 -1
- package/lib/actions/sendToContentHub.js.map +1 -1
- package/lib/actions/spotCheckMaterial.js +2 -1
- package/lib/actions/spotCheckMaterial.js.map +1 -1
- package/lib/index.js +173 -30
- package/lib/index.js.map +1 -1
- package/lib/methods/__tests__/contentHubMaterialGroup.test.d.ts +1 -0
- package/lib/methods/__tests__/contentHubMaterialGroup.test.js +97 -0
- package/lib/methods/__tests__/contentHubMaterialGroup.test.js.map +1 -0
- package/lib/methods/__tests__/contentHubMaterialGroupRule.test.d.ts +1 -0
- package/lib/methods/__tests__/contentHubMaterialGroupRule.test.js +168 -0
- package/lib/methods/__tests__/contentHubMaterialGroupRule.test.js.map +1 -0
- package/lib/methods/__tests__/contentHubPush.test.d.ts +1 -0
- package/lib/methods/__tests__/contentHubPush.test.js +568 -0
- package/lib/methods/__tests__/contentHubPush.test.js.map +1 -0
- package/lib/methods/__tests__/contentHubSyncRule.test.d.ts +1 -0
- package/lib/methods/__tests__/contentHubSyncRule.test.js +130 -0
- package/lib/methods/__tests__/contentHubSyncRule.test.js.map +1 -0
- package/lib/methods/__tests__/contentHubTagSync.test.d.ts +1 -0
- package/lib/methods/__tests__/contentHubTagSync.test.js +743 -0
- package/lib/methods/__tests__/contentHubTagSync.test.js.map +1 -0
- package/lib/methods/__tests__/tagLabel.test.d.ts +1 -0
- package/lib/methods/__tests__/tagLabel.test.js +136 -0
- package/lib/methods/__tests__/tagLabel.test.js.map +1 -0
- package/lib/methods/approval_engine.js +10 -7
- package/lib/methods/approval_engine.js.map +1 -1
- package/lib/methods/consts.d.ts +4 -0
- package/lib/methods/consts.js.map +1 -1
- package/lib/methods/content_hub.d.ts +8 -1
- package/lib/methods/content_hub.js +278 -27
- package/lib/methods/content_hub.js.map +1 -1
- package/lib/methods/content_hub_copyright.d.ts +15 -0
- package/lib/methods/content_hub_copyright.js +223 -0
- package/lib/methods/content_hub_copyright.js.map +1 -0
- package/lib/methods/content_hub_material_group.d.ts +29 -0
- package/lib/methods/content_hub_material_group.js +268 -0
- package/lib/methods/content_hub_material_group.js.map +1 -0
- package/lib/methods/content_hub_material_group_rule.d.ts +23 -0
- package/lib/methods/content_hub_material_group_rule.js +58 -0
- package/lib/methods/content_hub_material_group_rule.js.map +1 -0
- package/lib/methods/content_hub_sync_rule.d.ts +51 -0
- package/lib/methods/content_hub_sync_rule.js +122 -0
- package/lib/methods/content_hub_sync_rule.js.map +1 -0
- package/lib/methods/content_hub_tag.d.ts +57 -0
- package/lib/methods/content_hub_tag.js +830 -0
- package/lib/methods/content_hub_tag.js.map +1 -0
- package/lib/methods/pls.d.ts +4 -1
- package/lib/methods/pls.js +60 -51
- package/lib/methods/pls.js.map +1 -1
- package/lib/methods/tag_label.d.ts +19 -0
- package/lib/methods/tag_label.js +192 -0
- package/lib/methods/tag_label.js.map +1 -0
- package/main/default/applications/pepsico_content_setting.app.yml +8 -0
- package/main/default/objects/pepesio_ch__c/fields/media_type.field.yml +2 -0
- package/main/default/objects/pepesio_ch__c/fields/pepsico_material_approval.field.yml +6 -0
- package/main/default/objects/pepesio_ch__c/fields/push_status.field.yml +11 -0
- package/main/default/objects/pepesio_ch__c/fields/type.field.yml +48 -8
- package/main/default/objects/pepesio_ch__c/listviews/all.listview.yml +2 -0
- package/main/default/objects/pepsico_ch_material_group_rule/fields/active.field.yml +6 -0
- package/main/default/objects/pepsico_ch_material_group_rule/fields/classification.field.yml +73 -0
- package/main/default/objects/pepsico_ch_material_group_rule/fields/name.field.yml +9 -0
- package/main/default/objects/pepsico_ch_material_group_rule/fields/orgs.field.yml +10 -0
- package/main/default/objects/pepsico_ch_material_group_rule/fields/priority.field.yml +10 -0
- package/main/default/objects/pepsico_ch_material_group_rule/fields/query_mode.field.yml +12 -0
- package/main/default/objects/pepsico_ch_material_group_rule/fields/remark.field.yml +7 -0
- package/main/default/objects/pepsico_ch_material_group_rule/listviews/all.listview.yml +20 -0
- package/main/default/objects/pepsico_ch_material_group_rule/pepsico_ch_material_group_rule.object.yml +19 -0
- package/main/default/objects/pepsico_ch_material_group_rule/permissions/admin.permission.yml +15 -0
- package/main/default/objects/pepsico_ch_material_group_rule/permissions/user.permission.yml +14 -0
- package/main/default/objects/pepsico_ch_sync_rule/fields/active.field.yml +6 -0
- package/main/default/objects/pepsico_ch_sync_rule/fields/classification.field.yml +73 -0
- package/main/default/objects/pepsico_ch_sync_rule/fields/kol_classification.field.yml +67 -0
- package/main/default/objects/pepsico_ch_sync_rule/fields/name.field.yml +9 -0
- package/main/default/objects/pepsico_ch_sync_rule/fields/need_sync.field.yml +11 -0
- package/main/default/objects/pepsico_ch_sync_rule/fields/orgs.field.yml +10 -0
- package/main/default/objects/pepsico_ch_sync_rule/fields/priority.field.yml +10 -0
- package/main/default/objects/pepsico_ch_sync_rule/fields/product.field.yml +65 -0
- package/main/default/objects/pepsico_ch_sync_rule/fields/remark.field.yml +7 -0
- package/main/default/objects/pepsico_ch_sync_rule/fields/spot_check.field.yml +12 -0
- package/main/default/objects/pepsico_ch_sync_rule/listviews/all.listview.yml +24 -0
- package/main/default/objects/pepsico_ch_sync_rule/pepsico_ch_sync_rule.object.yml +19 -0
- package/main/default/objects/pepsico_ch_sync_rule/permissions/admin.permission.yml +15 -0
- package/main/default/objects/pepsico_ch_sync_rule/permissions/user.permission.yml +14 -0
- package/main/default/objects/pepsico_ch_tag_sync_log/fields/action.field.yml +6 -0
- package/main/default/objects/pepsico_ch_tag_sync_log/fields/changed_at.field.yml +6 -0
- package/main/default/objects/pepsico_ch_tag_sync_log/fields/duration.field.yml +6 -0
- package/main/default/objects/pepsico_ch_tag_sync_log/fields/error_code.field.yml +6 -0
- package/main/default/objects/pepsico_ch_tag_sync_log/fields/error_msg.field.yml +6 -0
- package/main/default/objects/pepsico_ch_tag_sync_log/fields/expired_date.field.yml +6 -0
- package/main/default/objects/pepsico_ch_tag_sync_log/fields/name.field.yml +7 -0
- package/main/default/objects/pepsico_ch_tag_sync_log/fields/parent_tag_val_id.field.yml +6 -0
- package/main/default/objects/pepsico_ch_tag_sync_log/fields/request_id.field.yml +7 -0
- package/main/default/objects/pepsico_ch_tag_sync_log/fields/result.field.yml +6 -0
- package/main/default/objects/pepsico_ch_tag_sync_log/fields/source_ip.field.yml +6 -0
- package/main/default/objects/pepsico_ch_tag_sync_log/fields/space.field.yml +6 -0
- package/main/default/objects/pepsico_ch_tag_sync_log/fields/tag_id.field.yml +6 -0
- package/main/default/objects/pepsico_ch_tag_sync_log/fields/tag_val_id.field.yml +7 -0
- package/main/default/objects/pepsico_ch_tag_sync_log/fields/tag_val_name.field.yml +6 -0
- package/main/default/objects/pepsico_ch_tag_sync_log/listviews/all.listview.yml +21 -0
- package/main/default/objects/pepsico_ch_tag_sync_log/pepsico_ch_tag_sync_log.object.yml +16 -0
- package/main/default/objects/pepsico_ch_tag_sync_log/permissions/admin.permission.yml +15 -0
- package/main/default/objects/pepsico_ch_tag_sync_log/permissions/user.permission.yml +14 -0
- package/main/default/objects/pepsico_ch_tag_sync_request/fields/changed_at.field.yml +6 -0
- package/main/default/objects/pepsico_ch_tag_sync_request/fields/duration.field.yml +6 -0
- package/main/default/objects/pepsico_ch_tag_sync_request/fields/expired_date.field.yml +6 -0
- package/main/default/objects/pepsico_ch_tag_sync_request/fields/item_count.field.yml +6 -0
- package/main/default/objects/pepsico_ch_tag_sync_request/fields/name.field.yml +7 -0
- package/main/default/objects/pepsico_ch_tag_sync_request/fields/request_id.field.yml +7 -0
- package/main/default/objects/pepsico_ch_tag_sync_request/fields/response.field.yml +9 -0
- package/main/default/objects/pepsico_ch_tag_sync_request/fields/result_code.field.yml +6 -0
- package/main/default/objects/pepsico_ch_tag_sync_request/fields/source_ip.field.yml +6 -0
- package/main/default/objects/pepsico_ch_tag_sync_request/fields/space.field.yml +6 -0
- package/main/default/objects/pepsico_ch_tag_sync_request/fields/tag_id.field.yml +6 -0
- package/main/default/objects/pepsico_ch_tag_sync_request/fields/tag_label.field.yml +6 -0
- package/main/default/objects/pepsico_ch_tag_sync_request/listviews/all.listview.yml +19 -0
- package/main/default/objects/pepsico_ch_tag_sync_request/pepsico_ch_tag_sync_request.object.yml +16 -0
- package/main/default/objects/pepsico_ch_tag_sync_request/permissions/admin.permission.yml +15 -0
- package/main/default/objects/pepsico_ch_tag_sync_request/permissions/user.permission.yml +14 -0
- package/main/default/objects/pepsico_material/fields/material_type_label.field.yml +10 -0
- package/main/default/objects/pepsico_material_approval/buttons/pepsico_confirm_online.button.yml +78 -1
- package/main/default/objects/pepsico_material_approval/buttons/pepsico_modify_pls.button.yml +1 -1
- package/main/default/objects/pepsico_material_approval/fields/available_channels.field.yml +2 -0
- package/main/default/objects/pepsico_material_approval/fields/available_channels_label.field.yml +10 -0
- package/main/default/objects/pepsico_material_approval/fields/ch_material_group.field.yml +56 -0
- package/main/default/objects/pepsico_material_approval/fields/ch_material_group_path.field.yml +14 -0
- package/main/default/objects/pepsico_material_approval/fields/ch_sync_enabled.field.yml +11 -0
- package/main/default/objects/pepsico_material_approval/fields/ch_sync_rule_check.field.yml +55 -0
- package/main/default/objects/pepsico_material_approval/fields/content_rule.field.yml +2 -0
- package/main/default/objects/pepsico_material_approval/fields/content_rule_label.field.yml +10 -0
- package/main/default/objects/pepsico_material_approval/fields/ec_check_food_name.field.yml +2 -2
- package/main/default/objects/pepsico_material_approval/fields/ec_check_ingredients.field.yml +2 -2
- package/main/default/objects/pepsico_material_approval/fields/ec_check_net_content.field.yml +2 -2
- package/main/default/objects/pepsico_material_approval/fields/ec_check_producer_address.field.yml +2 -2
- package/main/default/objects/pepsico_material_approval/fields/ec_check_producer_name.field.yml +2 -2
- package/main/default/objects/pepsico_material_approval/fields/ec_check_shelf_life.field.yml +2 -2
- package/main/default/objects/pepsico_material_approval/fields/ec_check_standard_code.field.yml +2 -2
- package/main/default/objects/pepsico_material_approval/fields/ec_check_storage_condition.field.yml +2 -2
- package/main/default/objects/pepsico_material_approval/fields/ec_has_detail_page.field.yml +1 -1
- package/main/default/objects/pepsico_material_approval/fields/ec_producers.field.yml +1 -1
- package/main/default/objects/pepsico_material_approval/fields/ec_qc_production_email.field.yml +2 -2
- package/main/default/objects/pepsico_material_approval/fields/ec_related_approval.field.yml +1 -1
- package/main/default/objects/pepsico_material_approval/fields/effective_date.field.yml +2 -0
- package/main/default/objects/pepsico_material_approval/fields/expiry_date.field.yml +2 -0
- package/main/default/objects/pepsico_material_approval/fields/form_version.field.yml +6 -2
- package/main/default/objects/pepsico_material_approval/fields/has_nutrition_table.field.yml +1 -1
- package/main/default/objects/pepsico_material_approval/fields/material_editable.field.yml +78 -0
- package/main/default/objects/pepsico_material_approval/fields/material_type.field.yml +2 -0
- package/main/default/objects/pepsico_material_approval/fields/material_type_label.field.yml +10 -0
- package/main/default/objects/pepsico_material_approval/fields/pls_file.field.yml +1 -1
- package/main/default/objects/pepsico_material_approval/fields/pls_km_files.field.yml +2 -5
- package/main/default/objects/pepsico_material_approval/fields/pls_upload_files.field.yml +1 -1
- package/main/default/objects/pepsico_material_approval/fields/product_label.field.yml +10 -0
- package/main/default/objects/pepsico_material_approval_data/fields/guding_pass.field.yml +9 -1
- package/main/default/objects/pepsico_material_data/fields/guding_pass.field.yml +9 -1
- package/main/default/objects/picklist_options/fields/ch_changed_at.field.yml +8 -0
- package/main/default/objects/picklist_options/picklist_options.object.yml +7 -0
- package/main/default/tabs/object_pepsico_ch_material_group_rule.tab.yml +8 -0
- package/main/default/tabs/object_pepsico_ch_sync_rule.tab.yml +8 -0
- package/main/default/triggers/pepsico_material_approval_tag_label.trigger.yml +80 -0
- package/main/default/triggers/pepsico_material_tag_label.trigger.yml +77 -0
- package/package.json +1 -1
- package/scripts/audit-legacy-tags.js +246 -0
- package/scripts/probe-content-hub-tags.js +134 -0
- package/main/default/objects/pepsico_material_approval/fields/has_detail_page.field.yml +0 -18
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 存量标签数据体检 —— issue #448 上线前必跑。
|
|
3
|
+
*
|
|
4
|
+
* 用法(直接对着历史库跑, 只读, 不写任何数据):
|
|
5
|
+
* mongosh "mongodb://127.0.0.1:27017/pepsico-9-17" steedos-packages/content-review/scripts/audit-legacy-tags.js
|
|
6
|
+
*
|
|
7
|
+
* 查七件事:
|
|
8
|
+
* 1. C2 存量 tagValId 是否只含字母数字下划线、≤64 —— 违规的将来被 40003 挡掉, 永远同步不了
|
|
9
|
+
* 2. C5 树深是否 ≤3 —— 超了的话给其下加子节点会被拒
|
|
10
|
+
* 3. C1 同一类目内 value 是否唯一 —— 重复会让定位键失效
|
|
11
|
+
* 4. 父级引用是否悬空 —— 悬空的在下拉里会被当成一级节点, 层级静默错位
|
|
12
|
+
* 5. 水位线 多少条缺 ch_changed_at —— 缺的没有乱序保护, 会被旧事件覆盖
|
|
13
|
+
* 6. 历史单据 引用了不可见/不存在选项的单据数 —— 方案 §10 要求历史单据不得显示为空
|
|
14
|
+
* 7. 硬编码路径 三个元数据文件里写死的 7 条素材类型路径是否仍然成立(见 P1)
|
|
15
|
+
*
|
|
16
|
+
* 判"可见"的口径照抄真实数据源, 不是想当然:
|
|
17
|
+
* 四个字段的 source 都是 picklist_options(filters:[["enable","=",true]]),
|
|
18
|
+
* adaptor 再按 parent.value 自底向上拼树。于是
|
|
19
|
+
* - enable 不等于 true(含字段缺失)的选项, 下拉里根本没有;
|
|
20
|
+
* - 父级被过滤掉时, 其下整棵子树都拼不进树, 即使子节点自己 enable=true。
|
|
21
|
+
* 只数"自己被停用"会显著少报受影响面。
|
|
22
|
+
*/
|
|
23
|
+
const WHITELIST = {
|
|
24
|
+
'223321001114213': '品牌/产品',
|
|
25
|
+
'223321001114222': '素材类型',
|
|
26
|
+
'223321001114944': '可使用渠道',
|
|
27
|
+
'223321022150011': '素材授权规则'
|
|
28
|
+
};
|
|
29
|
+
// 审批单上引用这四类标签的字段(值为 '-' 连接的完整路径, 可能是字符串或数组)
|
|
30
|
+
const APPROVAL_FIELDS = {
|
|
31
|
+
'223321001114213': ['product'],
|
|
32
|
+
'223321001114222': ['material_type'],
|
|
33
|
+
'223321001114944': ['available_channels'],
|
|
34
|
+
'223321022150011': ['content_rule']
|
|
35
|
+
};
|
|
36
|
+
// 写死在元数据里、驱动"上线必填失效日期+授权规则+渠道"这条规则的 7 条素材类型路径。
|
|
37
|
+
// 出处: fields/expiry_date.field.yml、fields/content_rule.field.yml、
|
|
38
|
+
// buttons/pepsico_confirm_online.button.yml(3 处)
|
|
39
|
+
const HARDCODED_PATHS = [
|
|
40
|
+
'223321001114363-223321001114372',
|
|
41
|
+
'223321001114363-223321001114381',
|
|
42
|
+
'223321001114363-223321001114395',
|
|
43
|
+
'223321001114363-2233305175173759',
|
|
44
|
+
'223321001114468-22334121112868126',
|
|
45
|
+
'2233305104650610-2233305104650621',
|
|
46
|
+
'2233305104650610-2233305104650637'
|
|
47
|
+
];
|
|
48
|
+
const VALID = /^[A-Za-z0-9_]+$/;
|
|
49
|
+
const MAX_LEN = 64, MAX_DEPTH = 3, SEP = '-';
|
|
50
|
+
|
|
51
|
+
const line = (s) => print(s);
|
|
52
|
+
const head = (s) => { print(''); print('═══ ' + s + ' ═══'); };
|
|
53
|
+
|
|
54
|
+
let totalProblems = 0;
|
|
55
|
+
const flag = (n) => { totalProblems += n; return n; };
|
|
56
|
+
|
|
57
|
+
head('0. 托管类目');
|
|
58
|
+
const picklists = {};
|
|
59
|
+
Object.keys(WHITELIST).forEach((code) => {
|
|
60
|
+
const pl = db.picklists.findOne({ code: code });
|
|
61
|
+
if (!pl) { line(' ✗ ' + code + ' ' + WHITELIST[code] + ' —— 类目不存在!接口会返回 50000'); flag(1); return; }
|
|
62
|
+
picklists[code] = pl;
|
|
63
|
+
line(' ✓ ' + code + ' ' + WHITELIST[code] + ' (_id=' + pl._id + ')');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// 各类目的索引, 后面硬编码路径那节要跨类目找, 所以存下来
|
|
67
|
+
const indexes = {};
|
|
68
|
+
|
|
69
|
+
Object.keys(picklists).forEach((code) => {
|
|
70
|
+
const pl = picklists[code];
|
|
71
|
+
head(WHITELIST[code] + ' ' + code);
|
|
72
|
+
|
|
73
|
+
const opts = db.picklist_options.find({ picklist: pl._id }).toArray();
|
|
74
|
+
const byId = {}, byValue = {};
|
|
75
|
+
opts.forEach((o) => { byId[o._id] = o; (byValue[o.value] = byValue[o.value] || []).push(o); });
|
|
76
|
+
|
|
77
|
+
// parent 存的是 _id, 但历史数据里也见过直接存 value 的, 两种都认
|
|
78
|
+
const parentOf = (o) => (o.parent ? (byId[o.parent] || (byValue[o.parent] && byValue[o.parent][0]) || null) : null);
|
|
79
|
+
const hasParentRef = (o) => o.parent !== null && o.parent !== undefined && o.parent !== '';
|
|
80
|
+
|
|
81
|
+
// value 允许含 '-'(恰恰是 C2 要查的违规), 所以解析末级不能 split('-').pop(),
|
|
82
|
+
// 那样 'LEGACY-OLD-01' 会被切成 '01'。改成在已知 value 里做最长后缀匹配。
|
|
83
|
+
const valuesByLen = Object.keys(byValue).sort((a, b) => b.length - a.length);
|
|
84
|
+
const leafCache = {};
|
|
85
|
+
const resolveLeaf = (path) => {
|
|
86
|
+
const p = String(path);
|
|
87
|
+
if (leafCache[p] !== undefined) return leafCache[p];
|
|
88
|
+
let hit = null;
|
|
89
|
+
for (let i = 0; i < valuesByLen.length; i++) {
|
|
90
|
+
const v = valuesByLen[i];
|
|
91
|
+
if (p === v || (p.length > v.length && p.slice(-(v.length + SEP.length)) === SEP + v)) { hit = byValue[v][0]; break; }
|
|
92
|
+
}
|
|
93
|
+
leafCache[p] = hit;
|
|
94
|
+
return hit;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// 可见性: 照抄数据源口径 —— 自己 enable=true, 且所有祖先都 enable=true
|
|
98
|
+
const visibilityOf = (o) => {
|
|
99
|
+
let c = o, guard = 0;
|
|
100
|
+
while (c && guard++ < 20) {
|
|
101
|
+
if (c.enable !== true) return c === o ? 'self_disabled' : 'ancestor_disabled';
|
|
102
|
+
if (!hasParentRef(c)) return 'visible';
|
|
103
|
+
const p = parentOf(c);
|
|
104
|
+
if (!p) return 'root_promoted'; // 父级悬空: 下拉里会被当一级节点, 层级错位但仍可见
|
|
105
|
+
c = p;
|
|
106
|
+
}
|
|
107
|
+
return 'cycle';
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
indexes[code] = { pl: pl, opts: opts, byId: byId, byValue: byValue, parentOf: parentOf, resolveLeaf: resolveLeaf, visibilityOf: visibilityOf };
|
|
111
|
+
|
|
112
|
+
const enabled = opts.filter((o) => o.enable === true);
|
|
113
|
+
line(' 选项总数 ' + opts.length + ' (enable=true ' + enabled.length +
|
|
114
|
+
' / 其余 ' + (opts.length - enabled.length) + ', 其余含 enable 字段缺失, 下拉里同样不出现)');
|
|
115
|
+
const invisible = opts.filter((o) => { const v = visibilityOf(o); return v === 'self_disabled' || v === 'ancestor_disabled'; });
|
|
116
|
+
const byAncestor = opts.filter((o) => visibilityOf(o) === 'ancestor_disabled');
|
|
117
|
+
line(' 下拉里不可见 ' + invisible.length + ' 条, 其中 ' + byAncestor.length + ' 条自己是启用的, 只因父级被停用而整棵子树消失');
|
|
118
|
+
|
|
119
|
+
// --- C2 字符集与长度 ---
|
|
120
|
+
const badChar = opts.filter((o) => !VALID.test(String(o.value)));
|
|
121
|
+
const tooLong = opts.filter((o) => String(o.value).length > MAX_LEN);
|
|
122
|
+
if (badChar.length) {
|
|
123
|
+
line(' ✗ C2 字符集违规 ' + flag(badChar.length) + ' 条 —— 这些标签将来无法同步(40003):');
|
|
124
|
+
badChar.slice(0, 10).forEach((o) => line(' ' + o.value + ' ' + o.name));
|
|
125
|
+
if (badChar.length > 10) line(' … 共 ' + badChar.length + ' 条');
|
|
126
|
+
} else { line(' ✓ C2 字符集'); }
|
|
127
|
+
if (tooLong.length) { line(' ✗ C2 超长 ' + flag(tooLong.length) + ' 条'); } else { line(' ✓ C2 长度'); }
|
|
128
|
+
|
|
129
|
+
// --- C1 value 唯一 ---
|
|
130
|
+
const dups = Object.keys(byValue).filter((v) => byValue[v].length > 1);
|
|
131
|
+
if (dups.length) {
|
|
132
|
+
line(' ✗ C1 value 重复 ' + flag(dups.length) + ' 个 —— 定位键失效:');
|
|
133
|
+
dups.slice(0, 10).forEach((v) => line(' ' + v + ' ×' + byValue[v].length +
|
|
134
|
+
' (' + byValue[v].map((o) => o.name).join(' / ') + ')'));
|
|
135
|
+
} else { line(' ✓ C1 value 唯一'); }
|
|
136
|
+
|
|
137
|
+
// --- 父级悬空 ---
|
|
138
|
+
const dangling = opts.filter((o) => hasParentRef(o) && !parentOf(o));
|
|
139
|
+
if (dangling.length) {
|
|
140
|
+
line(' ✗ 父级悬空 ' + flag(dangling.length) + ' 条 —— parent 指向的选项在本类目里找不到,');
|
|
141
|
+
line(' adaptor 会把它们当一级节点渲染, 层级静默错位:');
|
|
142
|
+
dangling.slice(0, 10).forEach((o) => line(' ' + o.value + ' ' + o.name + ' parent=' + o.parent));
|
|
143
|
+
} else { line(' ✓ 无父级悬空'); }
|
|
144
|
+
|
|
145
|
+
// --- C5 层级 ---
|
|
146
|
+
const depthOf = (o) => { let d = 1, c = o, guard = 0; while (c && parentOf(c) && guard++ < 20) { c = parentOf(c); d++; } return d; };
|
|
147
|
+
const deep = opts.filter((o) => depthOf(o) > MAX_DEPTH);
|
|
148
|
+
const maxDepth = opts.reduce((m, o) => Math.max(m, depthOf(o)), 0);
|
|
149
|
+
line(' 实际最大层级 ' + maxDepth);
|
|
150
|
+
if (deep.length) {
|
|
151
|
+
line(' ✗ C5 超过 ' + MAX_DEPTH + ' 层 ' + flag(deep.length) + ' 条:');
|
|
152
|
+
deep.slice(0, 10).forEach((o) => line(' ' + o.value + ' ' + o.name + ' 第' + depthOf(o) + '层'));
|
|
153
|
+
} else { line(' ✓ C5 层级'); }
|
|
154
|
+
|
|
155
|
+
// --- 水位线 ---
|
|
156
|
+
const noWm = opts.filter((o) => !o.ch_changed_at).length;
|
|
157
|
+
if (noWm) {
|
|
158
|
+
line(' ⚠ 缺 ch_changed_at ' + noWm + ' 条 —— 没有乱序保护, 首个到达的事件无论多旧都会被应用。');
|
|
159
|
+
line(' (四个类目的补基线命令统一打印在最后)');
|
|
160
|
+
} else { line(' ✓ 水位线齐全'); }
|
|
161
|
+
|
|
162
|
+
// --- 历史单据引用 ---
|
|
163
|
+
const fields = APPROVAL_FIELDS[code] || [];
|
|
164
|
+
fields.forEach((f) => {
|
|
165
|
+
const q = {}; q[f] = { $exists: true, $ne: null };
|
|
166
|
+
const proj = { _id: 1, name: 1 }; proj[f] = 1;
|
|
167
|
+
const rows = db.pepsico_material_approval.find(q, proj).toArray();
|
|
168
|
+
const invisibleHit = [], orphanHit = [];
|
|
169
|
+
let refCount = 0;
|
|
170
|
+
rows.forEach((r) => {
|
|
171
|
+
const vals = Array.isArray(r[f]) ? r[f] : [r[f]];
|
|
172
|
+
vals.forEach((path) => {
|
|
173
|
+
if (!path) return;
|
|
174
|
+
refCount++;
|
|
175
|
+
const hit = resolveLeaf(path);
|
|
176
|
+
if (!hit) { orphanHit.push(r._id + ' → ' + path); return; }
|
|
177
|
+
const vis = visibilityOf(hit);
|
|
178
|
+
if (vis === 'self_disabled' || vis === 'ancestor_disabled') {
|
|
179
|
+
invisibleHit.push(r._id + ' → ' + path + ' (' + hit.name + ', ' +
|
|
180
|
+
(vis === 'self_disabled' ? '本身停用' : '父级停用') + ')');
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
line(' 历史单据字段 ' + f + ': ' + rows.length + ' 张单据, ' + refCount + ' 处引用');
|
|
185
|
+
if (orphanHit.length) {
|
|
186
|
+
line(' ✗ 孤儿引用 ' + flag(orphanHit.length) + ' 处 —— 选项表里找不到, 表单已无法回显:');
|
|
187
|
+
orphanHit.slice(0, 5).forEach((s) => line(' ' + s));
|
|
188
|
+
if (orphanHit.length > 5) line(' … 共 ' + orphanHit.length + ' 处');
|
|
189
|
+
} else { line(' ✓ 无孤儿引用'); }
|
|
190
|
+
if (invisibleHit.length) {
|
|
191
|
+
line(' ⚠ 引用下拉里已不可见的选项 ' + invisibleHit.length + ' 处 —— 数据源过滤 enable=true,');
|
|
192
|
+
line(' 方案 §10 要求历史单据不得因停用而显示为空, 这些就是受影响面:');
|
|
193
|
+
invisibleHit.slice(0, 5).forEach((s) => line(' ' + s));
|
|
194
|
+
if (invisibleHit.length > 5) line(' … 共 ' + invisibleHit.length + ' 处');
|
|
195
|
+
} else { line(' ✓ 无引用不可见选项'); }
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// --- 硬编码路径(P1) ---
|
|
200
|
+
head('硬编码标签路径 7 条');
|
|
201
|
+
line(' 出处: fields/expiry_date.field.yml、fields/content_rule.field.yml、buttons/pepsico_confirm_online.button.yml');
|
|
202
|
+
line(' 这 7 条驱动"上线必填失效日期 + 授权规则 + 渠道", 形如 material_type.includes(\'…\')。');
|
|
203
|
+
line(' 匹配不上时必填校验静默失效, 没有任何报错 —— 所以这节要盯住"路径是否仍然成立"。');
|
|
204
|
+
const codes = Object.keys(indexes);
|
|
205
|
+
HARDCODED_PATHS.forEach((path) => {
|
|
206
|
+
let found = null, foundCode = null;
|
|
207
|
+
for (let i = 0; i < codes.length; i++) {
|
|
208
|
+
const hit = indexes[codes[i]].resolveLeaf(path);
|
|
209
|
+
if (hit) { found = hit; foundCode = codes[i]; break; }
|
|
210
|
+
}
|
|
211
|
+
if (!found) { line(' ✗ ' + path + ' 末级在四个托管类目里都找不到 —— 规则已失效'); flag(1); return; }
|
|
212
|
+
const ix = indexes[foundCode];
|
|
213
|
+
// 从末级往上拼回完整路径, 与写死的那条逐字比对 —— 这同时验了父子关系(C4)
|
|
214
|
+
const chain = []; let c = found, guard = 0;
|
|
215
|
+
while (c && guard++ < 20) { chain.unshift(c); c = ix.parentOf(c); }
|
|
216
|
+
const actual = chain.map((o) => o.value).join(SEP);
|
|
217
|
+
const names = chain.map((o) => o.name).join(' > ');
|
|
218
|
+
const vis = ix.visibilityOf(found);
|
|
219
|
+
const ok = actual === path;
|
|
220
|
+
line(' ' + (ok && vis === 'visible' ? '✓' : '✗') + ' ' + path);
|
|
221
|
+
line(' 名称 ' + names + ' (类目: ' + WHITELIST[foundCode] + ')');
|
|
222
|
+
if (!ok) { line(' ✗ 实际路径已变成 ' + actual + ' —— 父级被移动过, 公式匹配不上了'); flag(1); }
|
|
223
|
+
if (vis !== 'visible') { line(' ✗ 当前不可见(' + vis + ') —— 新单据选不到, 但公式仍在引用'); flag(1); }
|
|
224
|
+
const f = (APPROVAL_FIELDS[foundCode] || [])[0];
|
|
225
|
+
if (f) {
|
|
226
|
+
const q = {}; q[f] = path;
|
|
227
|
+
line(' 历史单据精确引用此路径 ' + db.pepsico_material_approval.countDocuments(q) + ' 张');
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// --- P0-b 补基线 ---
|
|
232
|
+
head('上线补基线(P0-b, 存量记录缺水位线则乱序保护整体失效)');
|
|
233
|
+
const ids = Object.keys(picklists).map((c) => '"' + picklists[c]._id + '"').join(', ');
|
|
234
|
+
line(' 初始导入完成、接口上线那一刻执行(把时间换成真实上线时刻):');
|
|
235
|
+
line('');
|
|
236
|
+
line(' db.picklist_options.updateMany(');
|
|
237
|
+
line(' { picklist: { $in: [' + ids + '] }, ch_changed_at: { $exists: false } },');
|
|
238
|
+
line(' { $set: { ch_changed_at: new Date("<接口上线时刻>") } })');
|
|
239
|
+
line('');
|
|
240
|
+
line(' 不补的话: 代码判据是 if (existing && existing.ch_changed_at), 存量记录整个跳过水位线比较,');
|
|
241
|
+
line(' 半年前的改名事件推进来也会被直接应用, 正确名称被覆盖。');
|
|
242
|
+
|
|
243
|
+
head('结论');
|
|
244
|
+
line(totalProblems === 0
|
|
245
|
+
? ' 未发现阻塞性问题(⚠ 项仍需按上面的提示处理)。'
|
|
246
|
+
: ' 发现 ' + totalProblems + ' 处需要处理的问题, 见上。');
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 拉取 Content Hub 真实标签树, 校验 issue #448 的 C1 / C2 / C5 约束。
|
|
4
|
+
*
|
|
5
|
+
* 用途: 回答 issue #448 待确认 #1「tagValId 是否满足 C1/C2」——
|
|
6
|
+
* 该项标注「不满足则方案重做」, 联调前必须先验。
|
|
7
|
+
*
|
|
8
|
+
* 凭据只从环境变量读, 不写进仓库:
|
|
9
|
+
* CH_BASE_URL=https://cback.getmogic.com \
|
|
10
|
+
* CH_APP_ID=... CH_APP_SECRET=... CH_TENANT_ID=... CH_WORKSPACE_ID=... \
|
|
11
|
+
* node scripts/probe-content-hub-tags.js
|
|
12
|
+
*/
|
|
13
|
+
const https = require('https');
|
|
14
|
+
const { URL } = require('url');
|
|
15
|
+
|
|
16
|
+
const BASE = (process.env.CH_BASE_URL || '').replace(/\/+$/, '');
|
|
17
|
+
const APP_ID = process.env.CH_APP_ID;
|
|
18
|
+
const APP_SECRET = process.env.CH_APP_SECRET;
|
|
19
|
+
const TENANT_ID = process.env.CH_TENANT_ID;
|
|
20
|
+
const WORKSPACE_ID = process.env.CH_WORKSPACE_ID;
|
|
21
|
+
|
|
22
|
+
if (!BASE || !APP_ID || !APP_SECRET || !TENANT_ID || !WORKSPACE_ID) {
|
|
23
|
+
console.error('缺少环境变量: CH_BASE_URL / CH_APP_ID / CH_APP_SECRET / CH_TENANT_ID / CH_WORKSPACE_ID');
|
|
24
|
+
process.exit(2);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 与服务端实现保持一致(见 src/methods/content_hub_tag.ts)
|
|
28
|
+
const TAG_VAL_ID_PATTERN = /^[A-Za-z0-9_]+$/;
|
|
29
|
+
const MAX_TAG_VAL_ID_LENGTH = 64;
|
|
30
|
+
const MAX_TAG_DEPTH = 3;
|
|
31
|
+
|
|
32
|
+
const post = (path, body, headers) => new Promise((resolve, reject) => {
|
|
33
|
+
const payload = JSON.stringify(body);
|
|
34
|
+
const url = new URL(BASE + path);
|
|
35
|
+
const req = https.request({
|
|
36
|
+
hostname: url.hostname,
|
|
37
|
+
port: url.port || 443,
|
|
38
|
+
path: url.pathname + url.search,
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: Object.assign({
|
|
41
|
+
'Content-Type': 'application/json;charset=UTF-8',
|
|
42
|
+
'Content-Length': Buffer.byteLength(payload)
|
|
43
|
+
}, headers || {})
|
|
44
|
+
}, (res) => {
|
|
45
|
+
let raw = '';
|
|
46
|
+
res.on('data', (c) => { raw += c; });
|
|
47
|
+
res.on('end', () => {
|
|
48
|
+
try {
|
|
49
|
+
resolve({ status: res.statusCode, body: JSON.parse(raw) });
|
|
50
|
+
} catch (e) {
|
|
51
|
+
resolve({ status: res.statusCode, body: raw.slice(0, 400) });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
req.on('error', reject);
|
|
56
|
+
req.write(payload);
|
|
57
|
+
req.end();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const TAGS = [
|
|
61
|
+
{ key: 'brand-product', label: '品牌/产品', tagId: '223321001114213' },
|
|
62
|
+
{ key: 'channel', label: '可使用渠道', tagId: '223321001114944' },
|
|
63
|
+
{ key: 'material-type', label: '素材类型', tagId: '223321001114222' }
|
|
64
|
+
// 注意: 对方 API v2 文档里没有「素材授权规则」(223321022150011) 的查询接口,
|
|
65
|
+
// 但它在 issue #448 的托管白名单内 —— 需要向 Content Hub 确认。
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
(async () => {
|
|
69
|
+
console.log('=== T0 换取 tenant-access-token ===');
|
|
70
|
+
const auth = await post('/creativeSaas/auth/v1/tenantAccessToken/internal', { appId: APP_ID, appSecret: APP_SECRET });
|
|
71
|
+
const token = auth.body && auth.body.data && auth.body.data.tenantAccessToken;
|
|
72
|
+
if (!token) {
|
|
73
|
+
console.error('取 token 失败:', JSON.stringify(auth.body).slice(0, 300));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
console.log(' ok, expire =', auth.body.data.expire);
|
|
77
|
+
|
|
78
|
+
const problems = [];
|
|
79
|
+
for (const tag of TAGS) {
|
|
80
|
+
console.log(`\n=== ${tag.label} (${tag.tagId}) ===`);
|
|
81
|
+
const res = await post(`/creativeSaas/pepsico/compliance/v1/tags/${tag.key}`,
|
|
82
|
+
{ tenantId: TENANT_ID, workspaceId: WORKSPACE_ID, keyword: null },
|
|
83
|
+
{ 'tenant-access-token': token });
|
|
84
|
+
const list = res.body && res.body.data && (res.body.data.list || res.body.data);
|
|
85
|
+
if (!Array.isArray(list)) {
|
|
86
|
+
console.error(' 返回结构不是数组:', JSON.stringify(res.body).slice(0, 300));
|
|
87
|
+
problems.push(`${tag.label}: 返回结构异常`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const seen = new Map();
|
|
92
|
+
let total = 0, maxDepth = 0, disabled = 0;
|
|
93
|
+
const badCharset = [], tooLong = [], tooDeep = [], duplicated = [];
|
|
94
|
+
|
|
95
|
+
const walk = (nodes, depth) => {
|
|
96
|
+
for (const node of nodes || []) {
|
|
97
|
+
total += 1;
|
|
98
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
99
|
+
const id = String(node.id);
|
|
100
|
+
if (!TAG_VAL_ID_PATTERN.test(id)) badCharset.push(`${id} (${node.value})`);
|
|
101
|
+
if (id.length > MAX_TAG_VAL_ID_LENGTH) tooLong.push(id);
|
|
102
|
+
if (depth > MAX_TAG_DEPTH) tooDeep.push(`${id} 第 ${depth} 层 (${node.value})`);
|
|
103
|
+
if (seen.has(id)) duplicated.push(`${id}: ${seen.get(id)} / ${node.value}`);
|
|
104
|
+
else seen.set(id, node.value);
|
|
105
|
+
if (node.disable === true || node.status === 0) disabled += 1;
|
|
106
|
+
walk(node.childTagList, depth + 1);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
walk(list, 1);
|
|
110
|
+
|
|
111
|
+
console.log(` 节点总数 ${total}, 最大层级 ${maxDepth}, 停用节点 ${disabled}`);
|
|
112
|
+
const report = (name, arr, constraint) => {
|
|
113
|
+
if (arr.length) {
|
|
114
|
+
console.log(` ✗ ${constraint} 违反 ${arr.length} 处: ${arr.slice(0, 5).join('; ')}${arr.length > 5 ? ' …' : ''}`);
|
|
115
|
+
problems.push(`${tag.label} ${constraint}: ${arr.length} 处`);
|
|
116
|
+
} else {
|
|
117
|
+
console.log(` ✓ ${constraint}`);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
report('charset', badCharset, 'C2 只含字母数字下划线');
|
|
121
|
+
report('length', tooLong, `C2 长度 ≤ ${MAX_TAG_VAL_ID_LENGTH}`);
|
|
122
|
+
report('depth', tooDeep, `C5 层级 ≤ ${MAX_TAG_DEPTH}`);
|
|
123
|
+
report('dup', duplicated, 'C1 ID 唯一');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log('\n================ 结论 ================');
|
|
127
|
+
if (problems.length) {
|
|
128
|
+
console.log('发现约束冲突, 联调前必须先解决:');
|
|
129
|
+
problems.forEach((p) => console.log(' - ' + p));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
console.log('三个标签类目均满足 C1 / C2 / C5, 可以按现方案联调。');
|
|
133
|
+
console.log('另需向 Content Hub 确认: 素材授权规则 223321022150011 的查询接口(v2 文档未提供)。');
|
|
134
|
+
})().catch((e) => { console.error('探测失败:', e.message); process.exit(1); });
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
name: has_detail_page
|
|
2
|
-
label: 素材是否包含商品详情页
|
|
3
|
-
type: select
|
|
4
|
-
data_type: text
|
|
5
|
-
group: 基本信息
|
|
6
|
-
inlineHelpText: 涉及到商品详情页的素材,请关联pls文件
|
|
7
|
-
sort_no: 805
|
|
8
|
-
is_wide: true
|
|
9
|
-
required: true
|
|
10
|
-
amis:
|
|
11
|
-
type: button-group-select
|
|
12
|
-
multiple: false
|
|
13
|
-
options:
|
|
14
|
-
- label: 是
|
|
15
|
-
value: 'yes'
|
|
16
|
-
- label: 否
|
|
17
|
-
value: 'no'
|
|
18
|
-
visible_on: (classification == 'MD-PO1-PO15' || classification == 'SLS-PO2-PO25') && form_version == 'v1'
|