第 4 章 数据库架构与核心业务模型深度解析

2025-12-06T10:00:00+08:00 | 19分钟阅读 | 更新于 2025-12-06T10:00:00+08:00

@

第 4 章 数据库架构与核心业务模型深度解析

1 学习目标

学完本章你应该能够:

  1. 说出 13 张表的归属族群(资源统一模型族 / 类型明细 / 任务 / 账单 / 凭证),并指出每族的代表表,同时说清"表清单是从代码哪里反推出来的";
  2. 解释统一资源模型 + 垂直分表的设计:为什么 Resource 拆成 meta/spec/cost/status 四张表,以及垂直分表带来的三个收益;
  3. 说明 resource_id 关联模式与 ContentHash 增量同步设计的原理——为什么"哈希没变不写库";
  4. 画出"凭证 → 任务 → 资源"三者的数据关联链路,并用它回答审计三问:谁、什么时候、同步了哪些资源;
  5. 说出多租户三件套(domain/namespace/env)与统一枚举(全项目的"普通话词典")分别解决什么问题。

前置知识

  • 第 1~3 章的模块划分(resource / secret / task / host 等模块各自的职责);
  • MySQL 基础:主键、索引、外键的区别;
  • 知道 proto 生成的 Go message 长什么样(不要求会写 .proto)。

本章你会动手做的事

  1. 从 apps/resource/impl/model.go 的 6 个 TableName() 和各模块 impl/sql.go 的 SQL 常量出发,亲手把 13 张表按族群归类数一遍;
  2. 在 proto 的 ContentHash 里找到六个 hash 字段,理解它们在 resource_meta 表上的落位;
  3. 顺着 credential_id 把 secrets → task → task_record → resource_meta 的血缘链在代码里走一遍。

先看本章的知识地图,心里有全景再进细节:

第 4 章总览五大家族 13 表垂直分表增量同步血缘与租户资源统一模型 6 张类型明细 2 张任务/账单/凭证 5 张Meta/Spec/Cost/Status明细表 host·rds主键=云厂商ID六个 Hash 字段哈希没变不写库空间换时间secret→task→resourcedomain/namespace/env统一枚举普通话

2 知识点通俗系统化讲解

2.1 先看全景:13 张表分成五个族群

先交代一个读库姿势:这个仓库没有把建表 DDL 文件随代码发布——cmd/init.go:68 里 init 子命令的默认参数是读取 docs/schema/tables.sql 执行,但仓库里并没有 docs/ 目录。所以表清单要从代码事实反推,依据有两处:

  1. GORM 的 TableName() 方法:模型告诉 GORM 自己映射到哪张表(apps/resource/impl/model.go:84-139 一口气定义了 6 张);
  2. 各模块 impl/sql.go 里的 SQL 常量:INSERT INTO xxx / FROM xxx 里的表名是最诚实的证据。

把两处证据汇总,全项目一共 13 张表:

家族一:资源统一模型族(核心中的核心,6 张)

表说明证据
resource_meta资源基础信息(所有资源公共,全局检索入口)model.go:84 常量 RESOURCE_META_TABLE_NAME
resource_spec资源规格(厂商/类型/地域/配置等)model.go:101
resource_status资源状态(phase/内外网 IP)model.go:119
resource_cost资源费用(付费方式/价格)model.go:110
resource_tag资源标签(1:N)model.go:130
resource_relation资源关系(表映射定义了,代码未使用)model.go:139

家族二:类型明细族(2 张):resource_host(主机专属:os/镜像/带宽/安全组,apps/host/impl/sql.go:4-10)、resource_rds(RDS 专属:引擎/端口/白名单,apps/rds/impl/sql.go:4-10)。

家族三:任务族(2 张):task(同步任务主表)、task_record(任务明细,每实例成功/失败),见 apps/task/impl/sql.go:4-25。

家族四:账单族(2 张):bill_raw(厂商月账单原始数据)、bill(按资源聚合后的账单),见 apps/bill/impl/sql.go。

家族五:凭证族(1 张):secrets(同步凭证,注意是复数表名,apps/secret/secret.go:5-7 的 TableName() 返回 "secrets")。

