| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125 |
- package fixture
- import (
- "database/sql"
- "fmt"
- "time"
- )
- // 本文件是第三批的**纯映射规则**(不连库、不取当前时间)。
- //
- // 三条需要写清楚、否则会被当成"随手填"的判断:
- //
- // 1. `status`:legacy 是 `tinyint(1)`(1=正常 / 0=停用),target 是 `VARCHAR(16)`(active/disabled)。
- // 这是**值域翻译**,不是占位:1→active、0→disabled;出现 1/0 之外的值直接报错,
- // 不猜(更不默认 active —— 那会把已停用的机构/校区悄悄"复活")。
- // 2. `institution_code` / `address` / `phone`:legacy **根本没有这三列**
- // (t_school 只有 province_id/city_id/district_id)→ 一律 NULL,不编造。
- // `uk_institution_code` 是 UNIQUE 但允许 NULL(MySQL 唯一键对 NULL 放行多行),不会撞键。
- // 3. `created_at` / `updated_at`:legacy 无时间列 → **epoch 哨兵**(与 t_user_divide_into 同口径)。
- // 这两列虽然已有 `DEFAULT CURRENT_TIMESTAMP(3)`,但取默认值会让同一份数据每次迁移结果不同,
- // 双跑/回滚就失去可比性。审计列也不需要"迁移时刻"以外的语义。
- // legacyStatusToTarget 把 legacy 的 tinyint 状态翻译成 target 的 active/disabled。
- //
- // 只接受 1 和 0:其它值说明这一列在旧系统里有我们没搞懂的含义,报错比猜安全。
- func legacyStatusToTarget(v int8, what string) (string, error) {
- switch v {
- case 1:
- return "active", nil
- case 0:
- return "disabled", nil
- default:
- return "", fmt.Errorf("%s.status = %d,超出可解释范围(1=active/0=disabled),拒绝猜测", what, v)
- }
- }
- // MapInstitution 映射一个机构(D1:institution_id 原值)。
- func MapInstitution(l LegacySchool) (TargetInstitution, []string, error) {
- var warns []string
- status, err := legacyStatusToTarget(l.Status, fmt.Sprintf("t_school(id=%d)", l.ID))
- if err != nil {
- return TargetInstitution{}, warns, err
- }
- if l.Name == "" {
- return TargetInstitution{}, warns, fmt.Errorf("t_school(id=%d).name 为空,目标态 institution_name NOT NULL,拒绝填占位值", l.ID)
- }
- // 省市区有值但 target 无对应列(address 是自由文本)—— 记录一条告警,不编造地址
- if l.ProvinceID > 0 || l.CityID > 0 || l.DistrictID > 0 {
- warns = append(warns, fmt.Sprintf(
- "t_school(id=%d) 有省市区 id(%d/%d/%d)但 target t_institution 无对应列,address 落 NULL",
- l.ID, l.ProvinceID, l.CityID, l.DistrictID))
- }
- return TargetInstitution{
- InstitutionID: l.ID, // D1
- InstitutionName: l.Name,
- InstitutionCode: sql.NullString{}, // legacy 无此列
- Address: sql.NullString{}, // 同上
- Phone: sql.NullString{}, // 同上
- Status: status,
- CreatedAt: epochSentinel(),
- UpdatedAt: epochSentinel(),
- }, warns, nil
- }
- // MapCampus 映射一个校区(D1:campus_id 原值)。
- //
- // `institution_id` **如实保留** legacy `school_id`(R17):campus 1/2 的 school_id=1,
- // 而数据里它们常与 schoolid=63317 一起出现 —— 这是旧系统的跨机构校区,
- // 这里不修正、不反推、不特判,只在报告里标 cross-org-campus。
- func MapCampus(l LegacyCampus) (TargetCampus, []string, error) {
- var warns []string
- if l.ID == 0 {
- return TargetCampus{}, warns, fmt.Errorf("campus_id=0 不建行(0 是机构级约定值),调用方应过滤")
- }
- if l.SchoolID == 0 {
- return TargetCampus{}, warns, fmt.Errorf(
- "t_school_campus(id=%d).school_id=0,无法确定所属机构,拒绝建孤儿校区", l.ID)
- }
- if l.Name == "" {
- return TargetCampus{}, warns, fmt.Errorf(
- "t_school_campus(id=%d).name 为空,目标态 campus_name NOT NULL,拒绝填占位值", l.ID)
- }
- status, err := legacyStatusToTarget(l.Status, fmt.Sprintf("t_school_campus(id=%d)", l.ID))
- if err != nil {
- return TargetCampus{}, warns, err
- }
- if l.SelfSupport != 0 && l.SelfSupport != 1 {
- return TargetCampus{}, warns, fmt.Errorf(
- "t_school_campus(id=%d).self_support = %d,bit(1) 只应取 0/1,拒绝猜测", l.ID, l.SelfSupport)
- }
- if l.ProvinceID > 0 || l.CityID > 0 || l.DistrictID > 0 {
- warns = append(warns, fmt.Sprintf(
- "t_school_campus(id=%d) 有省市区 id(%d/%d/%d)但 target t_campus 无对应列,address 落 NULL",
- l.ID, l.ProvinceID, l.CityID, l.DistrictID))
- }
- return TargetCampus{
- CampusID: l.ID, // D1
- InstitutionID: l.SchoolID,
- CampusName: l.Name,
- CampusCode: truncateTo(l.Code, 32, fmt.Sprintf("t_school_campus(id=%d).code", l.ID), &warns),
- Address: sql.NullString{},
- Phone: sql.NullString{},
- Status: status,
- SelfSupport: l.SelfSupport,
- CreatedAt: epochSentinel(),
- UpdatedAt: epochSentinel(),
- }, warns, nil
- }
- // IsCrossOrgCampus 判定「数据里的 (schoolid, campusid) 与校区的真实所属机构不一致」。
- //
- // 只用于报告标注(R17),不参与任何修正。campusOwner 是 campus_id → 真实 school_id 的索引。
- func IsCrossOrgCampus(schoolID, campusID int64, campusOwner map[int64]int64) bool {
- if campusID == 0 {
- return false
- }
- owner, ok := campusOwner[campusID]
- if !ok {
- return false
- }
- return owner != schoolID
- }
- // orgEpoch 便于单测断言哨兵值。
- func orgEpoch() time.Time { return epochSentinel() }
|