Răsfoiți Sursa

docs(spec): 家庭管理优惠券数量列 + 家庭优惠券三合一页 设计规格 v1

- Families.vue: 新增优惠券数量列(可点击),成员/报告/方案/任务计数列改可点击,
  去掉查看信息(OverviewDialog)与三个优惠券弹窗,改路由跳转家庭优惠券统一页
- 后端: Family 实体加 couponCount(@TableField exist=false),AdminController.getFamilies 回填
- 新增 FamilyCouponManage.vue: 发放/券列表/使用记录 三栏 Tabs,familyId 过滤
- 附架构文档与 OpenAPI 契约调研产物

Co-Authored-By: WorkBuddy <noreply@workbuddy.ai>
Agnes Agent 1 săptămână în urmă
părinte
comite
0069225b28

+ 258 - 0
docs/api/openapi.yaml

@@ -0,0 +1,258 @@
+openapi: 3.0.3
+info:
+  title: CFC 后台 API - 家庭优惠券特性契约
+  description: |
+    家庭管理列表新增 couponCount 回填;统一优惠券页(Tabs:模板管理/发放记录/使用记录/转赠记录)复用既有 coupon 端点。
+    变更范围:POST /api/admin/families 响应新增 couponCount;其余端点为复用,无契约破坏。
+  version: 1.0.0
+servers:
+  - url: /api
+paths:
+  /admin/families:
+    post:
+      summary: 家庭分页列表(含 couponCount 回填)
+      operationId: getFamilies
+      requestBody:
+        required: false
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                page: { type: integer, default: 1 }
+                size: { type: integer, default: 10 }
+                name: { type: string, description: 家庭名称模糊搜索 }
+                sort:
+                  type: array
+                  items:
+                    type: object
+                    properties:
+                      prop: { type: string }
+                      order: { type: string, enum: [asc, desc] }
+      responses:
+        '200':
+          description: 家庭分页
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/FamilyPage'
+  /admin/coupon/list:
+    post:
+      summary: 优惠券模板分页列表(统一页-模板管理 Tab)
+      operationId: couponList
+      requestBody:
+        required: false
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                page: { type: integer, default: 1 }
+                size: { type: integer, default: 20 }
+                keyword: { type: string }
+      responses:
+        '200':
+          description: 模板分页
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/CouponPage'
+  /admin/coupon/grant-log:
+    post:
+      summary: 发券流水(统一页-发放记录 Tab,支持 familyId 过滤)
+      operationId: couponGrantLog
+      requestBody:
+        required: false
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                familyId: { type: integer }
+                couponId: { type: integer }
+                grantType: { type: string }
+                source: { type: string }
+      responses:
+        '200':
+          description: 发券流水列表(含 familyName)
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/GrantLogList'
+  /admin/coupon/family-coupon-usage:
+    post:
+      summary: 家庭券使用记录(统一页-使用记录 Tab,支持 familyId 过滤)
+      operationId: couponUsage
+      requestBody:
+        required: false
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                familyId: { type: integer }
+                page: { type: integer, default: 1 }
+                size: { type: integer, default: 20 }
+      responses:
+        '200':
+          description: 使用记录分页
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/CouponUsagePage'
+  /admin/coupon/gift-transfers:
+    post:
+      summary: 会员券转赠记录(统一页-转赠记录 Tab,支持赠送方/接收方家庭过滤)
+      operationId: couponGiftTransfers
+      requestBody:
+        required: false
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                giverFamilyId: { type: integer }
+                receiverFamilyId: { type: integer }
+                status: { type: string, enum: [GIFTED, USED, REVOKED] }
+                couponId: { type: integer }
+                page: { type: integer, default: 1 }
+                size: { type: integer, default: 20 }
+      responses:
+        '200':
+          description: 转赠记录分页
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/GiftTransferPage'
+components:
+  schemas:
+    ApiResult:
+      type: object
+      required: [code, data]
+      properties:
+        code: { type: integer, description: 0=成功, 非0=错误码 }
+        data: {}
+        message: { type: string }
+    Family:
+      type: object
+      properties:
+        id: { type: integer }
+        name: { type: string }
+        inviteCode: { type: string }
+        memberCount: { type: integer }
+        reportCount: { type: integer }
+        planCount: { type: integer }
+        taskCount: { type: integer }
+        couponCount:
+          type: integer
+          description: 新增:该家庭在 family_coupon 表的记录数
+    FamilyPage:
+      allOf:
+        - $ref: '#/components/schemas/ApiResult'
+        - type: object
+          properties:
+            data:
+              type: object
+              properties:
+                records:
+                  type: array
+                  items:
+                    $ref: '#/components/schemas/Family'
+                total: { type: integer }
+                current: { type: integer }
+                size: { type: integer }
+    Coupon:
+      type: object
+      properties:
+        id: { type: integer }
+        name: { type: string }
+        type: { type: string, enum: [FIXED, DISCOUNT] }
+        value: { type: integer }
+        discountRate: { type: integer }
+        minSpend: { type: integer }
+        applicableTo: { type: string }
+        status: { type: string, enum: [ACTIVE, DISABLED] }
+    CouponPage:
+      allOf:
+        - $ref: '#/components/schemas/ApiResult'
+        - type: object
+          properties:
+            data:
+              type: object
+              properties:
+                records:
+                  type: array
+                  items:
+                    $ref: '#/components/schemas/Coupon'
+                total: { type: integer }
+    GrantLog:
+      type: object
+      properties:
+        id: { type: integer }
+        familyId: { type: integer }
+        couponId: { type: integer }
+        grantType: { type: string }
+        period: { type: string }
+        quantity: { type: integer }
+        source: { type: string }
+        createdAt: { type: string, format: date-time }
+        familyName: { type: string }
+    GrantLogList:
+      allOf:
+        - $ref: '#/components/schemas/ApiResult'
+        - type: object
+          properties:
+            data:
+              type: array
+              items:
+                $ref: '#/components/schemas/GrantLog'
+    CouponUsageItem:
+      type: object
+      properties:
+        id: { type: integer }
+        familyId: { type: integer }
+        couponId: { type: integer }
+        usedAt: { type: string, format: date-time }
+        orderId: { type: integer }
+        name: { type: string }
+        type: { type: string }
+        value: { type: integer }
+        discountRate: { type: integer }
+        minSpend: { type: integer }
+    CouponUsagePage:
+      allOf:
+        - $ref: '#/components/schemas/ApiResult'
+        - type: object
+          properties:
+            data:
+              type: object
+              properties:
+                records:
+                  type: array
+                  items:
+                    $ref: '#/components/schemas/CouponUsageItem'
+                total: { type: integer }
+    GiftTransferItem:
+      type: object
+      properties:
+        id: { type: integer }
+        giverFamilyId: { type: integer }
+        receiverFamilyId: { type: integer }
+        couponId: { type: integer }
+        status: { type: string }
+        createdAt: { type: string, format: date-time }
+    GiftTransferPage:
+      allOf:
+        - $ref: '#/components/schemas/ApiResult'
+        - type: object
+          properties:
+            data:
+              type: object
+              properties:
+                records:
+                  type: array
+                  items:
+                    $ref: '#/components/schemas/GiftTransferItem'
+                total: { type: integer }
+                current: { type: integer }
+                size: { type: integer }

