浏览代码

feat: admin/user/search 供应商选管理员接口 + 修复 CircleService 循环依赖

- AdminController: 新增 /api/admin/user/search 接口,支持昵称/手机号/姓名模糊搜索
- CircleService: 添加 @Lazy 注解打破 CircleService ↔ CircleMatchService 循环依赖
- cfc-web: 新增 ProductPpointManage 积分管理页面,完善供应商体系表单
- cfc-frontend: product-detail 页面更新
Xiaogang Liao 3 月之前
父节点
当前提交
54bffd4954

+ 23 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java

@@ -17,6 +17,7 @@ import org.springframework.web.bind.annotation.*;
 
 
 import java.util.Date;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Map;
 
 
 @RestController
 @RestController
@@ -143,6 +144,28 @@ public class AdminController {
         return Result.error("重置密码失败");
         return Result.error("重置密码失败");
     }
     }
 
 
+    /**
+     * 用户搜索(供应商体系选择管理员用)
+     * 支持按 nickname/phone/realName 模糊搜索,返回列表用于下拉选择
+     */
+    @PostMapping("/user/search")
+    public Result<List<User>> searchUsers(@RequestBody Map<String, Object> params) {
+        String keyword = params.get("keyword") != null ? String.valueOf(params.get("keyword")) : "";
+        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
+        if (!keyword.isEmpty()) {
+            wrapper.and(w -> w.like(User::getNickname, keyword)
+                    .or().like(User::getPhone, keyword)
+                    .or().like(User::getRealName, keyword));
+        }
+        wrapper.orderByDesc(User::getUpdatedAt).last("LIMIT 50");
+        List<User> users = userMapper.selectList(wrapper);
+        // 清除密码
+        for (User u : users) {
+            u.setPassword(null);
+        }
+        return Result.success(users);
+    }
+
     @DeleteMapping("/users/{id}")
     @DeleteMapping("/users/{id}")
     public Result<Boolean> deleteUser(@PathVariable Long id) {
     public Result<Boolean> deleteUser(@PathVariable Long id) {
         userMapper.deleteById(id);
         userMapper.deleteById(id);

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/CircleService.java

@@ -6,6 +6,7 @@ import com.etotem.cfc.entity.SocialCircle;
 import com.etotem.cfc.entity.SocialCircleMember;
 import com.etotem.cfc.entity.SocialCircleMember;
 import com.etotem.cfc.mapper.SocialCircleMapper;
 import com.etotem.cfc.mapper.SocialCircleMapper;
 import com.etotem.cfc.mapper.SocialCircleMemberMapper;
 import com.etotem.cfc.mapper.SocialCircleMemberMapper;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.annotation.Transactional;
 
 
@@ -24,6 +25,7 @@ public class CircleService extends ServiceImpl<SocialCircleMapper, SocialCircle>
     private SocialCircleMemberMapper socialCircleMemberMapper;
     private SocialCircleMemberMapper socialCircleMemberMapper;
 
 
     @Resource
     @Resource
+    @Lazy
     private CircleMatchService circleMatchService;
     private CircleMatchService circleMatchService;
 
 
     /**
     /**

+ 22 - 0
cfc-frontend/pages/discover/product-detail/product-detail.vue

@@ -17,6 +17,10 @@
           <text class="price">{{ formatPriceWithSymbol(displayPrice) }}</text>
           <text class="price">{{ formatPriceWithSymbol(displayPrice) }}</text>
           <text v-if="product.memberPrice" class="member-price">会员价 {{ formatPriceWithSymbol(product.memberPrice) }}</text>
           <text v-if="product.memberPrice" class="member-price">会员价 {{ formatPriceWithSymbol(product.memberPrice) }}</text>
         </view>
         </view>
+        <view v-if="effectivePpointText" class="ppoint-row">
+          <text class="ppoint-label">可分配利润:</text>
+          <text class="ppoint-value">{{ effectivePpointText }}</text>
+        </view>
         <view class="meta-row">
         <view class="meta-row">
           <text class="meta-item" v-if="product.productType">{{ typeLabel }}</text>
           <text class="meta-item" v-if="product.productType">{{ typeLabel }}</text>
           <text class="meta-item" v-if="product.domain">{{ domainLabel }}</text>
           <text class="meta-item" v-if="product.domain">{{ domainLabel }}</text>
@@ -134,6 +138,10 @@ export default {
         }
         }
       }
       }
       return parts.join(', ')
       return parts.join(', ')
+    },
+    effectivePpointText() {
+      if (!this.product.effectivePpoint && this.product.effectivePpoint !== 0) return ''
+      return (this.product.effectivePpoint / 100).toFixed(2) + '%'
     }
     }
   },
   },
   onLoad(options) {
   onLoad(options) {
@@ -340,6 +348,20 @@ export default {
   font-size: 26rpx;
   font-size: 26rpx;
   color: #999;
   color: #999;
 }
 }
+.ppoint-row {
+  display: flex;
+  align-items: center;
+  margin-bottom: 12rpx;
+}
+.ppoint-label {
+  font-size: 24rpx;
+  color: #666;
+}
+.ppoint-value {
+  font-size: 26rpx;
+  color: #10B981;
+  font-weight: 500;
+}
 .meta-row {
 .meta-row {
   display: flex;
   display: flex;
   flex-wrap: wrap;
   flex-wrap: wrap;

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-a39b48ee4f99c443023d9e56bded82acd25fd508
+6a0ede75f4768d3c54726d2e626addead82a8884

+ 1 - 1
cfc-web/package.json

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

+ 71 - 0
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,77 @@
 # 更新日志 (Changelog)
 # 更新日志 (Changelog)
 
 
 此文件记录所有构建版本的变更。
 此文件记录所有构建版本的变更。
+## v1.0.251 (2026-07-13)
+
+### Bug 修复
+- ProductController spec map JSON.parseArray type inference for Java 8
+
+### 新功能
+- 维度页面缺口修补(Phase1) + 圈子系统(Phase2)
+
+### 其他
+- - 身: 健康打卡入口(HealthCheckinCard) + body/index.vue
+- - 智: 阅读时长展示(wisdom/index.vue reading stats)
+- - 心: 天盘相性入门版(已有完整天盘/兼容性模块)
+- - EnergyService: 心子维度(情绪稳定/理解包容/传承传递) + 五行克生算法v2.0
+- - MemberEnergyDTO: 心子维度字段
+- - ProductController: spec/map 规格映射接口
+- Phase2 珍珠图/圈子系统:
+- - 后端: SocialCircle/SocialCircleMember实体+Mapper
+- - 后端: CircleService CRUD + CircleMatchService匹配引擎
+- - 后端: CircleController API + DatabaseInitializer迁移85 + schema.sql
+- - 前端: PearlDiagram.vue 珍珠图横向滚动
+- - 前端: CircleCard.vue 圈子卡片组件
+- - 前端: CircleDetail.vue 圈子详情弹窗
+- - 前端: action/index.vue 集成PearlDiagram + CircleDetail
+- - 前端: api.js 圈子API函数
+- 
+
+
+## v1.0.250 (2026-07-13)
+
+### 新功能
+- 商品推荐 Phase 3-5 — FamilyContextService Dify输入 + AIChatController userId + 复购提醒
+- 电商 + 推荐系统 + 阅读统计 综合升级
+
+### 其他
+- - FamilyContextService: 新增3个Dify inputs (dimensionScores/recentCognitiveResult/purchasedProductTags) + 3私有辅助方法
+- - AIChatController: RecommendationQuery传入userId和familyId用于已购过滤
+- Phase 4 (P1):
+- - AssessmentService已有异步hook: DanAssessmentResult parent_upload 触发认知弱维度商品推荐
+- Phase 5 (P2):
+- - RepurchaseReminderService: @Scheduled 每日09:00扫描30-60天订单创建复购提醒 + getPendingReminders/onReminderClicked/onReminderPurchased
+- - RepurchaseReminderController: GET list + POST click/purchased 3个端点
+- - RepurchaseReminder.vue: 前端待处理复购提醒组件
+- 
+- - wisdom/index.vue: 阅读时长条件从 !== undefined 改为 > 0
+- 
+- - cart: 支持 SKU 选择 (sku_id, spec_option_ids), 字段扩展  (skuId, specDesc, supplierName)
+- - DatabaseInitializer: 迁移84 — cart 表 sku_id, spec_option_ids 列
+- - OrderItemVO: 新增 skuId, specOptionIds
+- - ProductDTO: 新增 effectivePpoint, ppointSource, profitRate
+- - CartService: addToCart 接收 skuId, 回填 supplierName 从 vendorName
+- ### P点管理
+- - PpointService: adminList() 管理端分页列表
+- - AdminProductPpointController: 新增管理端 P点配置控制器
+- ### 商品推荐系统
+- - ProductRecommendationService: 维度匹配引擎 (dimensionScore 排序 + 90天过滤 + 购买历史排除)
+- - ProductRecommendationController: POST /api/recommend/dimension-products
+- - 5维页面 (wisdom/body/mind/action/wealth): DimensionProductList 替换 DimensionProducts
+- ### 健康打卡
+- - HealthCheckinCard.vue: 新增健康打卡卡片组件
+- - body/index.vue: 集成 HealthCheckinCard
+- ### 智慧维度
+- - wisdom/index.vue: 本月阅读卡片 + 阅读统计 API 接入
+- ### 文档
+- - 五维能量设计: 更新 v2.1 富维度子维度算法 + 身克富杠杆文档
+- - 五维五行哲学: 补充富子维度表 + 身克富三态公式
+- 
+
+### Bug 修复
+- CartService 回填 skuId + wisdom 阅读统计条件优化
+
+
 ## v1.0.249 (2026-07-13)
 ## v1.0.249 (2026-07-13)
 
 
 ### 文档
 ### 文档

+ 6 - 0
cfc-web/src/router/index.js

@@ -320,6 +320,12 @@ const routes = [
         component: () => import('@/views/admin/ProductProfitRate.vue'),
         component: () => import('@/views/admin/ProductProfitRate.vue'),
         meta: { title: '产品利润率', perm: 'commerce:profit-rate' }
         meta: { title: '产品利润率', perm: 'commerce:profit-rate' }
       },
       },
+      {
+        path: 'product-ppoint',
+        name: 'ProductPpointManage',
+        component: () => import('@/views/admin/ProductPpointManage.vue'),
+        meta: { title: 'P点配置', perm: 'commerce:ppoint' }
+      },
       {
       {
         path: 'withdraw-audit',
         path: 'withdraw-audit',
         name: 'WithdrawAudit',
         name: 'WithdrawAudit',

+ 244 - 0
cfc-web/src/views/admin/ProductPpointManage.vue

@@ -0,0 +1,244 @@
+<template>
+  <div class="product-ppoint admin-page">
+    <div class="header">
+      <h2>P点(可分配利润)配置</h2>
+      <div class="filters">
+        <el-input v-model="searchText" placeholder="商品名称搜索" prefix-icon="el-icon-search" style="width: 200px;" clearable @keyup.enter.native="loadList" />
+        <el-button type="primary" @click="loadList">查询</el-button>
+        <el-button type="success" @click="openAddDialog">新增配置</el-button>
+      </div>
+    </div>
+    <div style="overflow:auto;max-height:calc(100vh - 300px);">
+    <el-table style="max-height:calc(100vh - 300px);" :data="list" v-loading="loading" border stripe>
+      <el-table-column prop="id" label="ID" width="70" />
+      <el-table-column label="商品图片" width="70">
+        <template slot-scope="{ row }">
+          <img v-if="row.coverImage" :src="row.coverImage" class="product-thumb" />
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="productName" label="商品名称" min-width="160" show-overflow-tooltip />
+      <el-table-column prop="price" label="售价(元)" width="90">
+        <template slot-scope="{ row }">
+          {{ row.price != null ? (row.price / 100).toFixed(2) : '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="利润率" width="100">
+        <template slot-scope="{ row }">
+          {{ row.profitRate != null ? formatBps(row.profitRate) : '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="P点基点(bps)" width="120">
+        <template slot-scope="{ row }">
+          <span v-if="editId !== row.id">{{ row.ppoint != null ? row.ppoint + ' bps' : '-' }}</span>
+          <el-input-number
+            v-else
+            v-model="editPpoint"
+            :min="0"
+            :max="10000"
+            :step="100"
+            size="small"
+            style="width: 100px;"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="生效日期" width="100">
+        <template slot-scope="{ row }">
+          {{ row.effectiveDate || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" width="80">
+        <template slot-scope="{ row }">
+          <el-tag v-if="row.effectiveDate" :type="isEffectived(row.effectiveDate) ? 'success' : 'warning'" size="small">
+            {{ isEffectived(row.effectiveDate) ? '生效中' : '待生效' }}
+          </el-tag>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="150" fixed="right">
+        <template slot-scope="{ row }">
+          <template v-if="editId !== row.id">
+            <el-button type="primary" size="small" @click="startEdit(row)">编辑</el-button>
+            <el-button type="danger" size="small" @click="deleteRow(row)">删除</el-button>
+          </template>
+          <template v-else>
+            <el-button type="success" size="small" @click="saveRow(row)">保存</el-button>
+            <el-button size="small" @click="cancelEdit">取消</el-button>
+          </template>
+        </template>
+      </el-table-column>
+    </el-table></div>
+
+    <el-pagination
+      @size-change="onPageChange"
+      @current-change="onPageChange"
+      :current-page="page"
+      :page-size="size"
+      :total="total"
+      layout="total, prev, pager, next"
+      style="margin-top: 20px; text-align: right;"
+    />
+
+    <el-dialog title="新增P点配置" :visible.sync="addDialogVisible" width="500px">
+      <el-form :model="addForm" label-width="100px">
+        <el-form-item label="选择商品">
+          <el-select v-model="addForm.productId" filterable placeholder="搜索商品" style="width: 100%;">
+            <el-option v-for="p in productOptions" :key="p.id" :label="p.name" :value="p.id">
+              <span style="float:left">{{ p.name }}</span>
+              <span style="float:right;color:#999;font-size:12px;">{{ p.price != null ? (p.price/100).toFixed(2) + '元' : '' }}</span>
+            </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="P点(bps)">
+          <el-input-number v-model="addForm.ppoint" :min="0" :max="10000" :step="100" style="width: 100%;" />
+          <div style="color:#999;font-size:12px;margin-top:4px;">10000 bps = 100%,例:1000 bps = 10%</div>
+        </el-form-item>
+        <el-form-item label="生效日期">
+          <el-date-picker v-model="addForm.effectiveDate" type="date" placeholder="选择日期" value-format="yyyy-MM-dd" style="width: 100%;" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="addDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submitAdd">确定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getProductPpointList, saveProductPpoint, deleteProductPpoint, getProductList } from '@/api/admin'
+
+export default {
+  data() {
+    return {
+      list: [],
+      loading: false,
+      page: 1,
+      size: 20,
+      total: 0,
+      searchText: '',
+      editId: null,
+      editPpoint: 0,
+      addDialogVisible: false,
+      addForm: {
+        productId: null,
+        ppoint: 0,
+        effectiveDate: ''
+      },
+      productOptions: []
+    }
+  },
+  mounted() {
+    this.loadList()
+  },
+  methods: {
+    async loadList() {
+      this.loading = true
+      try {
+        const res = await getProductPpointList({ keyword: this.searchText, page: this.page, size: this.size })
+        this.list = res.data.records || res.data || []
+        this.total = res.data.total || 0
+      } catch (e) {
+        this.$message.error('加载失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    onPageChange(val) {
+      this.page = val
+      this.loadList()
+    },
+    formatBps(bps) {
+      return (bps / 100).toFixed(2) + '%'
+    },
+    isEffectived(dateStr) {
+      if (!dateStr) return false
+      return new Date(dateStr) <= new Date()
+    },
+    startEdit(row) {
+      this.editId = row.id
+      this.editPpoint = row.ppoint != null ? row.ppoint : 0
+    },
+    cancelEdit() {
+      this.editId = null
+      this.editPpoint = 0
+    },
+    async saveRow(row) {
+      if (this.editPpoint == null) {
+        return this.$message.warning('请设置P点')
+      }
+      try {
+        await saveProductPpoint({ id: row.id, ppoint: this.editPpoint })
+        this.$message.success('保存成功')
+        this.editId = null
+        this.loadList()
+      } catch (e) {
+        this.$message.error('保存失败')
+      }
+    },
+    async deleteRow(row) {
+      try {
+        await this.$confirm('确认删除该配置?', '提示', { type: 'warning' })
+        await deleteProductPpoint({ id: row.id })
+        this.$message.success('删除成功')
+        this.loadList()
+      } catch (e) {
+        if (e !== 'cancel') this.$message.error('删除失败')
+      }
+    },
+    async openAddDialog() {
+      this.addDialogVisible = true
+      this.addForm = { productId: null, ppoint: 0, effectiveDate: '' }
+      try {
+        const res = await getProductList({ status: 'online', keyword: '', productType: '' })
+        this.productOptions = res.data || []
+      } catch (e) {
+        this.productOptions = []
+      }
+    },
+    async submitAdd() {
+      if (!this.addForm.productId) return this.$message.warning('请选择商品')
+      if (!this.addForm.ppoint && this.addForm.ppoint !== 0) return this.$message.warning('请设置P点')
+      if (!this.addForm.effectiveDate) return this.$message.warning('请选择生效日期')
+      try {
+        await saveProductPpoint({
+          productId: this.addForm.productId,
+          ppoint: this.addForm.ppoint,
+          effectiveDate: this.addForm.effectiveDate
+        })
+        this.$message.success('添加成功')
+        this.addDialogVisible = false
+        this.loadList()
+      } catch (e) {
+        this.$message.error('添加失败')
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.product-ppoint {
+  padding: 20px;
+}
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+.header h2 {
+  margin: 0;
+}
+.filters {
+  display: flex;
+  gap: 10px;
+  align-items: center;
+}
+.product-thumb {
+  width: 50px;
+  height: 50px;
+  object-fit: cover;
+  border-radius: 4px;
+}
+</style>

+ 1 - 1
cfc-web/src/views/admin/supply-system/Form.vue

@@ -53,7 +53,7 @@
 </template>
 </template>
 
 
 <script>
 <script>
-import { createSupplySystem, updateSupplySystem, getSupplySystemDetail } from '@/api/admin'
+import { createSupplySystem, updateSupplySystem, getSupplySystemDetail, searchUsers } from '@/api/admin'
 
 
 export default {
 export default {
   name: 'SupplySystemForm',
   name: 'SupplySystemForm',