2026-07-27-membership-fix-plan.md 23 KB

会员管理系统修复实施计划

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: 修复会员管理系统审计发现的 6 个问题(2 个严重数据问题 + 4 个功能缺口)

Architecture: 后端 DDL 补充 → 前端 API 补齐 → 前端页面补全 → 硬编码修复 → 管理端补充

Tech Stack: Spring Boot 2.7 + MyBatis-Plus / uni-app Vue 2 小程序 / Vue 2 + Element UI 管理端

参考文档:

  • docs/flows/membership-flow.md — 会员与订阅流程图
  • docs/superpowers/specs/2026-07-09-family-membership-design.md — 完整设计方案
  • cfc-backend/src/main/java/com/etotem/cfc/entity/MembershipLevel.java — membership_levels 实体
  • cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyMembership.java — family_memberships 实体
  • cfc-backend/src/main/java/com/etotem/cfc/controller/MembershipController.java — 会员控制器
  • cfc-backend/src/main/java/com/etotem/cfc/controller/subscription/SubscriptionController.java — 订阅控制器

Task 1: 补充 membership_levels 和 family_memberships 表 DDL

Files:

  • Modify: cfc-backend/src/main/resources/schema.sql — 追加 CREATE TABLE
  • Modify: cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java — 追加迁移

  • [ ] Step 1: schema.sql 追加 membership_levels 表定义

在 member_upgrade_records 表(行 ~1325)之后,-- 规划师服务模块表 之前插入:

