央企,刚被通知要裁员。给了我3个选项。。
刚刷到这个帖子,我第一反应就是:央企也开始整这种选择题了啊,挺魔幻的。
三个选项看着像让你选,其实哪个都不舒服。
1、辞退,补偿按N或者N+1
2、改为劳务派遣,其他照常没有企业年金。
3.单位发最低工资,自己去找工作,多久时间没说。
所以真不是选哪个好的问题,是哪个坑浅一点。要我说,先别急着签,所有东西让单位白纸黑字写清楚,补偿怎么算、社保怎么交、年金怎么处理、最低工资发多久,别光听口头。
这种时候别讲情怀了,先把自己的钱和后路看住。央企这俩字,有时候也就听着稳。
算法题:账户合并
user_id 不重复,不代表账号就干净。
最烦的是这种数据:
uid=1001 [email protected] phone=13800000001
uid=1008 [email protected] phone=
uid=1044 email= phone=13800000001
uid=1090 [email protected] phone=13800000001
产品那边看的是 4 个账号,风控那边看的是 1 个人,客服那边一查,订单、优惠券、登录设备全散了。
这种问题我一般不急着改表,也不急着写一堆 update user set main_uid = ?。先把“凭什么合并”这个条件拎清楚。
账户合并最常见的依据,不是用户名,也不是昵称。昵称这种字段我第一眼就不信,太脏了。能用的通常是邮箱、手机号、三方 openid、身份证 hash、设备指纹这类相对稳定的标识。
比如现在给一批账号数据:
accounts = [
{"uid": "1001", "email": "[email protected]", "phone": "13800000001"},
{"uid": "1008", "email": "[email protected]", "phone": ""},
{"uid": "1044", "email": "", "phone": "13800000001"},
{"uid": "1090", "email": "[email protected]", "phone": "13800000001"},
{"uid": "2011", "email": "[email protected]", "phone": "13900000002"},
]
这里的合并规则很简单:只要两个账号命中了同一个有效标识,就认为它们属于同一组。
这地方用并查集最顺手。
别看名字有点算法题味,落到业务里就一句话:先把有关联的账号拉到同一个坑里,最后按坑分组。
我会这么写:
classAccountUnion:
def__init__(self):
self.parent = {}
defadd(self, uid):
if uid notin self.parent:
self.parent[uid] = uid
deffind(self, uid):
self.add(uid)
while self.parent[uid] != uid:
self.parent[uid] = self.parent[self.parent[uid]]
uid = self.parent[uid]
return uid
defunion(self, left, right):
root_left = self.find(left)
root_right = self.find(right)
if root_left == root_right:
return
# 这里我习惯让较小 uid 当主账号,线上也方便排查
self.parent[root_right] = root_left if root_left < root_right else root_right
self.parent[root_left] = self.parent[root_right]
defclean_value(value):
value = (value or"").strip().lower()
return value if value elseNone
defmerge_accounts(rows):
uf = AccountUnion()
mark_owner = {}
for row in rows:
uid = str(row["uid"])
uf.add(uid)
marks = [
("email", clean_value(row.get("email"))),
("phone", clean_value(row.get("phone"))),
]
for mark_type, mark_value in marks:
ifnot mark_value:
continue
mark_key = f"{mark_type}:{mark_value}"
if mark_key in mark_owner:
uf.union(uid, mark_owner[mark_key])
else:
mark_owner[mark_key] = uid
groups = {}
for row in rows:
uid = str(row["uid"])
root = uf.find(uid)
groups.setdefault(root, []).append(uid)
return groups
跑一下:
merged = merge_accounts(accounts)
for main_uid, members in merged.items():
print(main_uid, sorted(members))
输出大概是:
1001 ['1001', '1008', '1044', '1090']
2011 ['2011']
这结果看着有点吓人。
1090 的邮箱是 [email protected],但手机号跟前面几个账号一样,所以它也被拉进来了。这就是账户合并里最容易出事故的地方:规则只要放得太宽,就会误合并。
我见过有人直接拿手机号合并,结果把企业前台电话、测试手机号、空号段全合到一起。后面修数据,比写代码痛苦多了。
所以真实场景里,我一般会加一层“可疑标识过滤”。
比如这种日志,看到就不能直接信:
[merge-scan] mark=phone:13800000000 hit_uid_count=381
[merge-scan] mark=email:[email protected] hit_uid_count=94
一个手机号挂几百个账号,大概率不是一个自然人,而是脏数据、测试数据、默认值,或者被批量撞库过。
代码可以加个阈值,超过阈值的标识不参与自动合并,只进人工表。
from collections import defaultdict
defcollect_mark_usage(rows):
usage = defaultdict(set)
for row in rows:
uid = str(row["uid"])
for field in ("email", "phone"):
value = clean_value(row.get(field))
if value:
usage[f"{field}:{value}"].add(uid)
return usage
defmerge_accounts_safe(rows, max_hit=5):
usage = collect_mark_usage(rows)
dirty_marks = {mark for mark, uids in usage.items() if len(uids) > max_hit}
uf = AccountUnion()
mark_owner = {}
for row in rows:
uid = str(row["uid"])
uf.add(uid)
for field in ("email", "phone"):
value = clean_value(row.get(field))
ifnot value:
continue
mark_key = f"{field}:{value}"
if mark_key in dirty_marks:
print(f"[skip-dirty-mark] {mark_key} hit={len(usage[mark_key])}")
continue
old_uid = mark_owner.get(mark_key)
if old_uid:
uf.union(uid, old_uid)
else:
mark_owner[mark_key] = uid
groups = defaultdict(list)
for row in rows:
uid = str(row["uid"])
groups[uf.find(uid)].append(uid)
return dict(groups)
这个阈值别写死到业务里,最好放配置。不同业务差异很大。社交产品、支付产品、企业 SaaS,对“同手机号多账号”的容忍度完全不一样。
合并完也不要马上改主表。
我更喜欢先落一张中间表:
createtable account_merge_plan (
batch_no varchar(64) notnull,
main_uid varchar(64) notnull,
sub_uid varchar(64) notnull,
reason varchar(128) notnull,
statusvarchar(32) notnulldefault'INIT',
created_at datetime notnull,
primary key (batch_no, sub_uid)
);
先把计划写进去,再抽样查。
select main_uid, count(*) as cnt
from account_merge_plan
where batch_no = '202602_merge_01'
groupby main_uid
having cnt > 10;
这条 SQL 我一定会跑。
只要发现某个主账号下面挂了一堆子账号,先停。别相信程序,先看样本。账户合并不是排序去重,错了不是重跑一次就完事,订单归属、会员等级、积分流水、登录态都会被带着跑偏。
真正执行合并时,也不要想着一个事务吞完整批。
我一般按主账号分批处理,每批只合一小撮账号。失败了能回滚,日志也能看清楚。
defbuild_merge_plan(groups):
plan = []
for main_uid, members in groups.items():
members = sorted(set(members))
for uid in members:
if uid == main_uid:
continue
plan.append({
"main_uid": main_uid,
"sub_uid": uid,
"reason": "same_email_or_phone"
})
return plan
这个函数看着普通,但它有个细节:主账号不参与自己的合并计划。线上脚本里这种小判断漏了,后面很容易出现自己合自己,状态机还走了一遍,日志看起来像成功,实际啥也没干。