Просмотр исходного кода

feat(growth): 商品赠送会员功能 - 成长档案关联商品配置(GrowthArchiveProduct) + 后端 Controller/Service/Mapper/Entity + 前端 ProductEdit 商品编辑页

liaoxg 3 месяцев назад
Родитель
Сommit
0cc0d98b27

+ 18 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -7495,6 +7495,7 @@ private void runMigration100() {
 		ensureColumn("growth_records", "schoolStreet", "VARCHAR(100) COMMENT '学校所在街道'");
 		log.info("已确保growth_records学校/年级相关列存在");
 
+<<<<<<< Updated upstream
 		// 迁移152: 创建 product_gift_rules 表(商品赠送会员规则)
 		try {
 			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_gift_rules (" +
@@ -7511,5 +7512,22 @@ private void runMigration100() {
 		} catch (Exception e) {
 			log.warn("创建product_gift_rules表失败: {}", e.getMessage());
 		}
+
+		// 迁移153: 创建 growth_archive_products 表(成长档案关联商品配置)
+		try {
+			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS growth_archive_products (" +
+					"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+					"product_id BIGINT NOT NULL COMMENT '关联products表', " +
+					"status TINYINT(1) DEFAULT 1 COMMENT '1=启用 0=停用', " +
+					"sort_order INT DEFAULT 0 COMMENT '展示排序(升序)', " +
+					"created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
+					"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', " +
+					"UNIQUE KEY uk_product_id (product_id), " +
+					"KEY idx_status (status)" +
+					") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成长档案关联商品配置'");
+			log.info("已创建growth_archive_products表");
+		} catch (Exception e) {
+			log.warn("创建growth_archive_products表失败: {}", e.getMessage());
+		}
 	}
 }

+ 73 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminGrowthArchiveProductController.java

@@ -0,0 +1,73 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.GrowthArchiveProduct;
+import com.etotem.cfc.service.GrowthArchiveProductService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.Map;
+
+/**
+ * 管理端-成长档案关联商品配置控制器
+ */
+@Tag(name = "管理端-成长档案关联商品")
+@RestController
+@RequestMapping("/api/admin/growth-archive-product")
+public class AdminGrowthArchiveProductController {
+
+    @Resource
+    private GrowthArchiveProductService growthArchiveProductService;
+
+    @Operation(summary = "保存成长档案关联商品规则")
+    @PostMapping("/save")
+    public Result<String> save(@RequestBody Map<String, Object> params) {
+        Long productId = params.get("productId") != null
+                ? Long.valueOf(params.get("productId").toString())
+                : null;
+        Integer status = params.get("status") != null
+                ? Integer.valueOf(params.get("status").toString())
+                : null;
+        Integer sortOrder = params.get("sortOrder") != null
+                ? Integer.valueOf(params.get("sortOrder").toString())
+                : 0;
+        if (productId == null) {
+            return Result.error("productId不能为空");
+        }
+        if (status == null || (status != 0 && status != 1)) {
+            return Result.error("status不能为空且必须为0或1");
+        }
+        growthArchiveProductService.saveRule(productId, status, sortOrder);
+        return Result.success("保存成功");
+    }
+
+    @Operation(summary = "成长档案关联商品规则详情")
+    @PostMapping("/detail")
+    public Result<GrowthArchiveProduct> detail(@RequestBody Map<String, Object> params) {
+        Long productId = params.get("productId") != null
+                ? Long.valueOf(params.get("productId").toString())
+                : null;
+        if (productId == null) {
+            return Result.error("productId不能为空");
+        }
+        return Result.success(growthArchiveProductService.getByProductId(productId));
+    }
+
+    @Operation(summary = "删除成长档案关联商品规则")
+    @PostMapping("/delete")
+    public Result<String> delete(@RequestBody Map<String, Object> params) {
+        Long productId = params.get("productId") != null
+                ? Long.valueOf(params.get("productId").toString())
+                : null;
+        if (productId == null) {
+            return Result.error("productId不能为空");
+        }
+        growthArchiveProductService.deleteByProductId(productId);
+        return Result.success("删除成功");
+    }
+}

+ 1 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/ProductListQueryDTO.java

@@ -12,4 +12,5 @@ public class ProductListQueryDTO {
     private Long vendorId;
     private String keyword;
     private Long categoryId;
+    private Boolean growthRelated;   // true=仅返回成长档案关联商品
 }

+ 27 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/GrowthArchiveProduct.java

@@ -0,0 +1,27 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("growth_archive_products")
+public class GrowthArchiveProduct implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long productId;
+
+    private Integer status;      // 1=启用 0=停用
+
+    private Integer sortOrder;   // 展示排序(升序)
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/GrowthArchiveProductMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.GrowthArchiveProduct;
+
+public interface GrowthArchiveProductMapper extends BaseMapper<GrowthArchiveProduct> {
+}

+ 64 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/GrowthArchiveProductService.java

@@ -0,0 +1,64 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.GrowthArchiveProduct;
+import com.etotem.cfc.mapper.GrowthArchiveProductMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class GrowthArchiveProductService {
+
+    @Resource
+    private GrowthArchiveProductMapper mapper;
+
+    public GrowthArchiveProduct getByProductId(Long productId) {
+        if (productId == null) {
+            return null;
+        }
+        return mapper.selectOne(new LambdaQueryWrapper<GrowthArchiveProduct>()
+                .eq(GrowthArchiveProduct::getProductId, productId));
+    }
+
+    /**
+     * 幂等保存规则:存在则更新,不存在则插入
+     */
+    @Transactional
+    public void saveRule(Long productId, Integer status, Integer sortOrder) {
+        Date now = new Date();
+        GrowthArchiveProduct existing = getByProductId(productId);
+        if (existing != null) {
+            existing.setStatus(status);
+            existing.setSortOrder(sortOrder);
+            existing.setUpdatedAt(now);
+            mapper.updateById(existing);
+        } else {
+            GrowthArchiveProduct rule = new GrowthArchiveProduct();
+            rule.setProductId(productId);
+            rule.setStatus(status);
+            rule.setSortOrder(sortOrder != null ? sortOrder : 0);
+            rule.setCreatedAt(now);
+            rule.setUpdatedAt(now);
+            mapper.insert(rule);
+        }
+    }
+
+    /**
+     * 启用中的关联规则,按 sort_order 升序、id 升序
+     */
+    public List<GrowthArchiveProduct> getActiveRules() {
+        return mapper.selectList(new LambdaQueryWrapper<GrowthArchiveProduct>()
+                .eq(GrowthArchiveProduct::getStatus, 1)
+                .orderByAsc(GrowthArchiveProduct::getSortOrder)
+                .orderByAsc(GrowthArchiveProduct::getId));
+    }
+
+    public void deleteByProductId(Long productId) {
+        mapper.delete(new LambdaQueryWrapper<GrowthArchiveProduct>()
+                .eq(GrowthArchiveProduct::getProductId, productId));
+    }
+}

+ 27 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java

@@ -8,6 +8,7 @@ import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.ProductDTO;
 import com.etotem.cfc.dto.ProductListQueryDTO;
 import com.etotem.cfc.entity.AssessmentProduct;
+import com.etotem.cfc.entity.GrowthArchiveProduct;
 import com.etotem.cfc.entity.Product;
 import com.etotem.cfc.entity.ProductGiftRule;
 import com.etotem.cfc.entity.User;
@@ -17,6 +18,7 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
+import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
@@ -41,6 +43,9 @@ public class ProductService {
     @Resource
     private AssessmentProductService assessmentProductService;
 
+    @Resource
+    private GrowthArchiveProductService growthArchiveProductService;
+
     public Product getById(Long id) {
         return productMapper.selectById(id);
     }
@@ -48,8 +53,28 @@ public class ProductService {
     public Result<Map<String, Object>> list(ProductListQueryDTO query, Long userId) {
         Page<Product> page = new Page<>(query.getPage(), query.getSize());
         LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<Product>()
-            .eq(Product::getStatus, "on_shelf")
-            .orderByDesc(Product::getCreatedAt);
+            .eq(Product::getStatus, "on_shelf");
+        // growthRelated=true: 仅返回成长档案关联商品,按 sort_order 升序展示
+        if (Boolean.TRUE.equals(query.getGrowthRelated())) {
+            List<Long> productIds = growthArchiveProductService.getActiveRules().stream()
+                .map(GrowthArchiveProduct::getProductId)
+                .collect(Collectors.toList());
+            if (productIds.isEmpty()) {
+                Map<String, Object> empty = new HashMap<>();
+                empty.put("records", new ArrayList<>());
+                empty.put("total", 0L);
+                empty.put("page", page.getCurrent());
+                empty.put("size", page.getSize());
+                return Result.success(empty);
+            }
+            wrapper.in(Product::getId, productIds);
+            String ids = productIds.stream()
+                .map(String::valueOf)
+                .collect(Collectors.joining(","));
+            wrapper.last("ORDER BY FIELD(id, " + ids + ")");
+        } else {
+            wrapper.orderByDesc(Product::getCreatedAt);
+        }
         // 游客过滤:排除会员专属商品
         if (userId == null) {
             wrapper.ne(Product::getMemberEligible, "yes");

+ 12 - 0
cfc-backend/src/main/resources/schema.sql

@@ -2567,6 +2567,18 @@ CREATE TABLE IF NOT EXISTS assessment_products (
     INDEX idx_product_id (product_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测评商品扩展信息';
 
+-- 成长档案关联商品配置
+CREATE TABLE IF NOT EXISTS growth_archive_products (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    product_id BIGINT NOT NULL COMMENT '关联products表',
+    status TINYINT(1) DEFAULT 1 COMMENT '1=启用 0=停用',
+    sort_order INT DEFAULT 0 COMMENT '展示排序(升序)',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    UNIQUE KEY uk_product_id (product_id),
+    KEY idx_status (status)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成长档案关联商品配置';
+
 -- 测评额度表
 CREATE TABLE IF NOT EXISTS assessment_quotas (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,

+ 139 - 2
cfc-frontend/pages/growth/upload.vue

@@ -15,6 +15,29 @@
         </view>
       </view>
 
+      <!-- 成长报告推荐区 -->
+      <view class="purchase-section" v-if="recommendedProducts.length > 0">
+        <view class="purchase-header">
+          <text class="purchase-title">还没有成长报告?</text>
+          <text class="purchase-subtitle">购买测评,获取专业成长报告</text>
+        </view>
+        <scroll-view class="purchase-scroll" scroll-x enable-flex show-scrollbar="false">
+          <view
+            class="purchase-card"
+            v-for="item in recommendedProducts"
+            :key="getProductKey(item)"
+            @click="goToProduct(item.id)"
+          >
+            <image class="purchase-cover" :src="getImageUrl(item.coverImage) || '/static/default-product.png'" mode="aspectFill" />
+            <text class="purchase-name">{{ item.name }}</text>
+            <text class="purchase-price">{{ formatPrice(item.price) }}</text>
+            <view class="purchase-btn">
+              <text class="purchase-btn-text">查看详情</text>
+            </view>
+          </view>
+        </scroll-view>
+      </view>
+
       <view class="form-item">
         <text class="label">选择报告文件</text>
         <view class="upload-area" @click="chooseFile">
@@ -63,7 +86,8 @@
 </template>
 
 <script>
-import { uploadAndParseReport, createGrowthRecordFromUpload, checkGrowthDuplicate, getChildren } from '../../utils/api.js'
+import { uploadAndParseReport, createGrowthRecordFromUpload, checkGrowthDuplicate, getChildren, productList, getChildRecords } from '../../utils/api.js'
+import config from '@/config.js'
 
 export default {
   data() {
@@ -76,7 +100,9 @@ export default {
       sourceFileUrl: '',
       parsedData: null,
       reportDateStr: '',
-      manualDate: ''
+      manualDate: '',
+      recommendedProducts: [],
+      growthLoading: false
     }
   },
   computed: {
@@ -102,6 +128,7 @@ export default {
           }
           if (target) {
             this.selectedChild = target
+            this.loadRecommend()
           }
         }
       } catch (e) {
@@ -117,6 +144,7 @@ export default {
           var idx = res.tapIndex
           if (idx >= 0 && idx < self.children.length) {
             self.selectedChild = self.children[idx]
+            self.loadRecommend()
           }
         }
       })
@@ -212,6 +240,45 @@ export default {
       this.parsedData = null
       this.reportDateStr = ''
       this.manualDate = ''
+    },
+    loadRecommend: async function() {
+      this.recommendedProducts = []
+      var child = this.selectedChild
+      if (!child) {
+        this.growthLoading = false
+        return
+      }
+      this.growthLoading = true
+      try {
+        var res = await getChildRecords(child.id)
+        if (res && res.code === 200) {
+          var records = res.data || []
+          if (records.length === 0) {
+            var prodRes = await productList({ growthRelated: true, size: 10 })
+            if (prodRes && prodRes.code === 200) {
+              var arr = (prodRes.data && prodRes.data.list) || (prodRes.data && prodRes.data.records) || []
+              this.recommendedProducts = arr
+            }
+          }
+        }
+      } catch (e) {
+        this.recommendedProducts = []
+      } finally {
+        this.growthLoading = false
+      }
+    },
+    goToProduct: function(id) {
+      uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + id })
+    },
+    formatPrice: function(cents) {
+      if (cents === null || cents === undefined) return '¥0'
+      return '¥' + (cents / 100).toFixed(2)
+    },
+    getImageUrl: function(path) {
+      return config.API_BASE_URL + path
+    },
+    getProductKey: function(item) {
+      return item.id
     }
   }
 }
@@ -372,4 +439,74 @@ export default {
 .btn-group .btn-submit {
   flex: 1;
 }
+.purchase-section {
+  background: #F0F9FF;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  margin-bottom: 30rpx;
+  border: 2rpx solid #BAE6FD;
+}
+.purchase-header {
+  display: flex;
+  flex-direction: column;
+  margin-bottom: 16rpx;
+}
+.purchase-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #0369A1;
+}
+.purchase-subtitle {
+  font-size: 24rpx;
+  color: #0284C7;
+  margin-top: 6rpx;
+}
+.purchase-scroll {
+  display: flex;
+  flex-direction: row;
+  white-space: nowrap;
+}
+.purchase-card {
+  display: inline-flex;
+  flex-direction: column;
+  width: 220rpx;
+  background: #fff;
+  border-radius: 12rpx;
+  padding: 16rpx;
+  margin-right: 16rpx;
+  flex-shrink: 0;
+}
+.purchase-cover {
+  width: 188rpx;
+  height: 120rpx;
+  border-radius: 8rpx;
+  background: #F1F5F9;
+  margin-bottom: 8rpx;
+}
+.purchase-name {
+  font-size: 22rpx;
+  color: #333;
+  line-height: 1.3;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  margin-bottom: 4rpx;
+}
+.purchase-price {
+  font-size: 24rpx;
+  color: #EF4444;
+  font-weight: bold;
+  margin-bottom: 8rpx;
+}
+.purchase-btn {
+  background: #3B82F6;
+  border-radius: 8rpx;
+  padding: 8rpx 0;
+  text-align: center;
+}
+.purchase-btn-text {
+  font-size: 22rpx;
+  color: #fff;
+  font-weight: 500;
+}
 </style>

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.765",
+"version": "1.0.765",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 14 - 0
cfc-web/public/CHANGELOG-v$(node.undefined.md

@@ -3,6 +3,20 @@
 [« 返回最新版本](CHANGELOG.md)
 
 ---
+## v$(node (2026-08-03)
+
+### 新功能
+- 健康报告确认页指标按类别分组折叠, 支持全部展开/折叠与需处理计数
+
+### Bug 修复
+- 迁移151 growth_records 补齐学校/年级7列 - 修复 Unknown column 'school'
+
+### 其他
+- 且其 snake_case 列名与实体不匹配, 导致全列查询报 Unknown column 'school'。
+- schema.sql 同步为 camelCase 与实体一致。
+- 
+
+
 ## v$(node (2026-07-20)
 
 ### 其他

+ 4 - 103
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.765
+> 当前版本: v$(node
 
 ## 历史版本
 
@@ -8,110 +8,11 @@
 
 ---
 
-## v1.0.765 (2026-08-03)
-
-### Bug 修复
-- PaymentService 微信支付V3解密用原始API密钥而非SHA-256哈希
-- 商品详情原生分享补上 setShareInfo - 分享路径带商品ID和邀请码
-
-### 其他
-- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
-- 
-- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
-- 
-
-
-## v1.0.764 (2026-08-03)
-
-### Bug 修复
-- resolve merge conflict in tap-water-pollution.html (accept upstream version)
-
-
-## v1.0.763 (2026-08-03)
-
-### 新功能
-- 商品赠送会员-管理端配置区块
-
-### Bug 修复
-- 圈子页无 childId 时跳过请求, 后端 memberId 空串防御 - 修复 discover 500 与 my-circles 400
-
-
-## v1.0.762 (2026-08-03)
-
-### 其他
-- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
-- 
-- - 首页能量沙盘下方新增会员入口卡(非会员开通 / 会员≤30天续费)
-- - 活动详情页非会员显示会员价引流链接
-- - 活动列表会员价点击跳转开通
-- Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
-- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
-- 
-- (list(Map,Long) / create(Activity,Long) / update(Activity,Long,String) / end(Map,Long,String))
-- Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
-- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
-- 
-- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
-- 
-- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
-- 
-- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
-- 
-- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
-- 
-- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
-- 
-- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
-- 
-
-### 新功能
-- 会员中心页与会员价引导交互
-- 活动/商城会员价展示、分转元显示与未登录会员状态处理
-- 活动详情支持游客浏览与会员价 - detail(id,userId) 解析会员价
-
-### Bug 修复
-- ActivityControllerTest 适配活动模块升级后的接口签名
-- 上传存储路径统一 upload.base-dir - 修复 transferTo 相对路径落盘失败与 /uploads URL 404
-- VirtualGoodsConfig.save() 重命名 saveConfig() 避免 saveOrUpdate 递归 StackOverflow
-- 会员接口匿名访问返回 FREE 空数据 + getMemberLevel 查询 LIMIT 1
-
-### 重构
-- 健康报告上传页 report-upload.vue 重构
-
-
-## v1.0.761 (2026-08-03)
-
-### Bug 修复
-- 修复成员条移除后不刷新 + 可见成员列表字段缺失
-
-
-## v1.0.760 (2026-08-03)
-
-### 文档
-- 商品赠送会员实施计划
-
-### 新功能
-- 活动模块升级——付费报名订单/审核发布流/总结资料
-
-### 其他
-- Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
-- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
-- 
+# v1.0 版本历史
 
+[« 返回最新版本](CHANGELOG.md)
 
-## v1.0.759 (2026-08-03)
-
-### 新功能
-- 健康报告确认页指标按类别分组折叠, 支持全部展开/折叠与需处理计数
-
-### Bug 修复
-- 迁移151 growth_records 补齐学校/年级7列 - 修复 Unknown column 'school'
-
-### 其他
-- 且其 snake_case 列名与实体不匹配, 导致全列查询报 Unknown column 'school'。
-- schema.sql 同步为 camelCase 与实体一致。
-- 
-
+---
 
 ## v1.0.758 (2026-08-03)
 

+ 16 - 0
cfc-web/src/api/admin.js

@@ -542,6 +542,22 @@ export function copyProduct(id) {
   })
 }
 
+export function getGrowthArchiveProduct(productId) {
+  return request({
+    url: '/api/admin/growth-archive-product/detail',
+    method: 'post',
+    data: { productId }
+  })
+}
+
+export function saveGrowthArchiveProduct(data) {
+  return request({
+    url: '/api/admin/growth-archive-product/save',
+    method: 'post',
+    data
+  })
+}
+
 // ========== SKU 管理 API ==========
 
 export function getProductSkuList(productId) {

+ 51 - 4
cfc-web/src/views/admin/ProductEdit.vue

@@ -53,6 +53,21 @@
           </el-row>
         </template>
 
+        <!-- 关联成长档案 -->
+        <el-divider content-position="left">关联成长档案</el-divider>
+        <el-form-item label="关联成长档案">
+          <el-switch
+            v-model="growthArchive.enabled"
+            active-text="开启"
+            inactive-text="关闭"
+          />
+          <span style="margin-left: 10px; font-size: 12px; color: #999;">在成长档案上传页推荐该商品</span>
+        </el-form-item>
+        <el-form-item label="排序值">
+          <el-input-number v-model="growthArchive.sortOrder" :min="0" :precision="0" style="width: 200px;" controls />
+          <span style="margin-left: 10px; font-size: 12px; color: #999;">越小越靠前</span>
+        </el-form-item>
+
         <el-form-item label="商品简介" prop="intro">
           <el-input v-model="form.intro" type="textarea" :rows="2" placeholder="简短描述" maxlength="200" />
         </el-form-item>
@@ -216,7 +231,7 @@
 </template>
 
 <script>
-import { getProduct, createProduct, updateProduct, listAllSupplySystems, getPredefinedPurchaseFields, getProductPurchaseFields, saveProductPurchaseFields, getMySupplySystem, saveProductGiftRule, listProductGiftRules, listMembershipLevels } from '@/api/admin'
+import { getProduct, createProduct, updateProduct, listAllSupplySystems, getPredefinedPurchaseFields, getProductPurchaseFields, saveProductPurchaseFields, getMySupplySystem, saveProductGiftRule, listProductGiftRules, listMembershipLevels, getGrowthArchiveProduct, saveGrowthArchiveProduct } from '@/api/admin'
 import { saveDimensionWeights, getDimensionWeights } from '@/api/dimension-weight'
 import DimensionWeightPicker from '@/components/DimensionWeightPicker.vue'
 import ProductSkuEditor from './components/ProductSkuEditor.vue'
@@ -251,6 +266,10 @@ export default {
         validityDays: 365,
         guideScope: 'all'
       },
+      growthArchive: {
+        enabled: false,
+        sortOrder: 0
+      },
       imageList: [],
       supplySystems: [],
       giftRuleEnabled: false,
@@ -402,8 +421,10 @@ export default {
           this.loadDimensionWeights(p.id)
           // Load purchase fields config
           this.loadPurchaseFields(p.id)
-          // 赠送会员规则回显
+// 赠送会员规则回显
           this.loadGiftRule(p.id)
+          // Load growth archive config
+          this.loadGrowthArchive(p.id)
           // v-model 已绑定 form.description,editor 内容自动更新
         }
       } catch (e) {
@@ -457,7 +478,7 @@ export default {
         console.warn('加载购买字段配置失败', e)
       }
     },
-    async loadGiftRule(productId) {
+async loadGiftRule(productId) {
       try {
         const lvRes = await listMembershipLevels()
         if (lvRes.code === 200 && Array.isArray(lvRes.data)) {
@@ -475,6 +496,19 @@ export default {
         console.warn('加载赠送会员规则失败', e)
       }
     },
+    async loadGrowthArchive(productId) {
+      try {
+        const res = await getGrowthArchiveProduct(productId)
+        if (res.code === 200 && res.data) {
+          this.growthArchive = {
+            enabled: res.data.status === 1,
+            sortOrder: res.data.sortOrder || 0
+          }
+        }
+      } catch (e) {
+        console.warn('加载成长档案关联配置失败', e)
+      }
+    },
     handlePurchaseFieldsToggle(val) {
       if (!val) {
         this.predefinedFields.forEach(pf => { pf.selected = false })
@@ -590,7 +624,7 @@ export default {
                 console.warn('保存购买字段配置失败', e)
               }
             }
-            // 保存赠送会员规则
+// 保存赠送会员规则
             if (productId) {
               try {
                 await saveProductGiftRule(productId, this.giftRuleEnabled ? 1 : 0,
@@ -599,6 +633,19 @@ export default {
                 console.warn('保存赠送会员规则失败', e)
               }
             }
+            // Save growth archive config (only for existing products)
+            if (this.form.id) {
+              try {
+                await saveGrowthArchiveProduct({
+                  productId: this.form.id,
+                  status: this.growthArchive.enabled ? 1 : 0,
+                  sortOrder: this.growthArchive.sortOrder
+                })
+              } catch (e) {
+                this.$message.error('保存成长档案关联失败')
+                console.warn('保存成长档案关联失败', e)
+              }
+            }
             this.$message.success(this.isEdit ? '保存成功' : '创建成功')
             this.$router.push('/product-manage')
           } else {