类比:这 13 张表像一栋大楼里的档案室:资源统一模型族是"住户登记总册"(每家每户的公共信息都登记在这),类型明细是"户型附页"(别墅登记花园面积、公寓登记物业费),任务是"抄表工单",账单是"物业收费台账",凭证是"门禁卡发放记录"。对应到工程里就是:读代码前先分清"哪张表属于哪个档案室",你就再也不会在表堆里迷路——遇到一张新表,先问一句"它属于哪个家族、跟 resource_meta 什么关系"。
⚠️ 新手必踩的坑:host 和 rds 模块的查询 SQL(apps/host/impl/sql.go:20-34 的 queryHostSQL/countHostSQL、apps/rds/impl/sql.go:17 的 queryRdsSQL)还在 JOIN 一张叫 resource 的旧大表——这是垂直分表改造前的历史遗留 schema,仓库里已经没有它的模型定义。也就是说项目处于"写入走新表(meta/spec/cost/status 分表)、部分查询还引用旧表"的过渡态。真实开源项目就是这样长满藤蔓的,读的时候要分清"设计方向"和"迁移进度"。

2.2 统一资源模型:为什么是 Meta+Spec+Cost+Status 四件套?

先看 proto 里定义的领域模型(apps/resource/resource.pb.go):

Resource
 ├── Meta        → id / domain / namespace / env / sync_at / credential_id / create_at / usage_mode
 ├── Spec        → vendor / resource_type / region / zone / owner / name / cpu / memory / tags / ...
 ├── Cost        → pay_mode / 官网价 / 实付 / 折扣
 ├── Status      → phase(运行状态) / public_ip / private_ip
 ├── ContentHash → spec_hash / cost_hash / status_hash / tag_hash / relation_hash / custom_hash
 └── RelatedResources
类比:把一台云主机想象成一辆二手车:
• Meta 是行驶证:车牌号(id)、登记时间(create_at)、最近年检时间(sync_at)、归哪个车主公司(domain/namespace)——不管什么车,行驶证的栏目都一样;
• Spec 是配置单:品牌(vendor)、车型(resource_type)、排量(cpu)、座位数(memory)——不同车型配置单栏目差异大,但归纳成"规格"这一类;
• Cost 是购车发票:全款还是分期(pay_mode)、原价、成交价、折扣;
• Status 是仪表盘:当前状态(运行/熄火/故障)、行驶里程——高频变化的信息单独放;
• ContentHash 是车况指纹:把 Spec/Cost/Status 分别算一个哈希,下次同步时对比哈希就知道"这车有没有被改过",没变就不写库(增量同步的基础)。
对应到工程里就是:检索要用到的"身份与管理字段"进 Meta,描述"这台机器是什么"的进 Spec,"花多少钱"进 Cost,"现在跑得怎么样"进 Status——按变化频率和查询维度切表,而不是把所有字段堆在一起。

2.3 垂直分表:公共栏放主表,专属栏放明细表

四件套落到数据库就是四张表,加上类型明细表:

resource_meta (id = 云厂商资源ID,全局主键)
     │ 1:1
     ├── resource_spec   (resource_id)  公共规格
     ├── resource_cost   (resource_id)  费用
     ├── resource_status (resource_id)  状态
     ├── resource_tag    (resource_id)  标签(1:N)
     │ 1:1(类型专属明细)
     ├── resource_host   (resource_id)  主机专属:os/镜像/带宽/安全组
     └── resource_rds    (resource_id)  RDS专属:引擎/端口/白名单

为什么要垂直分表,而不是一张大宽表? 三个理由:

  1. 扩展性:新增 MongoDB 资源类型,只需要加一张 resource_mongodb 明细表,主模型四件套一个字段都不用改——如果是一张大宽表,每加一种资源就要 ALTER TABLE 加 N 个列,线上大表加列是高危操作;
  2. 读写分离关注点:检索列表页只需要 meta+spec+cost+status(JOIN 四张表,apps/resource/impl/dao.go:12-17 的 BuildQuery 就是这么干的);状态频繁变化只 UPDATE resource_status 一张窄表,锁范围小;
  3. 空值管理:宽表里 RDS 的"引擎版本"对主机来说永远是 NULL,垂直分表天然规避了稀疏列。

主键设计的巧思:resource_meta.id 直接使用云厂商自己的资源 ID(如阿里云的实例 ID i-bp1xxxxx)。证据在 model.go:25-26:NewResource 一进来就取 res.Meta.Id 当 ResourceId 分发给各子表。好处:天然唯一、同步对账时无需自造映射;代价:不同厂商理论上有撞 ID 风险(实践上各云 ID 格式带前缀,风险极低)。

