版本:v1 草案 | 2026-08-17 | 作者:backend-dev | 状态:待主会话评审(评审通过后才出 Alembic 迁移,本文只出设计)
上游输入:口径基线 v2.1 §0/§2/§5/§8(唯一口径源)| docs/dev/schema-notes.md(P0-2.2 旧库摸底:8 张达人域旧表逐字段、21 条迁移地雷、8 个开放问题)| docs/dev/jixing-api-notes.md(集星 API 字段与三坑)| docs/dev/msgbridge-contract.md §2.5/§3(binding_mirror 形状)| docs/dev-charter.md v1.3 §4(三分法 + 独立新库)| server/app/models/(现有模型风格)
下游:P0-2.4(influencers 域实现)、P0-2.6(集星富化)、P0-2.7(console 列表页)、P0-3.1(建联匹配/wcid 回流)、P0-5.1(商单模型,回写履约事实)、P1-6(推荐子系统)
xijian / xijian_test(charter §4),旧 lynoxi 表只是一次性导入源,导入后不再依赖;跨库只读仅用于历史成交价/旧订单等(charter §4 三分法「历史旧表」)。jixing_* 100+ 历史快照字段不搬:集星走 API 实时拉(dls.lynoxi.com),本设计只保留「档案级消费字段 + 一份最新指标快照」;P1-6 的特征快照库是另一张表,不在本设计范围。quality_douyin_account 弃用不迁:新「优质」= 系统按 cfg 阈值自动判定(基线 §2),不设人工优质池。人工重点名单概念留讨论位(§7 S5),本期不建表。layer 列。cfg()(app/domains/config_center/cache.py),表里只存判定结果 + 判定时间,绝不存阈值。fingerprint 比对,层级/分级值未变不写。| 约定 | 取值 | 理由 |
|---|---|---|
| 表名前缀 | 无前缀(influencer / douyin_account …) |
charter §4「新表名不再需要 xijian_ 前缀」;xijian_config/xijian_audit_log 是前定案时期的历史命名,不追改。代价与缓解:与 lynoxi 同名 → 纪律「跨库只读一律写全限定名 lynoxi.xxx 且走独立只读 engine」,禁止裸表名跨库 |
| 主键 | id BIGINT AUTO_INCREMENT |
与既有模型一致 |
| 枚举 | VARCHAR(16/24) + 应用层 StrEnum,全小写下划线;不用 MySQL ENUM |
规避地雷 1(旧库大写下划线/小写连字符两套);改枚举值不需要 DDL |
| 金额 | BIGINT,列名带 _fen 后缀(分) |
规避地雷 19 + 基线 §8 单位坑(同表混用分/元差 100 倍),单位写进列名不靠注释 |
| 比率 | DECIMAL(10,4),标度统一 0–1 |
规避地雷 8(0-1 与 0-100 混用) |
| 多值 | JSON 数组(MySQL JSON / SQLite TEXT),不用逗号分隔 |
规避地雷 9(旧库 tags JSON、accountTypes 逗号分隔两种解析函数) |
| 时间 | DATETIME,created_at/updated_at 全表必备(server_default now / onupdate) |
|
| 软删 | deleted_at DATETIME NULL(NULL=未删),走标准 ORM 逻辑删除 |
规避地雷 13(旧库手写局部 UPDATE 生成无 SET 的坏 SQL) |
| 导入锚 | 每张有旧库映射源的表带 legacy_id BIGINT NULL UNIQUE |
导入脚本按此 upsert → 天然可重跑 + 可对账 |
| 外键 | 不建物理 FK,只建索引 + 应用层保证 | 跨域回写(商单/结算回写履约事实)与一次性导入的写入顺序自由;软删是主删除路径,CASCADE 无意义。取舍:孤儿行靠导入对账与定期巡检兜底 |
| # | 新表 | 职责一句话 | 旧库映射源 | 口径出处 |
|---|---|---|---|---|
| 1 | influencer |
达人主档(人维度):身份/联系/入驻/分级/推荐指数/黑名单当前态 | lynoxi.influencer |
基线 §0/§2 |
| 2 | douyin_account |
抖音账号(三层 layer 的唯一落点)+ 档案级集星字段 |
lynoxi.douyin_account(排除全部 jixing_* 快照列) |
基线 §2/§8、jixing §1 |
| 3 | douyin_account_metric |
账号近 30 天指标最新快照(1:1,高频刷新与档案分离) | 不迁旧值;集星 API 重新拉 | 基线 §8、jixing §1/§2 |
| 4 | influencer_binding |
企微绑定镜像(收编 msgbridge 契约的 binding_mirror,字段超集) |
旧库无;来源=消息台 wcid 回流 + 对账接口 | 契约 §2.5/§3.1 |
| 5 | influencer_payment_info |
收款信息(对公/对私)+ 灵工签约手机号一致校验落点 | lynoxi.influencer_payment_info |
基线 §7.6 |
| 6 | influencer_service_region |
达人接单地区(1:N) | lynoxi.influencer_region |
旧表沿用;基线未展开 |
| 7 | influencer_blacklist |
人工拉黑记录(可逆,保留恢复用信用分) | lynoxi.blacklist_douyin_account |
基线 §2「保留人工口子」 |
明确不建的表(各有理由,避免评审时误以为遗漏):
| 不建 | 理由 |
|---|---|
| 优质达人池表 | 基线 §2 的「优质」是系统自动分级,旧 quality_douyin_account 语义冲突弃用(schema-notes §5#6)。人工重点名单见 §7 S5 |
| 达人标签字典表 | 标签值走 JSON 列 + 选项集合走 cfg(§4 新增键,值待业务确认);建字典表属过度设计 |
省市区字典表(旧 region) |
存文本 + 可选行政区划 code(集星给 resident_city_code)已够;级联选择器是前端数据,不落库(§7 S8) |
团长表(旧 manager) |
基线 §0/§6.3「团长只开秒杀、不入库」;团长线在 P1-11 指挥台过渡(§7 S4) |
入园/入住缓存(旧 influencer_visitor_info) |
虽挂达人维度,但语义属商单履约链路 → 归 P0-5.1 商单模型设计 |
| 分级历史表 | 分级变更走既有 xijian_audit_log(action=influencer.grade.change),不新建历史表(YAGNI) |
| 达人档案快照/特征表 | P1-6 推荐子系统的特征快照库,本设计不预建 |
列格式:字段 / 类型 / 口径出处 / 导入映射(
旧表.旧字段 → 转换规则;—=旧库无此概念,新增)
influencer(达人主档,人维度)| 字段 | 类型 | 口径出处 | 导入映射 |
|---|---|---|---|
id |
BIGINT PK | — | 新生成 |
legacy_id |
BIGINT NULL UNIQUE | — | influencer.id(导入幂等锚) |
name |
VARCHAR(64) NULL | 旧表沿用 | influencer.name |
display_name |
VARCHAR(64) NULL | 旧表沿用 | influencer.display_name |
phone |
VARCHAR(20) NULL UNIQUE | 基线 §0 唯一识别键 / §6.11 企微只能搜手机号 | influencer.phone → 去空格/去 +86;空串归一为 NULL(空串会撞唯一键) |
wechat_id |
VARCHAR(64) NULL | 旧表沿用 | influencer.wechat_id(注:企微搜不到微信号,仅存档) |
wx_open_id |
VARCHAR(64) NULL UNIQUE | 小程序身份 | influencer.open_id |
wx_union_id |
VARCHAR(64) NULL | 小程序身份 | influencer.union_id |
gender |
TINYINT NOT NULL DEFAULT 0 | 0未知/1男/2女 | influencer.gender |
birth_year |
SMALLINT NULL | — | 由 influencer.age 反推:age <= 0 一律 NULL(地雷 10:抖音拿不到年龄返回 -1) |
onboarding_status |
VARCHAR(16) NOT NULL DEFAULT 'none' |
旧四态重命名:none/pending/approved/rejected | influencer.onboarding_status 0→none 1→pending 2→approved 3→rejected |
onboarding_at |
DATETIME NULL | — | 无对应,NULL |
reject_reason |
VARCHAR(200) NULL | 旧表沿用 | influencer.reject_reason |
contactable_at |
DATETIME NULL | 基线 §2「已建联」事实字段 | 派生:有 wx_open_id 取 created_at;企微绑定另行回填 |
contact_channel |
VARCHAR(16) NULL | wecom/miniapp/both | 派生(见 §3) |
grade |
VARCHAR(16) NOT NULL DEFAULT 'ungraded' |
基线 §2:excellent(优质)/low_recommend(低推荐)/normal(中间地带)/ungraded(未履约) | 旧库无此口径 → 导入后按 cfg 重算 |
grade_at |
DATETIME NULL | — | 分级值变更时间(非每次计算时间,见 §4) |
grade_evidence |
JSON NULL | 基线 §2/§7.4 | {roi, cpm_fen, primary_category, sample_order_ids, window_days, cfg_snapshot}——只存证据不存阈值,cfg_snapshot 记本次判定读到的阈值值供追溯 |
roi_recent |
DECIMAL(10,4) NULL | 基线 §7.4 ROI=(成交GMV−达人费用)/达人费用 |
不迁旧值,按公式重算(地雷 15:0701 公式实测是错的) |
cpm_recent_fen |
BIGINT NULL | 基线 §7.4 CPM=达人费用/曝光×1000 |
同上,重算;单位分 |
settled_order_count |
INT NOT NULL DEFAULT 0 | 基线 §2「已履约」 | 由旧 task_order.status=11 计数导入;上线后实时查询回写,不做多路径加减(地雷 3:旧计数字段会漂移) |
last_settled_at |
DATETIME NULL | 同上 | 同上 |
last_deal_price_fen |
BIGINT NULL | 基线 §5.3 估价行情锚 | task_order.actual_earnings(最近一笔已结算)→ 元转分 |
recommend_index |
DECIMAL(6,2) NULL | 基线 §2/§5.3 推荐指数 | P1-6 占位,P0 期恒 NULL |
recommend_index_at |
DATETIME NULL | 同上 | 同上 |
recommend_penalty_reason |
VARCHAR(64) NULL | 基线 §2「CPM>80 降推荐指数、沉底展示并标注原因」 | 分级计算时写,如 cpm_over_low_recommend_threshold |
blacklisted |
BOOL NOT NULL DEFAULT 0 | 基线 §2「保留人工口子」 | 由 blacklist_douyin_account 反查其账号所属达人置位;明细见表 7 |
credit_score |
INT NULL | 口径缺口(§7 S7) | influencer.credit_score 原样带入;新规则未定前不设默认值、不参与任何判定 |
admin_remark |
VARCHAR(500) NULL | 旧表沿用 | influencer.admin_remark |
deleted_at / created_at / updated_at |
DATETIME | — | deleted=1 → deleted_at=导入时间 |
不迁的旧字段:influencer.id_card(移到表 5,涉税场景才用,R6 权限收敛)、influencer.inviter_id(旧 invite_record 已判死表,邀请关系无消费场景)、influencer.age(改存 birth_year)。
douyin_account(抖音账号 / 三层落点)| 字段 | 类型 | 口径出处 | 导入映射 |
|---|---|---|---|
id |
BIGINT PK | — | 新生成 |
legacy_id |
BIGINT NULL UNIQUE | — | douyin_account.id |
douyin_id |
VARCHAR(64) NOT NULL UNIQUE | 基线 §0 唯一识别键 | douyin_account.douyin_id → 去空格/小写化;空值行进 reject 清单不导入 |
douyin_uid |
VARCHAR(32) NULL UNIQUE | jixing §1:集星 API 的调用参数(数字 UID,非抖音号) | douyin_account.uid |
influencer_id |
BIGINT NULL | 基线 §2:池层/旁类允许为 NULL | douyin_account.influencer_id → 映射到新 id;旧值指向不存在达人 → 置 NULL 并记对账 |
nickname / avatar_url / profile_url |
VARCHAR(128/512/512) | 基线 §8 采集字段(昵称/主页链接) | 同名字段;profile_url ← 旧库 douyin_account.homepage_url(P0-2.8b 实证修正:本行原写"旧库无"是笔误,homepage_url 就是源,达人主页链接反选场景要用) |
gender |
TINYINT NOT NULL DEFAULT 0 | — | 集星 gender |
fans_count / local_fans_count |
INT NULL | 基线 §5.1 品宣硬门槛 ≥20 万;jixing fans_count/local_fans_count |
fans_count;local_fans_count 旧库无 → 集星补 |
item_level / live_level / content_level / overall_level |
TINYINT NULL | 基线 §5.1(品效 LV6+/团购 LV4+);jixing 等级四件套 | video_carrying_power_level→item_level、live_carrying_power_level→live_level;-1 归一为 NULL(旧库 -1 渲染为「非团购达人」,不是等级值) |
account_types |
JSON NOT NULL DEFAULT [] |
基线 §1 品宣/品效/团购 | account_types 逗号分隔 → JSON 数组(地雷 9) |
categories |
JSON NOT NULL DEFAULT [] |
jixing cooperation_detail.item_great_product_category_list |
specialty_categories(JSON 串)解析;集星覆盖刷新 |
primary_category |
VARCHAR(64) NULL | 基线 §2「品类为酒旅(景点票券/游玩项目居首位)」 | categories[0];酒旅判定见 §4(类目集合走 cfg,值待确认) |
tags |
JSON NOT NULL DEFAULT [] |
基线 §8「达人标签入驻自填、客户按标签一筛即出」 | tags(JSON 串)解析 |
city_name / resident_city_name / resident_city_code |
VARCHAR(64/64/16) NULL | jixing §1:估价距离项 + 硬门槛「异地无法拍摄」 | region→city_name、resident_city→resident_city_name;code 由集星补 |
credit_score |
INT NULL | jixing credit_score |
集星 |
status |
VARCHAR(16) NOT NULL DEFAULT 'active' |
active/invalid(旧三态无审核流程,实为二值) | status 0/1→active、2→invalid |
is_primary |
BOOL NOT NULL DEFAULT 0 | 旧表沿用(一人多号时的主账号) | is_primary |
is_leader_reported |
BOOL NOT NULL DEFAULT 0 | 基线 §2 旁类「团长报来的临时抖音号」 | 旧库无 → 全部 0;团长线录入时置 1 |
layer |
VARCHAR(16) NOT NULL DEFAULT 'pool' |
基线 §2:pool/contacted/fulfilled/side_partner | 导入后统一按 §3 规则计算 |
layer_at |
DATETIME NULL | — | 层级值变更时间 |
first_settled_order_at |
DATETIME NULL | 基线 §2「至少一个商单走到已结算」 | 由旧 task_order.status=11 AND douyin_account_id=? 取最早时间 |
source |
VARCHAR(24) NOT NULL | 入库来源:pool_crawl/jixing/miniapp/wecom_bind/manual/leader_report | 旧库无 → 一律 legacy_import,再按有无 influencer 细分 |
source_batch |
VARCHAR(64) NULL | 采集批次/邀约批次追溯 | — |
jixing_synced_at |
DATETIME NULL | jixing §1 | — |
remark |
VARCHAR(500) NULL | — | remark(同名字段直迁,管理员备注;P0-2.8b 实证修正:本行原空白导入映射是笔误) |
deleted_at / created_at / updated_at |
DATETIME | — | 同 2.1 |
不迁:全部 jixing_*(约 100 列,走 API)、work_count(地雷 7:字段已不存在)、旧 GMV 类字段(基线 §8 单位存疑,只放行已验证的 → 一律从集星重拉,见 2.3)。
douyin_account_metric(近 30 天指标最新快照,与账号 1:1)为什么单独一张表:档案字段稳定、指标字段日更;分表后同步任务只写这一张,主表不被高频 UPDATE 污染(旧仓 binlog 日增 5G 的教训,铁律 3)。
| 字段 | 类型 | 口径出处 | 说明 |
|---|---|---|---|
id |
BIGINT PK | ||
account_id |
BIGINT NOT NULL UNIQUE | 1:1;不建物理 FK | |
window_days |
SMALLINT NOT NULL DEFAULT 30 | jixing §2 default_item_date_n=30 |
口径窗口显式化 |
gmv_fen |
BIGINT NULL | jixing talent_item_gmv |
单位=分(实测样本 value=44.21万 ↔ digit_value=44214338,44214338 分 = 44.21 万元;取 digit_value 不取 value,jixing 坑 1) |
video_avg_gmv_fen |
BIGINT NULL | jixing talent_video_avg_gmv |
基线 §5.2 评分卡「稿均销量」 |
video_avg_vv |
INT NULL | jixing talent_video_avg_vv |
基线 §5.2「近 30 天稿均播放量」 |
video_gpm_fen |
BIGINT NULL | jixing talent_video_gpm |
稿均千次曝光 GMV |
video_avg_finish_rate |
DECIMAL(10,4) NULL | jixing talent_video_avg_finish_rate |
标度 0–1(地雷 8) |
video_avg_anchor_click_rate |
DECIMAL(10,4) NULL | jixing talent_video_avg_anchor_click_rate |
同上 |
item_cnt |
INT NULL | jixing talent_item_cnt |
接单密度参考 |
video_avg_like_cnt / _comment_cnt / _share_cnt |
INT NULL | jixing 互动均值 | |
verify_amount_fen |
BIGINT NULL | 基线 §8 采集口径「30 天核销额」 | 集星未提供 → 来源为来客/中台;未验证前保持 NULL,不猜 |
quotation_min_fen / quotation_max_fen |
BIGINT NULL | jixing quotations |
基线 §5.3 估价行情锚直接数据源 |
source |
VARCHAR(16) NOT NULL | jixing/laike/manual | 逐来源可追溯 |
fetched_at |
DATETIME NOT NULL | 取数时间(爬坡保护判定用,基线 §8 满 7 天) | |
fingerprint |
CHAR(32) NOT NULL | 铁律 3 | 本轮全部指标值的 hash;与库中相同则整行不写(零 UPDATE、零 binlog) |
updated_at |
DATETIME |
influencer_binding(企微绑定镜像,收编契约 binding_mirror)契约 §2.5 要求的字段 (influencer_id, guid, peer_user_id, conv_id, source, synced_at) 全部包含,本表是其超集实现(表名差异见 §7 S6)。
| 字段 | 类型 | 口径出处 | 说明 |
|---|---|---|---|
id |
BIGINT PK | ||
influencer_id |
BIGINT NOT NULL | 契约 §2.5 | 反查索引(消息台缺口 2 的犀见侧自解) |
guid |
VARCHAR(64) NOT NULL | 契约 §2.5 | 我方企微账号 |
peer_user_id |
VARCHAR(64) NOT NULL | 契约 §2.5 | 企微外部联系人 userId |
conv_id |
VARCHAR(64) NULL | 契约 §2.5 | 会话 id(GET /api/conv-of 结果缓存) |
wcid |
VARCHAR(128) NULL | 契约 §2.1 批次归因键 | 达人点卡片后回流;(batch_id, influencer_id) 归因靠它 |
batch_id |
VARCHAR(64) NULL | 契约 §2.1/基线 §6.9 判据 2 | 邀约批次追溯 |
source |
VARCHAR(16) NOT NULL | 契约 §3.1/§3.2 | wcid / manual / reconcile(manual 优先级最高,不被自动覆盖) |
active |
BOOL NOT NULL DEFAULT 1 | 解绑=置 0,不物理删(留痕) | |
bound_at |
DATETIME NULL | 绑定发生时间 | |
synced_at |
DATETIME NOT NULL | 契约 §4.3 水位 | 对账轮询游标 |
created_at / updated_at |
DATETIME |
唯一约束 uq(guid, peer_user_id):一个企微身份在一个我方账号下只绑一个达人(与消息台 peer_binding 同语义)。一个达人可有多条(多个我方账号加过他)。
influencer_payment_info(收款信息,1:N)| 字段 | 类型 | 口径出处 | 导入映射 |
|---|---|---|---|
id / legacy_id |
BIGINT | — | influencer_payment_info.id |
influencer_id |
BIGINT NOT NULL | 映射新 id | |
account_type |
VARCHAR(16) NOT NULL | 基线 §7.6 | PERSONAL→personal、CORPORATE→corporate(地雷 1:统一小写) |
account_holder |
VARCHAR(64) NULL | account_name |
|
bank_name / bank_branch / bank_card_no |
VARCHAR(128/128/32) NULL | 同名 | |
id_card |
VARCHAR(32) NULL | 基线 §7.6 灵工签约/个税按身份证 | influencer.id_card 迁入此处(R6 权限收敛:达人档案主表不再存) |
payee_phone |
VARCHAR(20) NULL | 基线 §7.6「表中手机号必须与签约手机号一致」 | 旧库无独立字段 → 导入取 influencer.phone,并标 phone_source='inherited' 供后续人工核对 |
company_name / tax_no |
VARCHAR(128/32) NULL | 基线 §7.6 对公通道 | 同名;corporate 时必填(应用层校验) |
is_default |
BOOL NOT NULL DEFAULT 0 | 一人多条时的默认收款方式 | |
verified_at / verify_note |
DATETIME / VARCHAR(200) NULL | 与灵工签约记录比对结果(结算域 P1-9 写) | |
deleted_at / created_at / updated_at |
DATETIME |
权限:本表读取挂新权限码 influencer.payment.read;列表接口一律掩码(卡号/身份证只回后 4 位)。依据基线 §9.3 R6「底价/达人库字段权限收敛」。
influencer_service_region(接单地区,1:N)| 字段 | 类型 | 说明 |
|---|---|---|
id / legacy_id |
BIGINT | influencer_region.id |
influencer_id |
BIGINT NOT NULL | |
province / city / district |
VARCHAR(32) NULL | 旧库是纯文本,原样迁 |
region_code |
VARCHAR(16) NULL | 行政区划码(集星 resident_city_code 可补),为将来级联/距离计算预留 |
created_at |
DATETIME |
唯一约束 uq(influencer_id, province, city, district) 防重复录入。
influencer_blacklist(人工拉黑记录,可逆)| 字段 | 类型 | 口径出处 | 导入映射 |
|---|---|---|---|
id / legacy_id |
BIGINT | blacklist_douyin_account.id |
|
influencer_id |
BIGINT NULL | 由旧表账号反查所属达人 | |
douyin_account_id |
BIGINT NULL | douyin_account_id 映射新 id(旁类账号无达人时只填这个) |
|
reason |
VARCHAR(200) NOT NULL | 基线 §2「恶劣个案人工手动拉黑」 | reason(P0-2.8b 实证修正:本行原写源字段 remark 是笔误,旧表 blacklist_douyin_account 该列真实名是 reason,源码见 BlacklistDouyinAccount.java);空则填 legacy_import |
previous_credit_score |
INT NULL | schema-notes §2.1「取消拉黑时恢复信用分的可逆设计要保留」 | previous_credit_score |
created_by / created_at |
VARCHAR(64) / DATETIME | created_by |
|
revoked_at / revoked_by / revoke_reason |
DATETIME / VARCHAR(64) / VARCHAR(200) NULL | 取消拉黑=写这三列,不物理删 |
约束:应用层校验 influencer_id 与 douyin_account_id 至少一个非空(两列都可空,DB 级 CHECK 可选加 ck_influencer_blacklist_target)。生效中的黑名单 = revoked_at IS NULL;influencer.blacklisted 是它的冗余当前态,值真变才写。
layer 唯一挂在 douyin_account理由:池层(有信息、未建联)的实体只有账号没有人(influencer_id IS NULL),层级若挂人维度则池层无处安放。人维度不存 layer,避免两份真相;需要「人属于哪层」时由服务层函数实时算(见 3.3)。
def resolve_layer(acc, inf) -> str:
# 优先级自上而下,命中即返回
if acc.is_leader_reported and acc.influencer_id is None:
return "side_partner" # 旁类:已合作未建联(基线 §2 旁类行)
if acc.first_settled_order_at is not None:
return "fulfilled" # 已履约:至少一个商单走到已结算
if inf is not None and is_contactable(inf):
return "contacted" # 已建联:企微或小程序可触达
return "pool" # 达人广场池:有信息、未建联
def is_contactable(inf) -> bool:
return bool(inf.wx_open_id) or EXISTS(influencer_binding
WHERE influencer_id=inf.id AND active=1)
| 层 | 值 | 判定事实(基线 §2) | 落库字段 |
|---|---|---|---|
| 达人广场(池) | pool |
有信息、未建联 | 兜底分支 |
| 已建联 | contacted |
加上企微 或 小程序完成绑定 | influencer.wx_open_id / influencer_binding.active=1 |
| 已履约 | fulfilled |
至少一个商单走到已结算 | douyin_account.first_settled_order_at(商单域 P0-5 回写) |
| 旁类 已合作未建联 | side_partner |
团长报来的临时抖音号、无联系方式 | is_leader_reported=1 AND influencer_id IS NULL |
关键说明:
- 旁类不会与 fulfilled 打架:旁类走团长线、不产生犀见商单,first_settled_order_at 天然为 NULL;且一旦补齐联系方式并建了人(influencer_id 非空),判定自动升级,不需要人工改标。
- 有手机号 ≠ 已建联:手机号只是可发好友申请的前提(基线 §6.11),未加上企微仍是 pool——这正是「每周新增 500 名进企微」KPI 的目标池:layer='pool' AND influencer.phone IS NOT NULL。
- 无手机号也可能已建联:企微不给外部联系人手机号(契约 §2.5 实测),wcid 回流绑定的达人可能永远没有 phone。所以 phone 必须可空,见 §5。
def layer_of_influencer(inf) -> str:
accs = 其名下未软删账号
if accs: return max(acc.layer, key=层级序) # 序:fulfilled > contacted > side_partner > pool
return "contacted" if is_contactable(inf) else "pool" # 孤儿达人(有人无号,旧库存在)
| 触发 | 来源 | 动作 |
|---|---|---|
| 绑定写入/失效 | P0-3.1 wcid 回流、契约 §4.3 对账轮询 | 重算该达人名下全部账号 |
| 小程序绑定 | P0-3.1 | 同上 |
| 商单进入已结算 | P0-5 状态机事件 | 回写 first_settled_order_at(只在原值为 NULL 或更早时写)+ 重算该账号 |
| 采集/导入新账号 | P0-2.6 | 新行计算一次 |
| 团长线录入 | P1-11 | 置 is_leader_reported 后重算 |
纪律:layer 值未变则整行不 UPDATE(连 layer_at 也不动);layer_at 语义 = 层级变更时间,不是「上次计算时间」。计算过程走结构化日志,不落库。
统一由一个查询 helper 收口,不散落在各处 where 里:
# app/domains/influencers/scope.py
def outreach_scope(q): # 建联/邀约/加好友/催发类名单一律先过这个过滤器
return q.where(DouyinAccount.layer != "side_partner",
DouyinAccount.deleted_at.is_(None))
覆盖场景:②A 定向邀约名单(基线 §6.9)、批量好友申请(§6.11)、一键催发(§6.10)、推荐清单候选(§5.3)。旁类账号仅出现在「数据归档/对账」类视图,并在 console 三层 tab 中独立成一个 tab(不混入已建联)。
roi = (成交GMV_fen - 达人费用_fen) / 达人费用_fen # 基线 §7.4,绝不用旧库存的 roi 列(地雷 15)
cpm_fen = 达人费用_fen / 曝光数 * 1000 # 基线 §7.4
if cpm > cfg("grade.cpm_low_recommend_threshold"): grade = "low_recommend"
recommend_penalty_reason = "cpm_over_low_recommend_threshold" # 降推荐指数、沉底,不拉黑
elif roi > cfg("grade.roi_excellent_min") \
and cpm < cfg("grade.cpm_excellent_max") \
and primary_category ∈ cfg("grade.travel_category_whitelist"): grade = "excellent"
else: grade = "normal"
cfg("data.ramp_protection_days") 天的视频数据(基线 §8 爬坡保护)。ROI > 10(恰等于 10 不算优质)、CPM < 20、CPM > 80——P0-2.8 契约测试逐个边界值验。settled_order_count = 0)→ grade='ungraded',不参与优质/低推荐判定。| 触发 | 说明 |
|---|---|
| 商单进入已结算 | 该达人重算 |
| 视频/订单指标日更(数据域) | 只对已履约达人重算,且仅当参与判定的输入值变化 |
| cfg 阈值被改 | 配置写接口触发缓存失效 → 由夜间批任务(APScheduler)全量重算;不在写配置的请求路径里现算全量 |
| 人工触发 | console 详情页「重算分级」按钮(P0-2.7 可选) |
判值真变:grade 未变 → 不写 grade/grade_at;但 grade_evidence 内的 roi/cpm 变化属正常刷新,允许更新(它是证据不是判定结果)——若怕高频写,evidence 也用 fingerprint 比对(与 2.3 同一手法),建议采纳。
本期只建字段不算值:recommend_index、recommend_index_at、recommend_penalty_reason。P0 期 recommend_index 恒 NULL,console 显示「—」。recommend_penalty_reason 在 P0 就写(分级顺带产出),供 P1-6 沉底排序直接消费。
| 键 | 状态 | 出处 |
|---|---|---|
grade.roi_excellent_min(10) |
✅ 已在种子 | 基线 §2/§12A |
grade.cpm_excellent_max(20) |
✅ 已在种子 | 基线 §2/§12A |
grade.cpm_low_recommend_threshold(80) |
✅ 已在种子 | 基线 §2/§12A |
data.ramp_protection_days(7) |
✅ 已在种子 | 基线 §8/§12A |
grade.travel_category_whitelist |
⚠️ 新增,值待业务确认 | 基线 §2 只给了「酒旅(景点票券/游玩项目居首位)」的描述,未给可判定的类目集合;集星类目名如「酒店宾馆/景点票券」。不自补口径——建议以 value_type=json、value="待确认" 占位入种子(沿用 recommend.calibration_batch_k 的占位先例),确认前 excellent 判定短路为 false 并在日志显式告警 |
influencer.tag_options |
⚠️ 新增,值待业务确认 | 基线 §8 只说「标签入驻时自填」,未定选项集合;旧库 2026-07 改版有六项(亲子/情侣/攻略/国风/颜值/外国)但那是旧口径,不能直接当基线 |
两个新键都不进本次迁移的判定路径(占位即可),确认后由配置中心补种子迁移。
组合键在关系模型里自然分解为两个可空唯一键,靠 influencer_id 关联:
| 约束 | 表 | 说明 |
|---|---|---|
uq_douyin_account_douyin_id |
douyin_account | 抖音号全局唯一、NOT NULL(池/旁类最少也有抖音号) |
uq_douyin_account_douyin_uid |
douyin_account | 集星 UID 唯一、可空(采集时未必有;MySQL 唯一索引允许多个 NULL) |
uq_influencer_phone |
influencer | 手机号唯一、可空 |
uq_influencer_wx_open_id |
influencer | 小程序 openid 唯一、可空 |
uq_influencer_binding_peer |
influencer_binding | (guid, peer_user_id) |
uq_*_legacy_id |
各有映射源的表 | 导入幂等 |
中间态一览(都是合法状态,不能用 NOT NULL 堵):
| 态 | 抖音号 | 手机号 | 典型来源 | 层级 |
|---|---|---|---|---|
| 只有抖音号 | ✓ | ✗(连 influencer 都没有) | 达人广场采集 / 团长报送 | pool / side_partner |
| 有号有人无手机号 | ✓ | ✗ | 企微 wcid 回流绑定(企微不给外部联系人手机号) | contacted |
| 有人有手机号无抖音号 | ✗ | ✓ | 旧库孤儿达人 / 先加好友后补号 | 人维度算 contacted,不出现在账号三层列表 |
| 齐全 | ✓ | ✓ | 小程序入驻 | contacted / fulfilled |
应用层校验(DB 无法表达):influencer 至少要有 phone/wx_open_id/有效 binding 三者之一才允许创建;否则拒绝(否则会造出永远匹配不上的幽灵档案)。
匹配与冲突(P0-2.4/P0-3.1 共用):
1. 先按手机号查 influencer,再按抖音号查 douyin_account;
2. 两边都命中且 account.influencer_id != influencer.id → 冲突:返回 envelope code=409(HTTP 200,先例 app/domains/system/roles_service.py:20;P0-2.8 所写「唯一键冲突 409」即此 code,非 HTTP 状态码),并给出双方 id 供人工合并;
3. 只命中一边 → 建联即补齐另一边(不新建重复档案);
4. 都没命中 → 新建(P0-3.1 的「未命中建档进已建联」)。
软删与唯一键:软删行仍占用唯一号(deleted_at 不参与唯一索引,MySQL 无部分索引)。重新入库 = 复活原行(deleted_at=NULL),导入脚本按 legacy_id/douyin_id upsert 天然满足。
| 表 | 索引 | 用途 |
|---|---|---|
| douyin_account | ix(layer, grade_proxy)→实际为 ix(layer)、ix(influencer_id)、ix(status)、ix(fans_count)、ix(updated_at) |
三层 tab 筛选、按人反查、列表排序、增量同步 |
| douyin_account | ix(primary_category) |
品类筛选(客户「亲子/酒旅类达人一筛即出」,基线 §8) |
| influencer | ix(grade)、ix(recommend_index)、ix(blacklisted)、ix(updated_at) |
分级筛选、推荐排序(P1-6)、黑名单排除 |
| douyin_account_metric | uq(account_id)、ix(fetched_at) |
1:1、爬坡保护取数 |
| influencer_binding | uq(guid, peer_user_id)、ix(influencer_id)、ix(wcid)、ix(synced_at) |
反查(契约缺口 2)、归因、对账游标 |
| influencer_payment_info | ix(influencer_id)、ix(id_card) |
结算域按身份证匹配灵工签约(基线 §7.6) |
| influencer_service_region | uq(influencer_id, province, city, district) |
防重 |
| influencer_blacklist | ix(influencer_id)、ix(douyin_account_id)、ix(revoked_at) |
生效中黑名单查询 |
模糊搜索(昵称/姓名 kw):本量级(万级)用 LIKE 'kw%' 前缀索引即可,不引全文索引(YAGNI);LIKE '%kw%' 走全表在万级可接受,超 10 万行再议。
lynoxi_test 跑 information_schema.columns 导出达人域 8 表真实列清单,与 schema-notes 的源码推导逐列比对,差异先记录再导入;同时确认「达人广场 19,299 行采集数据是否就在 douyin_account」。lynoxi.xxx;分页按主键游标(WHERE id > ? ORDER BY id LIMIT n),禁 OFFSET 深分页(schema-notes §4-20)。influencer → douyin_account(补 influencer 映射)→ payment_info/region/blacklist → 计算 layer → 重算 ROI/CPM → 计算 grade。legacy_id upsert;逐字段比对,值未变不写(铁律 3)。influencer_id 悬空数、抖音号缺失数、重复手机号数、layer 分布、grade 分布。age=-1 等异常各成一节。| # | 地雷 | 本设计的规避措施 |
|---|---|---|
| 1 | 枚举大小写/命名不一致 | 全域枚举小写下划线 + 应用层 StrEnum;导入时统一映射(PERSONAL→personal) |
| 2 | 单条结算接口是 bug | 商单/结算域,本域不适用(留 P0-5.1) |
| 3 | 计数字段会漂移 | settled_order_count 不做多路径加减,只由「已结算」事件按实时查询回写;导入时按旧订单实查而非搬旧计数 |
| 4 | 时间格式两种 | 落库统一 DATETIME;序列化统一由 pydantic 输出 ISO(envelope 层统一,不各写各的) |
| 5 | task_order 无 projectId | 商单/项目域,本域不适用 |
| 6 | 任务状态 2 是派生态 | 商单域不适用;本域同类纪律:layer/grade 是存下来的判定结果,派生视图(人维度层级)不落库 |
| 7 | 达人「作品数」是死字段 | 新模型无 work_count,指标一律在 douyin_account_metric |
| 8 | 百分比标度两套 | 比率列统一 DECIMAL(10,4)、标度 0–1,列名不带 _pct |
| 9 | 标签两种格式 | tags/categories/account_types 全 JSON 数组,导入时把逗号分隔串解析归一 |
| 10 | 年龄可能是 -1 | 不存 age,存 birth_year;age<=0 → NULL |
| 11 | 脏 UTF-8 字节 | 导入用 errors="replace" 解码 + 逐行 try/except,脏行进 reject 清单并记录原始 bytes 长度 |
| 12 | 脱敏须保持跨表映射一致 | 导入脚本用统一 idmap/usermap(同一 legacy 值映射同一新值),身份证/卡号脱敏在测试库时按值映射不按行 id |
| 13 | 软删曾生成坏 SQL | deleted_at + 标准 ORM 逻辑删除,禁止手写局部 UPDATE |
| 14 | /admin/feishu/** 端点不能动 |
本设计零写旧库,旧 api 的 8 个定时任务(含「优质达人自动入池」写 quality_douyin_account)继续跑不受影响 |
| 15 | 0701 ROI 公式错(算成费用 MAX) | ROI/CPM 一律按基线 §7.4 重算,旧 work_review_record.roi/cpm 两列整列丢弃不迁 |
| 16 | review_type 注释过时 |
商单域不适用(本域不引用该表) |
| 17 | SUBMIT_FAIL 第 6 态 |
结算域不适用 |
| 18 | task_order ↔ feishu_execution_record 无外键 | 结算域不适用 |
| 19 | 时长单位 ms/s 混用 | 全域单位写进列名(_fen/_ms/_sec/_days) |
| 20 | 上线前必补索引 | 见 §5.2;跨库只读旧表的查询(导入期)走主键游标不依赖旧索引 |
| 21 | 灵工系数 1.06 vs 1.062 不一致 | 本设计不落任何系数(结算域的事);payment_info 只存账户信息 |
| 量 | 来源 | 单位处理 |
|---|---|---|
| 达人费用 | lynoxi.task_order.actual_earnings(已结算单);为空则 expected_earnings 不采用(schema-notes §2.2:expectedEarnings 会被列表页动态重算覆盖,库值不可信) |
元 → 分 |
| 成交 GMV | lynoxi.work_review_record.gmv |
单位存疑 → 导入前抽样人工核对,核对不通过则该达人 ROI 置 NULL 并记 reject,不猜 |
| 曝光 | lynoxi.work_review_record.play_count |
整数 |
| 时间窗 | 视频发布满 cfg("data.ramp_protection_days") 天 |
基线 §8 |
旧库已结算订单仅 13 条(schema-notes §2.2 实测)——历史 ROI/CPM 样本极稀,导入后绝大多数达人 grade='ungraded' 是预期结果,不是 bug;qa 验收按此口径核对。
主会话评审结论(2026-08-16):设计通过,S1–S11 全部按建议倾向裁决采纳。其中 S2/S5 已获用户定案(2026-08-16 二批确认):S2 白名单=酒店宾馆/景点票券/游玩项目(可配,excellent 短路解除);S5 建
influencer_watchlist表(用户:可以做且有必要,命名禁用「优质」);S7(credit_score 新口径)仍待业务;S6 契约加注已批准执行;S11 权限码扩充已批准。放行后半段:出 Alembic 迁移与模型代码。 状态原图例:待裁决=需主会话拍板 | 待业务=口径缺口需产品/业务确认 | 已自决=本设计已定
| # | 问题 | 建议倾向 | 状态 |
|---|---|---|---|
| S1 | 标签挂账号还是人(schema-notes §2.1 提出:旧库达人级完全无标签字段) | 挂账号(douyin_account.tags)。理由:标签是内容属性(品类/风格),一人多号风格可不同;采集与集星数据天然账号维度。筛「亲子类达人」= 账号标签 join 到人 |
待裁决 |
| S2 | 「酒旅品类」的可判定类目集合(基线 §2 只给描述,未给枚举) | 新增 cfg 键 grade.travel_category_whitelist,值待业务给;确认前 excellent 判定短路 false + 日志告警。不编默认值 |
待业务 |
| S3 | 分级/推荐指数落人维度(本设计如此)还是账号维度 | 落人:业务动作(「下轮优先复用」「沉底」)针对人;②A 邀约候选来自已建联达人库(必有 influencer);旁类账号不分级 | 待裁决 |
| S4 | 团长主档是否建表(旧 manager 表,与 influencer 同构) |
本期不建。基线 §0/§6.3「团长只开秒杀、不入库」;团长线走 P1-11 指挥台过渡,其报来的抖音号进旁类 | 待裁决 |
| S5 | 人工「重点关注名单」是否需要(schema-notes §5#6;旧 quality_douyin_account 弃用后的空位) |
本期不建表,留讨论位。若业务确需,用独立命名 influencer_watchlist(禁止复用「优质」一词,该词已被基线 §2 的自动分级占用) |
待业务 |
| S6 | 绑定镜像表名:influencer_binding(本设计)vs 契约 §2.5 字面的 binding_mirror |
用 influencer_binding(达人域命名统一、字段是契约超集),并在 msgbridge 契约 §2.5 加一行注明实现表名。改契约文档需主会话批准,故列此 |
待裁决 |
| S7 | credit_score 的新口径(初值/加减规则/与黑名单恢复的关系) |
基线无口径 → 本期只原样导入旧值、可空、不参与任何判定;新规则定案前不写默认值 | 待业务 |
| S8 | 省市区字典表是否需要(旧 region 表) |
不建:存文本 + region_code 预留;级联选择器用前端静态数据 |
已自决(可推翻) |
| S9 | 达人广场 19,299 行采集数据的真实落表(schema-notes §5#2) | 导入前连 lynoxi_test 实查确认(§6.1 步骤 1);若存在另一张未发现的采集表,映射源需补 |
待裁决(导入前必须闭环) |
| S10 | msgbridge_touch_log.influencer_id 是 String(64),新主键是 BIGINT |
不改已上线表:服务层写 str(id),不建 FK。理由:改列型要动已迁移表,收益仅为类型美观 |
已自决(可推翻) |
| S11 | 敏感字段权限(身份证/银行卡,基线 §9.3 R6) | 新增权限码 influencer.payment.read;列表默认掩码,明文需权限 + 审计打点 |
待裁决(涉及 RBAC 权限码清单扩充) |
模块布局(单模块 ≤200 行,路由/服务/模型分层):
app/domains/influencers/
├── router.py # 达人档案 CRUD 路由
├── accounts_router.py # 抖音账号路由
├── service.py # 档案读写(唯一键匹配/冲突 409)
├── layer.py # 三层判定纯函数(§3.2)+ 重算入口
├── grade.py # 分级计算(cfg 取阈值,§4.1)
├── scope.py # outreach_scope 等共享查询过滤器(§3.5)
└── schemas.py # pydantic 模型即文档
REST 草签名(全部走 envelope,业务错误 HTTP 200 + success:false):
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/admin/influencers |
分页列表:kw(姓名/手机/昵称)/grade/blacklisted/layer(按人维度聚合) |
| GET | /api/admin/influencers/{id} |
档案详情(含账号列表、绑定、地区;付款信息掩码) |
| POST | /api/admin/influencers |
建档;唯一键冲突 → code 409 |
| PUT | /api/admin/influencers/{id} |
编辑(值真变才写 + 审计) |
| DELETE | /api/admin/influencers/{id} |
软删 |
| GET | /api/admin/influencers/search |
搜索选择器专用(轻返回:id/姓名/手机/主账号),解 core-modules「账号绑定靠手填数字 id」的老问题 |
| POST | /api/admin/influencers/{id}/blacklist |
拉黑(记 previous_credit_score) |
| DELETE | /api/admin/influencers/{id}/blacklist |
取消拉黑(恢复信用分,可逆) |
| POST | /api/admin/influencers/{id}/recompute-grade |
手动重算分级(返回新旧值与证据) |
| GET | /api/admin/douyin-accounts |
分页列表:layer tab / kw / category / level_min / has_phone |
| GET | /api/admin/douyin-accounts/{id} |
账号详情(含最新指标快照 + 取数时间) |
| POST/PUT | /api/admin/douyin-accounts[/{id}] |
建/改(influencer_id 经搜索选择器传入) |
| POST | /api/admin/douyin-accounts/{id}/bind |
绑定/换绑达人(触发 layer 重算) |
| GET | /api/msgbridge/influencer |
契约 §3.4 供给侧:?douyin_id=|phone=|influencer_id= → 档案(基线 §6.8 判据) |
域内函数签名(供 P0-3.1/P0-5 调用,不走 HTTP):
layer.resolve(account, influencer) -> str # §3.2 纯函数
layer.recompute_for_influencer(session, influencer_id) -> int # 返回真正变更的行数
layer.mark_settled(session, account_id, settled_at) -> bool # 商单域回写履约事实,值真变才写
grade.evaluate(session, influencer_id) -> GradeResult # cfg 取阈值,返回 grade+evidence
match.find_by_unique_key(session, phone=None, douyin_id=None) -> MatchResult # 命中/冲突/未命中
docs/business/xijian-business-baseline.md v2.1 §0/§2/§5/§7.4/§7.6/§8/§9.3/§12Adocs/dev/schema-notes.md(P0-2.2)§1.1/§2.1/§2.2/§4(21 条地雷)/§5(8 个开放问题)docs/dev/jixing-api-notes.md(字段清单 + 三坑;digit_value 单位=分为本次由样本 44.21万↔44214338 反推核实)docs/dev/msgbridge-contract.md §2.1/§2.5/§3.1/§3.2/§4.3docs/dev-charter.md v1.3 §4(独立新库 xijian + Schema 三分法 + 表名前缀定案)server/app/models/{base,config,msgbridge}.py(命名约定、注释风格、无前缀先例)、server/app/envelope.py、server/app/domains/system/roles_service.py:20(409 先例)、server/app/domains/config_center/{cache,seed}.py(cfg 与占位行先例)| 日期 | 版本 | 变更 |
|---|---|---|
| 2026-08-17 | v1 草案 | 首版:7 张新表逐字段设计 + 三层/分级判定规则 + 唯一键中间态 + 21 条地雷对应 + 11 项开放问题 + P0-2.4 接口草签名。未出迁移、未写代码,待主会话评审 |
| 2026-08-16 | v1.1 | P0-2.8b 导入脚本实证修正 3 处笔误(主会话裁决):§2.2 douyin_account.profile_url 源改为旧库 homepage_url(原写"旧库无"有误);§2.2 remark 补上同名字段导入映射(原空白);§2.7 influencer_blacklist.reason 源字段改为 reason(原误写 remark,旧表无此列) |