diff --git a/README.md b/README.md
index a2ad1c5..8c3e127 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,2 @@
-# TopFans
\ No newline at end of file
+# TopFans
+背景有什么想法?边框想要什么样式的?有没有特别想加的装饰元素?整体材质的效果想要什么样的?主色调是应援色吗,还是别的颜色?
\ No newline at end of file
diff --git a/backend/docs/铸爱创作模块技术设计文档.md b/backend/docs/铸爱创作模块技术设计文档.md
new file mode 100644
index 0000000..7e4738c
--- /dev/null
+++ b/backend/docs/铸爱创作模块技术设计文档.md
@@ -0,0 +1,1540 @@
+# 铸爱创作模块 技术设计文档
+
+> **文档版本:** V1.0
+> **创建日期:** 2026-04-09
+> **关联产品文档:** 铸爱创作模块PRD.md
+> **状态:** 初稿
+
+---
+
+## 1. 产品需求概述
+
+### 1.1 核心功能
+
+| 功能模块 | 说明 |
+|----------|------|
+| 顶部运营轮播图 | 展示运营活动,点击跳转H5或站内页面 |
+| 主Tab(星卡/吧唧/海报) | 三大创作分类,点击直接进入铸造页面 |
+| 分类标签 | 筛选用户已铸造的藏品(热门/最新/各分类) |
+| 创作网格列表 | 双列瀑布流展示藏品卡片 |
+| 铸造页面 | 选择素材 → 输入描述词 → AI生成 |
+| AI处理中页面 | 展示创作进度 |
+| AI创作结果预览 | 预览结果 → 重新生成/确认铸造 |
+| 创作详情页 | 展示藏品信息、用户名、点赞、身份证编号 |
+| 身份证编号(上链) | 每份藏品全球唯一编号,用于确权追溯 |
+
+### 1.2 现有系统能力复用
+
+| 现有服务 | 可复用能力 |
+|----------|-----------|
+| AssetService | 数字藏品铸造、资产查询、OSS存储、上链 |
+| ActivityService | 运营活动管理(轮播图配置) |
+| GalleryService | 展馆展示(可参考布局) |
+| UserService | 用户信息、身份验证 |
+| SocialService | 点赞功能 |
+
+---
+
+## 2. 技术架构设计
+
+### 2.1 服务职责划分
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ API Gateway (8080) │
+│ 统一入口 / 认证 / 协议转换 / 请求路由 │
+└─────────────────────────────────────────────────────────────┘
+ │ │ │
+ ▼ ▼ ▼
+┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
+│ AssetService │ │ ActivityService │ │ SocialService │
+│ (tri:20003) │ │ (tri:20005?) │ │ (tri:20001) │
+├─────────────────┤ ├─────────────────┤ ├─────────────────┤
+│ 铸造订单管理 │ │ 运营活动配置 │ │ 藏品点赞 │
+│ 资产CRUD │ │ 轮播图管理 │ │ 评论(扩展) │
+│ AI任务调度 │ │ 活动跳转配置 │ │ │
+│ 上链状态 │ │ │ │ │
+└─────────────────┘ └─────────────────┘ └─────────────────┘
+```
+
+### 2.2 新增/扩展接口
+
+| 接口 | 方法 | 归属服务 | 说明 | 状态 |
+|------|------|----------|------|------|
+| `GET /api/v1/activities/banners` | GET | ActivityService | 获取轮播图列表 | **新增** |
+| `GET /api/v1/assets/cast/items` | GET | AssetService | 获取铸爱创作列表(按分类筛选) | **新增** |
+| `GET /api/v1/assets/cast/options` | GET | AssetService | 获取铸爱类型选项(吧唧装饰/海报风格) | **新增** |
+| `GET /api/v1/assets/:asset_id` | GET | AssetService | 获取藏品详情(**复用现有接口**) | 已存在 |
+| `POST /api/v1/social/assets/:asset_id/like` | POST | SocialService | 藏品点赞(**复用现有接口**) | 已存在 |
+| `POST /api/v1/assets/cast/mints` | POST | AssetService | 创建铸爱铸造订单 | **新增** |
+| `GET /api/v1/assets/cast/mints/:order_id` | GET | AssetService | 查询铸造订单状态 | **新增** |
+
+**接口复用说明:**
+- `GET /api/v1/assets/:asset_id` - 现有接口,可查询藏品详情
+- `POST /api/v1/social/assets/:asset_id/like` - 现有接口,可用于点赞
+
+---
+
+## 3. 数据模型设计
+
+### 3.1 铸爱藏品表(新建)
+
+**表名:** `cast_assets`
+
+```sql
+CREATE TABLE IF NOT EXISTS cast_assets (
+ id BIGSERIAL PRIMARY KEY,
+ owner_uid BIGINT NOT NULL COMMENT '所有者ID',
+ star_id BIGINT NOT NULL COMMENT '明星ID,用于数据隔离',
+ name VARCHAR(255) NOT NULL COMMENT '藏品名称',
+ material_url TEXT COMMENT '原始素材URL(用户相册图片)',
+ cover_url TEXT COMMENT 'AI生成结果图URL',
+ description TEXT COMMENT '藏品描述',
+ cast_type INT NOT NULL DEFAULT 1 COMMENT '藏品类型:1=星卡, 2=吧唧, 3=海报',
+ identity_no VARCHAR(50) UNIQUE COMMENT '身份证编号(全局唯一)',
+ tx_hash VARCHAR(100) COMMENT '上链交易哈希',
+ block_number BIGINT COMMENT '区块高度',
+ status INT NOT NULL DEFAULT 0 COMMENT '状态:0=处理中, 1=成功, 2=失败',
+ like_count INT NOT NULL DEFAULT 0 COMMENT '点赞数',
+ user_id BIGINT NOT NULL COMMENT '创作者ID',
+ minted_at BIGINT COMMENT '上链时间(毫秒)',
+ created_at BIGINT NOT NULL DEFAULT 0,
+ updated_at BIGINT NOT NULL DEFAULT 0,
+
+ -- 索引
+ INDEX idx_cast_assets_star_id (star_id),
+ INDEX idx_cast_assets_owner_uid_star_id (owner_uid, star_id),
+ INDEX idx_cast_assets_cast_type (cast_type),
+ INDEX idx_cast_assets_identity_no (identity_no),
+ INDEX idx_cast_assets_user_id (user_id),
+ INDEX idx_cast_assets_status (status),
+ INDEX idx_cast_assets_like_count (like_count),
+ INDEX idx_cast_assets_created_at (created_at)
+) COMMENT '铸爱藏品表';
+```
+
+### 3.2 铸爱铸造订单表(新建)
+
+**表名:** `cast_mint_orders`
+
+```sql
+CREATE TABLE IF NOT EXISTS cast_mint_orders (
+ order_id VARCHAR(50) PRIMARY KEY COMMENT '订单ID(UUID)',
+ user_id BIGINT NOT NULL COMMENT '用户ID',
+ star_id BIGINT NOT NULL COMMENT '明星ID',
+ asset_id BIGINT NOT NULL COMMENT '关联藏品ID',
+ cast_type INT NOT NULL COMMENT '藏品类型:1=星卡, 2=吧唧, 3=海报',
+ prompt_text TEXT COMMENT '用户输入的描述词',
+ material_url TEXT COMMENT '原始素材URL',
+ cover_url TEXT COMMENT 'AI生成结果URL(完成后回填)',
+ identity_no VARCHAR(50) COMMENT '身份证编号',
+ tx_hash VARCHAR(100) COMMENT '上链哈希',
+ status VARCHAR(20) NOT NULL DEFAULT 'PROCESSING' COMMENT 'PROCESSING/SUCCESS/FAILED',
+ error_message TEXT COMMENT '失败原因',
+ cost_crystal BIGINT DEFAULT 0 COMMENT '消耗水晶',
+ retry_count INT DEFAULT 0 COMMENT '重试次数',
+ -- 吧唧专属参数
+ decoration_type VARCHAR(50) COMMENT '吧唧装饰类型:cream_glue/metal_frame/shaped',
+ -- 海报专属参数
+ aspect_ratio VARCHAR(20) COMMENT '海报比例:landscape/portrait/square',
+ style_options TEXT COMMENT '海报风格选项:JSON数组,bg_replace/style_transfer/lighting',
+ created_at BIGINT NOT NULL DEFAULT 0,
+ updated_at BIGINT NOT NULL DEFAULT 0,
+ minted_at BIGINT COMMENT '完成时间',
+
+ -- 索引
+ INDEX idx_cast_mint_orders_user_star (user_id, star_id),
+ INDEX idx_cast_mint_orders_status (status),
+ INDEX idx_cast_mint_orders_asset_id (asset_id),
+ INDEX idx_cast_mint_orders_created_at (created_at)
+) COMMENT '铸爱铸造订单表';
+```
+
+### 3.3 铸爱点赞表(扩展现有 asset_likes)
+
+**扩展现有表,新增字段:**
+
+```sql
+ALTER TABLE asset_likes ADD COLUMN IF NOT EXISTS asset_type INT DEFAULT 1 COMMENT '资产类型:1=原有资产, 2=铸爱藏品';
+ALTER TABLE asset_likes ADD COLUMN IF NOT EXISTS cast_asset_id BIGINT COMMENT '铸爱藏品ID(asset_type=2时使用)';
+
+-- 添加联合唯一索引,防止重复点赞
+ALTER TABLE asset_likes ADD CONSTRAINT uk_asset_like UNIQUE (user_id, asset_id, asset_type);
+```
+
+### 3.4 铸爱类型选项配置表(新建)
+
+**表名:** `cast_type_options`
+
+**说明:** 运维后台上传和管理各类型的可选项(吧唧装饰类型、海报风格选项等),前端下发给用户选择,同时传给AI生成。
+
+```sql
+CREATE TABLE IF NOT EXISTS cast_type_options (
+ id BIGSERIAL PRIMARY KEY,
+ cast_type INT NOT NULL COMMENT '藏品类型:1=星卡, 2=吧唧, 3=海报',
+ option_key VARCHAR(50) NOT NULL COMMENT '选项标识,如 cream_glue / metal_frame / bg_replace',
+ option_name VARCHAR(100) NOT NULL COMMENT '选项显示名称,如 奶油胶 / 金属边框 / 背景替换',
+ option_type VARCHAR(30) NOT NULL COMMENT '选项类别:decoration / style',
+ icon_url TEXT COMMENT '选项图标URL',
+ sort_order INT DEFAULT 0 COMMENT '排序',
+ status INT DEFAULT 1 COMMENT '状态:0=禁用, 1=启用',
+ created_at BIGINT NOT NULL DEFAULT 0,
+ updated_at BIGINT NOT NULL DEFAULT 0,
+
+ -- 索引
+ INDEX idx_cast_type_options_type (cast_type),
+ INDEX idx_cast_type_options_key (cast_type, option_key),
+ UNIQUE KEY uk_cast_type_option (cast_type, option_key)
+) COMMENT '铸爱类型选项配置表';
+```
+
+**示例数据:**
+
+| cast_type | option_key | option_name | option_type |
+|-----------|------------|-------------|-------------|
+| 2 (吧唧) | cream_glue | 奶油胶 | decoration |
+| 2 (吧唧) | metal_frame | 金属边框 | decoration |
+| 2 (吧唧) | shaped | 异形裁切 | decoration |
+| 3 (海报) | bg_replace | 背景替换 | style |
+| 3 (海报) | style_transfer | 风格迁移 | style |
+| 3 (海报) | lighting | 光影调整 | style |
+
+**运维后台功能:**
+- 新增/编辑/删除选项
+- 上传选项图标
+- 调整排序
+
+---
+
+## 4. 接口详细设计与代码实现
+
+### 4.1 获取轮播图列表(新增接口)
+
+**Endpoint:** `GET /api/v1/activities/banners`
+
+**归属服务:** ActivityService
+
+#### 4.1.1 请求参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| position | string | 否 | 位置标识,默认 "cast_home" |
+
+#### 4.1.2 响应格式
+
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "items": [
+ {
+ "id": 1,
+ "image_url": "https://xxx/banner1.jpg",
+ "title": "铸爱联名活动",
+ "description": "参与铸爱创作赢好礼",
+ "link_type": "h5",
+ "link_value": "https://xxx/activity"
+ }
+ ]
+ }
+}
+```
+
+#### 4.1.3 DTO 定义
+
+```go
+// BannerDTO 轮播图响应
+type BannerDTO struct {
+ ID int64 `json:"id"`
+ ImageURL string `json:"image_url"`
+ Title string `json:"title"`
+ Description string `json:"description"`
+ LinkType string `json:"link_type"` // h5 / activity / internal
+ LinkValue string `json:"link_value"` // 跳转目标
+}
+
+// BannerListResponse 轮播图列表响应
+type BannerListResponse struct {
+ Items []BannerDTO `json:"items"`
+}
+```
+
+#### 4.1.4 Service 层实现
+
+```go
+// ActivityService/service/activity_service.go
+
+// GetBanners 获取轮播图列表
+func (s *ActivityService) GetBanners(ctx context.Context, position string) ([]*BannerDTO, error) {
+ // 默认位置
+ if position == "" {
+ position = "cast_home"
+ }
+
+ // 查询数据库
+ banners, err := s.bannerRepo.FindByPosition(ctx, position)
+ if err != nil {
+ return nil, err
+ }
+
+ // 转换为DTO
+ dtos := make([]*BannerDTO, 0, len(banners))
+ for _, banner := range banners {
+ dtos = append(dtos, &BannerDTO{
+ ID: banner.ID,
+ ImageURL: banner.ImageURL,
+ Title: banner.Title,
+ Description: banner.Description,
+ LinkType: banner.LinkType,
+ LinkValue: banner.LinkValue,
+ })
+ }
+
+ return dtos, nil
+}
+```
+
+#### 4.1.5 Controller 层实现
+
+```go
+// Gateway/controller/activity_controller.go
+
+// GetBanners 获取轮播图列表
+// @Summary 获取轮播图列表
+// @Tags activity
+// @Param position query string false "位置标识"
+// @Success 200 {object} BannerListResponse
+// @Router /api/v1/activities/banners [get]
+func (c *ActivityController) GetBanners(ctx *gin.Context) {
+ position := ctx.DefaultQuery("position", "cast_home")
+
+ banners, err := c.activityClient.GetBanners(ctx, position)
+ if err != nil {
+ response.Fail(ctx, 500, "获取轮播图失败")
+ return
+ }
+
+ response.Success(ctx, gin.H{
+ "items": banners,
+ })
+}
+```
+
+---
+
+### 4.2 获取铸爱创作列表(新增接口)
+
+**Endpoint:** `GET /api/v1/assets/cast/items`
+
+**归属服务:** AssetService
+
+#### 4.2.1 请求参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| cast_type | int32 | 否 | 藏品类型:1=星卡, 2=吧唧, 3=海报 |
+| sort_by | string | 否 | 排序方式:hot(热门) / latest(最新),默认hot |
+| page | int32 | 否 | 页码,默认1 |
+| page_size | int32 | 否 | 每页数量,默认20 |
+
+#### 4.2.2 响应格式
+
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "total": 100,
+ "page": 1,
+ "page_size": 20,
+ "items": [
+ {
+ "asset_id": 12345,
+ "name": "我的铸爱作品",
+ "cover_url": "https://xxx/cover.jpg",
+ "cover_url_signed": "https://xxx/cover.jpg?Expires=...&Signature=...",
+ "identity_no": "ZUA-20260409-00001",
+ "cast_type": 1,
+ "user_id": 10001,
+ "user_nickname": "用户昵称",
+ "user_avatar": "https://xxx/avatar.jpg",
+ "like_count": 999,
+ "is_liked": false,
+ "created_at": 1705747200000
+ }
+ ]
+ }
+}
+```
+
+#### 4.2.3 DTO 定义
+
+```go
+// CastAssetItemDTO 铸爱藏品列表项
+type CastAssetItemDTO struct {
+ AssetID int64 `json:"asset_id"`
+ Name string `json:"name"`
+ CoverURL string `json:"cover_url"`
+ CoverURLSigned string `json:"cover_url_signed"`
+ IdentityNo string `json:"identity_no"`
+ CastType int32 `json:"cast_type"`
+ UserID int64 `json:"user_id"`
+ UserNickname string `json:"user_nickname"`
+ UserAvatar string `json:"user_avatar"`
+ LikeCount int32 `json:"like_count"`
+ IsLiked bool `json:"is_liked"`
+ CreatedAt int64 `json:"created_at"`
+}
+
+// CastAssetListResponse 铸爱藏品列表响应
+type CastAssetListResponse struct {
+ Total int64 `json:"total"`
+ Page int32 `json:"page"`
+ PageSize int32 `json:"page_size"`
+ Items []CastAssetItemDTO `json:"items"`
+}
+```
+
+#### 4.2.4 Service 层实现
+
+```go
+// AssetService/service/cast_service.go
+
+// GetCastItems 获取铸爱创作列表
+func (s *CastService) GetCastItems(ctx context.Context, req *GetCastItemsRequest) (*CastAssetListResponse, error) {
+ // 参数校验与默认值
+ if req.Page <= 0 {
+ req.Page = 1
+ }
+ if req.PageSize <= 0 || req.PageSize > 100 {
+ req.PageSize = 20
+ }
+ if req.SortBy == "" {
+ req.SortBy = "hot"
+ }
+
+ // 构建查询条件
+ query := &CastAssetQuery{
+ StarID: req.StarID,
+ CastType: req.CastType,
+ SortBy: req.SortBy,
+ Page: req.Page,
+ PageSize: req.PageSize,
+ Status: 1, // 只查询成功的
+ }
+
+ // 查询列表
+ assets, total, err := s.castRepo.FindList(ctx, query)
+ if err != nil {
+ return nil, err
+ }
+
+ // 获取当前用户已点赞的藏品ID列表
+ likedMap, err := s.getUserLikedMap(ctx, req.UserID, assets)
+ if err != nil {
+ return nil, err
+ }
+
+ // 转换为DTO
+ items := make([]CastAssetItemDTO, 0, len(assets))
+ for _, asset := range assets {
+ // 生成预签名URL
+ coverURLSigned, _ := s.generatePresignedURL(asset.CoverURL, 3600)
+
+ // 获取用户信息
+ user, _ := s.userClient.GetUserInfo(ctx, asset.UserID)
+
+ items = append(items, CastAssetItemDTO{
+ AssetID: asset.ID,
+ Name: asset.Name,
+ CoverURL: asset.CoverURL,
+ CoverURLSigned: coverURLSigned,
+ IdentityNo: asset.IdentityNo,
+ CastType: asset.CastType,
+ UserID: asset.UserID,
+ UserNickname: user.Nickname,
+ UserAvatar: user.Avatar,
+ LikeCount: asset.LikeCount,
+ IsLiked: likedMap[asset.ID],
+ CreatedAt: asset.CreatedAt,
+ })
+ }
+
+ return &CastAssetListResponse{
+ Total: total,
+ Page: req.Page,
+ PageSize: req.PageSize,
+ Items: items,
+ }, nil
+}
+
+// generatePresignedURL 生成预签名URL
+func (s *CastService) generatePresignedURL(url string, expireSeconds int64) (string, error) {
+ if url == "" {
+ return "", nil
+ }
+ // 调用OSS生成预签名URL
+ return s.ossClient.GetPresignedURL(ctx, url, expireSeconds)
+}
+
+// getUserLikedMap 获取用户已点赞的藏品Map
+func (s *CastService) getUserLikedMap(ctx context.Context, userID int64, assets []*CastAsset) (map[int64]bool, error) {
+ assetIDs := make([]int64, 0, len(assets))
+ for _, asset := range assets {
+ assetIDs = append(assetIDs, asset.ID)
+ }
+
+ likedAssetIDs, err := s.socialClient.GetUserLikedCastAssets(ctx, userID, assetIDs)
+ if err != nil {
+ return nil, err
+ }
+
+ likedMap := make(map[int64]bool)
+ for _, id := range likedAssetIDs {
+ likedMap[id] = true
+ }
+ return likedMap, nil
+}
+```
+
+#### 4.2.5 Repository 层实现
+
+```go
+// AssetService/repository/cast_asset_repo.go
+
+// FindList 查询铸爱藏品列表
+func (r *CastAssetRepository) FindList(ctx context.Context, query *CastAssetQuery) ([]*CastAsset, int64, error) {
+ db := r.db.WithContext(ctx)
+
+ // 条件构建
+ if query.StarID > 0 {
+ db = db.Where("star_id = ?", query.StarID)
+ }
+ if query.CastType > 0 {
+ db = db.Where("cast_type = ?", query.CastType)
+ }
+ if query.Status > 0 {
+ db = db.Where("status = ?", query.Status)
+ }
+
+ // 排序
+ switch query.SortBy {
+ case "latest":
+ db = db.Order("created_at DESC")
+ case "hot":
+ db = db.Order("like_count DESC, created_at DESC")
+ default:
+ db = db.Order("like_count DESC, created_at DESC")
+ }
+
+ // 查询总数
+ var total int64
+ if err := db.Model(&CastAsset{}).Count(&total).Error; err != nil {
+ return nil, 0, err
+ }
+
+ // 分页
+ offset := (query.Page - 1) * query.PageSize
+ db = db.Offset(offset).Limit(query.PageSize)
+
+ // 执行查询
+ var assets []*CastAsset
+ if err := db.Find(&assets).Error; err != nil {
+ return nil, 0, err
+ }
+
+ return assets, total, nil
+}
+```
+
+---
+
+### 4.3 获取铸爱藏品详情(复用现有接口)
+
+**Endpoint:** `GET /api/v1/assets/:asset_id`
+
+**归属服务:** AssetService
+
+**接口说明:** 复用现有的 `GetAsset` 接口,无需新增。铸爱藏品使用 `cast_assets` 表,扩展 `AssetService.GetAsset` 方法支持查询铸爱藏品。
+
+#### 4.3.1 响应格式
+
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "asset_id": 12345,
+ "name": "我的铸爱作品",
+ "cover_url": "https://xxx/cover.jpg",
+ "cover_url_signed": "https://xxx/cover.jpg?Expires=...&Signature=...",
+ "identity_no": "ZUA-20260409-00001",
+ "identity_no_chain_url": "https://xxx/chain/query?no=ZUA-20260409-00001",
+ "cast_type": 1,
+ "description": "藏品描述",
+ "user_id": 10001,
+ "user_nickname": "用户昵称",
+ "user_avatar": "https://xxx/avatar.jpg",
+ "like_count": 999,
+ "is_liked": false,
+ "created_at": 1705747200000
+ }
+}
+```
+
+#### 4.3.2 DTO 定义
+
+```go
+// CastAssetDetailDTO 铸爱藏品详情
+type CastAssetDetailDTO struct {
+ AssetID int64 `json:"asset_id"`
+ Name string `json:"name"`
+ CoverURL string `json:"cover_url"`
+ CoverURLSigned string `json:"cover_url_signed"`
+ IdentityNo string `json:"identity_no"`
+ IdentityNoChainURL string `json:"identity_no_chain_url"`
+ CastType int32 `json:"cast_type"`
+ Description string `json:"description"`
+ UserID int64 `json:"user_id"`
+ UserNickname string `json:"user_nickname"`
+ UserAvatar string `json:"user_avatar"`
+ LikeCount int32 `json:"like_count"`
+ IsLiked bool `json:"is_liked"`
+ CreatedAt int64 `json:"created_at"`
+}
+```
+
+#### 4.3.3 Service 层实现
+
+```go
+// GetCastAssetDetail 获取铸爱藏品详情
+func (s *CastService) GetCastAssetDetail(ctx context.Context, assetID, userID int64) (*CastAssetDetailDTO, error) {
+ // 查询藏品
+ asset, err := s.castRepo.FindByID(ctx, assetID)
+ if err != nil {
+ return nil, err
+ }
+ if asset == nil {
+ return nil, errors.New("藏品不存在")
+ }
+
+ // 生成预签名URL
+ coverURLSigned, _ := s.generatePresignedURL(asset.CoverURL, 3600)
+
+ // 获取用户信息
+ user, _ := s.userClient.GetUserInfo(ctx, asset.UserID)
+
+ // 查询用户是否已点赞
+ isLiked, _ := s.socialClient.IsUserLikedCastAsset(ctx, userID, assetID)
+
+ // 链上查询URL(预留)
+ chainURL := s.buildChainQueryURL(asset.IdentityNo)
+
+ return &CastAssetDetailDTO{
+ AssetID: asset.ID,
+ Name: asset.Name,
+ CoverURL: asset.CoverURL,
+ CoverURLSigned: coverURLSigned,
+ IdentityNo: asset.IdentityNo,
+ IdentityNoChainURL: chainURL,
+ CastType: asset.CastType,
+ Description: asset.Description,
+ UserID: asset.UserID,
+ UserNickname: user.Nickname,
+ UserAvatar: user.Avatar,
+ LikeCount: asset.LikeCount,
+ IsLiked: isLiked,
+ CreatedAt: asset.CreatedAt,
+ }, nil
+}
+
+// buildChainQueryURL 构建链上查询URL
+func (s *CastService) buildChainQueryURL(identityNo string) string {
+ // 预留链上查询功能
+ return fmt.Sprintf("https://xxx/chain/query?no=%s", identityNo)
+}
+```
+
+---
+
+### 4.4 创建铸爱铸造订单(新增接口)
+
+**Endpoint:** `POST /api/v1/assets/cast/mints`
+
+**归属服务:** AssetService
+
+**页面说明:**
+- 用户点击主Tab(星卡/吧唧/海报)后进入对应类型的铸造页面
+- 铸造页面是同一个页面,通过 `cast_type` 参数区分类型
+-铸造流程:选择素材 → 输入描述词 → 发送 → AI处理中 → 结果预览
+
+**三种铸造类型:**
+
+| cast_type | 类型 | 素材形态 | 尺寸规格 | 创作特点 | 专属参数 |
+|-----------|------|----------|----------|----------|----------|
+| 1 | 星卡 | 卡片形态,类似明星小卡 | 1:1 或 3:4 | 适合人物特写、半身照 | 无 |
+| 2 | 吧唧 | 徽章/贴纸形态 | 圆形或异形 | 适合可爱风格、二次元、奶油胶/金属边框/异形裁切 | decoration_type |
+| 3 | 海报 | 大尺寸图片 | 16:9、4:3 或 2:3 | 适合全身照、场景图、背景替换/风格迁移/光影调整 | aspect_ratio, style_options |
+
+#### 4.4.1 请求格式
+
+```json
+{
+ "name": "我的铸爱作品",
+ "material_url": "https://xxx/material.jpg",
+ "description": "藏品描述",
+ "cast_type": 1,
+ "prompt_text": "请生成一张温暖风格的图片",
+ "decoration_type": "",
+ "aspect_ratio": "",
+ "style_options": []
+}
+```
+
+**请求参数说明:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| name | string | 是 | 藏品名称 |
+| material_url | string | 是 | 用户选择的相册图片URL |
+| description | string | 否 | 藏品描述 |
+| cast_type | int32 | 是 | 藏品类型:1=星卡, 2=吧唧, 3=海报 |
+| prompt_text | string | 是 | AI描述词 |
+| decoration_type | string | 否 | **吧唧专属**:装饰类型,记录用户选择,从 `/cast/options` 获取,如 cream_glue/metal_frame/shaped,可组合逗号分隔 |
+| aspect_ratio | string | 否 | **海报专属**:比例,landscape(横向16:9) / portrait(竖向2:3) / square(方形4:3) |
+| style_options | string[] | 否 | **海报专属**:风格选项,记录用户选择,从 `/cast/options` 获取,如 bg_replace/style_transfer/lighting,可多选 |
+
+**参数说明:**
+- `decoration_type` 和 `style_options` 仅记录用户选择,**不参与AI生成**,AI会根据cast_type自动生成对应效果
+- `aspect_ratio` 为海报比例参数,AI生成时使用
+
+**不同类型的参数组合:**
+
+| cast_type | 必须参数 | 可选参数 |
+|-----------|----------|----------|
+| 1 (星卡) | name, material_url, cast_type, prompt_text | description |
+| 2 (吧唧) | name, material_url, cast_type, prompt_text | description, decoration_type |
+| 3 (海报) | name, material_url, cast_type, prompt_text | description, aspect_ratio, style_options |
+
+#### 4.4.2 响应格式
+
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "order_id": "550e8400-e29b-41d4-a716-446655440000",
+ "asset_id": 12345,
+ "status": "PROCESSING",
+ "identity_no": "ZUA-20260409-00002",
+ "created_at": 1705747200000
+ }
+}
+```
+
+#### 4.4.3 DTO 定义
+
+```go
+// CreateCastMintRequest 创建铸造订单请求
+type CreateCastMintRequest struct {
+ Name string `json:"name" binding:"required"`
+ MaterialURL string `json:"material_url" binding:"required,url"`
+ Description string `json:"description"`
+ CastType int32 `json:"cast_type" binding:"required,oneof=1 2 3"`
+ PromptText string `json:"prompt_text" binding:"required"`
+ // 吧唧专属参数
+ DecorationType string `json:"decoration_type"` // cream_glue, metal_frame, shaped
+ // 海报专属参数
+ AspectRatio string `json:"aspect_ratio"` // landscape, portrait, square
+ StyleOptions []string `json:"style_options"` // bg_replace, style_transfer, lighting
+}
+
+// CreateCastMintResponse 创建铸造订单响应
+type CreateCastMintResponse struct {
+ OrderID string `json:"order_id"`
+ AssetID int64 `json:"asset_id"`
+ Status string `json:"status"`
+ IdentityNo string `json:"identity_no"`
+ CreatedAt int64 `json:"created_at"`
+}
+```
+
+#### 4.4.4 Service 层实现(核心流程)
+
+```go
+// CreateCastMint 创建铸爱铸造订单
+func (s *CastService) CreateCastMint(ctx context.Context, userID, starID int64, req *CreateCastMintRequest) (*CreateCastMintResponse, error) {
+ // 1. 参数校验
+ if req.Name == "" || req.MaterialURL == "" || req.PromptText == "" {
+ return nil, errors.New("参数不完整")
+ }
+ if req.CastType < 1 || req.CastType > 3 {
+ return nil, errors.New("无效的藏品类型")
+ }
+
+ // 2. 生成唯一订单ID
+ orderID := uuid.New().String()
+
+ // 3. 生成唯一身份证编号
+ identityNo, err := s.generateIdentityNo(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("生成身份证编号失败: %w", err)
+ }
+
+ // 4. 创建藏品记录(初始状态:处理中)
+ asset := &CastAsset{
+ OwnerUID: userID,
+ StarID: starID,
+ Name: req.Name,
+ MaterialURL: req.MaterialURL,
+ Description: req.Description,
+ CastType: req.CastType,
+ IdentityNo: identityNo,
+ Status: 0, // 处理中
+ UserID: userID,
+ CreatedAt: time.Now().UnixMilli(),
+ UpdatedAt: time.Now().UnixMilli(),
+ }
+ if err := s.castRepo.Create(ctx, asset); err != nil {
+ return nil, fmt.Errorf("创建藏品记录失败: %w", err)
+ }
+
+ // 5. 创建订单记录
+ order := &CastMintOrder{
+ OrderID: orderID,
+ UserID: userID,
+ StarID: starID,
+ AssetID: asset.ID,
+ CastType: req.CastType,
+ PromptText: req.PromptText,
+ MaterialURL: req.MaterialURL,
+ IdentityNo: identityNo,
+ Status: "PROCESSING",
+ DecorationType: req.DecorationType, // 吧唧专属
+ AspectRatio: req.AspectRatio, // 海报专属
+ StyleOptions: req.StyleOptions, // 海报专属
+ CreatedAt: time.Now().UnixMilli(),
+ UpdatedAt: time.Now().UnixMilli(),
+ }
+ if err := s.mintOrderRepo.Create(ctx, order); err != nil {
+ return nil, fmt.Errorf("创建订单记录失败: %w", err)
+ }
+
+ // 6. 启动异步AI生成任务(decoration_type/style_options仅记录,不传给AI)
+ go s.processCastAIGeneration(orderID, asset.ID, req.CastType, req.PromptText, req.MaterialURL, req.AspectRatio)
+
+ // 7. 返回响应
+ return &CreateCastMintResponse{
+ OrderID: orderID,
+ AssetID: asset.ID,
+ Status: "PROCESSING",
+ IdentityNo: identityNo,
+ CreatedAt: asset.CreatedAt,
+ }, nil
+}
+```
+
+#### 4.4.5 异步AI生成任务(核心代码)
+
+```go
+// processCastAIGeneration 异步处理AI生成
+func (s *CastService) processCastAIGeneration(orderID string, assetID int64, castType int32, promptText, materialURL, aspectRatio string) {
+ // 创建独立的context,避免请求超时
+ ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
+ defer cancel()
+
+ logger.Info("开始处理铸爱AI生成",
+ zap.String("order_id", orderID),
+ zap.Int64("asset_id", assetID),
+ zap.Int32("cast_type", castType),
+ zap.String("aspect_ratio", aspectRatio),
+ zap.String("prompt", promptText),
+ )
+
+ // 1. 构建AI请求参数
+ aiReq := &AIGenerateRequest{
+ Model: "minimax",
+ InputImage: materialURL,
+ Prompt: promptText,
+ AspectRatio: aspectRatio,
+ }
+
+ // 2. 根据cast_type设置AI生成参数
+ // decoration_type和style_options仅记录到订单,不参与AI生成
+ switch castType {
+ case 1: // 星卡
+ aiReq.AspectRatio = "1:1"
+ case 2: // 吧唧
+ // AI根据cast_type自动生成吧唧装饰效果
+ case 3: // 海报
+ // AI根据aspectRatio确定输出图片的尺寸比例
+ }
+
+ // 3. 调用MiniMax AI服务进行图生图
+ aiResult, err := s.aiClient.GenerateImage(ctx, aiReq)
+ if err != nil {
+ s.handleAIGenerationFailed(ctx, orderID, assetID, "AI服务调用失败: "+err.Error())
+ return
+ }
+
+ // 5. 下载AI生成的图片
+ generatedImage, err := s.downloadImage(ctx, aiResult.ImageURL)
+ if err != nil {
+ s.handleAIGenerationFailed(ctx, orderID, assetID, "下载AI图片失败: "+err.Error())
+ return
+ }
+
+ // 6. 上传到OSS
+ coverURL, err := s.uploadToOSS(ctx, generatedImage, assetID)
+ if err != nil {
+ s.handleAIGenerationFailed(ctx, orderID, assetID, "上传OSS失败: "+err.Error())
+ return
+ }
+
+ // 7. 生成模拟上链信息
+ txHash := s.generateMockTxHash()
+ blockNumber := s.generateMockBlockNumber()
+ mintedAt := time.Now().UnixMilli()
+
+ // 8. 更新藏品状态
+ if err := s.updateAssetSuccess(ctx, assetID, coverURL, txHash, blockNumber, mintedAt); err != nil {
+ s.handleAIGenerationFailed(ctx, orderID, assetID, "更新藏品状态失败: "+err.Error())
+ return
+ }
+
+ // 9. 更新订单状态
+ if err := s.updateOrderSuccess(ctx, orderID, coverURL, txHash); err != nil {
+ logger.Error("更新订单状态失败", zap.String("order_id", orderID), zap.Error(err))
+ return
+ }
+
+ logger.Info("铸爱AI生成完成",
+ zap.String("order_id", orderID),
+ zap.Int64("asset_id", assetID),
+ zap.String("cover_url", coverURL),
+ zap.String("tx_hash", txHash),
+ )
+}
+
+// handleAIGenerationFailed 处理AI生成失败
+func (s *CastService) handleAIGenerationFailed(ctx context.Context, orderID string, assetID int64, errorMsg string) {
+ logger.Error("铸爱AI生成失败",
+ zap.String("order_id", orderID),
+ zap.Int64("asset_id", assetID),
+ zap.String("error", errorMsg),
+ )
+
+ // 1. 更新订单状态为失败
+ order, _ := s.mintOrderRepo.FindByOrderID(ctx, orderID)
+ if order != nil {
+ order.Status = "FAILED"
+ order.ErrorMessage = errorMsg
+ order.UpdatedAt = time.Now().UnixMilli()
+ s.mintOrderRepo.Update(ctx, order)
+
+ // 2. 退回水晶费用
+ if order.CostCrystal > 0 {
+ s.refundCrystalBalance(ctx, order.UserID, order.StarID, order.CostCrystal)
+ }
+ }
+
+ // 3. 更新藏品状态
+ s.castRepo.UpdateStatus(ctx, assetID, 2) // 2=失败
+}
+```
+
+#### 4.4.6 身份证编号生成
+
+```go
+// generateIdentityNo 生成唯一身份证编号
+// 格式: ZUA-YYYYMMDD-XXXXX
+func (s *CastService) generateIdentityNo(ctx context.Context) (string, error) {
+ // 获取当前日期
+ dateStr := time.Now().Format("20060102")
+
+ // 使用Redis INCR获取当日序号(按日期key)
+ key := fmt.Sprintf("cast:identity:seq:%s", dateStr)
+ seq, err := s.redisClient.Incr(ctx, key)
+ if err != nil {
+ return "", err
+ }
+
+ // 设置过期时间(次日凌晨)
+ s.redisClient.ExpireAt(ctx, key, s.getNextDayMidnight())
+
+ // 格式化为5位序号
+ seqStr := fmt.Sprintf("%05d", seq)
+ return fmt.Sprintf("ZUA-%s-%s", dateStr, seqStr), nil
+}
+
+// getNextDayMidnight 获取次日凌晨时间
+func (s *CastService) getNextDayMidnight() time.Time {
+ tomorrow := time.Now().AddDate(0, 0, 1)
+ return time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 0, 0, 0, 0, tomorrow.Location())
+}
+```
+
+---
+
+### 4.5 查询铸造订单状态(新增接口)
+
+**Endpoint:** `GET /api/v1/assets/cast/mints/:order_id`
+
+**归属服务:** AssetService
+
+#### 4.5.1 响应格式
+
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "order_id": "550e8400-e29b-41d4-a716-446655440000",
+ "asset_id": 12345,
+ "status": "PROCESSING",
+ "cover_url": null,
+ "cover_url_signed": null,
+ "identity_no": "ZUA-20260409-00002",
+ "tx_hash": null,
+ "error_message": null,
+ "created_at": 1705747200000,
+ "updated_at": 1705747200000
+ }
+}
+```
+
+#### 4.5.2 DTO 定义
+
+```go
+// MintOrderStatusDTO 铸造订单状态
+type MintOrderStatusDTO struct {
+ OrderID string `json:"order_id"`
+ AssetID int64 `json:"asset_id"`
+ Status string `json:"status"` // PROCESSING / SUCCESS / FAILED
+ CoverURL string `json:"cover_url"`
+ CoverURLSigned string `json:"cover_url_signed"`
+ IdentityNo string `json:"identity_no"`
+ TxHash string `json:"tx_hash"`
+ ErrorMessage string `json:"error_message,omitempty"`
+ CreatedAt int64 `json:"created_at"`
+ UpdatedAt int64 `json:"updated_at"`
+}
+```
+
+#### 4.5.3 Service 层实现
+
+```go
+// GetMintOrderStatus 查询铸造订单状态
+func (s *CastService) GetMintOrderStatus(ctx context.Context, orderID string) (*MintOrderStatusDTO, error) {
+ // 查询订单
+ order, err := s.mintOrderRepo.FindByOrderID(ctx, orderID)
+ if err != nil {
+ return nil, err
+ }
+ if order == nil {
+ return nil, errors.New("订单不存在")
+ }
+
+ dto := &MintOrderStatusDTO{
+ OrderID: order.OrderID,
+ AssetID: order.AssetID,
+ Status: order.Status,
+ IdentityNo: order.IdentityNo,
+ ErrorMessage: order.ErrorMessage,
+ CreatedAt: order.CreatedAt,
+ UpdatedAt: order.UpdatedAt,
+ }
+
+ // 如果成功,填充图片URL
+ if order.Status == "SUCCESS" && order.CoverURL != "" {
+ dto.CoverURL = order.CoverURL
+ dto.CoverURLSigned, _ = s.generatePresignedURL(order.CoverURL, 3600)
+ dto.TxHash = order.TxHash
+ }
+
+ return dto, nil
+}
+```
+
+---
+
+### 4.6 铸爱藏品点赞(复用现有接口)
+
+**Endpoint:** `POST /api/v1/social/assets/:asset_id/like`
+
+**归属服务:** SocialService
+
+**接口说明:** 复用现有的 `LikeAsset` 接口。扩展 `asset_likes` 表的 `asset_type` 字段,支持区分原有资产(1)和铸爱藏品(2)。
+
+#### 4.6.1 请求格式
+
+```json
+{
+ "asset_id": 12345
+}
+```
+
+#### 4.6.2 响应格式
+
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "asset_id": 12345,
+ "is_liked": true,
+ "like_count": 1000
+ }
+}
+```
+
+#### 4.6.3 DTO 定义
+
+```go
+// LikeCastAssetRequest 点赞请求
+type LikeCastAssetRequest struct {
+ AssetID int64 `json:"asset_id" binding:"required"`
+}
+
+// LikeCastAssetResponse 点赞响应
+type LikeCastAssetResponse struct {
+ AssetID int64 `json:"asset_id"`
+ IsLiked bool `json:"is_liked"`
+ LikeCount int32 `json:"like_count"`
+}
+```
+
+#### 4.6.4 Service 层实现
+
+```go
+// LikeCastAsset 铸爱藏品点赞/取消点赞
+func (s *SocialService) LikeCastAsset(ctx context.Context, userID, assetID int64) (*LikeCastAssetResponse, error) {
+ // 查询是否已点赞
+ existing, err := s.assetLikeRepo.FindByUserAndAsset(ctx, userID, assetID, 2) // asset_type=2
+ if err != nil {
+ return nil, err
+ }
+
+ var isLiked bool
+ if existing != nil {
+ // 取消点赞
+ if err := s.assetLikeRepo.Delete(ctx, existing.ID); err != nil {
+ return nil, err
+ }
+ // 减少点赞数
+ s.castAssetRepo.DecrLikeCount(ctx, assetID)
+ isLiked = false
+ } else {
+ // 添加点赞
+ like := &AssetLike{
+ UserID: userID,
+ AssetID: assetID,
+ AssetType: 2, // 铸爱藏品
+ CastAssetID: assetID,
+ CreatedAt: time.Now().UnixMilli(),
+ }
+ if err := s.assetLikeRepo.Create(ctx, like); err != nil {
+ return nil, err
+ }
+ // 增加点赞数
+ s.castAssetRepo.IncrLikeCount(ctx, assetID)
+ isLiked = true
+ }
+
+ // 获取最新点赞数
+ likeCount, _ := s.castAssetRepo.GetLikeCount(ctx, assetID)
+
+ return &LikeCastAssetResponse{
+ AssetID: assetID,
+ IsLiked: isLiked,
+ LikeCount: likeCount,
+ }, nil
+}
+```
+
+---
+
+### 4.7 获取铸爱类型选项(新增接口)
+
+**Endpoint:** `GET /api/v1/assets/cast/options`
+
+**归属服务:** AssetService
+
+**接口说明:** 获取各类型的可选项(吧唧装饰类型、海报风格选项等),供前端下发给用户选择。
+
+#### 4.7.1 请求参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| cast_type | int32 | 是 | 藏品类型:1=星卡, 2=吧唧, 3=海报 |
+
+#### 4.7.2 响应格式
+
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "cast_type": 2,
+ "options": [
+ {
+ "key": "cream_glue",
+ "name": "奶油胶",
+ "type": "decoration",
+ "icon_url": "https://xxx/icons/cream_glue.png"
+ },
+ {
+ "key": "metal_frame",
+ "name": "金属边框",
+ "type": "decoration",
+ "icon_url": "https://xxx/icons/metal_frame.png"
+ },
+ {
+ "key": "shaped",
+ "name": "异形裁切",
+ "type": "decoration",
+ "icon_url": "https://xxx/icons/shaped.png"
+ }
+ ]
+ }
+}
+```
+
+#### 4.7.3 DTO 定义
+
+```go
+// CastTypeOptionDTO 铸爱类型选项
+type CastTypeOptionDTO struct {
+ Key string `json:"key"` // 选项标识
+ Name string `json:"name"` // 显示名称
+ Type string `json:"type"` // decoration / style
+ IconURL string `json:"icon_url"` // 图标
+}
+
+// CastTypeOptionsResponse 铸爱类型选项响应
+type CastTypeOptionsResponse struct {
+ CastType int32 `json:"cast_type"`
+ Options []CastTypeOptionDTO `json:"options"`
+}
+```
+
+#### 4.7.4 Service 层实现
+
+```go
+// GetCastTypeOptions 获取铸爱类型选项
+func (s *CastService) GetCastTypeOptions(ctx context.Context, castType int32) (*CastTypeOptionsResponse, error) {
+ // 查询启用的选项
+ options, err := s.castTypeOptionRepo.FindByCastType(ctx, castType, 1)
+ if err != nil {
+ return nil, err
+ }
+
+ // 转换为DTO
+ dtos := make([]CastTypeOptionDTO, 0, len(options))
+ for _, opt := range options {
+ dtos = append(dtos, CastTypeOptionDTO{
+ Key: opt.OptionKey,
+ Name: opt.OptionName,
+ Type: opt.OptionType,
+ IconURL: opt.IconURL,
+ })
+ }
+
+ return &CastTypeOptionsResponse{
+ CastType: castType,
+ Options: dtos,
+ }, nil
+}
+```
+
+#### 4.7.5 运维后台接口(新增)
+
+**Endpoint:** `GET /api/v1/admin/cast/options`
+
+| 接口 | 方法 | 说明 |
+|------|------|------|
+| `GET /api/v1/admin/cast/options` | GET | 获取选项列表(支持筛选cast_type) |
+| `POST /api/v1/admin/cast/options` | POST | 新增选项 |
+| `PUT /api/v1/admin/cast/options/:id` | PUT | 更新选项 |
+| `DELETE /api/v1/admin/cast/options/:id` | DELETE | 删除选项 |
+
+---
+
+## 5. AI 服务对接
+
+### 5.1 MiniMax AI 服务对接
+
+#### 5.1.1 AI 客户端接口
+
+```go
+// AI客户端接口
+type AIClient interface {
+ GenerateImage(ctx context.Context, req *AIGenerateRequest) (*AIGenerateResponse, error)
+}
+
+// AIGenerateRequest AI生成请求
+type AIGenerateRequest struct {
+ Model string // 模型标识: minimax / openai / sd
+ InputImage string // 输入图片URL
+ Prompt string // 描述词
+ AspectRatio string // 输出尺寸比例:1:1(星卡) / landscape(海报16:9) / portrait(海报2:3) / square(海报4:3)
+}
+
+// AIGenerateResponse AI生成响应
+type AIGenerateResponse struct {
+ ImageURL string // 生成的图片URL
+ RequestID string // 请求ID
+}
+```
+
+#### 5.1.2 MiniMax 实现
+
+```go
+// MiniMaxAIClient MiniMax AI客户端
+type MiniMaxAIClient struct {
+ apiKey string
+ apiURL string
+ httpClient *http.Client
+}
+
+// GenerateImage 调用MiniMax图生图API
+func (c *MiniMaxAIClient) GenerateImage(ctx context.Context, req *AIGenerateRequest) (*AIGenerateResponse, error) {
+ // 构造请求
+ payload := map[string]interface{}{
+ "model": "minimax-vl-01",
+ "image_url": req.InputImage,
+ "prompt": req.Prompt,
+ "num_images": 1,
+ "aspect_ratio": req.AspectRatio, // 1:1(星卡) / landscape(海报16:9) / portrait(海报2:3) / square(海报4:3)
+ }
+
+ httpReq, _ := http.NewRequestWithContext(ctx, "POST", c.apiURL+"/v1/image_generation", bytes.NewReader(mustMarshal(payload)))
+ httpReq.Header.Set("Content-Type", "application/json")
+ httpReq.Header.Set("Authorization", "Bearer "+c.apiKey)
+
+ // 发送请求
+ resp, err := c.httpClient.Do(httpReq)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != 200 {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("MiniMax API error: %d - %s", resp.StatusCode, string(body))
+ }
+
+ // 解析响应
+ var result struct {
+ Data []struct {
+ ImageURL string `json:"image_url"`
+ } `json:"data"`
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return nil, err
+ }
+
+ if len(result.Data) == 0 {
+ return nil, errors.New("AI返回数据为空")
+ }
+
+ return &AIGenerateResponse{
+ ImageURL: result.Data[0].ImageURL,
+ RequestID: resp.Header.Get("X-Request-ID"),
+ }, nil
+}
+```
+
+### 5.2 多模型切换策略(后续扩展)
+
+```go
+// MultiModelAIClient 多模型AI客户端(后续扩展)
+type MultiModelAIClient struct {
+ clients map[string]AIClient // 模型客户端Map
+ defaultModel string
+}
+
+// GenerateImage 根据模型生成图片
+func (c *MultiModelAIClient) GenerateImage(ctx context.Context, req *AIGenerateRequest) (*AIGenerateResponse, error) {
+ model := req.Model
+ if model == "" {
+ model = c.defaultModel
+ }
+
+ client, ok := c.clients[model]
+ if !ok {
+ return nil, fmt.Errorf("不支持的AI模型: %s", model)
+ }
+
+ return client.GenerateImage(ctx, req)
+}
+```
+
+---
+
+## 6. 服务间调用关系
+
+### 6.1 调用链
+
+```
+API Gateway
+ │
+ ├── ActivityService (gRPC)
+ │ └── GetBanners(position) → BannerDTO[]
+ │
+ ├── AssetService (gRPC)
+ │ ├── GetCastItems() → CastAssetListResponse
+ │ ├── GetCastAssetDetail() → CastAssetDetailDTO
+ │ ├── CreateCastMint() → CreateCastMintResponse
+ │ │ │
+ │ │ ├── UserService.UpdateCrystalBalance() 扣水晶
+ │ │ └── 启动异步任务 processCastAIGeneration()
+ │ └── GetMintOrderStatus() → MintOrderStatusDTO
+ │
+ └── SocialService (gRPC)
+ ├── GetUserLikedCastAssets() → assetID[]
+ ├── IsUserLikedCastAsset() → bool
+ └── LikeCastAsset() → LikeCastAssetResponse
+```
+
+### 6.2 gRPC Proto 定义
+
+```protobuf
+// AssetService Proto
+service AssetService {
+ // 获取铸爱创作列表
+ rpc GetCastItems(GetCastItemsRequest) returns (CastAssetListResponse);
+
+ // 获取铸爱类型选项(吧唧装饰/海报风格)
+ rpc GetCastTypeOptions(GetCastTypeOptionsRequest) returns (CastTypeOptionsResponse);
+
+ // 获取铸爱藏品详情
+ rpc GetCastAssetDetail(GetCastAssetDetailRequest) returns (CastAssetDetailDTO);
+
+ // 创建铸爱铸造订单
+ rpc CreateCastMint(CreateCastMintRequest) returns (CreateCastMintResponse);
+
+ // 查询铸造订单状态
+ rpc GetMintOrderStatus(GetMintOrderStatusRequest) returns (MintOrderStatusDTO);
+}
+
+// SocialService Proto
+service SocialService {
+ // 铸爱藏品点赞
+ rpc LikeCastAsset(LikeCastAssetRequest) returns (LikeCastAssetResponse);
+}
+```
+
+---
+
+## 7. 前端对接说明
+
+### 7.1 页面路由
+
+| 页面 | 路由 | 对应接口 |
+|------|------|----------|
+| 铸爱首页 | `/cast` | `GetBanners`, `GetCastItems` |
+| 铸造页面 | `/cast/create/:type` | `CreateCastMintOrder`, `GetCastMintStatus` |
+| AI处理中 | `/cast/processing/:order_id` | `GetMintOrderStatus`(轮询) |
+| 结果预览 | `/cast/result/:order_id` | `GetMintOrderStatus` |
+| 创作详情 | `/cast/detail/:asset_id` | `GetCastAssetDetail`, `LikeCastAsset` |
+
+### 7.2 前端轮询策略
+
+```javascript
+// 铸造订单状态轮询
+async function pollMintStatus(orderId) {
+ const maxAttempts = 60; // 最多60次
+ const interval = 3000; // 3秒
+
+ for (let i = 0; i < maxAttempts; i++) {
+ const resp = await api.getCastMintStatus(orderId);
+ const { status, cover_url, error_message } = resp.data;
+
+ if (status === 'SUCCESS') {
+ // 铸造成功,跳转结果页
+ router.push(`/cast/result/${orderId}`);
+ return;
+ }
+
+ if (status === 'FAILED') {
+ // 铸造失败,显示错误
+ showToast(error_message || '铸造失败');
+ return;
+ }
+
+ // 继续等待
+ await sleep(interval);
+ }
+
+ // 超时
+ showToast('铸造超时,请稍后重试');
+}
+```
+
+---
+
+## 8. 待确认事项
+
+| 问题 | 说明 | 状态 |
+|------|------|------|
+| 铸造费用 | 铸爱创作是否需要消耗水晶?收费标准? | 待确认 |
+| AI 模型 | 初期 MiniMax,后续多模型切换 | 已确认 |
+| 多模型切换 | 后续扩展时,模型切换策略(用户可选/后台配置)? | 待确认 |
+| 评论功能 | 是否需要评论功能? | 待确认 |
+| 分享功能 | 分享到外部平台的规则? | 待确认 |
+
+---
+
+## 9. 实现计划
+
+### Phase 1:基础设施(1-2天)
+- [ ] 创建数据库表 `cast_assets`、`cast_mint_orders`
+- [ ] 在 AssetService 中新增 Cast 相关方法
+- [ ] 在 ActivityService 中新增 Banner 轮播接口
+- [ ] 扩展 asset_likes 表支持 asset_type 字段
+
+### Phase 2:核心功能(3-5天)
+- [x] 实现 `GET /api/v1/activities/banners` ← **新增**
+- [x] 实现 `GET /api/v1/assets/cast/items` ← **新增**
+- [ ] 实现 `GET /api/v1/assets/:asset_id` ← **复用现有接口**,扩展支持铸爱藏品查询
+- [x] 实现 `POST /api/v1/assets/cast/mints` ← **新增**
+- [x] 实现 `GET /api/v1/assets/cast/mints/:order_id` ← **新增**
+- [x] 实现 AI 异步生成任务(MiniMax 图生图)← **新增**
+
+### Phase 3:社交功能(1-2天)
+- [ ] 实现 `POST /api/v1/social/assets/:asset_id/like` ← **复用现有接口**,扩展支持铸爱藏品点赞
+
+### Phase 4:联调测试(2-3天)
+- [ ] 前端页面联调
+- [ ] 全流程测试
+- [ ] 性能测试
+
+---
+
+**接口状态汇总:**
+
+| 接口 | 状态 | 说明 |
+|------|------|------|
+| `GET /api/v1/activities/banners` | 新增 | 轮播图列表 |
+| `GET /api/v1/assets/cast/items` | 新增 | 铸爱创作列表 |
+| `GET /api/v1/assets/cast/options` | 新增 | 铸爱类型选项 |
+| `GET /api/v1/assets/:asset_id` | 复用 | 藏品详情(需扩展) |
+| `POST /api/v1/assets/cast/mints` | 新增 | 创建铸造订单 |
+| `GET /api/v1/assets/cast/mints/:order_id` | 新增 | 查询订单状态 |
+| `POST /api/v1/social/assets/:asset_id/like` | 复用 | 点赞(需扩展) |
+
+---
+
+## 10. 参考文档
+
+| 文档 | 说明 |
+|------|------|
+| `docs/资产铸造AI生成流程设计文档.md` | 现有AI铸造流程设计参考 |
+| `PROJECT_SUMMARY.md` | 项目整体架构说明 |
+| `docs/资产服务设计文档.md` | AssetService 设计说明 |
+
+---
+
+> **下一步行动:** 请确认以上设计决策,特别是铸造费用和AI模型对接方案。
diff --git a/docs/superpowers/plans/2026-04-08-task-management-system-implementation-plan.md b/docs/superpowers/plans/2026-04-08-task-management-system-implementation-plan.md
new file mode 100644
index 0000000..38428ec
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-08-task-management-system-implementation-plan.md
@@ -0,0 +1,2417 @@
+# 任务管理系统实现计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 实现完整的任务管理系统,包括 activity-admin 运营后台和 Go taskService 移动后端,共用数据库
+
+**Architecture:**
+- activity-admin (Python FastAPI): 任务定义管理、进度查询、统计、手动重置
+- Go taskService (Dubbo-go Triple): 移动端 API、事件处理、奖励发放、每日重置
+- 共享 PostgreSQL 数据库,Go 服务通过 Advisory Lock 确保单实例重置
+
+**Tech Stack:**
+- Python: FastAPI, SQLAlchemy, Pydantic
+- Go: Dubbo-go, GORM, PostgreSQL
+- Frontend: Vue 3, Element Plus, ECharts
+
+---
+
+## Phase 1: 数据库设计与初始化
+
+### Task 1.1: 创建数据库表
+
+**Files:**
+- Create: `scripts/task_system_migration.sql`
+
+- [ ] **Step 1: 创建 SQL 迁移脚本**
+
+```sql
+-- 任务定义表
+CREATE TABLE IF NOT EXISTS task_definitions (
+ id BIGSERIAL PRIMARY KEY,
+ star_id BIGINT, -- NULL=全局默认
+ task_key VARCHAR(50) NOT NULL,
+ task_type VARCHAR(20) NOT NULL, -- 'daily' | 'onboarding'
+ name VARCHAR(100) NOT NULL,
+ description TEXT,
+ crystal_reward BIGINT DEFAULT 0,
+ exp_reward BIGINT DEFAULT 0,
+ sort_order INT DEFAULT 0,
+ is_active BOOLEAN DEFAULT true,
+ created_at BIGINT,
+ updated_at BIGINT
+);
+
+CREATE UNIQUE INDEX ix_task_def_star_key ON task_definitions(star_id, task_key);
+
+-- 每日任务进度表
+CREATE TABLE IF NOT EXISTS user_daily_task_progress (
+ id BIGSERIAL PRIMARY KEY,
+ user_id BIGINT NOT NULL,
+ star_id BIGINT NOT NULL,
+ task_key VARCHAR(50) NOT NULL,
+ status VARCHAR(20) DEFAULT 'pending', -- pending/completed/claimed
+ completed_at BIGINT,
+ claimed_at BIGINT,
+ created_at BIGINT,
+ updated_at BIGINT
+);
+
+CREATE UNIQUE INDEX ix_daily_progress_user_star_key ON user_daily_task_progress(user_id, star_id, task_key);
+
+-- 引导任务进度表
+CREATE TABLE IF NOT EXISTS user_onboarding_progress (
+ id BIGSERIAL PRIMARY KEY,
+ user_id BIGINT NOT NULL,
+ task_key VARCHAR(50) NOT NULL,
+ status VARCHAR(20) DEFAULT 'pending', -- pending/completed/claimed
+ completed_at BIGINT,
+ claimed_at BIGINT,
+ created_at BIGINT,
+ updated_at BIGINT
+);
+
+CREATE UNIQUE INDEX ix_onboard_progress_user_key ON user_onboarding_progress(user_id, task_key);
+
+-- 引导流程状态表
+CREATE TABLE IF NOT EXISTS user_onboarding_status (
+ user_id BIGINT PRIMARY KEY,
+ current_stage INT DEFAULT 0, -- 0=未开始,1~N=阶段
+ status VARCHAR(20) DEFAULT 'pending', -- pending/in_progress/completed/claimed
+ is_first_login_bonus_claimed BOOLEAN DEFAULT false,
+ has_friend_display_bonus BOOLEAN DEFAULT false,
+ completed_at BIGINT,
+ claimed_at BIGINT,
+ created_at BIGINT,
+ updated_at BIGINT
+);
+
+-- 引导阶段配置表
+CREATE TABLE IF NOT EXISTS onboarding_stage_config (
+ id BIGSERIAL PRIMARY KEY,
+ stage INT NOT NULL,
+ name VARCHAR(100) NOT NULL,
+ description TEXT,
+ required_task_keys TEXT[], -- PostgreSQL 数组类型
+ crystal_reward BIGINT DEFAULT 0,
+ exp_reward BIGINT DEFAULT 0,
+ sort_order INT DEFAULT 0,
+ is_active BOOLEAN DEFAULT true,
+ created_at BIGINT,
+ updated_at BIGINT
+);
+
+-- 展示收益记录表
+CREATE TABLE IF NOT EXISTS exhibition_revenue_records (
+ id BIGSERIAL PRIMARY KEY,
+ user_id BIGINT NOT NULL,
+ star_id BIGINT NOT NULL,
+ exhibition_id BIGINT NOT NULL,
+ asset_id BIGINT NOT NULL,
+ slot_id BIGINT NOT NULL,
+ slot_owner_uid BIGINT NOT NULL,
+ slot_type VARCHAR(20) NOT NULL, -- 'own' | 'friend'
+ crystal_amount BIGINT NOT NULL,
+ cycle_start_time BIGINT NOT NULL,
+ cycle_end_time BIGINT NOT NULL,
+ status VARCHAR(20) DEFAULT 'claimable', -- claimable/claimed
+ claimed_at BIGINT,
+ created_at BIGINT
+);
+
+CREATE INDEX ix_revenue_user_star_status ON exhibition_revenue_records(user_id, star_id, status);
+CREATE INDEX ix_revenue_star_status ON exhibition_revenue_records(star_id, status);
+
+-- 重置记录表(用于防止重复重置)
+CREATE TABLE IF NOT EXISTS task_reset_log (
+ id BIGSERIAL PRIMARY KEY,
+ reset_type VARCHAR(20) NOT NULL, -- 'daily' | 'manual'
+ star_id BIGINT,
+ last_reset_at BIGINT NOT NULL,
+ created_at BIGINT
+);
+```
+
+- [ ] **Step 2: 执行迁移脚本**
+
+Run: `psql -h localhost -U postgres -d topfans -f scripts/task_system_migration.sql`
+
+---
+
+### Task 1.2: 插入默认任务数据
+
+**Files:**
+- Create: `scripts/task_default_data.sql`
+
+- [ ] **Step 1: 创建默认任务数据脚本**
+
+```sql
+-- 插入默认每日任务(star_id = NULL 表示全局默认)
+INSERT INTO task_definitions (star_id, task_key, task_type, name, description, crystal_reward, exp_reward, sort_order, is_active, created_at, updated_at) VALUES
+(NULL, 'daily_login', 'daily', '每日首次登录', '每日首次登录 App', 20, 20, 1, true, EXTRACT(EPOCH FROM NOW())::bigint * 1000, EXTRACT(EPOCH FROM NOW())::bigint * 1000),
+(NULL, 'daily_browse_asset', 'daily', '每日首次浏览藏品', '每日首次浏览任意展品', 20, 40, 2, true, EXTRACT(EPOCH FROM NOW())::bigint * 1000, EXTRACT(EPOCH FROM NOW())::bigint * 1000),
+(NULL, 'daily_mint', 'daily', '每日首次铸造', '每日首次铸造藏品', 20, 60, 3, true, EXTRACT(EPOCH FROM NOW())::bigint * 1000, EXTRACT(EPOCH FROM NOW())::bigint * 1000),
+(NULL, 'daily_place_asset', 'daily', '每日首次上架藏品', '每日首次上架藏品到展厅', 20, 80, 4, true, EXTRACT(EPOCH FROM NOW())::bigint * 1000, EXTRACT(EPOCH FROM NOW())::bigint * 1000);
+
+-- 插入默认引导任务
+INSERT INTO task_definitions (star_id, task_key, task_type, name, description, crystal_reward, exp_reward, sort_order, is_active, created_at, updated_at) VALUES
+(NULL, 'onboarding_complete', 'onboarding', '完成新手引导', '完成全部新手引导流程', 420, 250, 1, true, EXTRACT(EPOCH FROM NOW())::bigint * 1000, EXTRACT(EPOCH FROM NOW())::bigint * 1000);
+
+-- 插入引导阶段配置(动态配置)
+INSERT INTO onboarding_stage_config (stage, name, required_task_keys, crystal_reward, exp_reward, sort_order, is_active, created_at, updated_at) VALUES
+(1, '入门引导', ARRAY['square_home', 'profile_edit'], 0, 0, 1, true, EXTRACT(EPOCH FROM NOW())::bigint * 1000, EXTRACT(EPOCH FROM NOW())::bigint * 1000),
+(2, '进阶引导', ARRAY['starbook_add', 'exhibition_add'], 0, 0, 2, true, EXTRACT(EPOCH FROM NOW())::bigint * 1000, EXTRACT(EPOCH FROM NOW())::bigint * 1000),
+(3, '完成引导', ARRAY['exhibition_operate'], 0, 0, 3, true, EXTRACT(EPOCH FROM NOW())::bigint * 1000, EXTRACT(EPOCH FROM NOW())::bigint * 1000);
+```
+
+---
+
+## Phase 2: Python Backend (activity-admin) 任务管理模块
+
+### Task 2.1: 创建 SQLAlchemy 模型
+
+**Files:**
+- Create: `backend/models/task_models.py`
+
+- [ ] **Step 1: 创建任务相关模型**
+
+```python
+from sqlalchemy import Column, BigInteger, String, Text, Boolean, Integer, Index
+from database import Base
+
+
+class TaskDefinition(Base):
+ """任务定义表"""
+ __tablename__ = "task_definitions"
+
+ id = Column(BigInteger, primary_key=True, autoincrement=True)
+ star_id = Column(BigInteger, nullable=True)
+ task_key = Column(String(50), nullable=False)
+ task_type = Column(String(20), nullable=False)
+ name = Column(String(100), nullable=False)
+ description = Column(Text)
+ crystal_reward = Column(BigInteger, default=0)
+ exp_reward = Column(BigInteger, default=0)
+ sort_order = Column(Integer, default=0)
+ is_active = Column(Boolean, default=True)
+ created_at = Column(BigInteger)
+ updated_at = Column(BigInteger)
+
+ __table_args__ = (
+ Index('ix_task_def_star_key', 'star_id', 'task_key'),
+ )
+
+
+class UserDailyTaskProgress(Base):
+ """每日任务进度表"""
+ __tablename__ = "user_daily_task_progress"
+
+ id = Column(BigInteger, primary_key=True, autoincrement=True)
+ user_id = Column(BigInteger, nullable=False)
+ star_id = Column(BigInteger, nullable=False)
+ task_key = Column(String(50), nullable=False)
+ status = Column(String(20), default='pending')
+ completed_at = Column(BigInteger)
+ claimed_at = Column(BigInteger)
+ created_at = Column(BigInteger)
+ updated_at = Column(BigInteger)
+
+ __table_args__ = (
+ Index('ix_daily_progress_user_star_key', 'user_id', 'star_id', 'task_key', unique=True),
+ )
+
+
+class UserOnboardingProgress(Base):
+ """引导任务进度表"""
+ __tablename__ = "user_onboarding_progress"
+
+ id = Column(BigInteger, primary_key=True, autoincrement=True)
+ user_id = Column(BigInteger, nullable=False)
+ task_key = Column(String(50), nullable=False)
+ status = Column(String(20), default='pending')
+ completed_at = Column(BigInteger)
+ claimed_at = Column(BigInteger)
+ created_at = Column(BigInteger)
+ updated_at = Column(BigInteger)
+
+ __table_args__ = (
+ Index('ix_onboard_progress_user_key', 'user_id', 'task_key', unique=True),
+ )
+
+
+class UserOnboardingStatus(Base):
+ """引导流程状态表"""
+ __tablename__ = "user_onboarding_status"
+
+ user_id = Column(BigInteger, primary_key=True)
+ current_stage = Column(Integer, default=0)
+ status = Column(String(20), default='pending')
+ is_first_login_bonus_claimed = Column(Boolean, default=False)
+ has_friend_display_bonus = Column(Boolean, default=False)
+ completed_at = Column(BigInteger)
+ claimed_at = Column(BigInteger)
+ created_at = Column(BigInteger)
+ updated_at = Column(BigInteger)
+
+
+class OnboardingStageConfig(Base):
+ """引导阶段配置表"""
+ __tablename__ = "onboarding_stage_config"
+
+ id = Column(BigInteger, primary_key=True, autoincrement=True)
+ stage = Column(Integer, nullable=False)
+ name = Column(String(100), nullable=False)
+ description = Column(Text)
+ required_task_keys = Column(Text) # PostgreSQL ARRAY stored as TEXT
+ crystal_reward = Column(BigInteger, default=0)
+ exp_reward = Column(BigInteger, default=0)
+ sort_order = Column(Integer, default=0)
+ is_active = Column(Boolean, default=True)
+ created_at = Column(BigInteger)
+ updated_at = Column(BigInteger)
+
+
+class ExhibitionRevenueRecord(Base):
+ """展示收益记录表"""
+ __tablename__ = "exhibition_revenue_records"
+
+ id = Column(BigInteger, primary_key=True, autoincrement=True)
+ user_id = Column(BigInteger, nullable=False)
+ star_id = Column(BigInteger, nullable=False)
+ exhibition_id = Column(BigInteger, nullable=False)
+ asset_id = Column(BigInteger, nullable=False)
+ slot_id = Column(BigInteger, nullable=False)
+ slot_owner_uid = Column(BigInteger, nullable=False)
+ slot_type = Column(String(20), nullable=False)
+ crystal_amount = Column(BigInteger, nullable=False)
+ cycle_start_time = Column(BigInteger, nullable=False)
+ cycle_end_time = Column(BigInteger, nullable=False)
+ status = Column(String(20), default='claimable')
+ claimed_at = Column(BigInteger)
+ created_at = Column(BigInteger)
+
+ __table_args__ = (
+ Index('ix_revenue_user_star_status', 'user_id', 'star_id', 'status'),
+ Index('ix_revenue_star_status', 'star_id', 'status'),
+ )
+
+
+class TaskResetLog(Base):
+ """重置记录表"""
+ __tablename__ = "task_reset_log"
+
+ id = Column(BigInteger, primary_key=True, autoincrement=True)
+ reset_type = Column(String(20), nullable=False)
+ star_id = Column(BigInteger)
+ last_reset_at = Column(BigInteger, nullable=False)
+ created_at = Column(BigInteger)
+```
+
+- [ ] **Step 2: 修改 backend/models/__init__.py**
+
+```python
+from models.models import Activity, ActivityItem, ActivityContribution, ActivityUserStats, Star, FanProfile
+from models.task_models import (
+ TaskDefinition, UserDailyTaskProgress, UserOnboardingProgress,
+ UserOnboardingStatus, OnboardingStageConfig, ExhibitionRevenueRecord, TaskResetLog
+)
+```
+
+---
+
+### Task 2.2: 创建 Pydantic Schema
+
+**Files:**
+- Create: `backend/schemas/task_schemas.py`
+
+- [ ] **Step 1: 创建任务相关 Schema**
+
+```python
+from pydantic import BaseModel
+from typing import Optional, List
+
+
+# ========== 任务定义 ==========
+
+class TaskDefinitionBase(BaseModel):
+ star_id: Optional[int] = None
+ task_key: str
+ task_type: str
+ name: str
+ description: Optional[str] = None
+ crystal_reward: int = 0
+ exp_reward: int = 0
+ sort_order: int = 0
+ is_active: bool = True
+
+
+class TaskDefinitionCreate(TaskDefinitionBase):
+ pass
+
+
+class TaskDefinitionUpdate(BaseModel):
+ star_id: Optional[int] = None
+ task_key: Optional[str] = None
+ task_type: Optional[str] = None
+ name: Optional[str] = None
+ description: Optional[str] = None
+ crystal_reward: Optional[int] = None
+ exp_reward: Optional[int] = None
+ sort_order: Optional[int] = None
+ is_active: Optional[bool] = None
+
+
+class TaskDefinitionResponse(TaskDefinitionBase):
+ id: int
+ created_at: Optional[int] = None
+ updated_at: Optional[int] = None
+
+ class Config:
+ from_attributes = True
+
+
+class TaskDefinitionListResponse(BaseModel):
+ items: List[TaskDefinitionResponse]
+ total: int
+ page: int
+ page_size: int
+
+
+# ========== 每日任务进度 ==========
+
+class DailyTaskProgressItem(BaseModel):
+ id: int
+ user_id: int
+ star_id: int
+ task_key: str
+ status: str
+ completed_at: Optional[int] = None
+ claimed_at: Optional[int] = None
+
+ class Config:
+ from_attributes = True
+
+
+class DailyTaskProgressListResponse(BaseModel):
+ items: List[DailyTaskProgressItem]
+ total: int
+ page: int
+ page_size: int
+
+
+# ========== 引导任务进度 ==========
+
+class OnboardingProgressItem(BaseModel):
+ id: int
+ user_id: int
+ task_key: str
+ status: str
+ completed_at: Optional[int] = None
+ claimed_at: Optional[int] = None
+
+ class Config:
+ from_attributes = True
+
+
+class OnboardingProgressListResponse(BaseModel):
+ items: List[OnboardingProgressItem]
+ total: int
+ page: int
+ page_size: int
+
+
+# ========== 引导流程状态 ==========
+
+class OnboardingStatusResponse(BaseModel):
+ user_id: int
+ current_stage: int
+ status: str
+ is_first_login_bonus_claimed: bool
+ has_friend_display_bonus: bool
+ completed_at: Optional[int] = None
+ claimed_at: Optional[int] = None
+
+ class Config:
+ from_attributes = True
+
+
+# ========== 引导阶段配置 ==========
+
+class StageConfigResponse(BaseModel):
+ id: int
+ stage: int
+ name: str
+ description: Optional[str] = None
+ required_task_keys: List[str] = []
+ crystal_reward: int
+ exp_reward: int
+ sort_order: int
+ is_active: bool
+
+ class Config:
+ from_attributes = True
+
+
+# ========== 展示收益 ==========
+
+class RevenueRecordItem(BaseModel):
+ id: int
+ user_id: int
+ star_id: int
+ exhibition_id: int
+ slot_type: str
+ crystal_amount: int
+ cycle_start_time: int
+ cycle_end_time: int
+ status: str
+ claimed_at: Optional[int] = None
+
+ class Config:
+ from_attributes = True
+
+
+class RevenueListResponse(BaseModel):
+ items: List[RevenueRecordItem]
+ total: int
+ page: int
+ page_size: int
+
+
+# ========== 统计 ==========
+
+class TaskStatsByStar(BaseModel):
+ star_id: int
+ total_users: int
+ total_completed: int
+ total_claimed: int
+ completion_rate: float
+ claim_rate: float
+
+
+class DailyTaskStats(BaseModel):
+ date: str
+ new_participants: int
+ total_completions: int
+ total_claims: int
+
+
+class TaskOverviewStats(BaseModel):
+ total_definitions: int
+ active_definitions: int
+ total_daily_progress: int
+ total_onboarding_progress: int
+ total_revenue_records: int
+ claimable_revenue: int
+```
+
+---
+
+### Task 2.3: 创建 CRUD 函数
+
+**Files:**
+- Create: `backend/crud/task_crud.py`
+
+- [ ] **Step 1: 创建任务 CRUD 函数**
+
+```python
+from sqlalchemy.orm import Session
+from sqlalchemy import select, func, and_, or_, distinct, update
+from typing import List, Optional, Tuple
+from models.task_models import (
+ TaskDefinition, UserDailyTaskProgress, UserOnboardingProgress,
+ UserOnboardingStatus, OnboardingStageConfig, ExhibitionRevenueRecord, TaskResetLog
+)
+import json
+import time
+
+
+# ========== 任务定义 CRUD ==========
+
+def create_task_definition(db: Session, task: dict) -> TaskDefinition:
+ now = int(time.time() * 1000)
+ db_task = TaskDefinition(
+ star_id=task.get('star_id'),
+ task_key=task['task_key'],
+ task_type=task['task_type'],
+ name=task['name'],
+ description=task.get('description'),
+ crystal_reward=task.get('crystal_reward', 0),
+ exp_reward=task.get('exp_reward', 0),
+ sort_order=task.get('sort_order', 0),
+ is_active=task.get('is_active', True),
+ created_at=now,
+ updated_at=now,
+ )
+ db.add(db_task)
+ db.commit()
+ db.refresh(db_task)
+ return db_task
+
+
+def get_task_definitions(
+ db: Session,
+ task_type: Optional[str] = None,
+ star_id: Optional[int] = None,
+ page: int = 1,
+ page_size: int = 10
+) -> Tuple[List[TaskDefinition], int]:
+ query = select(TaskDefinition)
+ count_query = select(func.count(TaskDefinition.id))
+
+ if task_type:
+ query = query.where(TaskDefinition.task_type == task_type)
+ count_query = count_query.where(TaskDefinition.task_type == task_type)
+ if star_id is not None:
+ query = query.where(or_(TaskDefinition.star_id == star_id, TaskDefinition.star_id == None))
+ count_query = count_query.where(or_(TaskDefinition.star_id == star_id, TaskDefinition.star_id == None))
+
+ total = db.execute(count_query).scalar()
+ query = query.order_by(TaskDefinition.sort_order, TaskDefinition.id)
+ query = query.offset((page - 1) * page_size).limit(page_size)
+ items = db.execute(query).scalars().all()
+
+ return list(items), total
+
+
+def update_task_definition(db: Session, task_id: int, updates: dict) -> Optional[TaskDefinition]:
+ task = db.execute(select(TaskDefinition).where(TaskDefinition.id == task_id)).scalar_one_or_none()
+ if not task:
+ return None
+
+ for key, value in updates.items():
+ if value is not None and key != 'id':
+ setattr(task, key, value)
+ task.updated_at = int(time.time() * 1000)
+ db.commit()
+ db.refresh(task)
+ return task
+
+
+def delete_task_definition(db: Session, task_id: int) -> bool:
+ task = db.execute(select(TaskDefinition).where(TaskDefinition.id == task_id)).scalar_one_or_none()
+ if not task:
+ return False
+ db.delete(task)
+ db.commit()
+ return True
+
+
+def toggle_task_definition(db: Session, task_id: int) -> Optional[TaskDefinition]:
+ task = db.execute(select(TaskDefinition).where(TaskDefinition.id == task_id)).scalar_one_or_none()
+ if not task:
+ return None
+ task.is_active = not task.is_active
+ task.updated_at = int(time.time() * 1000)
+ db.commit()
+ db.refresh(task)
+ return task
+
+
+# ========== 每日任务进度 ==========
+
+def get_daily_progress(
+ db: Session,
+ user_id: Optional[int] = None,
+ star_id: Optional[int] = None,
+ status: Optional[str] = None,
+ page: int = 1,
+ page_size: int = 20
+) -> Tuple[List[dict], int]:
+ query = select(UserDailyTaskProgress)
+ count_query = select(func.count(UserDailyTaskProgress.id))
+
+ if user_id:
+ query = query.where(UserDailyTaskProgress.user_id == user_id)
+ count_query = count_query.where(UserDailyTaskProgress.user_id == user_id)
+ if star_id:
+ query = query.where(UserDailyTaskProgress.star_id == star_id)
+ count_query = count_query.where(UserDailyTaskProgress.star_id == star_id)
+ if status:
+ query = query.where(UserDailyTaskProgress.status == status)
+ count_query = count_query.where(UserDailyTaskProgress.status == status)
+
+ total = db.execute(count_query).scalar()
+ query = query.order_by(UserDailyTaskProgress.updated_at.desc())
+ query = query.offset((page - 1) * page_size).limit(page_size)
+ items = db.execute(query).scalars().all()
+
+ return [dict(
+ id=i.id, user_id=i.user_id, star_id=i.star_id,
+ task_key=i.task_key, status=i.status,
+ completed_at=i.completed_at, claimed_at=i.claimed_at
+ ) for i in items], total
+
+
+def reset_daily_tasks(db: Session, star_id: Optional[int] = None) -> int:
+ now = int(time.time() * 1000)
+ query = (
+ update(UserDailyTaskProgress)
+ .where(UserDailyTaskProgress.status != 'pending')
+ .values(status='pending', completed_at=None, claimed_at=None, updated_at=now)
+ )
+ if star_id:
+ query = query.where(UserDailyTaskProgress.star_id == star_id)
+
+ result = db.execute(query)
+ db.commit()
+ return result.rowcount
+
+
+# ========== 引导任务进度 ==========
+
+def get_onboarding_progress(
+ db: Session,
+ user_id: Optional[int] = None,
+ status: Optional[str] = None,
+ page: int = 1,
+ page_size: int = 20
+) -> Tuple[List[dict], int]:
+ query = select(UserOnboardingProgress)
+ count_query = select(func.count(UserOnboardingProgress.id))
+
+ if user_id:
+ query = query.where(UserOnboardingProgress.user_id == user_id)
+ count_query = count_query.where(UserOnboardingProgress.user_id == user_id)
+ if status:
+ query = query.where(UserOnboardingProgress.status == status)
+ count_query = count_query.where(UserOnboardingProgress.status == status)
+
+ total = db.execute(count_query).scalar()
+ query = query.order_by(UserOnboardingProgress.updated_at.desc())
+ query = query.offset((page - 1) * page_size).limit(page_size)
+ items = db.execute(query).scalars().all()
+
+ return [dict(
+ id=i.id, user_id=i.user_id, task_key=i.task_key,
+ status=i.status, completed_at=i.completed_at, claimed_at=i.claimed_at
+ ) for i in items], total
+
+
+def get_onboarding_status(db: Session, user_id: int) -> Optional[UserOnboardingStatus]:
+ return db.execute(
+ select(UserOnboardingStatus).where(UserOnboardingStatus.user_id == user_id)
+ ).scalar_one_or_none()
+
+
+# ========== 引导阶段配置 ==========
+
+def get_stage_configs(db: Session) -> List[OnboardingStageConfig]:
+ result = db.execute(
+ select(OnboardingStageConfig)
+ .where(OnboardingStageConfig.is_active == True)
+ .order_by(OnboardingStageConfig.sort_order)
+ )
+ return list(result.scalars().all())
+
+
+# ========== 展示收益 ==========
+
+def get_revenue_records(
+ db: Session,
+ user_id: Optional[int] = None,
+ star_id: Optional[int] = None,
+ status: Optional[str] = None,
+ page: int = 1,
+ page_size: int = 20
+) -> Tuple[List[ExhibitionRevenueRecord], int]:
+ query = select(ExhibitionRevenueRecord)
+ count_query = select(func.count(ExhibitionRevenueRecord.id))
+
+ if user_id:
+ query = query.where(ExhibitionRevenueRecord.user_id == user_id)
+ count_query = count_query.where(ExhibitionRevenueRecord.user_id == user_id)
+ if star_id:
+ query = query.where(ExhibitionRevenueRecord.star_id == star_id)
+ count_query = count_query.where(ExhibitionRevenueRecord.star_id == star_id)
+ if status:
+ query = query.where(ExhibitionRevenueRecord.status == status)
+ count_query = count_query.where(ExhibitionRevenueRecord.status == status)
+
+ total = db.execute(count_query).scalar()
+ query = query.order_by(ExhibitionRevenueRecord.created_at.desc())
+ query = query.offset((page - 1) * page_size).limit(page_size)
+ items = db.execute(query).scalars().all()
+
+ return list(items), total
+
+
+# ========== 统计 ==========
+
+def get_task_overview_stats(db: Session) -> dict:
+ total_defs = db.execute(select(func.count(TaskDefinition.id))).scalar() or 0
+ active_defs = db.execute(select(func.count(TaskDefinition.id)).where(TaskDefinition.is_active == True)).scalar() or 0
+ total_daily = db.execute(select(func.count(UserDailyTaskProgress.id))).scalar() or 0
+ total_onboard = db.execute(select(func.count(UserOnboardingProgress.id))).scalar() or 0
+ total_revenue = db.execute(select(func.count(ExhibitionRevenueRecord.id))).scalar() or 0
+ claimable = db.execute(
+ select(func.count(ExhibitionRevenueRecord.id))
+ .where(ExhibitionRevenueRecord.status == 'claimable')
+ ).scalar() or 0
+
+ return {
+ "total_definitions": total_defs,
+ "active_definitions": active_defs,
+ "total_daily_progress": total_daily,
+ "total_onboarding_progress": total_onboard,
+ "total_revenue_records": total_revenue,
+ "claimable_revenue": claimable,
+ }
+
+
+def get_task_stats_by_star(db: Session, star_id: int) -> dict:
+ completed = db.execute(
+ select(func.count(UserDailyTaskProgress.id))
+ .where(and_(
+ UserDailyTaskProgress.star_id == star_id,
+ UserDailyTaskProgress.status.in_(['completed', 'claimed'])
+ ))
+ ).scalar() or 0
+
+ claimed = db.execute(
+ select(func.count(UserDailyTaskProgress.id))
+ .where(and_(
+ UserDailyTaskProgress.star_id == star_id,
+ UserDailyTaskProgress.status == 'claimed'
+ ))
+ ).scalar() or 0
+
+ total_users = db.execute(
+ select(func.count(distinct(UserDailyTaskProgress.user_id)))
+ .where(UserDailyTaskProgress.star_id == star_id)
+ ).scalar() or 0
+
+ total_progress = db.execute(
+ select(func.count(UserDailyTaskProgress.id))
+ .where(UserDailyTaskProgress.star_id == star_id)
+ ).scalar() or 0
+
+ return {
+ "star_id": star_id,
+ "total_users": total_users,
+ "total_completed": completed,
+ "total_claimed": claimed,
+ "completion_rate": round(completed / total_progress * 100, 2) if total_progress > 0 else 0,
+ "claim_rate": round(claimed / total_progress * 100, 2) if total_progress > 0 else 0,
+ }
+
+
+def get_daily_task_trend(db: Session, star_id: Optional[int] = None, days: int = 7) -> List[dict]:
+ # 简化实现,返回近7天每日统计
+ trends = []
+ for i in range(days - 1, -1, -1):
+ day_ts = int(time.time() * 1000) - i * 86400000
+ date = time.strftime("%Y-%m-%d", time.localtime(day_ts / 1000))
+ trends.append({
+ "date": date,
+ "new_participants": 0,
+ "total_completions": 0,
+ "total_claims": 0,
+ })
+ return trends
+```
+
+---
+
+### Task 2.4: 创建 API Handler
+
+**Files:**
+- Create: `backend/handlers/task_handler.py`
+
+- [ ] **Step 1: 创建任务 Handler**
+
+```python
+from fastapi import APIRouter, Depends, HTTPException, Query
+from sqlalchemy.orm import Session
+from database import get_db
+from schemas.task_schemas import (
+ TaskDefinitionCreate, TaskDefinitionUpdate, TaskDefinitionResponse,
+ TaskDefinitionListResponse, DailyTaskProgressListResponse,
+ OnboardingProgressListResponse, OnboardingStatusResponse,
+ RevenueListResponse, TaskStatsByStar, TaskOverviewStats
+)
+from crud.task_crud import (
+ create_task_definition, get_task_definitions, update_task_definition,
+ delete_task_definition, toggle_task_definition,
+ get_daily_progress, get_onboarding_progress, get_onboarding_status,
+ reset_daily_tasks, get_task_stats_by_star, get_revenue_records,
+ get_task_overview_stats, get_daily_task_trend, get_stage_configs
+)
+from middleware.auth import verify_token
+from typing import Optional
+
+router = APIRouter(prefix="/api/admin/tasks", tags=["任务管理"])
+
+
+# ========== 任务定义 CRUD ==========
+
+@router.post("/definitions", response_model=TaskDefinitionResponse)
+async def create_task(
+ task: TaskDefinitionCreate,
+ db: Session = Depends(get_db),
+ _: dict = Depends(verify_token)
+):
+ db_task = create_task_definition(db, task.model_dump())
+ return db_task
+
+
+@router.get("/definitions", response_model=TaskDefinitionListResponse)
+async def list_tasks(
+ task_type: Optional[str] = Query(None),
+ star_id: Optional[int] = Query(None),
+ page: int = Query(1, ge=1),
+ page_size: int = Query(10, ge=1, le=100),
+ db: Session = Depends(get_db),
+ _: dict = Depends(verify_token)
+):
+ items, total = get_task_definitions(db, task_type, star_id, page, page_size)
+ return TaskDefinitionListResponse(
+ items=[TaskDefinitionResponse.model_validate(t) for t in items],
+ total=total, page=page, page_size=page_size
+ )
+
+
+@router.put("/definitions/{task_id}", response_model=TaskDefinitionResponse)
+async def update_task(
+ task_id: int,
+ updates: TaskDefinitionUpdate,
+ db: Session = Depends(get_db),
+ _: dict = Depends(verify_token)
+):
+ task = update_task_definition(db, task_id, updates.model_dump(exclude_unset=True))
+ if not task:
+ raise HTTPException(status_code=404, detail="Task not found")
+ return task
+
+
+@router.delete("/definitions/{task_id}")
+async def delete_task(
+ task_id: int,
+ db: Session = Depends(get_db),
+ _: dict = Depends(verify_token)
+):
+ success = delete_task_definition(db, task_id)
+ if not success:
+ raise HTTPException(status_code=404, detail="Task not found")
+ return {"message": "Task deleted successfully"}
+
+
+@router.post("/definitions/{task_id}/toggle", response_model=TaskDefinitionResponse)
+async def toggle_task(
+ task_id: int,
+ db: Session = Depends(get_db),
+ _: dict = Depends(verify_token)
+):
+ task = toggle_task_definition(db, task_id)
+ if not task:
+ raise HTTPException(status_code=404, detail="Task not found")
+ return task
+
+
+# ========== 任务进度 ==========
+
+@router.get("/daily/progress", response_model=DailyTaskProgressListResponse)
+async def list_daily_progress(
+ user_id: Optional[int] = Query(None),
+ star_id: Optional[int] = Query(None),
+ status: Optional[str] = Query(None),
+ page: int = Query(1, ge=1),
+ page_size: int = Query(20, ge=1, le=100),
+ db: Session = Depends(get_db),
+ _: dict = Depends(verify_token)
+):
+ items, total = get_daily_progress(db, user_id, star_id, status, page, page_size)
+ return DailyTaskProgressListResponse(items=items, total=total, page=page, page_size=page_size)
+
+
+@router.get("/onboarding/progress", response_model=OnboardingProgressListResponse)
+async def list_onboarding_progress(
+ user_id: Optional[int] = Query(None),
+ status: Optional[str] = Query(None),
+ page: int = Query(1, ge=1),
+ page_size: int = Query(20, ge=1, le=100),
+ db: Session = Depends(get_db),
+ _: dict = Depends(verify_token)
+):
+ items, total = get_onboarding_progress(db, user_id, status, page, page_size)
+ return OnboardingProgressListResponse(items=items, total=total, page=page, page_size=page_size)
+
+
+@router.get("/onboarding/status/{user_id}", response_model=OnboardingStatusResponse)
+async def get_onboarding_status(
+ user_id: int,
+ db: Session = Depends(get_db),
+ _: dict = Depends(verify_token)
+):
+ status = get_onboarding_status(db, user_id)
+ if not status:
+ raise HTTPException(status_code=404, detail="Onboarding status not found")
+ return status
+
+
+@router.get("/onboarding/stages")
+async def list_stages(
+ db: Session = Depends(get_db),
+ _: dict = Depends(verify_token)
+):
+ stages = get_stage_configs(db)
+ return {"stages": stages}
+
+
+# ========== 手动操作 ==========
+
+@router.post("/reset/daily")
+async def reset_daily(
+ star_id: Optional[int] = Query(None),
+ db: Session = Depends(get_db),
+ _: dict = Depends(verify_token)
+):
+ count = reset_daily_tasks(db, star_id)
+ return {"message": f"Reset {count} daily task records", "affected": count}
+
+
+# ========== 统计 ==========
+
+@router.get("/stats/overview", response_model=TaskOverviewStats)
+async def get_overview(
+ db: Session = Depends(get_db),
+ _: dict = Depends(verify_token)
+):
+ return get_task_overview_stats(db)
+
+
+@router.get("/stats/star/{star_id}", response_model=TaskStatsByStar)
+async def get_star_stats(
+ star_id: int,
+ db: Session = Depends(get_db),
+ _: dict = Depends(verify_token)
+):
+ return get_task_stats_by_star(db, star_id)
+
+
+@router.get("/stats/daily-trend")
+async def get_daily_trend(
+ star_id: Optional[int] = Query(None),
+ days: int = Query(7, ge=1, le=30),
+ db: Session = Depends(get_db),
+ _: dict = Depends(verify_token)
+):
+ trends = get_daily_task_trend(db, star_id, days)
+ return {"trends": trends}
+
+
+# ========== 展示收益 ==========
+
+@router.get("/revenue", response_model=RevenueListResponse)
+async def list_revenue(
+ user_id: Optional[int] = Query(None),
+ star_id: Optional[int] = Query(None),
+ status: Optional[str] = Query(None),
+ page: int = Query(1, ge=1),
+ page_size: int = Query(20, ge=1, le=100),
+ db: Session = Depends(get_db),
+ _: dict = Depends(verify_token)
+):
+ items, total = get_revenue_records(db, user_id, star_id, status, page, page_size)
+ return RevenueListResponse(items=items, total=total, page=page, page_size=page_size)
+```
+
+- [ ] **Step 2: 修改 backend/router/__init__.py**
+
+```python
+from fastapi import APIRouter
+from handlers import auth, activity, star, asset, task_handler
+
+api_router = APIRouter()
+
+api_router.include_router(auth.router)
+api_router.include_router(activity.router)
+api_router.include_router(star.router)
+api_router.include_router(asset.router)
+api_router.include_router(task_handler.router) # 新增
+```
+
+---
+
+## Phase 3: Vue Frontend (activity-admin) 任务管理页面
+
+### Task 3.1: 任务定义列表页
+
+**Files:**
+- Create: `frontend/src/views/TaskDefinitionList.vue`
+
+- [ ] **Step 1: 创建任务定义列表页面**
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+
+
+
+ {{ row.task_type === 'daily' ? '每日' : '引导' }}
+
+
+
+
+
+
+
+
+ 💎 {{ row.crystal_reward }} | ✨ {{ row.exp_reward }}
+
+
+
+
+
+
+ {{ row.is_active ? '启用' : '禁用' }}
+
+
+
+
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 保存
+
+
+
+
+
+
+```
+
+---
+
+### Task 3.2: 任务进度查询页
+
+**Files:**
+- Create: `frontend/src/views/TaskProgress.vue`
+
+- [ ] **Step 1: 创建任务进度查询页面**
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+
+
+
+
+
+
+
+
+
+
+ {{ row.status }}
+
+
+
+
+ {{ formatTime(row.completed_at) }}
+
+
+
+
+ {{ formatTime(row.claimed_at) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+
+
+
+
+
+
+
+
+
+ {{ row.status }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+### Task 3.3: 任务统计页
+
+**Files:**
+- Create: `frontend/src/views/TaskStats.vue`
+
+- [ ] **Step 1: 创建任务统计页面**
+
+```vue
+
+
+
+
+
+
+
+
+
{{ stats.total_definitions }}
+
任务定义总数
+
+
+
+
+
+
+
{{ stats.active_definitions }}
+
启用任务数
+
+
+
+
+
+
+
{{ stats.total_daily_progress }}
+
每日任务进度记录
+
+
+
+
+
+
+
{{ stats.claimable_revenue }}
+
待领取收益
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+### Task 3.4: 手动重置页
+
+**Files:**
+- Create: `frontend/src/views/TaskReset.vue`
+
+- [ ] **Step 1: 创建手动重置页面**
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 执行重置
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+### Task 3.5: 展示收益页
+
+**Files:**
+- Create: `frontend/src/views/RevenueList.vue`
+
+- [ ] **Step 1: 创建展示收益页面**
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+
+
+
+
+
+
+
+
+
+
+ {{ row.slot_type === 'own' ? '自有' : '好友' }}
+
+
+
+
+
+ 💎 {{ row.crystal_amount }}
+
+
+
+
+ {{ formatTime(row.cycle_start_time) }}
+
+
+
+
+ {{ formatTime(row.cycle_end_time) }}
+
+
+
+
+
+ {{ row.status === 'claimable' ? '可领取' : '已领取' }}
+
+
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+### Task 3.6: 更新 API 和路由
+
+**Files:**
+- Modify: `frontend/src/api/admin.js`
+
+- [ ] **Step 1: 添加任务相关 API**
+
+```javascript
+// 任务定义
+export function getTaskDefinitions(params) {
+ return request.get('/tasks/definitions', { params })
+}
+
+export function createTask(data) {
+ return request.post('/tasks/definitions', data)
+}
+
+export function updateTask(id, data) {
+ return request.put(`/tasks/definitions/${id}`, data)
+}
+
+export function deleteTask(id) {
+ return request.delete(`/tasks/definitions/${id}`)
+}
+
+// 任务进度
+export function getDailyProgress(params) {
+ return request.get('/tasks/daily/progress', { params })
+}
+
+export function getOnboardingProgress(params) {
+ return request.get('/tasks/onboarding/progress', { params })
+}
+
+export function getOnboardingStatus(userId) {
+ return request.get(`/tasks/onboarding/status/${userId}`)
+}
+
+export function getOnboardingStages() {
+ return request.get('/tasks/onboarding/stages')
+}
+
+// 手动重置
+export function resetDailyTasks(starId) {
+ return request.post('/tasks/reset/daily', null, { params: { star_id: starId } })
+}
+
+// 统计
+export function getOverviewStats() {
+ return request.get('/tasks/stats/overview')
+}
+
+export function getDailyTrend(params) {
+ return request.get('/tasks/stats/daily-trend', { params })
+}
+
+export function getStarStats(starId) {
+ return request.get(`/tasks/stats/star/${starId}`)
+}
+
+// 展示收益
+export function getRevenueRecords(params) {
+ return request.get('/tasks/revenue', { params })
+}
+```
+
+- [ ] **Step 2: 修改 frontend/src/router/index.js**
+
+```javascript
+{
+ path: '/tasks',
+ component: Layout,
+ children: [
+ { path: '', redirect: '/tasks/definitions' },
+ { path: 'definitions', component: () => import('@/views/TaskDefinitionList.vue') },
+ { path: 'progress', component: () => import('@/views/TaskProgress.vue') },
+ { path: 'stats', component: () => import('@/views/TaskStats.vue') },
+ { path: 'reset', component: () => import('@/views/TaskReset.vue') },
+ { path: 'revenue', component: () => import('@/views/RevenueList.vue') },
+ ]
+}
+```
+
+---
+
+## Phase 4: Go taskService 实现
+
+### Task 4.1: 创建 Proto 定义
+
+**Files:**
+- Create: `backend/proto/task.proto`
+
+- [ ] **Step 1: 创建 task.proto**
+
+```protobuf
+syntax = "proto3";
+
+package topfans.task;
+
+option go_package = "github.com/topfans/backend/pkg/proto/task;task";
+
+import "proto/common.proto";
+
+// ========== 内部 RPC ==========
+
+// 展品到期完成事件参数
+message OnExhibitionCompletedParams {
+ int64 exhibition_id = 1;
+ int64 asset_id = 2;
+ int64 slot_id = 3;
+ int64 occupier_uid = 4; // 展出者 UID
+ int64 occupier_star_id = 5; // 展出者明星 ID
+ int64 slot_owner_uid = 6; // 槽位拥有者 UID(应收水晶的人)
+ int64 start_time = 7; // 开始时间(毫秒)
+ int64 expire_at = 8; // 过期时间(毫秒)
+}
+
+// 任务事件上报参数
+message ReportTaskEventParams {
+ int64 user_id = 1;
+ int64 star_id = 2;
+ string event_type = 3; // daily_login, daily_browse_asset, daily_mint, daily_place_asset
+ int64 timestamp = 4;
+}
+
+// 初始化用户任务数据
+message InitUserTasksRequest {
+ int64 user_id = 1;
+}
+
+message InitUserTasksResponse {
+ topfans.common.BaseResponse base = 1;
+ bool success = 2;
+}
+
+// ========== 内部 RPC Service ==========
+
+service TaskInternalService {
+ // 初始化用户任务数据(注册时调用)
+ rpc InitUserTasks(InitUserTasksRequest) returns (InitUserTasksResponse);
+
+ // 接收任务事件上报(其他服务调用)
+ rpc ReportTaskEvent(ReportTaskEventRequest) returns (ReportTaskEventResponse);
+
+ // 展品到期完成(galleryService 调用)
+ rpc OnExhibitionCompleted(OnExhibitionCompletedRequest) returns (OnExhibitionCompletedResponse);
+}
+
+message ReportTaskEventRequest {
+ int64 user_id = 1;
+ int64 star_id = 2;
+ string event_type = 3;
+ int64 timestamp = 4;
+}
+
+message ReportTaskEventResponse {
+ topfans.common.BaseResponse base = 1;
+ bool task_completed = 2; // 任务是否完成
+ string task_key = 3;
+}
+
+message OnExhibitionCompletedRequest {
+ OnExhibitionCompletedParams params = 1;
+}
+
+message OnExhibitionCompletedResponse {
+ topfans.common.BaseResponse base = 1;
+ int64 revenue_record_id = 2; // 创建的收益记录 ID
+}
+```
+
+- [ ] **Step 2: 重新生成 proto 代码**
+
+Run: `cd backend && protoc --go_out=. --go_opt=paths=source_relative proto/task.proto`
+
+---
+
+### Task 4.2: 创建 Go taskService 结构
+
+**Files:**
+- Create: `backend/services/taskService/main.go`
+- Create: `backend/services/taskService/config/task_config.go`
+- Create: `backend/services/taskService/repository/task_repository.go`
+- Create: `backend/services/taskService/service/daily_task_service.go`
+- Create: `backend/services/taskService/service/exhibition_revenue_service.go`
+- Create: `backend/services/taskService/service/onboarding_service.go`
+- Create: `backend/services/taskService/provider/task_provider.go`
+- Create: `backend/services/taskService/provider/task_internal_provider.go`
+- Create: `backend/services/taskService/worker/daily_reset_worker.go`
+- Create: `backend/services/taskService/client/user_rpc_client.go`
+
+- [ ] **Step 1: 创建 main.go**
+
+```go
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "os/signal"
+ "strconv"
+ "syscall"
+
+ dubboclient "dubbo.apache.org/dubbo-go/v3/client"
+ _ "dubbo.apache.org/dubbo-go/v3/imports"
+ "dubbo.apache.org/dubbo-go/v3/protocol"
+ "dubbo.apache.org/dubbo-go/v3/server"
+
+ "github.com/topfans/backend/pkg/database"
+ "github.com/topfans/backend/pkg/logger"
+ pbTask "github.com/topfans/backend/pkg/proto/task"
+ pbUser "github.com/topfans/backend/pkg/proto/user"
+ "github.com/topfans/backend/services/taskService/client"
+ "github.com/topfans/backend/services/taskService/config"
+ "github.com/topfans/backend/services/taskService/provider"
+ "github.com/topfans/backend/services/taskService/repository"
+ "github.com/topfans/backend/services/taskService/service"
+ "github.com/topfans/backend/services/taskService/worker"
+)
+
+var (
+ port = flag.Int("port", getEnvInt("PORT", 20005), "Dubbo service port")
+ dbHost = flag.String("db-host", getEnv("DB_HOST", "localhost"), "Database host")
+ dbPort = flag.Int("db-port", getEnvInt("DB_PORT", 5432), "Database port")
+ dbUser = flag.String("db-user", getEnv("DB_USER", "postgres"), "Database user")
+ dbPassword = flag.String("db-password", getEnv("DB_PASSWORD", ""), "Database password")
+ dbName = flag.String("db-name", getEnv("DB_NAME", "top-fans"), "Database name")
+ userServiceURL = flag.String("user-service-url", getEnv("USER_SERVICE_URL", "tri://localhost:20000"), "User service URL")
+)
+
+func getEnv(key, fallback string) string {
+ if v := os.Getenv(key); v != "" {
+ return v
+ }
+ return fallback
+}
+
+func getEnvInt(key string, fallback int) int {
+ if v := os.Getenv(key); v != "" {
+ if n, err := strconv.Atoi(v); err == nil {
+ return n
+ }
+ }
+ return fallback
+}
+
+func main() {
+ flag.Parse()
+
+ env := os.Getenv("ENV")
+ if env == "" {
+ env = "development"
+ }
+
+ if err := logger.Init(logger.Config{
+ ServiceName: "task-service",
+ Environment: env,
+ LogLevel: os.Getenv("LOG_LEVEL"),
+ }); err != nil {
+ panic(fmt.Sprintf("Failed to initialize logger: %v", err))
+ }
+ defer logger.Sync()
+
+ logger.Logger.Info("Starting Task Service...")
+
+ // 初始化数据库
+ dbConfig := database.Config{
+ Host: *dbHost,
+ Port: *dbPort,
+ User: *dbUser,
+ Password: *dbPassword,
+ DBName: *dbName,
+ SSLMode: "disable",
+ TimeZone: "Asia/Shanghai",
+ }
+
+ if err := database.Init(dbConfig); err != nil {
+ logger.Logger.Fatal(fmt.Sprintf("Failed to initialize database: %v", err))
+ }
+ logger.Logger.Info("Database initialized successfully")
+
+ // 创建 Repository
+ db := database.GetDB()
+ taskRepo := repository.NewTaskRepository(db)
+ logger.Logger.Info("Repository layer initialized")
+
+ // 创建 User Service RPC 客户端
+ userCli, err := dubboclient.NewClient(
+ dubboclient.WithClientURL(*userServiceURL),
+ )
+ if err != nil {
+ logger.Logger.Fatal(fmt.Sprintf("Failed to create User Service Dubbo client: %v", err))
+ }
+
+ userServiceClient, err := pbUser.NewUserSocialService(userCli)
+ if err != nil {
+ logger.Logger.Fatal(fmt.Sprintf("Failed to create User Service RPC client: %v", err))
+ }
+ userRPCClient := client.NewUserRPCClient(userServiceClient)
+ logger.Logger.Info("User Service RPC client initialized")
+
+ // 创建 Service 层
+ dailyTaskService := service.NewDailyTaskService(taskRepo, userRPCClient)
+ exhibitionRevenueService := service.NewExhibitionRevenueService(taskRepo, userRPCClient)
+ onboardingService := service.NewOnboardingService(taskRepo, userRPCClient)
+ logger.Logger.Info("Service layer initialized")
+
+ // 创建 Provider
+ taskProvider := provider.NewTaskProvider(dailyTaskService, exhibitionRevenueService, onboardingService)
+ taskInternalProvider := provider.NewTaskInternalProvider(dailyTaskService, exhibitionRevenueService, onboardingService)
+ logger.Logger.Info("Provider layer initialized")
+
+ // 创建并启动 DailyResetWorker
+ resetWorker := worker.NewDailyResetWorker(taskRepo, config.GetDailyResetTime())
+ go resetWorker.Start()
+ logger.Logger.Info("Daily reset worker started")
+
+ // 创建 Dubbo 服务器
+ srv, err := server.NewServer(
+ server.WithServerProtocol(
+ protocol.WithPort(*port),
+ protocol.WithTriple(),
+ ),
+ )
+ if err != nil {
+ logger.Logger.Fatal(fmt.Sprintf("Failed to create Dubbo server: %v", err))
+ }
+
+ // 注册服务
+ if err := pbTask.RegisterTaskInternalServiceHandler(srv, taskInternalProvider); err != nil {
+ logger.Logger.Fatal(fmt.Sprintf("Failed to register TaskInternalService: %v", err))
+ }
+
+ // 启动服务
+ if err := srv.Serve(); err != nil {
+ logger.Logger.Fatal(fmt.Sprintf("Failed to start Task Service: %v", err))
+ }
+
+ logger.Logger.Info(fmt.Sprintf("Task Service started successfully on port %d", *port))
+
+ // 等待退出信号
+ quit := make(chan os.Signal, 1)
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
+ <-quit
+
+ logger.Logger.Info("Shutting down Task Service...")
+ resetWorker.Stop()
+ logger.Logger.Info("Task Service shutdown complete")
+}
+```
+
+---
+
+## Phase 5: userService AddExperience RPC
+
+### Task 5.1: 修改 proto/user.proto
+
+**Files:**
+- Modify: `backend/proto/user.proto`
+
+- [ ] **Step 1: 添加 AddExperience 消息和 RPC**
+
+```protobuf
+// 更新经验值请求(内部RPC调用,用于taskService发放经验奖励)
+message AddExperienceRequest {
+ int64 user_id = 1;
+ int64 star_id = 2;
+ int64 delta = 3;
+}
+
+// 更新经验值响应
+message AddExperienceResponse {
+ topfans.common.BaseResponse base = 1;
+ int64 new_experience = 2;
+}
+
+// 在 service UserSocialService {} 中添加:
+// 内部RPC:更新经验值(仅供taskService调用)
+rpc AddExperience(AddExperienceRequest) returns (AddExperienceResponse);
+```
+
+- [ ] **Step 2: 重新生成 proto 代码**
+
+Run: `cd backend && protoc --go_out=. --go_opt=paths=source_relative proto/user.proto`
+
+---
+
+### Task 5.2: 修改 repository
+
+**Files:**
+- Modify: `backend/services/userService/repository/fan_profile_repository.go`
+
+- [ ] **Step 1: 添加 UpdateExperience 方法**
+
+```go
+// FanProfileRepository 接口添加:
+UpdateExperience(userID, starID int64, delta int64) (int64, error)
+
+// 实现:
+func (r *fanProfileRepository) UpdateExperience(userID, starID int64, delta int64) (int64, error) {
+ if userID <= 0 {
+ return 0, errors.New("user_id must be greater than 0")
+ }
+
+ var profile models.FanProfile
+ if err := r.db.Where("user_id = ? AND star_id = ?", userID, starID).First(&profile).Error; err != nil {
+ return 0, err
+ }
+
+ profile.Experience += delta
+ if err := r.db.Save(&profile).Error; err != nil {
+ return 0, err
+ }
+
+ return profile.Experience, nil
+}
+```
+
+---
+
+### Task 5.3: 修改 service
+
+**Files:**
+- Modify: `backend/services/userService/service/user_service.go`
+
+- [ ] **Step 1: 添加 AddExperience 方法**
+
+```go
+// UserService 接口添加:
+AddExperience(req *pb.AddExperienceRequest) (*pb.AddExperienceResponse, error)
+
+// 实现:
+func (s *userService) AddExperience(req *pb.AddExperienceRequest) (*pb.AddExperienceResponse, error) {
+ if !validator.ValidateUserID(req.UserId) || !validator.ValidateStarID(req.StarId) {
+ return &pb.AddExperienceResponse{
+ Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidUserID),
+ }, nil
+ }
+
+ newExp, err := s.fanProfileRepo.UpdateExperience(req.UserId, req.StarId, req.Delta)
+ if err != nil {
+ logger.Logger.Error("Failed to update experience", zap.Error(err))
+ return &pb.AddExperienceResponse{
+ Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer),
+ }, nil
+ }
+
+ return &pb.AddExperienceResponse{
+ Base: &pbCommon.BaseResponse{
+ Code: pbCommon.StatusCode_STATUS_OK,
+ Message: "",
+ },
+ NewExperience: newExp,
+ }, nil
+}
+```
+
+---
+
+### Task 5.4: 修改 provider
+
+**Files:**
+- Modify: `backend/services/userService/provider/user_provider.go`
+
+- [ ] **Step 1: 添加 AddExperience Provider 方法**
+
+```go
+// AddExperience 更新经验值(内部RPC调用)
+func (p *UserProvider) AddExperience(ctx context.Context, req *pb.AddExperienceRequest) (*pb.AddExperienceResponse, error) {
+ logger.Logger.Info("Received AddExperience request",
+ zap.Int64("user_id", req.UserId),
+ zap.Int64("star_id", req.StarId),
+ zap.Int64("delta", req.Delta),
+ )
+
+ resp, err := p.userService.AddExperience(req)
+ if err != nil {
+ logger.Logger.Error("AddExperience failed", zap.Error(err))
+ }
+
+ return resp, err
+}
+```
+
+- [ ] **Step 2: 修改 unified_provider.go**
+
+```go
+// 在 UnifiedProvider 中添加:
+func (p *UnifiedProvider) AddExperience(ctx context.Context, req *pb.AddExperienceRequest) (*pb.AddExperienceResponse, error) {
+ return p.userProvider.AddExperience(ctx, req)
+}
+```
+
+---
+
+## Phase 6: 前端 API 适配(移动端)
+
+### Task 6.1: 更新前端 API 调用
+
+**Files:**
+- Create/Modify: `frontend/src/api/task.js`(移动端任务 API)
+
+- [ ] **Step 1: 创建移动端任务 API**
+
+```javascript
+// 每日任务
+export function getDailyTasks(starId) {
+ return request.get('/tasks/daily', { params: { star_id: starId } })
+}
+
+export function claimTaskReward(taskKey, starId) {
+ return request.post('/tasks/claim', { task_key: taskKey, star_id: starId })
+}
+
+// 引导任务
+export function getOnboardingStages() {
+ return request.get('/tasks/onboarding/stages')
+}
+
+export function getOnboardingStatus() {
+ return request.get('/tasks/onboarding/status')
+}
+
+export function completeOnboarding(taskKey) {
+ return request.post('/tasks/onboarding/complete', { task_key: taskKey })
+}
+
+export function claimOnboardingReward(taskKey) {
+ return request.post('/tasks/onboarding/claim', { task_key: taskKey })
+}
+
+// 展示收益
+export function getExhibitionRevenue(starId, status, page, pageSize) {
+ return request.get('/tasks/exhibition-revenue', {
+ params: { star_id: starId, status, page, page_size: pageSize }
+ })
+}
+
+export function claimExhibitionRevenue(revenueId) {
+ return request.post('/tasks/exhibition-revenue/claim', { revenue_id: revenueId })
+}
+
+export function claimAllExhibitionRevenue() {
+ return request.post('/tasks/exhibition-revenue/claim-all')
+}
+
+// 等级
+export function getLevelInfo() {
+ return request.get('/tasks/level')
+}
+
+// 初始化
+export function initUserTasks() {
+ return request.post('/tasks/init')
+}
+```
+
+---
+
+## 实现检查点
+
+完成所有任务后,请验证以下功能:
+
+1. **数据库**:所有表已创建,默认数据已插入
+2. **activity-admin**:
+ - [ ] 任务定义列表页面可访问
+ - [ ] 新建/编辑/删除任务定义正常
+ - [ ] 任务进度查询正常
+ - [ ] 任务统计页面显示图表
+ - [ ] 手动重置功能正常
+ - [ ] 展示收益记录查询正常
+3. **Go taskService**:
+ - [ ] 服务启动正常
+ - [ ] 每日重置 Worker 运行正常(5:00 Asia/Shanghai)
+ - [ ] 内部 RPC 可被 galleryService 调用
+4. **userService**:
+ - [ ] AddExperience RPC 可正常调用
+5. **移动端**:
+ - [ ] 每日任务列表显示正常
+ - [ ] 任务完成上报正常
+ - [ ] 奖励领取正常
+ - [ ] 展示收益显示和领取正常
diff --git a/铸爱创作模块PRD.md b/铸爱创作模块PRD.md
deleted file mode 100644
index e33d47e..0000000
--- a/铸爱创作模块PRD.md
+++ /dev/null
@@ -1,502 +0,0 @@
-# 铸爱创作模块 产品需求文档(PRD)
-
-> **文档版本:** V1.0
-> **创建日期:** 2026-04-09
-> **文档状态:** 初稿
-
----
-
-## 1. 产品概述
-
-铸爱创作模块是一个基于 AI 技术的创作平台。平台提供粉丝需要的创作方向(如小卡、吧唧、海报),用户借助 AI 图生图能力进行二次创作,每份作品自动生成全球唯一的「身份证编号(上链)」,用于确权、追溯和防抄袭维权。
-
-**产品核心价值:** 降低创作门槛,让用户轻松完成 AI 创作,同时通过上链确权保护原创权益。
-
-**目标用户:** 对铸爱内容感兴趣的消费者与创作者、需要明星合照二创的用户、重视版权保护的创作者。
-
----
-
-## 2. 产品亮点
-
-| 亮点 | 说明 |
-|------|------|
-| AI 图生图 | 基于精选素材(星卡/吧唧/海报),AI 自动生成独一无二的作品 |
-| 唯一身份证编号 | 每份藏品上链,确权可追溯,防抄袭维权 |
-| 双路径设计 | 铸造创作与藏品浏览路径互不干扰,满足不同用户需求 |
-| 低门槛创作 | 用户只需选择素材、输入描述词即可完成 AI 创作,无需专业技能 |
-
----
-
-## 3. 产品整体介绍
-
-### 3.1 页面结构
-
-```
-┌─────────────────────────────────────┐
-│ 顶部运营轮播图 │ ← 区域一
-│ [图片1] [图片2] [图片3] ● ○ ○ │
-└─────────────────────────────────────┘
-┌─────────────────────────────────────┐
-│ [星卡] [吧唧] [海报] │ ← 区域二:主Tab
-└─────────────────────────────────────┘
-┌─────────────────────────────────────┐
-│ [热门作品] [最新作品] [星卡] [吧唧] [海报] │ ← 区域三:分类标签
-└─────────────────────────────────────┘
-┌─────────────────────────────────────┐
-│ ┌─────┐ ┌─────┐ ┌─────┐ │
-│ │ │ │ │ │ │ │
-│ │ IMG │ │ IMG │ │ IMG │ │
-│ │#ID │ │#ID │ │#ID │ │
-│ └─────┘ └─────┘ └─────┘ │ ← 区域四:创作网格
-│ ┌─────┐ ┌─────┐ ┌─────┐ │
-│ │ │ │ │ │ │ │
-│ │ IMG │ │ IMG │ │ IMG │ │
-│ │#ID │ │#ID │ │#ID │ │
-│ └─────┘ └─────┘ └─────┘ │
-└─────────────────────────────────────┘
-```
-
-### 3.2 四大区域概览
-
-| 区域 | 核心功能 |
-|------|----------|
-| 顶部运营轮播图 | 运营位,后台可配置,点击跳转活动/话题/H5 |
-| 主Tab(星卡/吧唧/海报) | 点击直接进入对应类型的 AI 铸造流程 |
-| 分类标签(热门/最新/各分类) | 筛选用户已铸造的藏品列表 |
-| 创作网格列表 | 双列瀑布流展示藏品,含身份证编号、创作者、点赞等 |
-
-### 3.3 铸造页面结构
-
-#### 3.3.1 星卡铸造页面
-
-```
-┌─────────────────────────────────────┐
-│ ← 返回 │
-└─────────────────────────────────────┘
-┌─────────────────────────────────────┐
-│ ┌─────────────────────────────────┐│
-│ │ ││
-│ │ ││
-│ │ 点击选择相册图片 ││ ← 素材预览区(单图,1:1或3:4)
-│ │ ││
-│ └─────────────────────────────────┘│
-└─────────────────────────────────────┘
-┌─────────────────────────────────────┐
-│ 藏品信息 │
-│ ┌─────────────────────────────────┐│
-│ │ 请输入描述词或上传参考图... ││ ← 藏品描述/参考输入
-│ └─────────────────────────────────┘│
-│ │
-│ 跳过 │ ← 跳过
-└─────────────────────────────────────┘
-┌─────────────────────────────────────┐
-│ 输入描述词 │
-│ ┌─────────────────────────────────┐│
-│ │ 请输入描述词,引导AI创作... 发送 ││ ← Prompt输入框(发送即开始创作)
-│ └─────────────────────────────────┘│
-└─────────────────────────────────────┘
-```
-
-**页面说明:** 点击主Tab后进入铸造页面,选择素材 → 输入描述词 → 发送 → 进入AI处理中页面
-
----
-
-#### 3.3.2 吧唧铸造页面
-
-```
-┌─────────────────────────────────────┐
-│ ← 返回 │
-└─────────────────────────────────────┘
-┌─────────────────────────────────────┐
-│ ┌─────────────────────────────────┐│
-│ │ ││
-│ │ ⚙️ 圆形/异形 ││ ← 素材预览区(吧唧为圆形或异形徽章形态)
-│ │ 点击选择相册图片 ││
-│ │ ││
-│ └─────────────────────────────────┘│
-└─────────────────────────────────────┘
-┌─────────────────────────────────────┐
-│ [奶油胶效果] [金属边框] [异形裁切] │ ← 吧唧专属装饰选项
-│ │
-│ 藏品信息 │
-│ ┌─────────────────────────────────┐│
-│ │ 请输入描述词或上传参考图... ││ ← 藏品描述/参考输入
-│ └─────────────────────────────────┘│
-│ │
-│ 跳过 │
-└─────────────────────────────────────┘
-┌─────────────────────────────────────┐
-│ 输入描述词 │
-│ ┌─────────────────────────────────┐│
-│ │ 请输入描述词,引导AI创作... 发送 ││
-│ └─────────────────────────────────┘│
-└─────────────────────────────────────┘
-```
-
-**页面说明:** 吧唧铸造页面在素材预览区体现圆形/异形形态,并提供奶油胶效果、金属边框、异形裁切等专属装饰选项
-
----
-
-#### 3.3.3 海报铸造页面
-
-```
-┌─────────────────────────────────────┐
-│ ← 返回 │
-└─────────────────────────────────────┘
-┌─────────────────────────────────────┐
-│ ┌─────────────────────────────────┐│
-│ │ ││
-│ │ ││
-│ │ 点击选择相册图片 ││ ← 素材预览区(单图,16:9/4:3/2:3比例)
-│ │ ││
-│ └─────────────────────────────────┘│
-│ [横向] [竖向] │ ← 比例切换
-└─────────────────────────────────────┘
-┌─────────────────────────────────────┐
-│ 藏品信息 │
-│ ┌─────────────────────────────────┐│
-│ │ 请输入描述词或上传参考图... ││ ← 藏品描述/参考输入
-│ └─────────────────────────────────┘│
-│ │
-│ 背景替换 风格迁移 光影调整 │ ← 海报专属选项
-│ │
-│ 跳过 │
-└─────────────────────────────────────┘
-┌─────────────────────────────────────┐
-│ 输入描述词 │
-│ ┌─────────────────────────────────┐│
-│ │ 请输入描述词,引导AI创作... 发送 ││
-│ └─────────────────────────────────┘│
-└─────────────────────────────────────┘
-```
-
-**页面说明:** 海报铸造页面素材预览区支持横向/竖向比例切换,并提供背景替换、风格迁移、光影调整等专属选项
-
----
-
-### 3.4 AI处理中页面结构
-
-```
-┌─────────────────────────────────────┐
-│ │
-│ │
-│ ◐◐◐◐◐◐◐◐◐◐◐ 35% │ ← AI生成进度
-│ │
-│ AI 正在创作中,请稍候... │
-│ │
-└─────────────────────────────────────┘
-```
-
-**页面说明:** 点击开始创作后进入此页面,展示AI处理进度,完成后自动跳转至结果预览页
-
----
-
-### 3.5 AI创作结果预览页面结构
-
-```
-┌─────────────────────────────────────┐
-│ ← 返回 ··· │ ← 顶部导航栏
-└─────────────────────────────────────┘
-┌─────────────────────────────────────┐
-│ ┌─────────────────────────────────┐│
-│ │ ││
-│ │ ││
-│ │ 生成结果图 ││
-│ │ ││
-│ │ ││
-│ └─────────────────────────────────┘│
-└─────────────────────────────────────┘
-┌─────────────────────────────────────┐
-│ │
-│ [重新生成] [确认铸造] │
-└─────────────────────────────────────┘
-```
-
-**页面说明:** AI处理完成后进入此页面,展示生成结果和身份证编号,用户可重新生成或确认铸造
-
----
-
-### 3.6 创作详情页结构
-
-```
-┌─────────────────────────────────────┐
-│ ← 返回 ··· │ ← 顶部导航栏
-└─────────────────────────────────────┘
-┌─────────────────────────────────────┐
-│ ┌─────────────────────────────────┐│
-│ │ ││
-│ │ ││
-│ │ AI 创作结果大图 ││
-│ │ ││
-│ │ ││
-│ └─────────────────────────────────┘│
-└─────────────────────────────────────┘
-┌─────────────────────────────────────┐
-│ 用户昵称 │ ← 用户名
-│ ♡ 999 💬 88 │ ← 互动栏
-└─────────────────────────────────────┘
-┌─────────────────────────────────────┐
-│ #ZUA-20260409-XXXXX ⧉ 已上链 │ ← 身份证编号(可复制)
-└─────────────────────────────────────┘
-```
-
-**页面说明:** 点击创作卡片后进入详情页,展示创作结果、用户名、互动信息、身份证编号
-
----
-
-## 4. 用户流程
-
-### 4.1 详细用户流程
-
-```
-[首页] → [铸爱创作模块]
- │
- ├─→ 轮播图点击 → 活动/话题详情页
- │
- ├─→ 选择Tab(星卡 / 吧唧 / 海报)──→ 直接进入铸造页面
- │ │
- │ └─→ 选择素材 → AI铸造 → 生成藏品
- │
- └─→ 选择分类(热门作品 / 最新作品 / 星卡 / 吧唧 / 海报)
- │
- └─→ 浏览藏品列表
- │
- └─→ 点击藏品卡片 → 创作详情页
- │
- ├─→ 点赞 / 评论 / 分享
- │
- └─→ [开始铸造] → AI铸造编辑器(以该藏品为原图)
-```
-
-### 4.2 核心交互路径
-
-```
-路径一(铸造路径):
-选择Tab(星卡/吧唧/海报)→ 进入铸造页面 → 选择素材 → AI图生图创作 → 铸造生成藏品
-
-路径二(浏览路径):
-选择分类标签 → 浏览藏品瀑布流 → 点击藏品 → 查看详情 / 以此藏品为原图继续铸造
-```
-
----
-
-## 5. 功能需求(分区介绍)
-
-### 5.1 区域一:顶部运营轮播图
-
-**位置:** 页面最顶部
-
-**功能说明:**
-- 铸爱界面顶部预留运营位
-- 支持后台配置运营活动,如联名创作等
-- 点击后可跳转 H5 链接或站内其他功能模块
-
-**数据字段:**
-
-| 字段 | 说明 |
-|------|------|
-| image_url | 图片地址 |
-| title | 标题(最多 20 字) |
-| description | 描述(最多 50 字) |
-| link_type | 跳转类型:活动 / 话题 / 外部链接 |
-| link_value | 跳转目标ID或URL |
-
-**交互:**
-- 手动滑动切换 + 自动轮播
-- 点击轮播项跳转到对应页面
-
----
-
-### 5.2 区域二:主Tab标签区
-
-**位置:** 轮播图下方
-
-**功能说明:**
-- 铸爱三大分类入口:星卡、吧唧、海报
-- **点击任意 Tab,直接进入对应类型的铸造页面**
-- 铸造页中可选择素材进行 AI 创作
-
-**Tab配置:**
-
-| Tab名称 | 素材形态 | 尺寸规格 | 创作特点 | 展示场景 |
-|---------|----------|----------|----------|----------|
-| 星卡 | 卡片形态,类似明星小卡、签名卡 | 通常 1:1 或 3:4 比例 | 适合人物特写、半身照,可添加装饰元素 | 手机壁纸、社交头像、收藏展示 |
-| 吧唧 | 徽章/贴纸形态,类似金属徽章或圆形立牌 | 通常圆形或异形,直径 5-8cm | 适合可爱风格、二次元形象,可添加边框特效 | 实物徽章展示、书包/衣服装饰 |
-| 海报 | 大尺寸图片素材 | 通常 16:9、4:3 或 2:3 比例 | 适合全身照、场景图、氛围感创作 | 手机壁纸、电脑壁纸、打印输出 |
-
-**各类型素材说明:**
-- **星卡**:以明星照片为核心素材,用户可选择不同模板、滤镜、装饰进行 AI 创作,生成专属小卡风格的作品
-- **吧唧**:以徽章/立牌风格为核心素材,用户可为图片添加金属质感边框、奶油胶效果、异形裁切等,生成可爱风格的徽章作品
-- **海报**:以高质量人物/风景照片为核心素材,用户可添加背景替换、风格迁移、光影调整等,生成海报级别的作品
-
-**交互:**
-- 横向滚动式设计
-- 选中态有视觉区分(下划线/高亮)
-- 默认选中"星卡"Tab
-
----
-
-### 5.3 区域三:分类标签区
-
-**位置:** 主Tab下方
-
-**功能说明:**
-- 用于筛选用户已铸造的藏品
-- 分类为固定配置
-
-**分类配置:**
-
-| 分类名称 | 排序规则 | 说明 |
-|----------|----------|------|
-| 热门作品 | 点赞数/访问数 | 查看所有类别的热门藏品 |
-| 最新作品 | 发布时间 | 查看所有类别的新发布藏品 |
-| 星卡 | 推荐算法 | 仅显示星卡类藏品 |
-| 吧唧 | 推荐算法 | 仅显示吧唧类藏品 |
-| 海报 | 推荐算法 | 仅显示海报类藏品 |
-
-**交互:**
-- 横向排列,支持滚动
-- 选中态有视觉区分
-- 选中后刷新下方创作列表
-
----
-
-### 5.4 区域四:创作网格列表
-
-**位置:** 分类标签下方
-
-**功能说明:**
-- 双列瀑布流展示用户铸造的藏品
-- 根据当前选中的分类标签动态加载
-
-**列表规则:**
-- 无限下拉加载,默认 10 条/页
-- 下拉刷新获取最新内容
-
-**卡片信息:**
-
-| 字段 | 说明 |
-|------|------|
-| 封面图 | AI 创作结果图 |
-| 身份证编号(上链) | 全局唯一,如 #ZUA-20260409-XXXXX |
-| 创作者 | 昵称 + 头像 |
-| 点赞数 | 当前点赞数量 |
-| 发布时间 | 相对时间(如"3小时前") |
-
-**卡片交互:**
-- 点击卡片 → 进入创作详情页
-- 支持点赞、评论、分享
-- 详情页支持「以此为原图」继续铸造
-
----
-
-### 5.5 AI 铸造编辑器
-
-**入口:** 点击主Tab(星卡/吧唧/海报)后进入
-
-**功能说明:**
-- 用户选择素材,AI 进行图生图创作
-
-**支持能力:**
-
-| 功能 | 状态 | 说明 |
-|------|------|------|
-| 图生图 | 支持 | 以用户选择的图片作为原图 |
-| 参数调节 | 支持 | 可输入描述词(Prompt)调节生成效果 |
-| 多轮对话式创作 | 待确定 | 是否支持连续对话优化 |
-| AI 模型 | 初期 MiniMax | 后期可扩展多模型 |
-
-**创作流程:**
-
-```
-1. 选择原图(素材库图片 或 用户铸造的藏品)
- │
- ▼
-2. 输入描述词(Prompt)
- │
- ▼
-3. AI 生成创作结果
- │
- ▼
-4. 预览 → 确认 / 调整描述词继续生成
- │
- ▼
-5. 确认铸造 → 生成带唯一身份证编号(上链)的藏品
-```
-
----
-
-### 5.6 创作详情页
-
-**入口:** 点击创作网格中的任意卡片
-
-**页面信息:**
-
-| 模块 | 说明 |
-|------|------|
-| 藏品大图 | AI 创作结果 |
-| 身份证编号 | 全局唯一编号,可复制 |
-| 原图入口 | 查看原始素材图 |
-| 相似创作 | 使用同原图的其他作品 |
-| 创作者信息 | 昵称、头像、发布时间 |
-| 互动区 | 点赞、评论、分享 |
-
-**核心操作:**
-- **"开始铸造"按钮**: 以此藏品为原图,进入 AI 创作编辑器进行二次创作
-
----
-
-## 6. 非功能性需求
-
-### 6.1 性能
-- 首屏加载时间 ≤ 2秒
-- 列表滑动流畅(60fps)
-
-### 6.2 兼容性
-- 适配 iOS 12+ / Android 6.0+
-- 主流屏幕尺寸适配(刘海屏、全面屏)
-
-### 6.3 数据要求
-- 轮播图支持运营后台配置
-- 分类标签为固定配置
-
----
-
-## 7. 风险与待确认项
-
-| 问题 | 说明 | 状态 |
-|------|------|------|
-| 分类数据来源 | 分类是固定配置 | 已确认 |
-| 轮播图数量上限 | 后台运营控制 | 已确认 |
-| 创作详情页 | 需要单独设计 | 已确认 |
-| 创作编辑器 | 铸造功能的实现范围? | 待确认 |
-| 登录态 | 浏览/创作需要登录 | 已确认 |
-| 举报/审核 | 是否有内容审核机制? | 待确认 |
-| 原图版权 | 使用藏品作为原图是否需要授权? | 已确认(与原创有合作授权) |
-| AI 生成相似性 | 同原图+相似描述词生成相似作品,归属权如何界定? | 待确认 |
-| 生成内容版权 | AI 二创作品的版权归属(用户/平台)? | 待确认 |
-| 溯源链路 | 身份证编号能否追溯到原始素材来源? | 待确认 |
-
----
-
-## 8. 附录
-
-### 8.1 术语说明
-
-| 术语 | 说明 |
-|------|------|
-| 铸爱 | 平台特有的AI创作行为/活动名称 |
-| 身份证编号(上链) | 每份AI创作作品的唯一标识符,全球唯一,用于确权和追溯 |
-| 星卡 | 一种卡片形态的素材(明星周边风格) |
-| 吧唧 | 网络用语,指徽章/贴纸类小物件 |
-| 海报 | 大尺寸图片素材(通常16:9或4:3) |
-| 铸造 | 用户借助AI能力基于原图进行二次创作的动作 |
-| 图生图 | 以某张图片为原图,AI 生成创作结果 |
-
-### 8.2 参考页面
-
-- (后续补充原型链接)
-
----
-
-> **下一步行动:** 请产品/运营确认待确认项后,更新本文档。