⚠️ 新手必踩的坑:看到一堆 1:1 的表,第一反应往往是"为什么不合成一张大宽表?多省事"——上面三个理由就是答案,尤其是"线上大表加列是高危操作"这一条,没踩过线上 DDL 的人体会不深。另外注意 resource_relation:模型里定义了 TableName(model.go:139),但业务代码里没有任何地方读写它。读开源项目时遇到"建而未用"的表,先别急着骂脏设计——它往往是作者预留的扩展位(资源间关系建模),但也提醒你:表结构是设计意图,代码才是事实标准,两者要对齐着读。

2.4 多租户三件套:domain / namespace / env

resource_meta 上有三个高频查询字段:domain(域,通常对应公司/事业群)、namespace(空间,通常对应项目组)、env(环境:开发/测试/生产)。secrets 表同样有 domain/namespace(apps/secret/pb/secret.proto:40-45)。这是多租户数据隔离的维度——同一套 CMDB 服务多家"租户",检索时按域过滤。它们从哪来?登录用户的 token 里带出来(apps/secret/interface.go:165 的 SetOwner(tk *token.Token) 就是把 token 的 Domain/Namespace 注入请求)。

类比:domain/namespace/env 就像快递地址里的"省 / 市 / 区":省(domain)圈定哪家公司,市(namespace)圈定哪个项目组,区(env)圈定哪套环境。对应到工程里就是:录入凭证的人带着自己的 domain/namespace,同步回来的资源自动归属这个"地址"(第 6 章会看到 InjectBaseFromSecret 做这件事),检索时再按地址过滤——一套系统、多个租户、数据互不串门。

2.5 凭证→任务→资源:一条"血缘链路"

secrets (凭证)
   │ id
   ▼
task (任务) ── credential_id ──→ secrets.id     「我用哪个凭证去同步」(task/impl/sql.go:5-9)
   │ id
   ▼
task_record (任务明细) ── task_id ──→ task.id   「这次同步每台机器的结果」(task/impl/sql.go:19-23)

resource_meta.credential_id ──→ secrets.id      「这条资源是哪个凭证同步来的」

这条链路回答了三个审计问题:谁(凭证)在什么时候(task.start_at/end_at)同步了哪些(task_record / resource_meta.credential_id)资源。CMDB 这类资产管理平台,可追溯性是刚需。

类比:这就是快递物流体系:凭证是"寄件人档案",任务是"一张运单"(什么时候发车、什么时候到站),task_record 是"签收明细"(每个包裹的成与败),resource_meta.credential_id 则是贴在每个包裹上的"寄件人编号"。对应到工程里就是:出了问题(比如"这台机器的数据怎么不对/怎么来的"),顺着 credential_id 反查就能还原完整链路——出审计报表时,这条链就是你的数据来源。

2.6 关键表字段精讲(全部来自代码证据)

resource_meta(字段来自 apps/resource/resource.pb.go 的 Meta + ContentHash,由 ResourceMeta 结构体同表存储,见 model.go:78-89):

字段含义
id全局唯一 Id,直接使用各云商自己的资源 Id
domain/namespace/env多租户隔离三件套
sync_at最近一次同步时间
credential_id用哪个凭证同步的(回填 secrets.id)
create_at创建时间
usage_mode使用方式:共享(0)/独占(1)
spec_hash/cost_hash/status_hash/tag_hash/relation_hash/custom_hash各类数据 Hash,用于变更对比/增量同步

secrets(字段来自 apps/secret/pb/secret.proto:15-46,proto 里 Meta/CreateSecretRequest 都带 gorm:"embedded" 嵌入落库):id、create_at、description、vendor(哪家云)、address(vsphere 场景的 vCenter 地址)、regions(允许同步的地域,源码字段名就叫 regions,存库时逗号拼接,支持 *,见 interface.go:116 的 strings.Join(s.Regions, ","))、crendential_type(源码就是这个拼写,少个 e)、api_key、api_secret(密文,@ciphered@ 前缀)、request_rate(请求速率限制,默认 1 秒 5 个)、domain、namespace。

task(列清单来自 apps/task/impl/sql.go:4-10 的 insertTaskSQL):id、region、resource_type、credential_id、credential_desc、timeout、status、message、start_at、end_at、total_succeed、total_failed、domain、namespace——一张表同时记录"任务参数"(前几列)和"任务执行结果"(后几列)。task_record 则是:instance_id、instance_name、is_success、message、task_id、create_at。

