|
|
@@ -0,0 +1,629 @@
|
|
|
+# 会员管理系统修复实施计划
|
|
|
+
|
|
|
+> **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)之后,`-- 规划师服务模块表` 之前插入:
|
|
|
+
|
|
|
+```sql
|
|
|
+-- 会员等级配置(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 之后:
|
|
|
+
|
|
|
+```sql
|
|
|
+-- 家庭会员记录
|
|
|
+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()` 中最新迁移编号(搜索 `// 迁移` 确认最新号),追加:
|
|
|
+
|
|
|
+```java
|
|
|
+// 迁移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: 编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile -q
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+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` 之前插入:
|
|
|
+
|
|
|
+```javascript
|
|
|
+// 激活试用会员
|
|
|
+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` 附近,追加:
|
|
|
+
|
|
|
+```javascript
|
|
|
+// 我的权益列表(订阅权益使用记录)
|
|
|
+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: 检查语法完整性**
|
|
|
+
|
|
|
+```bash
|
|
|
+node -e "require('./cfc-frontend/utils/api.js')" 2>&1 || true
|
|
|
+# 或使用 npm run build:mp-weixin 验证
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+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 { ... } } }` 的结构,修改为:
|
|
|
+
|
|
|
+```javascript
|
|
|
+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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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)之后,追加:
|
|
|
+
|
|
|
+```json
|
|
|
+ {
|
|
|
+ "root": "pages/subscription",
|
|
|
+ "pages": [
|
|
|
+ {
|
|
|
+ "path": "index",
|
|
|
+ "style": {
|
|
|
+ "navigationBarTitleText": "订阅管理",
|
|
|
+ "navigationStyle": "custom"
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 创建 `pages/subscription/index.vue`**
|
|
|
+
|
|
|
+组件结构:
|
|
|
+
|
|
|
+```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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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`**
|
|
|
+
|
|
|
+```javascript
|
|
|
+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` 的会员中心路由附近追加:
|
|
|
+
|
|
|
+```javascript
|
|
|
+{
|
|
|
+ path: 'subscription-manage',
|
|
|
+ name: 'SubscriptionManage',
|
|
|
+ component: () => import('@/views/admin/SubscriptionManage.vue'),
|
|
|
+ meta: { title: '订阅管理', perm: 'system:config' }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+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()`,循环渲染权益记录。
|
|
|
+
|
|
|
+```javascript
|
|
|
+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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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` 前已确认函数存在
|