| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600 |
- package fixture
- import (
- "context"
- "database/sql"
- "fmt"
- "sort"
- "strconv"
- "strings"
- )
- // 本文件是第三批的**核对**:逐列 + 机构集合 + 引用完整性(孤儿=0)+ R17 + bit(1)。
- //
- // 期望值来源与前三批一致:机构/校区字段由 **MySQL 服务端**取值(`self_support+0`、
- // `CAST(...)`),不由映射代码产生;只有 `status` 的 active/disabled 是规则翻译,
- // 单独作为断言说明(并在映射里对非法值报错)。
- // OrgVerifyReport 是第三批核对结果。
- type OrgVerifyReport struct {
- Options OrgOptions
- Cols []ColumnAgg
- Assertions []Assertion
- RefStats []RefStat
- CrossOrg []CrossOrgStat // 跨机构三类统计(A 课程级 / B 记录级 / C 记录 vs 课程)
- Disabled []DisabledCampusStat // 停用校区(status=0)的影响面
- TargetInst int64
- TargetCamp int64
- Warnings []string
- }
- // FailCount 统计不一致列 + 失败断言。
- func (r *OrgVerifyReport) FailCount() int {
- n := 0
- for _, c := range r.Cols {
- if !c.OK() {
- n++
- }
- }
- for _, a := range r.Assertions {
- if !a.Pass {
- n++
- }
- }
- return n
- }
- // OK 表示整轮通过。
- func (r *OrgVerifyReport) OK() bool { return r.FailCount() == 0 }
- // VerifyOrg 执行第三批核对。
- func VerifyOrg(ctx context.Context, cfg Config, opts OrgOptions) (*OrgVerifyReport, error) {
- src, err := cfg.OpenOrgSource()
- if err != nil {
- return nil, err
- }
- defer src.Close()
- legacy, err := cfg.OpenLegacy() // finance_bk:引用完整性统计用
- if err != nil {
- return nil, err
- }
- defer legacy.Close()
- target, err := cfg.OpenTarget()
- if err != nil {
- return nil, err
- }
- defer target.Close()
- rep := &OrgVerifyReport{Options: opts}
- // ---------- 1. t_institution 逐列 ----------
- expSchools, err := extractSchools(ctx, src, opts.InstitutionIDs)
- if err != nil {
- return nil, err
- }
- cID := newCol("t_institution", "institution_id", "D1:与 t_school.id 完全相同")
- cName := newCol("t_institution", "institution_name", "t_school.name 原样")
- cCode := newCol("t_institution", "institution_code", "legacy 无此列 → 必须 NULL")
- cAddr := newCol("t_institution", "address", "legacy 无此列 → 必须 NULL")
- cPhone := newCol("t_institution", "phone", "legacy 无此列 → 必须 NULL")
- cStatus := newCol("t_institution", "status", "tinyint 1→active / 0→disabled(规则翻译)")
- cCreated := newCol("t_institution", "created_at", "legacy 无时间列 → epoch 占位")
- cUpdated := newCol("t_institution", "updated_at", "legacy 无时间列 → epoch 占位")
- var legacyIDs []int64
- for _, s := range expSchools {
- legacyIDs = append(legacyIDs, s.ID)
- rowKey := "id=" + strconv.FormatInt(s.ID, 10)
- var got struct {
- ID int64
- Name string
- Code sql.NullString
- Addr sql.NullString
- Phone sql.NullString
- Status string
- Created sql.NullString
- Updated sql.NullString
- }
- err := target.QueryRowContext(ctx, `
- SELECT institution_id, institution_name, institution_code, address, phone, status,
- DATE_FORMAT(created_at,'%Y-%m-%d %H:%i:%s'), DATE_FORMAT(updated_at,'%Y-%m-%d %H:%i:%s')
- FROM t_institution WHERE institution_id = ?`, s.ID).Scan(
- &got.ID, &got.Name, &got.Code, &got.Addr, &got.Phone, &got.Status, &got.Created, &got.Updated)
- if err == sql.ErrNoRows {
- cID.cmp(rowKey, strconv.FormatInt(s.ID, 10), "<缺失>", false)
- continue
- }
- if err != nil {
- return nil, err
- }
- wantStatus := "active"
- if s.Status == 0 {
- wantStatus = "disabled"
- }
- cID.cmp(rowKey, strconv.FormatInt(s.ID, 10), strconv.FormatInt(got.ID, 10), got.ID == s.ID)
- cName.cmp(rowKey, s.Name, got.Name, got.Name == s.Name)
- cCode.cmp(rowKey, "NULL", nullStrOrNull(got.Code), !got.Code.Valid)
- cAddr.cmp(rowKey, "NULL", nullStrOrNull(got.Addr), !got.Addr.Valid)
- cPhone.cmp(rowKey, "NULL", nullStrOrNull(got.Phone), !got.Phone.Valid)
- cStatus.cmp(rowKey, fmt.Sprintf("%d→%s", s.Status, wantStatus), got.Status, got.Status == wantStatus)
- cCreated.cmp(rowKey, LegacyMissingTime[:10], nullStrOrNull(got.Created),
- strings.HasPrefix(nullStrOrNull(got.Created), LegacyMissingTime[:10]))
- cUpdated.cmp(rowKey, LegacyMissingTime[:10], nullStrOrNull(got.Updated),
- strings.HasPrefix(nullStrOrNull(got.Updated), LegacyMissingTime[:10]))
- }
- rep.Cols = append(rep.Cols, cID.done(), cName.done(), cCode.done(), cAddr.done(),
- cPhone.done(), cStatus.done(), cCreated.done(), cUpdated.done())
- // ---------- 2. 机构集合断言 ----------
- var gotIDs []int64
- rows, err := target.QueryContext(ctx, `SELECT institution_id FROM t_institution ORDER BY institution_id`)
- if err != nil {
- return nil, err
- }
- for rows.Next() {
- var id int64
- if err := rows.Scan(&id); err != nil {
- rows.Close()
- return nil, err
- }
- gotIDs = append(gotIDs, id)
- }
- rows.Close()
- rep.TargetInst = int64(len(gotIDs))
- want := append([]int64(nil), opts.InstitutionIDs...)
- sort.Slice(want, func(i, j int) bool { return want[i] < want[j] })
- rep.Assertions = append(rep.Assertions, Assertion{
- Name: "机构集合精确等于期望集合(不多不少)",
- Pass: int64SliceEqual(gotIDs, want),
- Detail: fmt.Sprintf("target t_institution = %v(%d 行);期望 = %v(%d 行)",
- gotIDs, len(gotIDs), want, len(want)),
- })
- // ---------- 3. t_campus 逐列 ----------
- expCampuses, err := extractCampuses(ctx, src, opts.InstitutionIDs)
- if err != nil {
- return nil, err
- }
- if !opts.WithAllCampuses {
- refd, err := extractReferencedCampuses(ctx, legacy)
- if err != nil {
- return nil, err
- }
- var kept []LegacyCampus
- for _, c := range expCampuses {
- if refd[c.ID] {
- kept = append(kept, c)
- }
- }
- expCampuses = kept
- }
- cCampusID := newCol("t_campus", "campus_id", "D1:与 t_school_campus.id 完全相同")
- cInstID := newCol("t_campus", "institution_id", "R17:如实保留 school_id(不修正跨机构校区)")
- cCampusName := newCol("t_campus", "campus_name", "name 原样")
- cCampusCode := newCol("t_campus", "campus_code", "code → campus_code(NULL 保留)")
- cCampusAddr := newCol("t_campus", "address", "legacy 无此列 → 必须 NULL")
- cCampusPhone := newCol("t_campus", "phone", "legacy 无此列 → 必须 NULL")
- cCampusStatus := newCol("t_campus", "status", "tinyint 1→active / 0→disabled")
- cSelfSupport := newCol("t_campus", "self_support", "bit(1) → TINYINT(1),逐行相等")
- cCampusCreated := newCol("t_campus", "created_at", "epoch 占位")
- cCampusUpdated := newCol("t_campus", "updated_at", "epoch 占位")
- campusOwner := map[int64]int64{}
- var gotCampusIDs []int64
- for _, c := range expCampuses {
- campusOwner[c.ID] = c.SchoolID
- rowKey := "id=" + strconv.FormatInt(c.ID, 10)
- var got struct {
- ID, InstID int64
- Name string
- Code sql.NullString
- Addr sql.NullString
- Phone sql.NullString
- Status string
- SelfSup int8
- Created sql.NullString
- Updated sql.NullString
- }
- err := target.QueryRowContext(ctx, `
- SELECT campus_id, institution_id, campus_name, campus_code, address, phone, status,
- self_support, DATE_FORMAT(created_at,'%Y-%m-%d %H:%i:%s'), DATE_FORMAT(updated_at,'%Y-%m-%d %H:%i:%s')
- FROM t_campus WHERE campus_id = ?`, c.ID).Scan(
- &got.ID, &got.InstID, &got.Name, &got.Code, &got.Addr, &got.Phone, &got.Status,
- &got.SelfSup, &got.Created, &got.Updated)
- if err == sql.ErrNoRows {
- cCampusID.cmp(rowKey, strconv.FormatInt(c.ID, 10), "<缺失>", false)
- continue
- }
- if err != nil {
- return nil, err
- }
- gotCampusIDs = append(gotCampusIDs, got.ID)
- wantStatus := "active"
- if c.Status == 0 {
- wantStatus = "disabled"
- }
- cCampusID.cmp(rowKey, strconv.FormatInt(c.ID, 10), strconv.FormatInt(got.ID, 10), got.ID == c.ID)
- cInstID.cmp(rowKey, strconv.FormatInt(c.SchoolID, 10), strconv.FormatInt(got.InstID, 10), got.InstID == c.SchoolID)
- cCampusName.cmp(rowKey, c.Name, got.Name, got.Name == c.Name)
- cCampusCode.cmp(rowKey, nullStrOrNull(c.Code), nullStrOrNull(got.Code), nullStrOrNull(c.Code) == nullStrOrNull(got.Code))
- cCampusAddr.cmp(rowKey, "NULL", nullStrOrNull(got.Addr), !got.Addr.Valid)
- cCampusPhone.cmp(rowKey, "NULL", nullStrOrNull(got.Phone), !got.Phone.Valid)
- cCampusStatus.cmp(rowKey, fmt.Sprintf("%d→%s", c.Status, wantStatus), got.Status, got.Status == wantStatus)
- cSelfSupport.cmp(rowKey, strconv.Itoa(int(c.SelfSupport)), strconv.Itoa(int(got.SelfSup)),
- got.SelfSup == c.SelfSupport)
- cCampusCreated.cmp(rowKey, LegacyMissingTime[:10], nullStrOrNull(got.Created),
- strings.HasPrefix(nullStrOrNull(got.Created), LegacyMissingTime[:10]))
- cCampusUpdated.cmp(rowKey, LegacyMissingTime[:10], nullStrOrNull(got.Updated),
- strings.HasPrefix(nullStrOrNull(got.Updated), LegacyMissingTime[:10]))
- }
- rep.Cols = append(rep.Cols, cCampusID.done(), cInstID.done(), cCampusName.done(),
- cCampusCode.done(), cCampusAddr.done(), cCampusPhone.done(), cCampusStatus.done(),
- cSelfSupport.done(), cCampusCreated.done(), cCampusUpdated.done())
- rep.TargetCamp = int64(len(gotCampusIDs))
- // ---------- 4. campus_id = 0 不得建行 ----------
- var zeroCampus int
- if err := target.QueryRowContext(ctx, `SELECT COUNT(*) FROM t_campus WHERE campus_id = 0`).Scan(&zeroCampus); err != nil {
- return nil, err
- }
- rep.Assertions = append(rep.Assertions, Assertion{
- Name: "t_campus 不建 campus_id=0 的行(0 是机构级约定值)",
- Pass: zeroCampus == 0,
- Detail: fmt.Sprintf("t_campus 里 campus_id=0 的行数 = %d(期望 0)", zeroCampus),
- })
- // ---------- 5. R17 ----------
- r17OK := true
- var r17Detail []string
- for _, cid := range []int64{1, 2} {
- var inst sql.NullInt64
- err := target.QueryRowContext(ctx, `SELECT institution_id FROM t_campus WHERE campus_id = ?`, cid).Scan(&inst)
- if err != nil {
- r17OK = false
- r17Detail = append(r17Detail, fmt.Sprintf("campus %d 读不到:%v", cid, err))
- continue
- }
- ok := inst.Valid && inst.Int64 == 1
- if !ok {
- r17OK = false
- }
- r17Detail = append(r17Detail, fmt.Sprintf("campus %d institution_id=%d(期望 1)", cid, inst.Int64))
- }
- var inst1 int
- if err := target.QueryRowContext(ctx, `SELECT COUNT(*) FROM t_institution WHERE institution_id = 1`).Scan(&inst1); err != nil {
- return nil, err
- }
- rep.Assertions = append(rep.Assertions, Assertion{
- Name: "R17:campus 1/2 的 institution_id = 1,且 t_institution(1) 存在",
- Pass: r17OK && inst1 == 1,
- Detail: strings.Join(r17Detail, ";") + fmt.Sprintf(";t_institution(1) 行数=%d", inst1),
- })
- // ---------- 6. 引用完整性(每个来源逐个统计孤儿) ----------
- for _, s := range orgRefSources() {
- st := RefStat{Source: s.Table}
- if err := legacy.QueryRowContext(ctx, fmt.Sprintf("SELECT COUNT(*) FROM `%s`", s.Table)).Scan(&st.Rows); err != nil {
- return nil, err
- }
- if err := legacy.QueryRowContext(ctx, fmt.Sprintf(
- "SELECT COUNT(DISTINCT `%s`) FROM `%s`", s.InstCol, s.Table)).Scan(&st.DistinctInst); err != nil {
- return nil, err
- }
- // 孤儿机构
- orphanInstSQL := fmt.Sprintf(`
- SELECT COUNT(*) FROM `+"`%s`"+` s
- LEFT JOIN %s.t_institution i ON i.institution_id = s.`+"`%s`"+`
- WHERE i.institution_id IS NULL`, s.Table, cfg.TargetDB, s.InstCol)
- if err := legacy.QueryRowContext(ctx, orphanInstSQL).Scan(&st.OrphanInst); err != nil {
- return nil, fmt.Errorf("%s 孤儿机构统计失败:%w", s.Table, err)
- }
- if st.OrphanInst > 0 {
- rows, err := legacy.QueryContext(ctx, fmt.Sprintf(`
- SELECT DISTINCT s.`+"`%s`"+` FROM `+"`%s`"+` s
- LEFT JOIN %s.t_institution i ON i.institution_id = s.`+"`%s`"+`
- WHERE i.institution_id IS NULL ORDER BY 1`, s.InstCol, s.Table, cfg.TargetDB, s.InstCol))
- if err == nil {
- for rows.Next() {
- var v int64
- _ = rows.Scan(&v)
- st.OrphanInstList = append(st.OrphanInstList, strconv.FormatInt(v, 10))
- }
- rows.Close()
- }
- }
- if s.CampusCol != "" {
- if err := legacy.QueryRowContext(ctx, fmt.Sprintf(
- "SELECT COUNT(DISTINCT `%s`) FROM `%s` WHERE `%s` <> 0", s.CampusCol, s.Table, s.CampusCol)).Scan(&st.DistinctCampus); err != nil {
- return nil, err
- }
- if err := legacy.QueryRowContext(ctx, fmt.Sprintf(
- "SELECT COUNT(*) FROM `%s` WHERE `%s` = 0", s.Table, s.CampusCol)).Scan(&st.CampusZeroRows); err != nil {
- return nil, err
- }
- orphanCampusSQL := fmt.Sprintf(`
- SELECT COUNT(*) FROM `+"`%s`"+` s
- LEFT JOIN %s.t_campus c ON c.campus_id = s.`+"`%s`"+`
- WHERE s.`+"`%s`"+` <> 0 AND c.campus_id IS NULL`, s.Table, cfg.TargetDB, s.CampusCol, s.CampusCol)
- if err := legacy.QueryRowContext(ctx, orphanCampusSQL).Scan(&st.OrphanCampus); err != nil {
- return nil, fmt.Errorf("%s 孤儿校区统计失败:%w", s.Table, err)
- }
- if st.OrphanCampus > 0 {
- rows, err := legacy.QueryContext(ctx, fmt.Sprintf(`
- SELECT DISTINCT s.`+"`%s`"+` FROM `+"`%s`"+` s
- LEFT JOIN %s.t_campus c ON c.campus_id = s.`+"`%s`"+`
- WHERE s.`+"`%s`"+` <> 0 AND c.campus_id IS NULL ORDER BY 1`, s.CampusCol, s.Table, cfg.TargetDB, s.CampusCol, s.CampusCol))
- if err == nil {
- for rows.Next() {
- var v int64
- _ = rows.Scan(&v)
- st.OrphanCampusList = append(st.OrphanCampusList, strconv.FormatInt(v, 10))
- }
- rows.Close()
- }
- }
- }
- rep.RefStats = append(rep.RefStats, st)
- }
- totalOrphanInst, totalOrphanCampus := int64(0), int64(0)
- var orphanDetail []string
- for _, st := range rep.RefStats {
- totalOrphanInst += st.OrphanInst
- totalOrphanCampus += st.OrphanCampus
- if st.OrphanInst > 0 {
- orphanDetail = append(orphanDetail, fmt.Sprintf("%s 孤儿机构 %d 行 %v", st.Source, st.OrphanInst, st.OrphanInstList))
- }
- if st.OrphanCampus > 0 {
- orphanDetail = append(orphanDetail, fmt.Sprintf("%s 孤儿校区 %d 行 %v", st.Source, st.OrphanCampus, st.OrphanCampusList))
- }
- }
- detail := fmt.Sprintf("8 个来源合计:孤儿机构 %d 行、孤儿校区 %d 行(期望都为 0)", totalOrphanInst, totalOrphanCampus)
- if len(orphanDetail) > 0 {
- detail += ";" + strings.Join(orphanDetail, ";")
- }
- rep.Assertions = append(rep.Assertions, Assertion{
- Name: "引用完整性:数据里出现过的每个 institution_id / campus_id 都能在 target 找到(孤儿 = 0)",
- Pass: totalOrphanInst == 0 && totalOrphanCampus == 0,
- Detail: detail,
- })
- // ---------- 7. 跨机构:三个来源分别统计 ----------
- rep.CrossOrg = append(rep.CrossOrg, crossOrgCourseLevel(ctx, legacy)...)
- rep.CrossOrg = append(rep.CrossOrg, crossOrgRecordLevel(ctx, legacy)...)
- rep.CrossOrg = append(rep.CrossOrg, crossOrgRecordVsCourse(ctx, legacy)...)
- // ---------- 8. 停用校区(status=0)的影响面 + 断言 ----------
- disabled, err := disabledCampusStats(ctx, legacy, opts)
- if err != nil {
- return nil, err
- }
- rep.Disabled = disabled
- var wantDisabled []int64
- var gotDisabled []int64
- for _, d := range disabled {
- wantDisabled = append(wantDisabled, d.CampusID)
- }
- rowsD, err := target.QueryContext(ctx,
- `SELECT campus_id FROM t_campus WHERE status = 'disabled' ORDER BY campus_id`)
- if err != nil {
- return nil, err
- }
- for rowsD.Next() {
- var id int64
- if err := rowsD.Scan(&id); err != nil {
- rowsD.Close()
- return nil, err
- }
- gotDisabled = append(gotDisabled, id)
- }
- rowsD.Close()
- rep.Assertions = append(rep.Assertions, Assertion{
- Name: "停用校区数量与 id 精确匹配(status=disabled 的必须恰好是 legacy status=0 的那批)",
- Pass: int64SliceEqual(gotDisabled, wantDisabled) && len(wantDisabled) > 0,
- Detail: fmt.Sprintf("target t_campus WHERE status='disabled' → %v(%d 个);legacy status=0 → %v(%d 个)",
- gotDisabled, len(gotDisabled), wantDisabled, len(wantDisabled)),
- })
- // 逐行再确认一次:这 8 个校区的 status 没有被"为了方便"改成 active
- badActive := 0
- for _, id := range wantDisabled {
- var st string
- if err := target.QueryRowContext(ctx, `SELECT status FROM t_campus WHERE campus_id = ?`, id).Scan(&st); err != nil {
- return nil, err
- }
- if st != "disabled" {
- badActive++
- }
- }
- rep.Assertions = append(rep.Assertions, Assertion{
- Name: "停用校区未被改成 active(不允许为了\"让数据可见\"篡改 status)",
- Pass: badActive == 0,
- Detail: fmt.Sprintf("%d 个 legacy status=0 的校区里,target 落成非 disabled 的个数 = %d(期望 0)", len(wantDisabled), badActive),
- })
- return rep, nil
- }
- // SnapshotOrg 采集 target 两张表的 (id → 名称) 快照,用于幂等比对的「id 不变」判定。
- func SnapshotOrg(ctx context.Context, cfg Config) (map[int64]string, map[int64]string, error) {
- db, err := cfg.OpenTarget()
- if err != nil {
- return nil, nil, err
- }
- defer db.Close()
- inst := map[int64]string{}
- camp := map[int64]string{}
- rows, err := db.QueryContext(ctx, `SELECT institution_id, institution_name FROM t_institution`)
- if err != nil {
- return nil, nil, err
- }
- for rows.Next() {
- var id int64
- var n string
- if err := rows.Scan(&id, &n); err != nil {
- rows.Close()
- return nil, nil, err
- }
- inst[id] = n
- }
- rows.Close()
- rows2, err := db.QueryContext(ctx, `SELECT campus_id, campus_name FROM t_campus`)
- if err != nil {
- return nil, nil, err
- }
- for rows2.Next() {
- var id int64
- var n string
- if err := rows2.Scan(&id, &n); err != nil {
- rows2.Close()
- return nil, nil, err
- }
- camp[id] = n
- }
- rows2.Close()
- return inst, camp, nil
- }
- // ---------- 跨机构三类统计 ----------
- // crossOrgCourseLevel(A)课程级:t_courses.schoolid vs 该课程 campusid 的校区真实所属。
- func crossOrgCourseLevel(ctx context.Context, db *sql.DB) []CrossOrgStat {
- st := CrossOrgStat{
- Key: "A", Name: "课程级:课程挂的机构 vs 校区的真实所属",
- Source: "finance_bk.t_courses.schoolid ↔ yuansiwei_i.t_school_campus.school_id(campusid)",
- Note: "R17 的主体:校区 1/2 属机构 1,但课程普遍挂在 63317 下",
- }
- _ = db.QueryRowContext(ctx, `
- SELECT COUNT(*),
- SUM(sc.id IS NULL),
- SUM(sc.id IS NOT NULL AND sc.school_id = c.schoolid),
- SUM(sc.id IS NOT NULL AND sc.school_id <> c.schoolid)
- FROM t_courses c LEFT JOIN yuansiwei_i.t_school_campus sc ON sc.id = c.campusid`).
- Scan(&st.Total, &st.Skipped, &st.Consistent, &st.Inconsistent)
- st.Samples = topCombos(ctx, db, `
- SELECT c.schoolid, c.campusid, ifnull(sc.school_id,0), COUNT(*)
- FROM t_courses c LEFT JOIN yuansiwei_i.t_school_campus sc ON sc.id = c.campusid
- WHERE sc.id IS NOT NULL AND sc.school_id <> c.schoolid
- GROUP BY 1,2,3 ORDER BY 4 DESC LIMIT 5`, "课程挂机构=%d 校区=%d(真实所属=%d) → %d 行")
- return []CrossOrgStat{st}
- }
- // crossOrgRecordLevel(B)记录级:t_courses_record.schoolid vs 该记录 campusid 的校区真实所属。
- func crossOrgRecordLevel(ctx context.Context, db *sql.DB) []CrossOrgStat {
- st := CrossOrgStat{
- Key: "B", Name: "记录级:上课记录挂的机构 vs 校区的真实所属",
- Source: "finance_bk.t_courses_record.schoolid ↔ yuansiwei_i.t_school_campus.school_id(campusid)",
- Note: "含反向跨机构(记录挂机构 1、校区属 63317)",
- }
- _ = db.QueryRowContext(ctx, `
- SELECT COUNT(*),
- SUM(sc.id IS NULL),
- SUM(sc.id IS NOT NULL AND sc.school_id = r.schoolid),
- SUM(sc.id IS NOT NULL AND sc.school_id <> r.schoolid)
- FROM t_courses_record r LEFT JOIN yuansiwei_i.t_school_campus sc ON sc.id = r.campusid`).
- Scan(&st.Total, &st.Skipped, &st.Consistent, &st.Inconsistent)
- st.Samples = topCombos(ctx, db, `
- SELECT r.schoolid, r.campusid, ifnull(sc.school_id,0), COUNT(*)
- FROM t_courses_record r LEFT JOIN yuansiwei_i.t_school_campus sc ON sc.id = r.campusid
- WHERE sc.id IS NOT NULL AND sc.school_id <> r.schoolid
- GROUP BY 1,2,3 ORDER BY 4 DESC LIMIT 5`, "记录挂机构=%d 校区=%d(真实所属=%d) → %d 行")
- return []CrossOrgStat{st}
- }
- // crossOrgRecordVsCourse(C)记录 vs 课程:t_courses_record.schoolid vs 其 coursesid 的 schoolid。
- //
- // 与 A/B 性质不同:这里两个都是"数据自己声明的机构",不一致说明记录挂错了机构,
- // 与校区归属无关。
- func crossOrgRecordVsCourse(ctx context.Context, db *sql.DB) []CrossOrgStat {
- st := CrossOrgStat{
- Key: "C", Name: "记录 vs 课程:记录的机构 vs 其所属课程的机构",
- Source: "finance_bk.t_courses_record.schoolid ↔ finance_bk.t_courses.schoolid(coursesid)",
- Note: "与校区归属无关,纯粹是记录挂错机构",
- }
- _ = db.QueryRowContext(ctx, `
- SELECT COUNT(*), SUM(r.schoolid = c.schoolid), SUM(r.schoolid <> c.schoolid)
- FROM t_courses_record r JOIN t_courses c ON c.id = r.coursesid`).
- Scan(&st.Total, &st.Consistent, &st.Inconsistent)
- st.Samples = topCombos3(ctx, db, `
- SELECT r.schoolid, c.schoolid, COUNT(*)
- FROM t_courses_record r JOIN t_courses c ON c.id = r.coursesid
- WHERE r.schoolid <> c.schoolid
- GROUP BY 1,2 ORDER BY 3 DESC LIMIT 5`, "记录挂机构=%d 课程属机构=%d → %d 行")
- return []CrossOrgStat{st}
- }
- // topCombos 执行「不一致组合 TOP N」查询并格式化成样例行。
- func topCombos(ctx context.Context, db *sql.DB, q, format string) []string {
- rows, err := db.QueryContext(ctx, q)
- if err != nil {
- return []string{"<查询失败:" + err.Error() + ">"}
- }
- defer rows.Close()
- var out []string
- for rows.Next() {
- var a, b, c, n int64
- if err := rows.Scan(&a, &b, &c, &n); err != nil {
- return out
- }
- out = append(out, fmt.Sprintf(format, a, b, c, n))
- }
- return out
- }
- // topCombos3 是 topCombos 的 3 列版本(格式串 verb 数与列数必须严格一致)。
- func topCombos3(ctx context.Context, db *sql.DB, q, format string) []string {
- rows, err := db.QueryContext(ctx, q)
- if err != nil {
- return []string{"<查询失败:" + err.Error() + ">"}
- }
- defer rows.Close()
- var out []string
- for rows.Next() {
- var a, b, n int64
- if err := rows.Scan(&a, &b, &n); err != nil {
- return out
- }
- out = append(out, fmt.Sprintf(format, a, b, n))
- }
- return out
- }
- // ---------- 停用校区影响面 ----------
- // disabledCampusStats 统计「所选机构里 status=0 的校区」各自挂了多少历史数据。
- //
- // 这不是为了"好看":迁过去之后这些校区是 disabled,历史数据在"按启用校区筛选"时看不到,
- // 而写入侧 assertInScope 会拒绝给停用校区新增数据(这是对的)——
- // 业务侧会看到"历史数据消失",必须让业务知情。
- func disabledCampusStats(ctx context.Context, db *sql.DB, opts OrgOptions) ([]DisabledCampusStat, error) {
- ph := placeholders(len(opts.InstitutionIDs))
- args := make([]any, len(opts.InstitutionIDs))
- for i, v := range opts.InstitutionIDs {
- args[i] = v
- }
- q := fmt.Sprintf(`
- SELECT sc.id, sc.name, sc.school_id,
- (SELECT COUNT(*) FROM t_courses x WHERE x.campusid = sc.id),
- (SELECT COUNT(*) FROM t_courses_record x WHERE x.campusid = sc.id),
- (SELECT COUNT(*) FROM t_user_expense_school x WHERE x.campusid = sc.id),
- (SELECT COUNT(*) FROM t_user_balance_request x WHERE x.campusid = sc.id),
- (SELECT COUNT(*) FROM t_expenditure_log x WHERE x.campusid = sc.id)
- FROM yuansiwei_i.t_school_campus sc
- WHERE sc.school_id IN (%s) AND sc.status = 0
- ORDER BY sc.id`, ph)
- rows, err := db.QueryContext(ctx, q, args...)
- if err != nil {
- return nil, fmt.Errorf("停用校区统计失败:%w", err)
- }
- defer rows.Close()
- var out []DisabledCampusStat
- for rows.Next() {
- var d DisabledCampusStat
- if err := rows.Scan(&d.CampusID, &d.Name, &d.InstitutionID, &d.Courses, &d.Records,
- &d.ExpenseSchool, &d.BalanceRequest, &d.ExpenditureLog); err != nil {
- return nil, err
- }
- out = append(out, d)
- }
- return out, rows.Err()
- }
|