bill_raw / bill(列清单来自 apps/bill/impl/sql.go):两张表列几乎一致——vendor、year、month、owner_id、owner_name、product_type、product_code、product_detail、pay_mode、order_id、resource_id、resource_name、public_ip、private_ip、instance_config、region_code、region_name、sale_price、save_cost、real_cost、credit_pay、voucher_pay、cash_pay、storedcard_pay、outstanding_amount、is_merged、task_id。mergeBillSQL 把 bill_raw 按 resource_id 分组、金额列 sum() 聚合后写入 bill——“原始流水 → 按资源汇总"的两级账单设计。

resource_host(列清单来自 apps/host/impl/sql.go:4-10):resource_id、gpu_spec、os_type、os_name、image_id、internet_max_bandwidth_out、internet_max_bandwidth_in、key_pair_name、security_groups。

resource_rds(列清单来自 apps/rds/impl/sql.go:4-10):resource_id、engine_type、engine_version、instance_class、class_type、export_type、network_type、type、db_max_quantity、account_max_quantity、max_connections、max_iops、collation、time_zone、storage_type、security_ip_mode、security_ip_list、connection_mode、ip_type、deploy_mode、port、extra。

2.7 枚举:全项目的"普通话词典”

统一模型靠枚举抹平厂商差异(数值来自 apps/resource/resource.pb.go 等生成代码):

枚举取值(节选)
VENDORALIYUN=0 TENCENT=1 HUAWEI=2 VSPHERE=3 AMAZON=4
TYPEHOST=0 RDS=1 REDIS=2 BUCKET=3 DISK=4 LB=5 DOMAIN=6 EIP=7 MONGODB=8 DATABASE=100 ACCOUNT=101 BILL=200 ORDER=201
PAY_MODEPRE_PAY=0(预付费/包年包月)POST_PAY=1(按量付费)RESERVED_PAY=2 SPOT_PAY=3(竞价)NULL=9
USAGE_MODESHARED=0 MONOPOLY=1
TAG_PURPOSEGROUP=0 SYSTEM=1 MONITOR=2 THIRDPARTY=3
STATUS(host)UNKNOW=0 RUNNING=11 STOPPED=21 ERROR=50 LOCKED=70 DESTROYED=90 …
secret.TYPEAPI_KEY=0 PASSWORD=1
task.TypeRESOURCE_SYNC=0 RESOURCE_RELEASE=1

举例说明"归一化"的意义:阿里云把"运行中"叫 Running,腾讯云叫 RUNNING,华为云传字符串 ACTIVE——mapping 层把它们统统翻译成 STATUS_RUNNING=11 存库。库里永远只有普通话(统一枚举),方言(厂商私有值)不出 provider 层。

⚠️ 新手必踩的坑:很多人写查询时会下意识去找"阿里云的状态字符串",比如想 WHERE phase = 'Running'——直接报错或查不到任何数据。库里存的永远是统一枚举数值(RUNNING=11),厂商原始字符串在 provider 的 mapping 层就被翻译掉了。对应到工程里就是:排查数据问题前,先回这张"普通话词典"表对一对枚举值,再去看厂商文档里的方言。

3 核心代码片段逐行解析

3.1 片段:proto 模型 → GORM 持久化模型的映射(apps/resource/impl/model.go:25-61)

// NewResource:把 proto 的 Resource "翻译"成可分表存储的 temp 结构
func NewResource(res *resource.Resource) *Resource {
	rid := &ResourceId{ResourceId: res.Meta.Id}  // 外键载体:各子表都要带 resource_id
	temp := &Resource{
		// meta 表:Meta + ContentHash 一起存(同表不同列)
		ResourceMeta: &ResourceMeta{
			Meta:        res.Meta,
			ContentHash: res.ContentHash,
		},
		// spec 表:resource_id 外键 + Spec 数据
		ResourceSpec: &ResourceSpec{
			ResourceId: rid,
			Spec:       res.Spec,
		},
		ResourceCost: &ResourceCost{
			ResourceId: rid,
			Cost:       res.Cost,
		},
		ResourceStatus: &ResourceStatus{
			ResourceId: rid,
			Status:     res.Status,
		},
		Tags: []*ResourceTag{},              // 标签单独收集
	}
	// Spec 里挂着的 Tags 拆出来,逐个包成 ResourceTag(带 resource_id)
	for i := range res.Spec.Tags {
		temp.Tags = append(temp.Tags, &ResourceTag{
			ResourceId: rid,
			Tag:        res.Spec.Tags[i],
		})
	}
	return temp
}

