pgy-schema.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. // 蒲公英博主字段 schema(结构化自《field_dictionary.md》)
  2. // 边界:仅沉淀字段定义 + 建表参考 + 选号查询映射,供未来建库/开发查询接口时直接消费,不在此建库。
  3. // 源码映射:hc_pgy_spider/pgy_field_map.py;采集接口共 9 个详情 + 1 个列表。
  4. // 采集接口(endpointId 供未来查询接口注册使用)
  5. const SOURCES = {
  6. list: { name: '列表 blogger/v2', method: 'POST', path: 'xiaohongshu-pgy/api/solar/cooperator/blogger/v2', priority: 3 },
  7. profile: { name: '个人简介', method: 'GET', path: 'cooperator/user/blogger/{id}', priority: 2 },
  8. notesRate: { name: '数据概括 notesRate?noteType=3', method: 'GET', path: 'kol/dataV3/notesRate', priority: 2 },
  9. noteData: { name: '笔记数据 notesRate×3', method: 'GET', path: 'kol/dataV3/notesRate', priority: 1 },
  10. notesDetail: { name: '笔记案例 notesDetail×3', method: 'GET', path: 'kol/dataV2/notesDetail', priority: 1 },
  11. fansSummary: { name: '粉丝概览 fansSummary', method: 'GET', path: 'kol/dataV3/fansSummary', priority: 2 },
  12. fansProfile: { name: '粉丝画像 fans_profile', method: 'GET', path: 'kol/data/{id}/fans_profile', priority: 1 },
  13. dataSummary: { name: '数据表现总览 data_summary', method: 'GET', path: 'kol/data_v3/data_summary', priority: 1 },
  14. costEffective: { name: '流量性价比 costEffective', method: 'GET', path: 'kol/dataV2/costEffective', priority: 0 }
  15. };
  16. // 选号查询维度 → 蒲公英字段 + MongoDB 查询示例
  17. // 这是未来「选号时开发接口查询」的核心映射:rank/platform-quality-rules 的每个维度对应哪些字段、怎么查。
  18. const DIMENSIONS = {
  19. fanRange: {
  20. zh: '粉丝范围',
  21. fields: ['follower_count'],
  22. type: 'number',
  23. unit: '人',
  24. query: { follower_count: { $gte: 50000, $lte: 500000 } },
  25. note: '腰部 KOL 5万-50万;对应 ranker.fanRange 与 platform-quality-rules 分级'
  26. },
  27. interaction: {
  28. zh: '互动率/互动中位数',
  29. fields: ['interaction_rate', 'interact_midnum', 'inter_midnum_30d_total'],
  30. type: 'number',
  31. unit: '% / 次',
  32. query: { interaction_rate: { $gte: 3 } },
  33. note: '小红书互动率 ≥3% 优秀、≥1% 合格;抖音 ≥3% 优秀、≥1.5% 合格'
  34. },
  35. price: {
  36. zh: '报价(必采)',
  37. fields: ['picture_price', 'video_price', 'lower_price'],
  38. type: 'number',
  39. unit: '元',
  40. query: { lower_price: { $lte: 3000 } },
  41. note: '⭐ 列表接口只采这 3 个报价字段'
  42. },
  43. verticality: {
  44. zh: '垂直度/类目',
  45. fields: ['trade_type', 'content_tags', 'feature_tags', 'note_type_share'],
  46. type: 'string/array',
  47. query: { trade_type: '美妆护肤', content_tags: { $all: ['成分党'] } },
  48. note: '小红书垂直度 ≥70% 合格、≥90% 优秀'
  49. },
  50. score: {
  51. zh: '评分/信用/等级',
  52. fields: ['credit_level', 'goodness_level', 'level_name'],
  53. type: 'string',
  54. query: { credit_level: { $in: ['Lv5', 'Lv6', 'Lv7'] } },
  55. note: '蒲公英评分 ≥4.0 合格、≥4.5 优秀、<3.5 暂不考虑'
  56. },
  57. location: {
  58. zh: '所在地/城市',
  59. fields: ['location'],
  60. type: 'string',
  61. query: { location: { $regex: '上海' } },
  62. note: '地域定向'
  63. },
  64. gender: {
  65. zh: '性别',
  66. fields: ['gender'],
  67. type: 'int',
  68. query: { gender: 1 },
  69. note: '1=女 2=男'
  70. },
  71. fansProfile: {
  72. zh: '粉丝画像(年龄/性别/城市)',
  73. fields: ['fans_age_dist', 'fans_gender_dist', 'fans_city_top'],
  74. type: 'array',
  75. query: { 'fans_age_dist': { $elemMatch: { age: '18-24', rate: { $gte: 40 } } } },
  76. note: '粉丝画像匹配度 ≥60% 合格、≥80% 优秀'
  77. },
  78. completionRate: {
  79. zh: '视频完播率',
  80. fields: ['video_full_view_rate', 'video_finish_rate'],
  81. type: 'number',
  82. unit: '%',
  83. query: { video_full_view_rate: { $gte: 20 } },
  84. note: '抖音完播率 ≥20% 合格、≥40% 优秀、<10% 危险'
  85. },
  86. likeCollectRatio: {
  87. zh: '赞藏比(收藏/点赞)',
  88. fields: ['like_median', 'collect_median'],
  89. type: 'number',
  90. query: { $expr: { $gte: [{ $divide: ['$collect_median', '$like_median'] }, 0.5] } },
  91. note: '小红书赞藏比 ≥0.5 合格、≥1.0 优秀'
  92. },
  93. cpm: {
  94. zh: 'CPM/CPE/阅读成本',
  95. fields: ['estimate_picture_cpm', 'estimate_video_cpm', 'estimate_picture_engage_cost', 'estimate_video_engage_cost'],
  96. type: 'number',
  97. unit: '元',
  98. query: { estimate_video_cpm: { $lte: 80 } },
  99. note: '抖音腰部 CPM 40-80、CPE 5-10'
  100. },
  101. growth: {
  102. zh: '粉丝增长',
  103. fields: ['fans_growth_rate', 'fans_30d_growth_rate', 'fans_30d_growth_num'],
  104. type: 'number',
  105. unit: '%',
  106. query: { fans_30d_growth_rate: { $gte: 0 } },
  107. note: '负增长 = 账号健康度风险(platform-quality-rules 已接入)'
  108. },
  109. activity: {
  110. zh: '活跃度',
  111. fields: ['active_days_in_last7', 'is_active', 'last_active_time'],
  112. type: 'mixed',
  113. query: { is_active: true, active_days_in_last7: { $gte: 3 } },
  114. note: '近30天无更新 = 数据真实性风险'
  115. },
  116. verified: {
  117. zh: '认证/合作权限',
  118. fields: ['verified', 'is_pgy_creator', 'has_buyer_auth', 'cooperate_state'],
  119. type: 'bool',
  120. query: { is_pgy_creator: true, has_buyer_auth: true },
  121. note: '蒲公英创作者 + 品牌合作权限是下单前提'
  122. }
  123. };
  124. // 核心字段表(字段名 → 中文/类型/来源接口/选号维度)
  125. const CORE_FIELDS = {
  126. // 顶级必采
  127. blogger_id: { zh: '博主ID', type: 'string', sources: ['list', 'profile'], dimension: 'id', primary: true },
  128. nickname: { zh: '昵称', type: 'string', sources: ['list', 'profile'], dimension: 'id' },
  129. follower_count: { zh: '粉丝数', type: 'int', sources: ['list', 'profile'], dimension: 'fanRange' },
  130. like_count: { zh: '获赞数', type: 'int', sources: ['list', 'profile'], dimension: 'interaction' },
  131. post_count: { zh: '笔记数', type: 'int', sources: ['list'], dimension: 'activity' },
  132. co_note_count: { zh: '合作笔记数', type: 'int', sources: ['list'], dimension: 'activity' },
  133. verified: { zh: '是否认证', type: 'bool', sources: ['list', 'profile'], dimension: 'verified' },
  134. tags: { zh: '个人标签', type: 'array', sources: ['list', 'profile'], dimension: 'verticality' },
  135. content_tags: { zh: '内容标签', type: 'array', sources: ['list'], dimension: 'verticality' },
  136. feature_tags: { zh: '特色标签', type: 'array', sources: ['list'], dimension: 'verticality' },
  137. trade_type: { zh: '行业', type: 'string', sources: ['list'], dimension: 'verticality' },
  138. gender: { zh: '性别', type: 'int', sources: ['list', 'profile'], dimension: 'gender' },
  139. location: { zh: '所在地', type: 'string', sources: ['list', 'profile'], dimension: 'location' },
  140. // 报价(必采)
  141. picture_price: { zh: '图文报价', type: 'number', sources: ['list'], dimension: 'price', priority: 3 },
  142. video_price: { zh: '视频报价', type: 'number', sources: ['list'], dimension: 'price', priority: 3 },
  143. lower_price: { zh: '报价下限', type: 'number', sources: ['list'], dimension: 'price', priority: 3 },
  144. // 互动/数据
  145. interaction_rate: { zh: '互动率', type: 'number', sources: ['notesRate'], dimension: 'interaction', priority: 2 },
  146. interact_midnum: { zh: '互动中位数', type: 'number', sources: ['list'], dimension: 'interaction' },
  147. read_midnum: { zh: '阅读中位数', type: 'number', sources: ['list'], dimension: 'cpm' },
  148. like_median: { zh: '中位点赞量', type: 'number', sources: ['notesRate'], dimension: 'likeCollectRatio' },
  149. collect_median: { zh: '中位收藏量', type: 'number', sources: ['notesRate'], dimension: 'likeCollectRatio' },
  150. video_full_view_rate: { zh: '视频完播率', type: 'number', sources: ['notesRate'], dimension: 'completionRate' },
  151. hundred_like_rate: { zh: '百赞笔记比例', type: 'number', sources: ['notesRate'], dimension: 'interaction' },
  152. kilo_like_rate: { zh: '千赞笔记比例', type: 'number', sources: ['notesRate'], dimension: 'interaction' },
  153. // 粉丝增长/画像
  154. fans_growth_rate: { zh: '30天增粉率', type: 'number', sources: ['fansSummary'], dimension: 'growth' },
  155. fans_30d_growth_rate: { zh: '30天粉丝增长率', type: 'number', sources: ['list', 'dataSummary'], dimension: 'growth' },
  156. fans_active_l28_rate: { zh: '28天活跃粉丝占比', type: 'number', sources: ['list'], dimension: 'activity' },
  157. fans_engage_30d_rate: { zh: '30天互动率', type: 'number', sources: ['list'], dimension: 'interaction' },
  158. fans_age_dist: { zh: '粉丝年龄分布', type: 'array', sources: ['fansProfile'], dimension: 'fansProfile' },
  159. fans_gender_dist: { zh: '粉丝性别分布', type: 'array', sources: ['fansProfile'], dimension: 'fansProfile' },
  160. fans_city_top: { zh: '粉丝城市TOP', type: 'array', sources: ['fansProfile'], dimension: 'fansProfile' },
  161. // 成本/CPM
  162. estimate_picture_cpm: { zh: '估算图文CPM', type: 'number', sources: ['list', 'costEffective'], dimension: 'cpm' },
  163. estimate_video_cpm: { zh: '估算视频CPM', type: 'number', sources: ['list', 'costEffective'], dimension: 'cpm' },
  164. estimate_picture_engage_cost: { zh: '估算图文互动成本', type: 'number', sources: ['list', 'costEffective'], dimension: 'cpm' },
  165. estimate_video_engage_cost: { zh: '估算视频互动成本', type: 'number', sources: ['list', 'costEffective'], dimension: 'cpm' },
  166. // 信用/合作
  167. credit_level: { zh: '信用等级', type: 'string', sources: ['profile'], dimension: 'score' },
  168. goodness_level: { zh: '诚信等级', type: 'string', sources: ['profile'], dimension: 'score' },
  169. level_name: { zh: '等级名称', type: 'string', sources: ['profile'], dimension: 'score' },
  170. is_pgy_creator: { zh: '蒲公英创作者', type: 'bool', sources: ['profile'], dimension: 'verified' },
  171. has_buyer_auth: { zh: '品牌合作权限', type: 'bool', sources: ['list'], dimension: 'verified' },
  172. cooperate_state: { zh: '合作状态', type: 'string', sources: ['list'], dimension: 'verified' },
  173. active_days_in_last7: { zh: '近7天活跃天数', type: 'int', sources: ['dataSummary'], dimension: 'activity' },
  174. is_active: { zh: '是否活跃', type: 'bool', sources: ['dataSummary'], dimension: 'activity' },
  175. last_active_time: { zh: '最后活跃时间', type: 'string', sources: ['profile'], dimension: 'activity' },
  176. response_rate: { zh: '回复率', type: 'number', sources: ['dataSummary'], dimension: 'activity' }
  177. };
  178. // 建议索引(选号查询热路径)
  179. const INDEXES = [
  180. { name: 'uk_blogger_id', spec: { blogger_id: 1 }, options: { unique: true } },
  181. { name: 'idx_fans_desc', spec: { follower_count: -1 }, options: {} },
  182. { name: 'idx_trade_fans', spec: { trade_type: 1, follower_count: -1 }, options: {} },
  183. { name: 'idx_interaction_desc', spec: { interaction_rate: -1 }, options: {} },
  184. { name: 'idx_price_asc', spec: { lower_price: 1 }, options: {} },
  185. { name: 'idx_tags', spec: { content_tags: 1, feature_tags: 1 }, options: {} },
  186. { name: 'idx_location', spec: { location: 1 }, options: {} },
  187. { name: 'idx_active', spec: { is_active: 1, active_days_in_last7: -1 }, options: {} },
  188. { name: 'idx_cpm_asc', spec: { estimate_video_cpm: 1, estimate_picture_cpm: 1 }, options: {} }
  189. ];
  190. // 生成 MongoDB 建表 + 索引 DDL 文本(未来建库直接抄)
  191. function buildDdl(collectionName = 'pgy_blogger') {
  192. const lines = [];
  193. lines.push('// 蒲公英博主 collection(MongoDB)—— 由 pgy-schema 生成,未来建库直接执行');
  194. lines.push(`db.createCollection('${collectionName}');`);
  195. lines.push('');
  196. for (const index of INDEXES) {
  197. lines.push(`db.${collectionName}.createIndex(${JSON.stringify(index.spec)}, ${JSON.stringify({ name: index.name, ...index.options })});`);
  198. }
  199. lines.push('');
  200. lines.push(`// 必采报价字段(列表接口)示例文档:`);
  201. lines.push('// { blogger_id, nickname, follower_count, picture_price, video_price, lower_price, trade_type, content_tags }');
  202. return lines.join('\n');
  203. }
  204. // 生成选号查询映射 markdown(未来接口开发参考)
  205. function buildQueryMap() {
  206. const lines = [];
  207. lines.push('# 蒲公英选号查询映射');
  208. lines.push('');
  209. lines.push('> 由 pgy-schema 生成。选号 SOP 的每个筛选维度 → 蒲公英字段 → MongoDB 查询。');
  210. lines.push('');
  211. lines.push('| 选号维度 | 蒲公英字段 | 查询示例 |');
  212. lines.push('| --- | --- | --- |');
  213. for (const [key, dim] of Object.entries(DIMENSIONS)) {
  214. lines.push(`| ${dim.zh} | ${dim.fields.join('、')} | \`${JSON.stringify(dim.query)}\` |`);
  215. }
  216. lines.push('');
  217. lines.push('## 采集接口优先级');
  218. lines.push('');
  219. lines.push('| 优先级 | 接口 | 核心字段 |');
  220. lines.push('| --- | --- | --- |');
  221. const priorityMap = { 3: '⭐⭐⭐ 必采', 2: '⭐⭐ 高优先', 1: '⭐ 中优先', 0: '低优先' };
  222. for (const [key, source] of Object.entries(SOURCES)) {
  223. lines.push(`| ${priorityMap[source.priority] || '-'} | ${source.name}(\`${source.method} ${source.path}\`) | ${fieldsBySource(key).join('、') || '-'} |`);
  224. }
  225. lines.push('');
  226. lines.push('> 接口总数:9 个详情 + 1 个列表 = 10 个接口。');
  227. return lines.join('\n');
  228. }
  229. function fieldsBySource(sourceKey) {
  230. return Object.entries(CORE_FIELDS)
  231. .filter(([, field]) => field.sources.includes(sourceKey))
  232. .map(([name]) => name)
  233. .slice(0, 12);
  234. }
  235. module.exports = {
  236. SOURCES,
  237. DIMENSIONS,
  238. CORE_FIELDS,
  239. INDEXES,
  240. buildDdl,
  241. buildQueryMap
  242. };