# 光栅卡多素材架构升级技术设计 > **创建日期:** 2026-05-15 > **项目:** TopFans 光栅卡铸造流程 > **服务:** assetService (Go Dubbo-go) + 前端铸爱模块 > **状态:** 审核通过,待开发实施 > **版本:** v1.0 --- ## 一、背景与问题确认 ### 1.1 当前故障 **问题定性:光栅卡铸造流程中,背景图从未被持久化到后端。确认铸造时仅上传主体图,背景图永久丢失。** ``` create.vue (双图上传) → buildCraftFormData() → CASTLOVE_FORM_KEY → ... ✅ bgPath / subjectPath 完整传递到本地 Storage ✅ buildLenticularLayersTwo(bgPath, subjectPath) 正确构建图层 ❌ handleCraftMint() 只取 mid 层 → submitCraftMintFromPath(单 imagePath) ❌ 后端 material_url 单字段 → 背景图丢失 ``` ### 1.2 根本技术缺陷 现有 `assets.material_url` 单字段存储存在以下瓶颈: | 缺陷 | 说明 | |------|------| | 无法支撑多素材 | 单一字段存储,多素材需内嵌 JSON(受限于 `varchar(500)`,最多 3-4 个 URL) | | 数据语义模糊 | 无法区分主图、背景、遮罩、特效等不同角色 | | 查询效率低下 | 无法针对特定素材类型建立索引 | | 复用能力缺失 | 素材无法被多个资产共享 | | 长期扩展性差 | 未来 3 年单资产素材数预计增长至 30+,现有模型无法承载 | --- ## 二、方案设计:新增资产-素材关联表 ### 2.1 方案概述 采用新增 `materials`(素材主表)+ `asset_material_relations`(资产-素材关联表)的方案,彻底解耦素材与资产的强绑定关系。 ### 2.2 表关系定义 ``` assets ◄── 1:N ──► asset_material_relations ◄── N:1 ──► materials ``` - **资产主表 ↔ 关联表**:一对多关系,单个资产可绑定多个素材关联记录 - **素材主表 ↔ 关联表**:多对多关系,单个素材可被多个资产关联复用 ### 2.3 数据库模型设计 #### 2.3.1 素材主表(materials) ```sql CREATE TABLE materials ( id BIGSERIAL PRIMARY KEY, oss_key VARCHAR(255) NOT NULL, original_name VARCHAR(255) NOT NULL, file_size BIGINT NOT NULL, mime_type VARCHAR(100) NOT NULL, width INT, height INT, hash VARCHAR(64) NOT NULL, created_by BIGINT NOT NULL, star_id BIGINT NOT NULL DEFAULT 0, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, deleted_at BIGINT ); CREATE UNIQUE INDEX uk_materials_oss_key ON materials(oss_key); CREATE INDEX idx_materials_hash ON materials(hash); CREATE INDEX idx_materials_created_by ON materials(created_by); CREATE INDEX idx_materials_star_id ON materials(star_id); ``` | 字段 | 类型 | 说明 | |------|------|------| | `id` | `BIGSERIAL` | 主键 | | `oss_key` | `VARCHAR(255) UNIQUE` | OSS 对象唯一标识 | | `original_name` | `VARCHAR(255)` | 原始文件名 | | `file_size` | `BIGINT` | 文件大小(字节) | | `mime_type` | `VARCHAR(100)` | MIME 类型 | | `width / height` | `INT` | 图片尺寸 | | `hash` | `VARCHAR(64)` | 文件 SHA256 哈希,用于去重 | | `created_by` | `BIGINT` | 创建者用户 ID | | `star_id` | `BIGINT` | 多星数据隔离 | | `created_at / updated_at` | `BIGINT` | 毫秒时间戳 | | `deleted_at` | `BIGINT` | 软删除时间戳 | #### 2.3.2 资产-素材关联表(asset_material_relations) ```sql CREATE TABLE asset_material_relations ( id BIGSERIAL PRIMARY KEY, asset_id BIGINT NOT NULL REFERENCES assets(id) ON DELETE CASCADE, material_id BIGINT NOT NULL REFERENCES materials(id) ON DELETE RESTRICT, material_type VARCHAR(50) NOT NULL, layer_order INT NOT NULL DEFAULT 0, -- 渲染定位字段(NULL = 拉伸填满容器) pos_x DOUBLE PRECISION, -- 距左上角 X 偏移量(px),NULL=拉伸模式 pos_y DOUBLE PRECISION, -- 距左上角 Y 偏移量(px),NULL=拉伸模式 opacity DOUBLE PRECISION DEFAULT 1.0, -- 不透明度 0~1 rotation DOUBLE PRECISION DEFAULT 0, -- 旋转角度(度),正值为顺时针 scale_x DOUBLE PRECISION DEFAULT 1.0, -- 水平缩放比例 scale_y DOUBLE PRECISION DEFAULT 1.0, -- 垂直缩放比例 version INT NOT NULL DEFAULT 1, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, deleted_at BIGINT ); ``` **索引与约束:** ```sql CREATE INDEX idx_amr_asset_id ON asset_material_relations(asset_id) WHERE deleted_at IS NULL; CREATE INDEX idx_amr_material_id ON asset_material_relations(material_id) WHERE deleted_at IS NULL; CREATE INDEX idx_amr_asset_type_layer ON asset_material_relations(asset_id, material_type, layer_order) WHERE deleted_at IS NULL; CREATE UNIQUE INDEX uk_amr_asset_type_active ON asset_material_relations(asset_id, material_type) WHERE deleted_at IS NULL; CREATE UNIQUE INDEX uk_amr_asset_layer_active ON asset_material_relations(asset_id, layer_order) WHERE deleted_at IS NULL; ``` **字段说明:** | 字段 | 类型 | 说明 | |------|------|------| | `asset_id` | `BIGINT FK` | 关联资产 ID | | `material_id` | `BIGINT FK` | 关联素材 ID | | `material_type` | `VARCHAR(50)` | 素材角色:`main`/`bg`/`star_map`/`mask`/`effect` 等 | | `layer_order` | `INT` | 图层渲染顺序,数值越小越靠下 | | `pos_x / pos_y` | `DOUBLE PRECISION` | 距左上角偏移量(px),NULL 为拉伸填满容器模式 | | `opacity` | `DOUBLE PRECISION` | 不透明度 0~1,默认 1 | | `rotation` | `DOUBLE PRECISION` | 旋转角度(度),正值为顺时针,默认 0 | | `scale_x / scale_y` | `DOUBLE PRECISION` | 缩放比例,默认 1 | | `version` | `INT` | 素材版本号,乐观锁控制并发 | | `deleted_at` | `BIGINT` | 软删除时间戳 | ### 2.4 Go Model 定义 ```go // Material 素材表模型 type Material struct { ID int64 `gorm:"primaryKey;autoIncrement;column:id"` OssKey string `gorm:"type:varchar(255);not null;uniqueIndex;column:oss_key"` OriginalName string `gorm:"type:varchar(255);not null;column:original_name"` FileSize int64 `gorm:"not null;column:file_size"` MimeType string `gorm:"type:varchar(100);not null;column:mime_type"` Width *int `gorm:"column:width"` Height *int `gorm:"column:height"` Hash string `gorm:"type:varchar(64);not null;index;column:hash"` CreatedBy int64 `gorm:"not null;index;column:created_by"` StarID int64 `gorm:"not null;index;column:star_id"` CreatedAt int64 `gorm:"not null;column:created_at"` UpdatedAt int64 `gorm:"not null;column:updated_at"` DeletedAt *int64 `gorm:"index;column:deleted_at"` } func (Material) TableName() string { return "materials" } func (m *Material) BeforeCreate(tx *gorm.DB) error { now := time.Now().UnixMilli() m.CreatedAt = now m.UpdatedAt = now return nil } func (m *Material) BeforeUpdate(tx *gorm.DB) error { m.UpdatedAt = time.Now().UnixMilli() return nil } ``` ```go // AssetMaterialRelation 资产-素材关联表模型 type AssetMaterialRelation struct { ID int64 `gorm:"primaryKey;autoIncrement;column:id"` AssetID int64 `gorm:"not null;index:idx_amr_asset_id;column:asset_id"` MaterialID int64 `gorm:"not null;index:idx_amr_material_id;column:material_id"` MaterialType string `gorm:"type:varchar(50);not null;column:material_type"` LayerOrder int `gorm:"not null;default:0;column:layer_order"` PosX *float64 `gorm:"column:pos_x"` PosY *float64 `gorm:"column:pos_y"` Opacity *float64 `gorm:"default:1.0;column:opacity"` Rotation *float64 `gorm:"default:0;column:rotation"` ScaleX *float64 `gorm:"default:1.0;column:scale_x"` ScaleY *float64 `gorm:"default:1.0;column:scale_y"` Version int `gorm:"not null;default:1;column:version"` CreatedAt int64 `gorm:"not null;column:created_at"` UpdatedAt int64 `gorm:"not null;column:updated_at"` DeletedAt *int64 `gorm:"index;column:deleted_at"` Asset Asset `gorm:"foreignKey:AssetID;references:ID;constraint:OnDelete:CASCADE"` Material Material `gorm:"foreignKey:MaterialID;references:ID;constraint:OnDelete:RESTRICT"` } func (AssetMaterialRelation) TableName() string { return "asset_material_relations" } ``` --- ## 三、数据一致性保障方案 ### 3.1 事务隔离级别与约束规则 - **隔离级别**:所有跨表操作用 `REPEATABLE READ` - **级联规则**:资产删除时 `ON DELETE CASCADE` 级联删除关联记录;素材删除时 `ON DELETE RESTRICT` 禁止删除被引用的素材 - **唯一约束**:通过 PostgreSQL 部分唯一索引 `WHERE deleted_at IS NULL` 实现,允许同一资产同一类型在不同版本间切换 ### 3.2 异常场景回滚机制 | 异常场景 | 回滚机制 | |---------|---------| | 素材上传失败 | 回滚 `materials` 插入记录,删除 OSS 部分文件 | | 关联绑定异常 | 回滚关联表插入,保留素材记录用于后续复用 | | 资产删除中断 | 事务回滚,恢复资产和关联记录 | | 素材版本更新冲突 | 乐观锁(`version` 字段),冲突时提示用户刷新 | | 跨节点事务超时 | SAGA 模式,超时后执行补偿操作 | | 非法参数注入 | 入参校验失败直接拒绝,不执行 DB 操作 | | 脏数据写入 | 外键 + 唯一索引双重阻拦 | | 并发绑定锁竞争 | `SELECT ... FOR UPDATE` 行级锁 | | 软删除标记异常 | 定期数据校验任务修正 | | 批量导入中断 | 分批事务提交,已提交批次保留,未提交回滚 | ### 3.3 脏数据清理规则 | 规则项 | 配置 | |--------|------| | 清理周期 | 每日凌晨 3 点 | | 软删除清理 | 超过 30 天的软删除记录 | | 孤立素材清理 | 创建超过 7 天且无任何关联记录的素材 | | 重复素材清理 | 相同 `hash` 值的重复记录 | | 备份要求 | 清理前备份到冷存储,保留 90 天 | | 安全间隔 | 先标记 24 小时后无异常再物理删除 | --- ## 四、核心业务流程 ### 4.1 资产创建与素材关联 ``` 用户 → 前端:上传多个素材文件 前端 → OSS:分片上传素材 OSS → 前端:返回 oss_key 前端 → 后端:创建资产请求(含素材列表和图层信息) 后端 → materials:批量插入素材记录(hash 去重) 后端 → assets:插入资产记录 后端 → asset_material_relations:批量插入关联记录(含渲染定位字段) 后端 → 前端:返回资产 ID 和素材列表 ``` ### 4.2 图层顺序调整 ``` 用户 → 前端:拖拽调整图层顺序 前端 → 后端:PUT /api/v1/assets/{id}/materials/layer-order 后端 → 关联表:开启事务 → SELECT FOR UPDATE 锁住该资产所有关联记录 后端 → 关联表:批量更新 layer_order 后端 → 前端:返回更新结果 ``` ### 4.3 资产删除 ``` 用户 → 前端:删除资产请求 后端 → assets:软删除(设置 deleted_at) 后端 → asset_material_relations:级联软删除所有关联记录 后端 → 前端:返回删除成功 ``` --- ## 五、API 接口规范 ### 5.1 素材上传 ``` POST /api/v1/materials/upload Content-Type: multipart/form-data 入参: - file: 文件(必填) - type: 素材类型(可选) 出参: { "code": 0, "data": { "material_id": 123, "oss_key": "assets/123.jpg", "url": "https://oss.example.com/assets/123.jpg", "width": 1080, "height": 1920 } } ``` ### 5.2 资产-素材关联 ``` POST /api/v1/assets/{asset_id}/materials Content-Type: application/json 入参: { "materials": [ { "material_id": 123, "material_type": "main", "layer_order": 0, "pos_x": null, "pos_y": null, "opacity": 1.0, "rotation": 0, "scale_x": 1.0, "scale_y": 1.0 }, { "material_id": 456, "material_type": "bg", "layer_order": 1 } ] } 出参: { "code": 0, "message": "关联成功" } ``` ### 5.3 资产素材查询 ``` GET /api/v1/assets/{asset_id}/materials 出参: { "code": 0, "data": [ { "relation_id": 1, "material_id": 123, "material_type": "main", "layer_order": 0, "url": "https://oss.example.com/assets/123.jpg", "pos_x": null, "pos_y": null, "opacity": 1.0, "rotation": 0, "scale_x": 1.0, "scale_y": 1.0, "width": 1080, "height": 1920 } ] } ``` ### 5.4 图层顺序更新 ``` PUT /api/v1/assets/{asset_id}/materials/layer-order 入参: { "orders": [ { "relation_id": 1, "layer_order": 0 }, { "relation_id": 2, "layer_order": 1 } ] } ``` ### 5.5 权限控制 | 角色 | 权限边界 | |------|---------| | 普通用户 | 仅可操作自己创建的素材和资产,校验 `created_by` / `owner_uid` | | 运营人员 | 可管理所有用户的素材和资产,支持批量操作 | | 管理员 | 所有权限,含数据清理任务执行权限 | > MVP 阶段简化为「用户仅可操作自身数据」,运营人员角色后置实现。 --- ## 六、Proto 扩展 ```protobuf message Material { int64 material_id = 1; string oss_key = 2; string original_name = 3; int64 file_size = 4; string mime_type = 5; int32 width = 6; int32 height = 7; string hash = 8; int64 created_by = 9; int64 star_id = 10; int64 created_at = 11; } message AssetMaterialRelation { int64 relation_id = 1; int64 asset_id = 2; int64 material_id = 3; string material_type = 4; int32 layer_order = 5; string material_url_signed = 6; double pos_x = 7; double pos_y = 8; double opacity = 9; double rotation = 10; double scale_x = 11; double scale_y = 12; } service AssetService { rpc UploadMaterial(UploadMaterialRequest) returns (UploadMaterialResponse); rpc BindAssetMaterials(BindAssetMaterialsRequest) returns (BindAssetMaterialsResponse); rpc GetAssetMaterials(GetAssetMaterialsRequest) returns (GetAssetMaterialsResponse); rpc UpdateMaterialLayerOrder(UpdateMaterialLayerOrderRequest) returns (UpdateMaterialLayerOrderResponse); rpc UnbindAssetMaterial(UnbindAssetMaterialRequest) returns (UnbindAssetMaterialResponse); } ``` --- ## 七、性能优化 ### 7.1 缓存策略 | 层级 | 实现 | 配置 | |------|------|------| | L1(Redis) | Hash: `asset:materials:{asset_id}` | TTL 1 小时,写入时主动 INVALIDATE | | L2(go-cache) | 热点资产 Top 1000 | TTL 5 分钟,访问频率 > 10次/分钟 提升到热点 | ### 7.2 查询优化 ```go // GORM Preload 方式(推荐) var asset Asset db.Preload("Materials", func(db *gorm.DB) *gorm.DB { return db.Where("asset_material_relations.deleted_at IS NULL"). Order("asset_material_relations.layer_order ASC") }).First(&asset, assetID) // 批量查询 Raw SQL(高性能场景) rows, _ := db.Raw(` SELECT a.*, amr.material_type, amr.layer_order, m.oss_key FROM assets a LEFT JOIN asset_material_relations amr ON amr.asset_id = a.id AND amr.deleted_at IS NULL LEFT JOIN materials m ON amr.material_id = m.id AND m.deleted_at IS NULL WHERE a.id = ? AND a.deleted_at IS NULL ORDER BY amr.layer_order ASC `, assetID).Rows() ``` ### 7.3 分库分表预案 | 触发条件 | 分表策略 | |---------|---------| | `assets` > 1000 万行 | 按 `asset_id` 哈希分 16 表 | | `asset_material_relations` > 1 亿行 | 同上 | ### 7.4 性能预估 | 场景 | 预估耗时 | |------|---------| | 单资产 10 素材查询 | < 5ms | | 单资产 50 素材查询 | < 15ms | | 批量 100 资产(平均 5 素材/资产) | < 50ms | --- ## 八、数据迁移与兼容性 ### 8.1 迁移策略 | 阶段 | 天数 | 内容 | |------|------|------| | 双写 | 第 1-3 天 | 同时写入 `material_url` 和关联表 | | 灰度切流 | 第 4-5 天 | 1% → 50% → 100% 逐步切读 | | 观察期 | 第 6-7 天 | 全量走新模型,持续监测 | | 清理 | 第 14 天 | 停止双写,material_url 设为可空 | ### 8.2 迁移脚本核心逻辑 ```go func migrateMaterials() error { for offset := 0; ; offset += 1000 { assets := queryAssets(offset, 1000) if len(assets) == 0 { break } for _, asset := range assets { tx := db.Begin() // 解析 material_url(兼容单字符串和 JSON 格式) materials := parseMaterialURL(asset.MaterialURL) // 插入 materials 表(hash 去重) materialIDs := batchInsertMaterials(tx, materials) // 插入关联表 batchInsertRelations(tx, asset.ID, materialIDs, materials) tx.Commit() } } } ``` ### 8.3 灰度观测指标与回滚 | 指标 | 正常阈值 | 回滚触发 | |------|---------|---------| | 接口错误率 | < 0.01% | > 1% | | 查询响应时间 | < 50ms(P99) | > 200ms | | 数据一致性 | 100% | < 99.9% | --- ## 九、测试验收标准 ### 9.1 测试场景 | 类型 | 场景 | |------|------| | 单元测试 | 关联表 CRUD 事务逻辑、乐观锁冲突、外键约束、唯一约束 | | 集成测试 | 资产创建 → 素材上传 → 关联绑定 → 图层调整 → 资产删除全流程 | | 压力测试 | 单资产关联 50 个素材,QPS=1000,验证系统稳定性 | ### 9.2 验收指标 | 指标 | 目标值 | |------|--------| | 单资产 10 素材查询 | < 50ms | | 单资产 50 素材查询 | < 100ms | | 接口错误率 | < 0.01% | | 数据一致性通过率 | 100% | | 支持并发 QPS | ≥ 1000 | --- ## 十、实施计划 ``` 第 1-2 天:DDL 建表 → 测试环境验证 第 3-4 天:Go Model + Proto + 网关 DTO + Converter 第 5-6 天:Service CRUD + 事务 + 缓存 第 7 天: 前端适配 + 数据迁移脚本 第 8-9 天:灰度发布(双写 → 切流 → 观察) 第 10 天: 上线确认 + 回滚预案待命 ``` | 合计 | 10 个工作日 | --- ## 十一、关联文档 | 文档 | 说明 | |------|------| | [Asset 数据模型](file:///e:/develop/code/topfans/backend/pkg/models/asset.go) | 现有 assets/mint_orders 表结构 | | [Proto 定义](file:///e:/develop/code/topfans/backend/proto/asset.proto) | 现有 RPC 消息定义 | | [资产控制器](file:///e:/develop/code/topfans/backend/gateway/controller/asset_controller.go) | 现有网关层处理逻辑 | | [Redis 配置](file:///e:/develop/code/topfans/backend/pkg/database/redis.go) | 现有 Redis 客户端 | | [铸爱提交流程](file:///e:/develop/code/topfans/frontend/utils/craftMintSubmit.js) | OSS 上传 + 创建订单 | | [铸造路由管理](file:///e:/develop/code/topfans/frontend/utils/castloveGenerationFlow.js) | Storage Key 管理 + 页面跳转 |