// Resource 聚合结构:同时"伪装"成四张表的行
type Resource struct {
	*ResourceMeta
	*ResourceSpec
	*ResourceCost
	*ResourceStatus
	Tags []*ResourceTag
}

再看每个子结构怎么"告诉"GORM 自己属于哪张表:

type ResourceMeta struct {
	*resource.Meta         // 嵌入 proto 生成的 message(复用字段定义)
	*resource.ContentHash  // 同上:hash 列也在 meta 表
}
func (s *ResourceMeta) TableName() string {
	return RESOURCE_META_TABLE_NAME  // "resource_meta",GORM 钩子:决定 CRUD 的目标表
}

type ResourceSpec struct {
	*ResourceId             // 提供 resource_id 列
	*resource.Spec
}
func (s *ResourceSpec) TableName() string { return "resource_spec" }

设计精髓:GORM 模型嵌入 proto message 而不是重新定义一份字段——proto 改了字段,持久化模型自动跟着变,消灭了"两处维护"。

3.2 片段:host 如何复用统一模型(apps/host/app.go:21-26)

// host 的领域对象:统一模型 + 主机专属描述
func NewDefaultHost() *Host {
	return &Host{
		Resource: resource.NewDefaultResource(resource.TYPE_HOST), // 复用!类型标记为 HOST
		Describe: &Describe{},                                      // 主机专属明细
	}
}

proto 定义里 Host 持有 *resource.Resource 字段——组合而非继承。主机 = 统一资源 + 主机明细,RDS 也是同样套路(统一资源 + RDS 明细)。

3.3 片段:Hash 的生成(apps/host/app.go:70 + utils/hash.go)

func (h *Host) GenHash() error {
	// Spec 的哈希 + 主机明细(Describe)的自定义哈希
	h.Resource.ContentHash.SpecHash = h.Resource.Spec.Hash()
	h.Resource.ContentHash.CustomHash = utils.Hash(h.Describe)
	return nil
}

Spec.Hash() 由 proto 生成代码实现(对整个 Spec 结构序列化后取哈希)。用途:同步时先查库里那条记录的 hash,与新数据的 hash 对比——相同说明没变化(可跳过写库),不同才更新。这就是 resource_meta 里六个 hash 列的意义:用空间换时间,跳过无变化的写入。

4 业务逻辑流程图与核心要点总结

4.1 数据库全景关系图

                    ┌──────────────────┐
                    │ secrets (凭证)    │ api_secret 密文(@ciphered@)
                    └────┬────────┬────┘
              credential_id│      │1:N
                         │      ▼
        ┌────────────────▼──┐  ┌─────────────┐     ┌───────────────┐
        │ resource_meta      │  │ task          │────│ task_record    │
        │ (id=云厂商ID)      │  │ (同步任务)     │ 1:N │ (每实例成败)    │
        └───┬────┬────┬────┬─┘  └───────────────┘     └───────────────┘
   1:1      │    │    │    │  resource_id
   ┌────────▼┐ ┌─▼────────┐ ┌▼──────────┐ ┌▼──────────────┐ ┌▼─────────────┐
   │resource_│ │resource_  │ │resource_  │ │resource_tag    │ │resource_host  │
   │spec     │ │cost       │ │status     │ │(1:N)           │ │resource_rds   │
   └─────────┘ └───────────┘ └───────────┘ └───────────────┘ └───────────────┘

   账单族: bill_raw ──sum()按resource_id聚合──▶ bill
   关系表: resource_relation(定义了映射,业务代码未使用)
   遗留表: resource(host/rds 的查询 SQL 仍 JOIN 它,垂直分表前的旧 schema)

4.2 核心要点总结

  1. 13 张表五个族群:统一模型 6 张、类型明细 2 张、任务 2 张、账单 2 张、凭证 1 张(secrets 是复数表名)——表清单从 TableName() + impl/sql.go 反推,DDL 文件未随仓库发布(cmd/init.go:68 默认读 docs/schema/tables.sql,该目录不存在);
  2. 垂直分表三收益:加类型不动主模型、窄表 UPDATE 锁范围小、无稀疏 NULL 列;
  3. resource_meta.id 直接用云厂商 ID,天然对账键;
  4. 六个 hash 列支撑增量同步:“哈希没变不写库”;
  5. 凭证→任务→资源血缘链路是审计刚需;
  6. 库里只存统一枚举(普通话),厂商方言被 mapping 挡在 provider 层。

5 自测题(对话式)

