| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123 |
- package migrate
- import (
- "fmt"
- "sort"
- "strings"
- )
- // MigrateReport 一次迁移运行的完整报告(dry-run 与 run 共用,dry-run 的 *Inserted 恒 0)。
- //
- // 字段分组:
- // - 机构/校区:Institutions / Campuses 处理量 + 冲突
- // - 用户归一:UsersTotal / UsersInserted / UsersSkipped(重跑幂等跳过)
- // - 身份:各 type 写入量(Phone / Email / Username / Wechat)
- // - 微信锚点:WechatUnion
- // - 成员关系:Memberships
- // - 冲突/异常清单:PhoneConflicts / UsernameConflicts / PhoneInvalid / WechatOrphans
- // (P0-08 人工复核输入,不自动处理)
- type MigrateReport struct {
- DryRun bool // true=仅预演不写库
- // 机构/校区
- InstitutionsTotal int
- InstitutionsCreated int
- CampusesTotal int
- CampusesCreated int
- // 用户归一
- UsersTotal int
- UsersCreated int // 新插入 t_user
- UsersSkipped int // 重跑幂等:canonical_user_id 已存在
- UsersDisabled int // status 映射为 disabled(0/3)
- // 身份(t_identity)
- IdentitiesPhone int
- IdentitiesEmail int
- IdentitiesUsername int
- IdentitiesWechat int
- IdentityDuplicates int // 全局唯一键已存在而跳过的 identity(幂等)
- // 微信锚点(t_wechat_union)
- WechatUnionCreated int
- // 成员关系(t_membership)
- MembershipsCreated int
- // 遗留映射(t_legacy_mapping)
- LegacyMappingsCreated int
- // 冲突/异常清单(P0-08 人工复核输入,不自动合并/丢弃)
- PhoneConflicts []ConflictItem // phone 撞号(同归一 phone >1 legacy id)
- UsernameConflicts []ConflictItem // username 归一后全局重复
- PhoneInvalid []ConflictItem // phone 归一失败(phone_invalid)
- WechatOrphans []ConflictItem // 微信身份 userid 不在 t_user(孤儿,丢弃)
- // 错误(非阻断性警告:单行失败记录,不中断整批)
- Warnings []string
- }
- // ConflictItem 冲突/异常清单的单项(进 P0-08 人工复核)。
- type ConflictItem struct {
- LegacyID int32 // legacy t_user.id(或关联表主键)
- Detail string // 人类可读描述(phone 值 / username 值 / 孤儿 openid)
- }
- // String 渲染为可读报告文本(stdout 输出 / 留档)。
- func (r *MigrateReport) String() string {
- var b strings.Builder
- mode := "RUN"
- if r.DryRun {
- mode = "DRY-RUN"
- }
- b.WriteString(fmt.Sprintf("=== 数据迁移报告(%s)===\n", mode))
- b.WriteString(fmt.Sprintf("机构: %d 读取 / %d 新建\n", r.InstitutionsTotal, r.InstitutionsCreated))
- b.WriteString(fmt.Sprintf("校区: %d 读取 / %d 新建\n", r.CampusesTotal, r.CampusesCreated))
- b.WriteString(fmt.Sprintf("用户: %d 读取 / %d 新建 / %d 幂等跳过 / %d disabled\n",
- r.UsersTotal, r.UsersCreated, r.UsersSkipped, r.UsersDisabled))
- b.WriteString(fmt.Sprintf("身份: phone=%d email=%d username=%d wechat=%d(幂等跳过 %d)\n",
- r.IdentitiesPhone, r.IdentitiesEmail, r.IdentitiesUsername, r.IdentitiesWechat, r.IdentityDuplicates))
- b.WriteString(fmt.Sprintf("微信锚点(t_wechat_union): %d 新建\n", r.WechatUnionCreated))
- b.WriteString(fmt.Sprintf("成员关系(t_membership): %d 新建\n", r.MembershipsCreated))
- b.WriteString(fmt.Sprintf("遗留映射(t_legacy_mapping): %d 新建\n", r.LegacyMappingsCreated))
- if len(r.PhoneConflicts) > 0 {
- b.WriteString(fmt.Sprintf("\n[冲突] phone 撞号 %d 项(转 P0-08 人工):\n", len(r.PhoneConflicts)))
- for _, c := range sortedConflicts(r.PhoneConflicts) {
- b.WriteString(fmt.Sprintf(" legacy_id=%d %s\n", c.LegacyID, c.Detail))
- }
- }
- if len(r.UsernameConflicts) > 0 {
- b.WriteString(fmt.Sprintf("\n[冲突] username 归一后重复 %d 项(转 P0-08 人工):\n", len(r.UsernameConflicts)))
- for _, c := range sortedConflicts(r.UsernameConflicts) {
- b.WriteString(fmt.Sprintf(" legacy_id=%d %s\n", c.LegacyID, c.Detail))
- }
- }
- if len(r.PhoneInvalid) > 0 {
- b.WriteString(fmt.Sprintf("\n[异常] phone 归一失败 %d 项(phone_invalid,原始值留 t_legacy_mapping):\n", len(r.PhoneInvalid)))
- for _, c := range sortedConflicts(r.PhoneInvalid) {
- b.WriteString(fmt.Sprintf(" legacy_id=%d %s\n", c.LegacyID, c.Detail))
- }
- }
- if len(r.WechatOrphans) > 0 {
- b.WriteString(fmt.Sprintf("\n[丢弃] 微信孤儿 %d 项(userid 不在 t_user,openid 无法归属):\n", len(r.WechatOrphans)))
- for _, c := range sortedConflicts(r.WechatOrphans) {
- b.WriteString(fmt.Sprintf(" legacy_id=%d %s\n", c.LegacyID, c.Detail))
- }
- }
- if len(r.Warnings) > 0 {
- b.WriteString(fmt.Sprintf("\n[警告] %d 条:\n", len(r.Warnings)))
- for _, w := range r.Warnings {
- b.WriteString(" " + w + "\n")
- }
- }
- return b.String()
- }
- // sortedConflicts 按 LegacyID 排序(报告稳定输出,防 map 遍历序漂移)。
- func sortedConflicts(items []ConflictItem) []ConflictItem {
- out := make([]ConflictItem, len(items))
- copy(out, items)
- sort.Slice(out, func(i, j int) bool { return out[i].LegacyID < out[j].LegacyID })
- return out
- }
|