+ 240 - 0
docs/architecture/coupon-family-unified-architecture.md

@@ -0,0 +1,240 @@
+# 家庭管理「优惠券数量」列 + 统一优惠券页 —— 架构设计文档
+
+> 首席架构师:高见远 · 2026-11-01
+> 项目:cfc-coupon-family(cfc-backend + cfc-web)
+
+---
+
+## 0. 决策摘要(TL;DR)
+
+| 决策点 | 结论 |
+|--------|------|
+| 前端框架 | **Vue 2.6.14 + Element UI 2.15.13**(现有栈,不引入新框架) |
+| 后端框架 | **Spring Boot(Java 8)+ MyBatis-Plus 3.5.3.1**(现有栈,不引入新框架) |
+| 图标库(P0 锁定) | **`@ant-design/icons-vue@5.1.9`**(Vue 2 兼容的真实 SVG 图标库) |
+| 后端变更 | 仅 `Family` 实体 + `AdminController.getFamilies` 增量回填 `couponCount`,**3 行代码** |
+| 数据库变更 | **无**(`family_coupon` 表已存在,`idx_family_coupon(family_id, coupon_id)` 已覆盖统计查询) |
+| API 变更 | **复用现有端点**,零新增后端端点(统一优惠券页前端 Tabs 调 4 个既有接口) |
+| 路由变更 | 新增/合并 1 个前端路由承载统一优惠券页,接受 `familyId` query 参数 |
+
+---
+
+## 1. 技术栈选型矩阵
+
+### 1.1 前端(cfc-web,现役)
+
+| 层 | 依赖 | 版本 | 说明 |
+|----|------|------|------|
+| 框架 | `vue` | ^2.6.14 | 现有,Vue 2 Options API |
+| 路由 | `vue-router` | ^3.5.1 | 现有 |
+| 状态 | `vuex` | ^3.6.2 | 现有 |
+| UI 组件库 | `element-ui` | ^2.15.13 | 现有,Element UI 2.x(Vue 2 版) |
+| HTTP | `axios` | ^1.6.0 | 现有,封装于 `src/utils/request` |
+| 图表 | `echarts` | ^6.1.0 | 现有 |
+| 富文本 | `@wangeditor/editor(-for-vue)` | ^5.x | 现有 |
+| **图标库(P0 锁定)** | `@ant-design/icons-vue` | **5.1.9** | **新增锁定依赖**(见 §5) |
+
+### 1.2 后端(cfc-backend,现役)
+
+| 层 | 依赖 | 版本 | 说明 |
+|----|------|------|------|
+| 框架 | `spring-boot-starter-*` | parent(Java 8) | 现有 |
+| ORM | `mybatis-plus-boot-starter` | 3.5.3.1 | 现有 |
+| 数据库 | MySQL(InnoDB / utf8mb4) | — | 现有 |
+| 认证 | `spring-boot-starter-security` | — | 现有,JWT + 角色 |
+| Web | `spring-boot-starter-web` | — | 现有 |
+
+### 1.3 部署(现役,未变更)
+
+| 维度 | 方案 |
+|------|------|
+| 前端 | `vue-cli-service build` 产物静态部署 |
+| 后端 | Spring Boot 可执行 Jar |
+| 环境 | dev / test / prod |
+
+---
+
+## 2. 核心功能可行性验证结论
+
+### 2.1 后端 `couponCount` 回填 —— ✅ 可行,增量 3 行
+
+**依据**(已核实源码):
+
+1. **`Family` 实体已采用 `@TableField(exist = false)` 模式**(`Family.java` L40-50):
+   - `memberCount` / `reportCount` / `planCount` / `taskCount` 四个统计字段均为该注解的瞬态字段。
+   - 新增 `couponCount` 完全复用同一模式,**无需 DB Schema 变更**。
+   - MyBatis-Plus 3.5.3.1 官方支持 `@TableField(exist = false)` 标记非表字段。
+
+2. **`AdminController.countByFamilyIds(BaseMapper, ids, extraCond)` 为泛型方法**(`AdminController.java` L843):
+   - 内部 `QueryWrapper.select("family_id as k", "count(*) as v").in("family_id", ids).groupBy("family_id")` + `mapper.selectMaps(qw)`。
+   - 可传入**任意** `BaseMapper<T>`,`FamilyCouponMapper extends BaseMapper<FamilyCoupon>` 完全兼容。
+   - `getFamilies` 已在 L825-828 对 member/report/plan/task 调用该方法,L829-834 回填。
+
+3. **增量代码**(`AdminController.getFamilies`,约 3 行):
+
+   ```java
+   // 注入字段(类顶部 @Resource)
+   @Resource
+   private FamilyCouponMapper familyCouponMapper;
+
+   // getFamilies 内,L828 之后追加
+   Map<Long, Long> couponCnt = countByFamilyIds(familyCouponMapper, ids, null);
+   // 回填循环内追加
+   f.setCouponCount(couponCnt.getOrDefault(f.getId(), 0L));
+   ```
+
+   - `extraCond = null`:统计该家庭在 `family_coupon` 表的**全部记录数**(含 AVAILABLE/USED/RESERVED/EXPIRED/REVOKED),与需求「统计 family_coupon 表中该家庭的记录数」一致。
+
+### 2.2 统一优惠券页 —— ✅ 可行,零新增后端端点
+
+现有 `AdminCouponController`(`@RequestMapping("/api/admin/coupon")`)已提供全部四个 Tabs 的数据源,且均支持 `familyId` 过滤:
+
+| Tab | 现有端点 | 是否支持 familyId 过滤 | 说明 |
+|-----|----------|------------------------|------|
+| 模板管理 | `POST /api/admin/coupon/list` | 否(全局模板列表) | 复用;家庭维度无需过滤 |
+| 发放记录 | `POST /api/admin/coupon/grant-log` | ✅ `familyId` | L193-215 已验证,已批量填充 `familyName` |
+| 使用记录 | `POST /api/admin/coupon/family-coupon-usage` | ✅ `familyId` | L314-364 已验证 |
+| 转赠记录 | `POST /api/admin/coupon/gift-transfers` | ✅ `giverFamilyId` / `receiverFamilyId` | L238-251 已验证 |
+
+> 结论:**不新增后端端点**。前端统一页用 Tabs 切换,调用上表 4 个既有接口并带 `familyId`,满足「点击家庭优惠券数量 → 进入该家庭优惠券页(默认展示发放/使用记录)」。
+
+### 2.3 路由重复检查 —— ✅ 无真实冲突
+
+- 对 `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/` 全量 `@*Mapping` 做「类级前缀 + 方法级路径」拼接去重:
+  - 未发现真实重复路径(子路径如 `/list`、`/delete` 多次出现,但类级 `@RequestMapping` 前缀不同,属正常)。
+  - 未发现任何 `families/coupon` 或 `coupon/families` 路径。
+- **无路径冲突**,即使未来新增端点也不会撞车;本次采用复用方案更无需新增。
+
+---
+
+## 3. API 端点清单
+
+### 3.1 后端(全部复用,无新增/废弃)
+
+| 方法 | 路径 | 状态 | 用途 |
+|------|------|------|------|
+| POST | `/api/admin/families` | **增强(回填 couponCount)** | 家庭分页列表,新增 `couponCount` 统计列 |
+| POST | `/api/admin/coupon/list` | 复用 | 统一页「模板管理」Tab |
+| POST | `/api/admin/coupon/grant-log` | 复用 | 统一页「发放记录」Tab(`familyId`) |
+| POST | `/api/admin/coupon/family-coupon-usage` | 复用 | 统一页「使用记录」Tab(`familyId`) |
+| POST | `/api/admin/coupon/gift-transfers` | 复用 | 统一页「转赠记录」Tab(`giverFamilyId`/`receiverFamilyId`) |
+
+> 响应统一为 `{ code, data, message }`。`/families` 的 `data.records[]` 每项新增 `couponCount` 字段(Long)。
+> 复用端点无需变更契约,前端仅按既有返回结构消费。
+
+### 3.2 前端路由
+
+| 路由 | 组件 | 状态 |
+|------|------|------|
+| `/admin/coupon` | `CouponUnified.vue`(合并新页) | **新增/改造**,接受 `query.familyId` |
+| `/admin/coupon-grant-log` | — | **废弃**(并入统一页 Tab) |
+| `/admin/coupon-gift-transfers` | — | **废弃**(并入统一页 Tab) |
+
+> 统一页默认高亮 Tab 依据 `familyId`:有 `familyId` 时默认「发放记录」,无则「模板管理」。
+> 菜单中 3 个独立入口收敛为 1 个「优惠券管理」,`CouponGrantLog` / `CouponGiftTransfers` 两个 .vue 文件迁移为统一页的 Tab 子组件或内联。
+
+---
+
+## 4. 数据库变更
+
+| 对象 | 变更 | 说明 |
+|------|------|------|
+| `family_coupon` 表 | **无变更** | 已存在(`schema.sql` L4712),含 `idx_family_coupon(family_id, coupon_id)` 索引,`COUNT(*) WHERE family_id IN (...)` 走该索引 |
+| `Family` 实体 | **新增瞬态字段** | `@TableField(exist = false) private Long couponCount;`(同 memberCount 模式) |
+| 其他表 | 无变更 | — |
+
+---
+
+## 5. 图标库锁定(P0 绝对规则)
+
+### 5.1 选型结论:`@ant-design/icons-vue@5.1.9`
+
+- **为何选它**:
+  1. **真实 SVG 图标**(path-based,非字体),满足「SVG 图标库」P0 要求。
+  2. **Vue 2 兼容**:官方版本矩阵确认 v5.x 支持 Vue 2,v6.x+ 要求 Vue ≥3.0.3(本项目 Vue 2.6.14),故锁定 **5.1.9**(5.x 末版)。
+  3. **可锁定 npm 依赖**,满足「前端依赖随之锁定」。
+  4. **免 webpack 改造**:组件库按需 import,无需 svg-sprite-loader / require.context / SvgIcon 组件,符合「MVP 恰到好处、不做过度设计」。
+- **备选方案对比**(供 PM 权衡):
+  - `svg-sprite-loader + 本地 SVG + SvgIcon`(vue-element-admin 模式):更贴近 Element UI 视觉,但需 webpack 配置 + 组件 + 资源目录,基建成本高,MVP 不取。
+  - `@element-plus/icons-vue`:**Vue 3 only**,不可用。
+  - `el-icon-*`(Element UI 字体图标):非 SVG 体系,不满足「SVG 图标库」字面要求。
+
+### 5.2 使用约定
+
+1. **本特性新增的交互图标**必须从 `@ant-design/icons-vue@5.1.9` 引入(如 `TicketOutlined` / `FileTextOutlined` / `TeamOutlined` / `CalendarOutlined`)。
+2. **禁止 emoji 作为功能图标**(P0)。
+3. **全项目统一不混用**:被本特性触达的组件(`Families.vue`、合并后的 `CouponUnified.vue` 及迁移的 GrantLog/GiftTransfers 子组件)内,将 `el-icon-*` 使用迁移为 SVG 图标库,保证「触碰面 = 一致」。
+
+### 5.3 技术债提示(非阻塞,交 PM 排期)
+
+> 全项目约百余个 `.vue` 仍使用 `el-icon-*`。将「全量 `el-icon-*` → SVG 图标库」列为**后续专项**(不在本 MVP 范围),避免一次性大重构引入回归风险。
+
+---
+
+## 6. 技术约束与风险
+
+| 类别 | 约束/风险 | 等级 | 应对 |
+|------|-----------|------|------|
+| 兼容性 | `@ant-design/icons-vue` 必须锁 5.1.9,**严禁**升到 6.x+(Vue 3 only) | 高 | package.json 锁定精确版本 + package-lock 提交 |
+| 性能 | `countByFamilyIds` 对 `family_coupon` 做 `GROUP BY family_id`,当 `family_coupon` 达百万级且 `family_id` 高基数时,单页 100 家庭可能偏慢 | 中 | `idx_family_coupon(family_id, coupon_id)` 覆盖;MVP 数据量下可接受;超阈值再考虑预聚合/计数表 |
+| 权限 | `getFamilies` 已做 B-5 行级隔离(platform/butler/bound),回填统计不影响权限模型 | 低 | 无变更,统计与列表同一数据范围 |
+| 分页 | 统计仅针对**当前页** `ids`(非全量),避免全表聚合 | 低 | 既有设计,保持 |
+| 前端 | 3 个旧优惠券页合并为 1 页,需保证既有功能(发券、模板 CRUD、转赠过滤)不回归 | 中 | 拆分 Tab 子组件复用原逻辑,PM 补充回归用例 |
+| 路由 | 废弃 `/coupon-grant-log`、`/coupon-gift-transfers` 需同步菜单与 `perm` 权限点 | 低 | 保留 `marketing:coupon` 权限点,仅收敛路由 |
+
+---
+
+## 7. 分层架构(ASCII)
+
+```
+┌──────────────────────────────────────────────────────────────────┐
+│  cfc-web 表现层(Vue2 + Element UI 2.x + @ant-design/icons-vue)   │
+│  Families.vue(家庭列表)                                          │
+│    ├─ 新增「优惠券数量」列(couponCount)  ──点击──▶ /admin/coupon?familyId=xx │
+│    ├─ member/report/plan/task 计数列改可点击 ──▶ 对应明细列表          │
+│    └─ 下拉菜单移除「查看信息」项                                    │
+│  CouponUnified.vue(统一优惠券页, 4 Tabs)                          │
+│     ├─ 模板管理 Tab → POST /coupon/list                            │
+│     ├─ 发放记录 Tab → POST /coupon/grant-log   (familyId)          │
+│     ├─ 使用记录 Tab → POST /coupon/family-coupon-usage (familyId)  │
+│     └─ 转赠记录 Tab → POST /coupon/gift-transfers (familyId)       │
+└──────────────┬───────────────────────────────────────────────────┘
+               │ axios + JWT(src/utils/request)
+┌──────────────▼───────────────────────────────────────────────────┐
+│  cfc-backend 业务层(Spring Boot + MyBatis-Plus 3.5.3.1)          │
+│  AdminController.getFamilies                                       │
+│    ├─ familyMapper.selectPage(分页 + B-5 行级隔离)                │
+│    └─ countByFamilyIds(ids) × {member,report,plan,task,coupon}    │
+│        └─ FamilyCouponMapper(新增注入, couponCount 回填 3 行)      │
+│  AdminCouponController(复用): list/grant-log/                    │
+│      family-coupon-usage/gift-transfers(均支持 familyId)          │
+└──────────────┬───────────────────────────────────────────────────┘
+               │ MyBatis-Plus
+┌──────────────▼───────────────────────────────────────────────────┐
+│  数据层(MySQL InnoDB)                                            │
+│  families / family_coupon / family_coupon_grant_log /             │
+│  coupon / coupon_gift_transfer                                    │
+│  family_coupon: idx_family_coupon(family_id, coupon_id)           │
+└──────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 8. 变更文件清单(预估)
+
+| 文件 | 变更 | 预估 |
+|------|------|------|
+| `cfc-backend/.../entity/Family.java` | 新增 `couponCount` 瞬态字段 | +1 字段 |
+| `cfc-backend/.../controller/admin/AdminController.java` | 注入 `FamilyCouponMapper` + 回填 2 行 | +3 行 |
+| `cfc-web/src/api/admin.js`(或 `coupon.js`) | 无新增(复用) | 0 |
+| `cfc-web/src/views/Families.vue` | 新增 couponCount 列 + 计数列可点击 + 移除查看信息 | 中 |
+| `cfc-web/src/views/admin/CouponUnified.vue` | 新建统一优惠券页(4 Tabs) | 新文件 |
+| `cfc-web/src/router/index.js` | 收敛 3 路由 → 1,支持 `familyId` | 小 |
+| `cfc-web/src/views/admin/CouponGrantLog.vue` / `CouponGiftTransfers.vue` | 迁移为统一页 Tab 子组件 | 中 |
+| `cfc-web/package.json` | 锁定 `@ant-design/icons-vue@5.1.9` | 依赖 |
+
+---
+
+## 9. 机器可读产出物
+
+- `docs/api/openapi.yaml`:OpenAPI 3.0 契约(含 `/api/admin/families` 新增 `couponCount` 字段 + 4 个复用端点),前后端联调唯一依据。

+ 182 - 0
docs/superpowers/specs/2026-11-01-family-coupon-unified-design.md

@@ -0,0 +1,182 @@
+# 设计规格:家庭管理页优化 + 家庭优惠券三合一页
+
+- 日期:2026-11-01
+- 模块:cfc-web(后台 Vue2 + Element UI)+ cfc-backend(Spring Boot 2.7 / MyBatis-Plus)
+- 状态:待实施
+- 参考:docs/architecture/coupon-family-unified-architecture.md(架构)、docs/api/openapi.yaml(契约)、PRD-家庭管理-优惠券一体化(PM)、设计调研(设计师)
+
+## 1. 背景与目标
+
+用户要求:
+1. 后台「家庭管理」列表页新增「优惠券数量」列,数字可点击,点击后进入该家庭的优惠券管理页(合并发放、券列表、使用记录)。
+2. 现有计数列(成员/报告/方案/任务)改为可点击,点击后查看对应实际列表(非总览弹窗)。
+3. 去掉下拉菜单里的「查看信息」(总览弹窗),移除现有的三个优惠券弹窗(发放/券列表/使用记录),改为点击计数列跳转。
+
+## 2. 不做(YAGNI)
+
+- 不改全局的优惠券模板管理(CouponManagement.vue)、发券流水(CouponGrantLog.vue)、转赠记录(CouponGiftTransfers.vue)——不在本次范围。
+- 不引入新图标库(沿用项目既定 el-icon-*,符合 P0-1 一套不混用)。
+- 不做排序列新增(计数列默认不可排序)。
+- 不在后端新增 API 端点(复用现有 + 回填计数字段)。
+- 不改动 memberCount/reportCount/planCount/taskCount 的语义和统计口径。
+
+## 3. 后端变更(cfc-backend)
+
+### 3.1 `src/main/java/com/etotem/cfc/entity/Family.java`
+
+在现有 `taskCount` 字段后新增:
+
+```java
+@TableField(exist = false)
+private Long couponCount;     // 优惠券数量(family_coupon 记录总数)
+```
+
+Lombok @Data 自动生成 getter/setter。
+
+### 3.2 `src/main/java/com/etotem/cfc/controller/admin/AdminController.java`
+
+- 在 `@Resource` 区注入 `FamilyCouponMapper familyCouponMapper`(与既有 healthReportMapper/healthPlanMapper/taskMapper/familyMemberMapper 同级风格)。
+- 在 `getFamilies()`(约 L775-837)已有 memberCnt/reportCnt/planCnt/taskCnt 的批量统计区(约 L825-834)追加:
+
+```java
+Map<Long, Long> couponCnt = countByFamilyIds(familyCouponMapper, ids, null);
+```
+
+并在 `for (Family f : records)` 循环内追加:
+
+```java
+f.setCouponCount(couponCnt.getOrDefault(f.getId(), 0L));
+```
+
+### 3.3 无需 DDL
+
+`family_coupon` 表已存在(schema.sql L4712 起),已有索引 `idx_family_coupon(family_id, coupon_id)` 覆盖本次统计。Family 计数字段均为 `@TableField(exist=false)` 瞬态,无迁移。
+
+### 3.4 验证
+
+`mvn -pl cfc-backend -q compile` 通过;不新增单元测试(复用 AdminController 既有路径),派发时做接口冒烟。
+
+## 4. 前端变更 A:`src/views/Families.vue`
+
+### 4.1 列表列改造
+
+- **新增「优惠券数量」列**:位于 taskCount 列之后、操作列之前,`width="100"`。渲染模式:
+
+```html
+<el-table-column prop="couponCount" label="优惠券数量" width="100">
+  <template slot-scope="scope">
+    <el-link v-if="scope.row.couponCount > 0" type="primary" :underline="false" @click.stop="goCouponManage(scope.row.id)">
+      {{ scope.row.couponCount }}
+    </el-link>
+    <span v-else class="cell-zero">0</span>
+  </template>
+</el-table-column>
+```
+
+- **现有 4 个计数列(memberCount/reportCount/planCount/taskCount)改 el-link 可点击**,点击分别打开对应明细弹窗(见 4.3)。示例(memberCount):
+
+```html
+<el-table-column prop="memberCount" label="当前成员数" width="110">
+  <template slot-scope="scope">
+    <el-link v-if="scope.row.memberCount > 0" type="primary" :underline="false" @click.stop="openDetailDialog(scope.row.id, 'members')">{{ scope.row.memberCount }}</el-link>
+    <span v-else class="cell-zero">0</span>
+  </template>
+</el-table-column>
+```
+
+### 4.2 去掉「查看信息」下拉项
+
+- 删除操作列下拉中的 `<el-dropdown-item command="viewOverview">查看信息</el-dropdown-item>`(约 L76)。
+- 删除 `<FamilyOverviewDialog>` 组件标签(约 L310)、`import FamilyOverviewDialog`(约 L319)、components 注册、`overviewDialogVisible`/`overviewFamilyId` data 字段、`handleViewOverview()` 方法。
+
+### 4.3 去三个优惠券弹窗 + 对应下拉项
+
+移除模板中的:
+- 发放优惠券弹窗(issueDialogVisible 区,约 L184-212)
+- 优惠券列表弹窗(couponDialogVisible 区,约 L214-266)
+- 使用记录弹窗(usageDialogVisible 区,约 L268-304)
+
+操作列下拉:删除「优惠券列表」「使用记录」「发放优惠券」三个 el-dropdown-item(约 L78-80);保留「查看成员」「删除」「编辑」。
+
+删除对应 data 字段与全部方法:`issueDialogVisible/issueForm/issueSubmitting/couponDialogVisible/couponListData/couponLoading/couponPagination/couponStatusFilter/usageListData/usageLoading/usagePagination/currentRow` 及 `handleIssueCoupon/confirmIssueCoupon/couponLabel/handleViewCoupons/loadCoupons/onCouponPageChange/onCouponSizeChange/handleViewUsage/loadUsage/onUsagePageChange/onUsageSizeChange`。
+
+删除不再使用的 import:`getCouponList, issueFamilyCoupon, getFamilyCoupons, getFamilyCouponUsage`(`@/api/coupon` 若全部不再使用则整行删除)。
+
+### 4.4 新增方法
+
+```js
+goCouponManage(familyId) {
+  this.$router.push({ path: '/admin/family-coupon', query: { familyId } })
+},
+// 成员/报告/方案/任务 计数列点击 → 打开对应明细弹窗
+openDetailDialog(familyId, type) {
+  this.detailFamilyId = familyId
+  this.detailType = type        // 'members' | 'reports' | 'plans' | 'tasks'
+  this.detailDialogVisible = true
+  this.loadDetail()
+},
+async loadDetail() {
+  // members 走 getFamilyMembers(familyId);reports/plans/tasks 走 getFamilyOverview(familyId) 取对应数组
+  // 全量展示,不分页(数据量可控)
+}
+```
+
+### 4.5 明细弹窗(复用原 FamilyOverviewDialog 列表代码)
+
+在 Families.vue 内新增单个 `detailDialogVisible` 弹窗,内部按 `detailType` 用 `v-if` 切换展示对应列表(成员表 / 报告表 / 方案表 / 任务表),复用原 FamilyOverviewDialog 的表格列定义与 StatusTag options。成员列表直接复用现有 membersDialog 逻辑(可保留成员弹窗独立,成员计数点击走既有 membersDialog)。
+
+简化:**成员计数点击 → 复用现有「查看成员」弹窗**(handleViewMembers);报告/方案/任务计数点击 → 新增一个 detail 弹窗,内部三块用 getFamilyOverview 全量数据切换。这样改动最小。
+
+### 4.6 样式
+
+- `.cell-zero { color: #999; }` 新增(或复用既有灰色文字类)。
+- `el-link` hover 状态 Element UI 自带。
+
+## 5. 前端变更 B:新建 `src/views/admin/FamilyCouponManage.vue`
+
+### 5.1 路由
+
+`src/router/index.js`:在 /admin 子路由组内新增:
+
+```js
+{
+  path: 'family-coupon',
+  name: 'FamilyCouponManage',
+  component: () => import('@/views/admin/FamilyCouponManage.vue'),
+  meta: { title: '家庭优惠券', perm: 'marketing:coupon' }
+}
+```
+
+### 5.2 页面结构
+
+- `created()` 读 `this.$route.query.familyId`,为空则 `this.$router.replace({ path: '/admin/families' })` 并提示。
+- `el-card` + `PageHeader`,标题「家庭优惠券管理」副标题「家庭 ID:{{ familyId }}」。
+- `el-tabs v-model="activeTab"` 三段:
+  - **Tab「发放」**(activeTab='issue'):选券 `el-select`(`getCouponList({page:1,size:999})` 填充 couponOptions)+ `el-input-number` 数量 + 「确认发放」按钮 → `issueFamilyCoupon({ familyId, couponId, quantity })`。
+  - **Tab「券列表」**(activeTab='list'):状态筛选(可用/已使用)+ 表格(券名称/类型/面值折扣/状态/领取时间/使用时间)+ 分页,数据 `getFamilyCoupons({ familyId, status, page, size })`。
+  - **Tab「使用记录」**(activeTab='usage'):表格(券名称/类型/面值折扣/订单号/使用时间)+ 分页,数据 `getFamilyCouponUsage({ familyId, page, size })`。
+- 默认激活:若带 familyId 进入,默认 `activeTab='list'`(贴近查看意图);无 familyId 不渲染(已 redirect)。
+
+### 5.3 复用接口
+
+全部走 `@/api/coupon.js`:`issueFamilyCoupon` / `getFamilyCoupons` / `getFamilyCouponUsage` / `getCouponList`。
+
+### 5.4 样式
+
+沿用 CouponManagement.vue 同款 `.admin-page` / `.adm-table-wrap` / `.pagination-wrap` 结构。
+
+## 6. 验收标准(EARS)
+
+- Given 后端已回填 couponCount,When 运营进入 /admin/families,Then 每行「优惠券数量」列显示整数(0 显示 0 而非空白),且为可点击链接(>0 时)。
+- Given 家庭券数量 > 0,When 点击该数字,Then 跳至 /admin/family-coupon?familyId=xx,显示发放/券列表/使用记录三栏,默认激活「券列表」。
+- Given 家庭券数量 = 0,When 点击 0(仍可点击进入),Then 跳至统一页,券列表与使用记录为空态「暂无数据」,不报错。
+- Given 点击成员/报告/方案/任务计数,When 打开对应明细弹窗,Then 展示该家庭对应列表(成员含管理员标记;报告/方案/任务按 type 切换)。
+- Given 去掉「查看信息」,When 检查 Families.vue 操作列下拉,Then 无「查看信息」命令;无 FamilyOverviewDialog 组件引用。
+- Given 去三个优惠券弹窗,When 检查 Families.vue 模板,Then 无 issueDialogVisible/couponDialogVisible/usageDialogVisible 相关 DOM。
+- Given 后端编译,When `mvn -pl cfc-backend -q compile`,Then 无编译错误。
+
+## 7. 测试与验证
+
+- 后端:`mvn -pl cfc-backend -q compile` 通过;对 /api/admin/families 做一次冒烟确认 couponCount 字段返回。
+- 前端:`npm run serve`(或 vue-cli-service build)确认无编译错误;人工核对 Families.vue 与 FamilyCouponManage.vue 交互。
+- 派发会话自 commit+push 到 `feature/family-coupon-unified`(不合并主干);核验 commit hash + 关键 diff。