学完不等于学会。下面三道题是原书课后思考题的"面试现场版"——先自己作答,再看候选人的参考答案。

面试官
这个仓库没有随代码发布建表 DDL 文件(cmd/init.go 默认读的 docs/schema/tables.sql 并不存在)。如果你要本地跑起来,怎么知道该建哪些表?secret 模块的 TableName() 返回的是 "secrets"(复数),这里藏着什么坑?
候选人
表清单从代码两处事实反推:① GORM 模型的 TableName()(resource 模块 model.go:84-139 定义了 6 张 resource 族表,secret 模块返回 "secrets");② 各模块 impl/sql.go 里的 SQL 常量(INSERT INTO / FROM 后面的表名),凑齐 host/rds/task/task_record/bill/bill_raw 共 13 张。自己照着模型字段写 DDL,或用 -s 参数把 init 指到自己的 SQL 文件。

secrets 的坑:表名以代码为准是复数——如果你凭直觉建了单数表 secret,GORM 一执行就报 Table 'cmdb.secrets' doesn't exist。这引出一个通用纪律:开源项目没有 DDL 时,TableName() 和 SQL 常量就是唯一事实来源,别猜。
面试官
resource_meta.id 直接使用云厂商 ID 有"撞 ID"的理论风险。你会如何验证这个风险是否真实存在?如果要做防御,主键方案可以怎么改、代价是什么?
候选人
验证分两步:先做存量检测——对 resource_meta.id 做 COUNT(DISTINCT id) 与 COUNT(*) 对比,两者不等就说明已经撞了;更细一点可以按 vendor 分组看组间是否出现同 ID。再做格式分析——梳理各厂商 ID 的格式规则(长度、字符集、前缀,如阿里云实例 ID 带 i- 前缀),评估理论命名空间是否可能相交。实践上各云 ID 格式带前缀,风险极低。

防御方案:主键改用自增 id 或 xid,把厂商 ID 挪到 (vendor, resource_id) 唯一索引上。代价是失去"主键即对账键"的便利——同步对账时不能直接按主键 upsert,得先按唯一索引查到内部主键再更新,多一次查询或改用 ON DUPLICATE KEY 类能力,同步路径变复杂。这是"省一次映射"与"主键空间自主可控"之间的取舍。
面试官
resource.Put 目前每次同步都会 Save 全量数据(第 5 章会看到)。结合 resource_meta 的 hash 列,设计一个"哈希未变化则跳过写库"的增量同步方案,需要改动哪些函数?
候选人
思路:Save 之前先"查旧、比哈希"。

① resource 模块的 impl 里需要一个按 ID 查询旧记录的方法(类似 host 模块的 DescribeHost 按 Meta.Id 查重);② 修改 service.Put 的入口逻辑:开事务前先查旧记录,把新数据的六个 hash(spec/cost/status/tag/relation/custom)与库内值逐一对比——全相同直接 return 跳过写库;任一不同才走原有 tx.Save 流程;③ 同步侧配合:task.doSyncHost 调 SyncHost 前先调 GenHash 算好新数据哈希(host 模块 app.go:70 已有此方法,填 SpecHash 和 CustomHash),保证对比双方"口径一致"。

两个语义取舍要明确:一是跳过写库时 sync_at 是否刷新(不刷新可保留"真实变更时间",刷新则反映"最近确认时间");二是对比必须覆盖六类哈希,漏一类就会漏更新。收益就是原书说的:用空间换时间,跳过无变化的写入。

6 本章小结

复习提示: - 13 张表 = 五个族群(统一模型 6 + 明细 2 + 任务 2 + 账单 2 + 凭证 1),resource_meta 是宇宙中心:Meta/Spec/Cost/Status 四件套垂直分表 + resource_id 关联 + 六个 hash 列做"哈希没变不写库"的增量同步; - 表清单的出处是 TableName() 与 impl/sql.go——DDL 文件没随仓库发布,"代码才是事实标准";host/rds 的查询 SQL 还引用着垂直分表前的旧 resource 大表,读代码要分清设计方向与迁移进度; - 凭证→任务→资源的血缘链路回答审计三问(谁/何时/哪些),多租户靠 domain/namespace/env 三件套隔离,库里永远只存统一枚举(普通话),方言不出 provider 层; - 拿着这张"数据地图",下一章我们去精读路由、控制器、Service、DAO 各层的源码——你会看到第 5 章的 DAO 层代码就是把本章的表结构"翻译"成 Go 调用,届时会非常顺滑。
About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!