-- 会员等级配置(FREE/FAMILY/PROVIDER)
CREATE TABLE IF NOT EXISTS membership_levels (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    level_code VARCHAR(20) NOT NULL COMMENT '等级编码: FREE/FAMILY/PROVIDER',
    level_name VARCHAR(50) NOT NULL COMMENT '等级名称',
    level_desc VARCHAR(500) COMMENT '等级描述',
    price_monthly INT DEFAULT 0 COMMENT '月费(分)',
    price_quarterly INT DEFAULT 0 COMMENT '季费(分)',
    price_yearly INT DEFAULT 0 COMMENT '年费(分)',
    features TEXT COMMENT '功能权限列表(JSON)',
    max_children INT DEFAULT 3 COMMENT '可添加孩子数量',
    max_tasks_per_day INT DEFAULT 5 COMMENT '每日任务上限',
    ai_review_enabled TINYINT(1) DEFAULT 0 COMMENT 'AI审核',
    priority_support TINYINT(1) DEFAULT 0 COMMENT '优先客服',
    teacher_consultation TINYINT(1) DEFAULT 0 COMMENT '老师咨询',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_level_code (level_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员等级配置';
  • Step 2: schema.sql 追加 family_memberships 表定义

紧跟 membership_levels 之后:

-- 家庭会员记录
CREATE TABLE IF NOT EXISTS family_memberships (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    family_id BIGINT NOT NULL COMMENT '家庭ID',
    level_code VARCHAR(20) NOT NULL COMMENT '会员等级: FREE/BASIC/PROFESSIONAL/ENTERPRISE',
    start_date DATETIME NOT NULL COMMENT '开始时间',
    end_date DATETIME NOT NULL COMMENT '到期时间',
    payment_method VARCHAR(20) COMMENT '支付方式: wechat/alipay/card',
    payment_status VARCHAR(20) DEFAULT 'pending' COMMENT '支付状态: pending/paid/expired/refunded',
    order_no VARCHAR(64) COMMENT '订单号',
    transaction_id VARCHAR(64) COMMENT '交易流水号',
    amount INT DEFAULT 0 COMMENT '支付金额(分)',
    auto_renew TINYINT(1) DEFAULT 0 COMMENT '自动续费',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_family_id (family_id),
    INDEX idx_level_code (level_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭会员记录';
  • Step 3: DatabaseInitializer 追加迁移 90 和 91

找到 runMigrations() 中最新迁移编号(搜索 // 迁移 确认最新号),追加:

// 迁移90: 创建 membership_levels 表(会员等级配置)
try {
    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS membership_levels (" +
            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
            "level_code VARCHAR(20) NOT NULL COMMENT '等级编码: FREE/FAMILY/PROVIDER', " +
            "level_name VARCHAR(50) NOT NULL COMMENT '等级名称', " +
            "level_desc VARCHAR(500) COMMENT '等级描述', " +
            "price_monthly INT DEFAULT 0 COMMENT '月费(分)', " +
            "price_quarterly INT DEFAULT 0 COMMENT '季费(分)', " +
            "price_yearly INT DEFAULT 0 COMMENT '年费(分)', " +
            "features TEXT COMMENT '功能权限列表(JSON)', " +
            "max_children INT DEFAULT 3 COMMENT '可添加孩子数量', " +
            "max_tasks_per_day INT DEFAULT 5 COMMENT '每日任务上限', " +
            "ai_review_enabled TINYINT(1) DEFAULT 0 COMMENT 'AI审核', " +
            "priority_support TINYINT(1) DEFAULT 0 COMMENT '优先客服', " +
            "teacher_consultation TINYINT(1) DEFAULT 0 COMMENT '老师咨询', " +
            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
            "UNIQUE KEY uk_level_code (level_code)" +
            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员等级配置'");
    log.info("已创建 membership_levels 表");
} catch (Exception e) {
    log.warn("创建 membership_levels 表失败: {}", e.getMessage());
}

// 迁移91: 创建 family_memberships 表(家庭会员记录)
try {
    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_memberships (" +
            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
            "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
            "level_code VARCHAR(20) NOT NULL COMMENT '会员等级', " +
            "start_date DATETIME NOT NULL COMMENT '开始时间', " +
            "end_date DATETIME NOT NULL COMMENT '到期时间', " +
            "payment_method VARCHAR(20) COMMENT '支付方式', " +
            "payment_status VARCHAR(20) DEFAULT 'pending' COMMENT '支付状态', " +
            "order_no VARCHAR(64) COMMENT '订单号', " +
            "transaction_id VARCHAR(64) COMMENT '交易流水号', " +
            "amount INT DEFAULT 0 COMMENT '支付金额(分)', " +
            "auto_renew TINYINT(1) DEFAULT 0 COMMENT '自动续费', " +
            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
            "INDEX idx_family_id (family_id), " +
            "INDEX idx_level_code (level_code)" +
            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭会员记录'");
    log.info("已创建 family_memberships 表");
} catch (Exception e) {
    log.warn("创建 family_memberships 表失败: {}", e.getMessage());
}
  • [ ] Step 4: 编译验证

    cd cfc-backend && mvn clean compile -q
    
  • [ ] Step 5: Commit

    git add cfc-backend/src/main/resources/schema.sql cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
    git commit -m "fix: 补充 membership_levels 和 family_memberships 表 DDL
    - schema.sql + DatabaseInitializer 迁移 90/91
    - 两个表被实体引用但无 DDL 创建语句,数据库重建时会员系统会不可用"
    

Task 2: 补齐缺少的前端 API 封装

Files:

  • Modify: cfc-frontend/utils/api.js

在 api.js 的会员模块区域(约行 287-306,getMembershipLevels/getMyMembership 附近)追加以下 8 个函数。在订阅模块区域(约行 1750-1755,getSubscriptionPlans 附近)追加缺失的 2 个。

  • Step 1: 追加 membership 模块缺失的 API 封装

找到 export const getCurrentLevel 之后,在 export const createOrder 之前插入:

// 激活试用会员
export const activateTrial = () => {
  return request('/api/membership/trial', 'POST', {})
}

// 获取价格计划
export const getMembershipPlans = () => {
  return request('/api/membership/plans', 'POST')
}

// 检查功能权限
export const canUseFeature = (feature) => {
  return request('/api/membership/can-use', 'POST', { feature })
}

// 应用会员折扣
export const applyMemberDiscount = (amount) => {
  return request('/api/membership/discount', 'POST', { amount })
}
  • Step 2: 追加 subscription 模块缺失的 API 封装

找到 export const checkBenefit 附近,追加:

// 我的权益列表(订阅权益使用记录)
export const getMyBenefits = () => {
  return request('/api/subscription/benefits/my', 'POST')
}

// 确认订阅(参数式)
export const subscribe = (data) => {
  return request('/api/subscription/subscribe', 'POST', data)
}

// 获取推广等级
export const getSubscriptionTier = () => {
  return request('/api/subscription/tier', 'POST')
}
  • [ ] Step 3: 检查语法完整性

    node -e "require('./cfc-frontend/utils/api.js')" 2>&1 || true
    # 或使用 npm run build:mp-weixin 验证
    
  • [ ] Step 4: Commit

    git add cfc-frontend/utils/api.js
    git commit -m "fix: 补充 11 个缺失的会员/订阅 API 封装
    activateTrial / getMembershipPlans / canUseFeature / applyMemberDiscount
    getMyBenefits / subscribe / getSubscriptionTier"
    

Task 3: plans.vue 从后端读取价格替代硬编码

Files:

  • Modify: cfc-frontend/pages/membership/plans.vue

当前问题:页面硬编码了 ¥1,314/年和 ¥1,314/月 两个价格,后端 /api/membership/plans 已实现但未调用。

  • Step 1: 修改 plans.vue 的 script 部分

在 data 中添加 levels: [] 和 loading: true;在 onLoad 中调用 getMembershipPlans;使用响应式数据替换模板中的硬编码价格。

找到 script 部分类似 export default { data() { return { ... } } } 的结构,修改为:

import { getMembershipPlans } from '../../utils/api'

export default {
  data() {
    return {
      loading: true,
      levels: [],
      l0Features: [
        '基础健康档案',
        '每日任务管理',
        '社区互动'
      ],
      l1Features: [],
      l2Features: []
    }
  },
  onLoad: function() {
    this.loadPlans()
  },
  methods: {
    loadPlans: function() {
      var self = this
      getMembershipPlans().then(function(res) {
        if (res.code === 200 && Array.isArray(res.data)) {
          self.levels = res.data
          // 从后端数据填充 feature 列表(从 level.features JSON 解析或使用默认值)
          var l1 = res.data.find(function(l) { return l.levelCode === 'FAMILY' })
          var l2 = res.data.find(function(l) { return l.levelCode === 'PROVIDER' })
          if (l1) {
            self.l1Features = ['全年会员(¥' + (l1.yearly / 100).toLocaleString() + ')']
          }
          if (l2) {
            self.l2Features = ['高端会员服务']
          }
        }
        self.loading = false
      }).catch(function() {
        self.loading = false
      })
    },
    // 保留原有 purchasePlan 方法不变
    purchasePlan: function(level) {
      uni.navigateTo({ url: '/pages/membership/upgrade?level=' + level })
    },
    formatPrice: function(priceInCents) {
      if (!priceInCents) return '0'
      return '¥' + (priceInCents / 100).toLocaleString()
    }
  }
}

替换模板中硬编码的 ¥1,314 为 {{ formatPrice(planYearlyPrice) }}。关键修改位置是计划卡片 L1 和 L2 的 plan-price 区域。

  • Step 2: 验证模板正确引用数据

确认模板中使用 v-for 渲染 levels 数据,或至少将 L1/L2 价格绑定到从 API 获取的值而非字面量。

  • [ ] Step 3: Commit

    git add cfc-frontend/pages/membership/plans.vue
    git commit -m "fix: plans.vue 从后端 /api/membership/plans 读取价格
    替代硬编码的 ¥1,314/年 和 ¥1,314/月"
    

Task 4: 创建订阅管理页面(小程序)

Files:

  • Modify: cfc-frontend/pages.json — 注册 subscription 分包
  • Create: cfc-frontend/pages/subscription/index.vue — 订阅管理首页(状态 + 取消 + 自动续费)

订阅管理页面包含:

  • 当前订阅状态卡片(活跃/已过期/已取消 + 到期时间)
  • 自动续费开关
  • 取消订阅按钮
  • 权益列表入口

  • [ ] Step 1: pages.json 注册 subscription 分包

在 pages/membership 分包(约行 678)之后,追加:

    {
      "root": "pages/subscription",
      "pages": [
        {
          "path": "index",
          "style": {
            "navigationBarTitleText": "订阅管理",
            "navigationStyle": "custom"
          }
        }
      ]
    },
  • Step 2: 创建 pages/subscription/index.vue

组件结构:

<template>
  <view class="sub-container">
    <!-- 自定义导航栏 -->
    <view class="custom-nav">
      <view class="nav-header">
        <view class="nav-back" @click="goBack"><text class="nav-back-icon">‹</text></view>
        <text class="nav-title">订阅管理</text>
        <view class="nav-placeholder"></view>
      </view>
    </view>

    <scroll-view class="sub-list" scroll-y refresher-enabled :refresher-triggered="refreshing" @refresherrefresh="loadData">
      <!-- 当前订阅状态 -->
      <view class="status-card" v-if="subscription">
        <view class="status-header">
          <text class="status-level">{{ subscription.level === 'L2' ? '💎 久久一生' : '❤️ 一生一世' }}</text>
          <text :class="['status-badge', subActive ? 'active' : 'inactive']">{{ subActive ? '生效中' : '已失效' }}</text>
        </view>
        <view class="status-body">
          <view class="status-row">
            <text class="status-label">到期时间</text>
            <text class="status-value">{{ formatDate(subscription.expireTime) }}</text>
          </view>
          <view class="status-row">
            <text class="status-label">自动续费</text>
            <switch :checked="subscription.autoRenew === 1" @change="onAutoRenewChange" />
          </view>
        </view>
        <button class="btn-cancel" @click="onCancel" v-if="subActive">取消订阅</button>
      </view>
      <view class="no-sub" v-else>
        <text class="no-sub-text">暂无有效订阅</text>
        <button class="btn-go" @click="goPlans">查看会员计划</button>
      </view>

      <!-- 我的权益入口 -->
      <view class="benefit-entry" @click="goBenefits">
        <text class="benefit-title">🎁 我的权益</text>
        <text class="benefit-arrow">›</text>
      </view>
    </scroll-view>
  </view>
</template>

<script>
import { getMySubscription, setAutoRenew, cancelSubscription } from '../../utils/api'

export default {
  data() {
    return {
      refreshing: false,
      subscription: null,
      subActive: false
    }
  },
  onLoad() { this.loadData() },
  methods: {
    loadData() {
      var self = this
      getMySubscription().then(function(res) {
        if (res.code === 200 && res.data) {
          self.subscription = res.data.subscription || null
          self.subActive = res.data.hasActiveSubscription || false
        }
        self.refreshing = false
      }).catch(function() {
        self.refreshing = false
      })
    },
    onAutoRenewChange(e) {
      var self = this
      setAutoRenew({ enabled: e.detail.value ? 1 : 0 }).then(function(res) {
        if (res.code === 200) {
          uni.showToast({ title: e.detail.value ? '已开启' : '已关闭', icon: 'success' })
          self.loadData()
        }
      })
    },
    onCancel() {
      var self = this
      uni.showModal({
        title: '确认取消',
        content: '取消后订阅到期将不再续费,确定要取消吗?',
        success: function(r) {
          if (r.confirm) {
            cancelSubscription({}).then(function(res) {
              if (res.code === 200) {
                uni.showToast({ title: '已取消', icon: 'success' })
                self.loadData()
              }
            })
          }
        }
      })
    },
    goBenefits() { uni.navigateTo({ url: '/pages/membership/benefits' }) },
    goPlans() { uni.navigateTo({ url: '/pages/membership/plans' }) },
    goBack() { uni.navigateBack() },
    formatDate(d) { return d ? d.slice(0, 10) : '' }
  }
}
</script>

<style scoped>
.sub-container { min-height: 100vh; background: #FFF7ED; }
.custom-nav { padding-top: var(--status-bar-height); background: #fff; }
.nav-header { display: flex; align-items: center; height: 88rpx; }
.nav-back { width: 88rpx; height: 88rpx; display: flex; align-items: center; justify-content: center; }
.nav-back-icon { font-size: 48rpx; color: #333; }
.nav-title { flex: 1; text-align: center; font-size: 34rpx; font-weight: 600; color: #333; }
.nav-placeholder { width: 88rpx; }
.sub-list { height: calc(100vh - var(--status-bar-height) - 88rpx); padding: 20rpx; }
.status-card { background: #fff; border-radius: 20rpx; padding: 30rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
.status-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24rpx; }
.status-level { font-size: 32rpx; font-weight: 600; color: #333; }
.status-badge { font-size: 24rpx; padding: 4rpx 16rpx; border-radius: 20rpx; }
.status-badge.active { background: #E8F5E9; color: #2E7D32; }
.status-badge.inactive { background: #FBE9E7; color: #BF360C; }
.status-body { display: flex; flex-direction: column; gap: 16rpx; margin-bottom: 24rpx; }
.status-row { display: flex; justify-content: space-between; align-items: center; }
.status-label { font-size: 28rpx; color: #666; }
.status-value { font-size: 28rpx; color: #333; }
.btn-cancel { width: 100%; height: 80rpx; border-radius: 40rpx; background: #fff; color: #FF4444; border: 2rpx solid #FF4444; font-size: 28rpx; display: flex; align-items: center; justify-content: center; }
.btn-cancel:active { opacity: 0.8; }
.no-sub { background: #fff; border-radius: 20rpx; padding: 60rpx 30rpx; text-align: center; }
.no-sub-text { font-size: 28rpx; color: #999; margin-bottom: 24rpx; display: block; }
.btn-go { background: #F97316; color: #fff; border: none; border-radius: 40rpx; padding: 20rpx 40rpx; font-size: 28rpx; }
.benefit-entry { margin-top: 20rpx; background: #fff; border-radius: 16rpx; padding: 30rpx; display: flex; justify-content: space-between; align-items: center; }
.benefit-title { font-size: 28rpx; font-weight: 500; color: #333; }
.benefit-arrow { font-size: 36rpx; color: #ccc; }
</style>
  • [ ] Step 3: Commit

    git add cfc-frontend/pages.json cfc-frontend/pages/subscription/index.vue
    git commit -m "feat: 创建订阅管理页面(小程序)
    pages/subscription/index.vue: 订阅状态卡片、自动续费开关、取消订阅、权益入口"
    

Task 5: 创建管理端订阅管理页面

Files:

  • Modify: cfc-web/src/router/index.js — 添加订阅管理路由
  • Create: cfc-web/src/views/admin/SubscriptionManage.vue — 订阅列表 + 详情
  • Create/Modify: cfc-web/src/api/subscription.js — 管理端 API 封装

  • [ ] Step 1: 创建管理端 API 文件 cfc-web/src/api/subscription.js

    import request from '@/utils/request'
    
    // 订阅列表
    export function getSubscriptionList(params) {
    return request({
    url: '/api/admin/subscription/list',
    method: 'post',
    data: params
    })
    }
    
    // 手动取消订阅
    export function adminCancelSubscription(data) {
    return request({
    url: '/api/admin/subscription/cancel',
    method: 'post',
    data: data
    })
    }
    
    // 续费
    export function adminRenewSubscription(data) {
    return request({
    url: '/api/admin/subscription/renew',
    method: 'post',
    data: data
    })
    }
    
  • [ ] Step 2: 创建 cfc-web/src/views/admin/SubscriptionManage.vue

使用 Element UI 表格展示订阅列表(el-table),包含字段:ID / 家庭ID / 等级 / 状态 / 到期时间 / 自动续费 / 操作(取消/续费)。

  • Step 3: 添加路由

在 router/index.js 的会员中心路由附近追加:

{
  path: 'subscription-manage',
  name: 'SubscriptionManage',
  component: () => import('@/views/admin/SubscriptionManage.vue'),
  meta: { title: '订阅管理', perm: 'system:config' }
}
  • [ ] Step 4: Commit

    git add cfc-web/src/api/subscription.js cfc-web/src/views/admin/SubscriptionManage.vue cfc-web/src/router/index.js
    git commit -m "feat: 管理端订阅管理页面
    SubscriptionManage.vue: 订阅列表/取消/续费
    cfc-web/src/api/subscription.js: 管理端 API 封装"
    

Task 6: 小程序权益页对接后端 API

Files:

  • Modify: cfc-frontend/pages/membership/benefits.vue

当前 benefits.vue 可能使用静态数据或未调用后端。修改为调用 getMyBenefits() 和 getMySubscription() 展示权益使用记录。

  • Step 1: 修改 benefits.vue script

在 data 中添加 benefitLogs: [],onLoad 中调用 getMyBenefits(),循环渲染权益记录。

import { getMyBenefits, getMySubscription } from '../../utils/api'

export default {
  data() {
    return {
      loading: true,
      subscription: null,
      benefitLogs: []
    }
  },
  onLoad() {
    this.loadData()
  },
  methods: {
    loadData() {
      var self = this
      Promise.all([
        getMySubscription(),
        getMyBenefits()
      ]).then(function(results) {
        if (results[0].code === 200) {
          self.subscription = results[0].data.subscription
        }
        if (results[1].code === 200 && Array.isArray(results[1].data)) {
          self.benefitLogs = results[1].data
        }
        self.loading = false
      }).catch(function() {
        self.loading = false
      })
    }
  }
}
  • [ ] Step 2: Commit

    git add cfc-frontend/pages/membership/benefits.vue
    git commit -m "fix: benefits.vue 对接后端 /api/subscription/benefits/my
    替代静态数据,展示权益使用记录"
    

执行顺序与依赖

Task 1 (DDL) ──→ 无依赖,可先执行
Task 2 (API) ──→ 无依赖,可先执行
Task 3 (plans) ──→ 依赖 Task 2 (getMembershipPlans)
Task 4 (订阅页) ──→ 依赖 Task 2 (getMySubscription/setAutoRenew/cancelSubscription)
Task 5 (管理端) ──→ 无依赖,但需后端 AdminSubscriptionController 已存在(✅ 已有)
Task 6 (权益页) ──→ 依赖 Task 2 (getMyBenefits/getMySubscription)

可并行执行: Task 1 + Task 2 + Task 5
Task 3/4/6 需在 Task 2 之后

自检清单

  • Task 1 补充的 DDL 字段与实体类 MembershipLevel.java / FamilyMembership.java 字段一一对应
  • Task 2 的 API 封装路径与后端 @PostMapping 路径一致(如 /api/membership/trial)
  • Task 3 无硬编码价格残留
  • Task 4 的 subscription 分包已在 pages.json 注册
  • Task 5 的管理端路由在 admin layout 的 sidebar 中有入口
  • Task 6 的 benefits.vue 引用 getMyBenefits 前已确认函数存在