526 lines
18 KiB
Markdown
526 lines
18 KiB
Markdown
# 光栅卡多素材架构升级技术设计
|
||
|
||
> **创建日期:** 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 管理 + 页面跳转 |
|