41 KiB
41 KiB
微信小程序会员绑定系统需求规格说明书
文档信息
项目名称: 小智AI微信小程序会员绑定系统
文档版本: v1.0
创建日期: 2025年9月30日
项目负责人: 开发团队
文档类型: 需求规格说明书
1. 项目概述
1.1 项目背景
小智AI微信小程序需要建立完善的会员体系,通过会员绑定系统实现用户身份验证、会员等级管理和权益分配,提升用户体验和产品价值。
1.2 项目目标
- 无缝对接:实现微信小程序与会员购买系统的无缝对接
- 等级管理:提供VIP和SVIP两档会员绑定功能
- 权益匹配:确保会员权益与购买等级准确匹配
- 用户体验:提供流畅的会员绑定和管理体验
1.3 项目范围
- 微信小程序会员绑定功能开发
- 会员等级管理系统
- 会员权益验证机制
- 相关数据库设计和API接口开发
2. 功能需求分析
2.1 核心功能模块
2.1.1 用户授权登录模块
功能描述: 通过微信授权获取用户身份信息
主要功能:
- 微信授权登录
- 获取用户openid
- 用户信息存储
- 登录状态管理
业务流程:
用户点击登录 → 微信授权 → 获取code → 后端换取openid → 用户信息入库 → 登录成功
2.1.2 会员身份验证模块
功能描述: 验证用户会员身份和等级
主要功能:
- 会员身份验证
- 会员等级识别
- 会员状态查询
- 会员信息同步
验证逻辑:
输入openid → 查询会员表 → 验证会员状态 → 返回会员等级 → 更新绑定状态
2.1.3 会员绑定管理模块
功能描述: 管理用户与会员身份的绑定关系
主要功能:
- 会员绑定操作
- 绑定状态更新
- 绑定历史记录
- 解绑功能
2.1.4 会员等级管理模块
功能描述: 管理不同等级会员的权益和特权
会员等级定义:
| 等级 | 名称 | 权益描述 | 有效期 |
|---|---|---|---|
| 普通用户 | 免费用户 | 基础功能使用 | 永久 |
| VIP | 基础会员 | 高级功能 + 优先支持 | 按购买周期 |
| SVIP | 高级会员 | 全功能 + 专属服务 | 按购买周期 |
2.2 功能优先级
| 优先级 | 功能模块 | 开发周期 |
|---|---|---|
| P0 | 用户授权登录 | 1周 |
| P0 | 会员身份验证 | 1周 |
| P1 | 会员绑定管理 | 1周 |
| P1 | 会员等级管理 | 1周 |
| P2 | 会员权益展示 | 0.5周 |
3. 技术架构设计
3.1 系统架构图
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 微信小程序 │ │ 后端API服务 │ │ 数据库存储 │
│ │ │ │ │ │
│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ 会员绑定页面 │ │◄──►│ │ 会员验证API │ │◄──►│ │ 会员信息表 │ │
│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │
│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ 会员中心页面 │ │◄──►│ │ 权益查询API │ │◄──►│ │ 绑定关系表 │ │
│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │
│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ 权益说明弹窗 │ │◄──►│ │ 绑定管理API │ │◄──►│ │ 用户信息表 │ │
│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
3.2 技术栈选择
前端技术栈:
- 微信小程序原生框架
- WXML + WXSS + JavaScript
- 微信小程序API
后端技术栈:
- Java Spring Boot
- MyBatis Plus
- MySQL 数据库
- Redis 缓存
第三方服务:
- 微信开放平台API
- 微信支付API
4. 数据库设计
4.1 数据表结构
4.1.1 会员信息表 (member_info)
CREATE TABLE `member_info` (
`member_id` varchar(64) NOT NULL COMMENT '会员ID,主键',
`openid` varchar(128) NOT NULL COMMENT '微信用户openid',
`member_level` varchar(16) NOT NULL DEFAULT 'FREE' COMMENT '会员等级:FREE-免费,VIP-基础会员,SVIP-高级会员',
`member_status` varchar(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '会员状态:ACTIVE-有效,EXPIRED-过期,SUSPENDED-暂停',
`start_time` datetime DEFAULT NULL COMMENT '会员开始时间',
`end_time` datetime DEFAULT NULL COMMENT '会员结束时间',
`purchase_order_id` varchar(64) DEFAULT NULL COMMENT '购买订单ID',
`auto_renew` tinyint DEFAULT 0 COMMENT '是否自动续费:0-否,1-是',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` tinyint DEFAULT 0 COMMENT '是否删除:0-未删除,1-已删除',
PRIMARY KEY (`member_id`),
UNIQUE KEY `uk_openid` (`openid`),
KEY `idx_member_level` (`member_level`),
KEY `idx_member_status` (`member_status`),
KEY `idx_end_time` (`end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员信息表';
4.1.2 会员绑定记录表 (member_bind_log)
CREATE TABLE `member_bind_log` (
`log_id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志ID,主键',
`member_id` varchar(64) NOT NULL COMMENT '会员ID',
`openid` varchar(128) NOT NULL COMMENT '微信用户openid',
`bind_type` varchar(16) NOT NULL COMMENT '绑定类型:BIND-绑定,UNBIND-解绑,UPGRADE-升级,DOWNGRADE-降级',
`old_level` varchar(16) DEFAULT NULL COMMENT '原会员等级',
`new_level` varchar(16) NOT NULL COMMENT '新会员等级',
`bind_source` varchar(32) DEFAULT NULL COMMENT '绑定来源:PURCHASE-购买,GIFT-赠送,ADMIN-管理员',
`related_order_id` varchar(64) DEFAULT NULL COMMENT '关联订单ID',
`bind_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '绑定时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`log_id`),
KEY `idx_member_id` (`member_id`),
KEY `idx_openid` (`openid`),
KEY `idx_bind_type` (`bind_type`),
KEY `idx_bind_time` (`bind_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员绑定记录表';
4.1.3 会员权益配置表 (member_benefit_config)
CREATE TABLE `member_benefit_config` (
`config_id` int NOT NULL AUTO_INCREMENT COMMENT '配置ID,主键',
`member_level` varchar(16) NOT NULL COMMENT '会员等级',
`benefit_code` varchar(32) NOT NULL COMMENT '权益代码',
`benefit_name` varchar(100) NOT NULL COMMENT '权益名称',
`benefit_desc` varchar(500) DEFAULT NULL COMMENT '权益描述',
`benefit_value` varchar(100) DEFAULT NULL COMMENT '权益值',
`is_enabled` tinyint DEFAULT 1 COMMENT '是否启用:0-禁用,1-启用',
`sort_order` int DEFAULT 0 COMMENT '排序顺序',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`config_id`),
UNIQUE KEY `uk_level_code` (`member_level`, `benefit_code`),
KEY `idx_member_level` (`member_level`),
KEY `idx_benefit_code` (`benefit_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员权益配置表';
4.2 初始化数据
-- 插入会员权益配置数据
INSERT INTO `member_benefit_config` (`member_level`, `benefit_code`, `benefit_name`, `benefit_desc`, `benefit_value`, `sort_order`) VALUES
('FREE', 'basic_chat', '基础对话', '每日基础AI对话功能', '50', 1),
('FREE', 'basic_voice', '基础语音', '每日语音识别次数', '20', 2),
('VIP', 'advanced_chat', '高级对话', '无限制AI对话功能', 'unlimited', 1),
('VIP', 'advanced_voice', '高级语音', '无限制语音识别', 'unlimited', 2),
('VIP', 'priority_support', '优先支持', '客服优先响应', 'enabled', 3),
('VIP', 'custom_model', '自定义模型', '使用自定义AI模型', 'enabled', 4),
('SVIP', 'premium_chat', '专属对话', '专属AI模型对话', 'unlimited', 1),
('SVIP', 'premium_voice', '专属语音', '高质量语音合成', 'unlimited', 2),
('SVIP', 'exclusive_support', '专属服务', '一对一专属客服', 'enabled', 3),
('SVIP', 'api_access', 'API访问', '开放API接口调用', 'enabled', 4),
('SVIP', 'data_export', '数据导出', '对话数据导出功能', 'enabled', 5);
5. API接口设计
5.1 会员验证相关接口
5.1.1 用户登录接口
接口地址: POST /api/member/login
请求参数:
{
"code": "微信授权code",
"userInfo": {
"nickName": "用户昵称",
"avatarUrl": "头像URL"
}
}
响应数据:
{
"code": 200,
"message": "登录成功",
"data": {
"openid": "用户openid",
"sessionKey": "会话密钥",
"memberInfo": {
"memberId": "会员ID",
"memberLevel": "VIP",
"memberStatus": "ACTIVE",
"endTime": "2025-12-31 23:59:59"
}
}
}
5.1.2 会员信息查询接口
接口地址: GET /api/member/info
请求参数:
Header: Authorization: Bearer {token}
响应数据:
{
"code": 200,
"message": "查询成功",
"data": {
"memberId": "会员ID",
"memberLevel": "VIP",
"memberStatus": "ACTIVE",
"startTime": "2025-01-01 00:00:00",
"endTime": "2025-12-31 23:59:59",
"autoRenew": true,
"benefits": [
{
"benefitCode": "advanced_chat",
"benefitName": "高级对话",
"benefitDesc": "无限制AI对话功能",
"benefitValue": "unlimited"
}
]
}
}
5.1.3 会员绑定接口
接口地址: POST /api/member/bind
请求参数:
{
"orderId": "购买订单ID",
"memberLevel": "VIP",
"duration": 12,
"durationUnit": "MONTH"
}
响应数据:
{
"code": 200,
"message": "绑定成功",
"data": {
"memberId": "会员ID",
"memberLevel": "VIP",
"startTime": "2025-01-01 00:00:00",
"endTime": "2025-12-31 23:59:59"
}
}
5.2 会员权益相关接口
5.2.1 权益列表查询接口
接口地址: GET /api/member/benefits
请求参数:
Query: memberLevel=VIP (可选,不传则返回当前用户等级权益)
响应数据:
{
"code": 200,
"message": "查询成功",
"data": {
"memberLevel": "VIP",
"benefits": [
{
"benefitCode": "advanced_chat",
"benefitName": "高级对话",
"benefitDesc": "无限制AI对话功能",
"benefitValue": "unlimited",
"isEnabled": true
}
]
}
}
5.2.2 权益验证接口
接口地址: POST /api/member/verify-benefit
请求参数:
{
"benefitCode": "advanced_chat",
"requestCount": 1
}
响应数据:
{
"code": 200,
"message": "验证成功",
"data": {
"hasPermission": true,
"remainingCount": "unlimited",
"resetTime": null
}
}
6. 前端页面设计
6.1 页面结构
pages/
├── member/
│ ├── bind/
│ │ ├── bind.wxml # 会员绑定页面
│ │ ├── bind.wxss # 样式文件
│ │ ├── bind.js # 逻辑文件
│ │ └── bind.json # 配置文件
│ ├── center/
│ │ ├── center.wxml # 会员中心页面
│ │ ├── center.wxss
│ │ ├── center.js
│ │ └── center.json
│ └── benefits/
│ ├── benefits.wxml # 权益说明页面
│ ├── benefits.wxss
│ ├── benefits.js
│ └── benefits.json
└── components/
├── member-card/ # 会员卡片组件
├── benefit-item/ # 权益项组件
└── login-modal/ # 登录弹窗组件
6.2 关键页面设计
6.2.1 会员绑定页面 (pages/member/bind/bind.wxml)
<view class="container">
<!-- 页面标题 -->
<view class="header">
<text class="title">会员绑定</text>
<text class="subtitle">绑定会员身份,享受专属权益</text>
</view>
<!-- 用户信息卡片 -->
<view class="user-card">
<image class="avatar" src="{{userInfo.avatarUrl}}" mode="aspectFill"></image>
<view class="user-info">
<text class="nickname">{{userInfo.nickName}}</text>
<text class="openid">ID: {{userInfo.openid}}</text>
</view>
</view>
<!-- 会员状态 -->
<view class="member-status">
<view class="status-item">
<text class="label">当前状态:</text>
<text class="value {{memberInfo.memberLevel}}">{{memberLevelText}}</text>
</view>
<view class="status-item" wx:if="{{memberInfo.endTime}}">
<text class="label">到期时间:</text>
<text class="value">{{memberInfo.endTime}}</text>
</view>
</view>
<!-- 绑定操作区域 -->
<view class="bind-section">
<view class="section-title">会员绑定</view>
<!-- 订单号输入 -->
<view class="input-group">
<text class="input-label">订单号</text>
<input class="input-field"
placeholder="请输入购买订单号"
value="{{orderId}}"
bindinput="onOrderIdInput" />
</view>
<!-- 绑定按钮 -->
<button class="bind-btn"
bindtap="onBindMember"
disabled="{{!orderId || binding}}">
{{binding ? '绑定中...' : '绑定会员'}}
</button>
</view>
<!-- 权益说明 -->
<view class="benefits-section">
<view class="section-title">会员权益</view>
<view class="benefits-list">
<view class="benefit-item" wx:for="{{benefits}}" wx:key="benefitCode">
<icon class="benefit-icon" type="success" size="16"></icon>
<text class="benefit-name">{{item.benefitName}}</text>
<text class="benefit-desc">{{item.benefitDesc}}</text>
</view>
</view>
<text class="view-more" bindtap="onViewMoreBenefits">查看详细权益说明 ></text>
</view>
</view>
6.2.2 会员中心页面 (pages/member/center/center.wxml)
<view class="container">
<!-- 会员卡片 -->
<view class="member-card {{memberInfo.memberLevel}}">
<view class="card-header">
<image class="avatar" src="{{userInfo.avatarUrl}}" mode="aspectFill"></image>
<view class="member-info">
<text class="nickname">{{userInfo.nickName}}</text>
<view class="member-badge">
<text class="level-text">{{memberLevelText}}</text>
</view>
</view>
<view class="member-status">
<text class="status-text">{{memberStatusText}}</text>
<text class="expire-time" wx:if="{{memberInfo.endTime}}">
到期:{{memberInfo.endTime}}
</text>
</view>
</view>
<view class="card-body">
<view class="progress-section" wx:if="{{memberInfo.memberLevel !== 'FREE'}}">
<text class="progress-label">会员有效期</text>
<view class="progress-bar">
<view class="progress-fill" style="width: {{memberProgress}}%"></view>
</view>
<text class="progress-text">剩余 {{remainingDays}} 天</text>
</view>
</view>
</view>
<!-- 功能菜单 -->
<view class="menu-section">
<view class="menu-item" bindtap="onRenewMember">
<icon class="menu-icon" type="success" size="20"></icon>
<text class="menu-text">续费会员</text>
<icon class="arrow-icon" type="arrow" size="16"></icon>
</view>
<view class="menu-item" bindtap="onViewBenefits">
<icon class="menu-icon" type="info" size="20"></icon>
<text class="menu-text">权益说明</text>
<icon class="arrow-icon" type="arrow" size="16"></icon>
</view>
<view class="menu-item" bindtap="onViewBindLog">
<icon class="menu-icon" type="search" size="20"></icon>
<text class="menu-text">绑定记录</text>
<icon class="arrow-icon" type="arrow" size="16"></icon>
</view>
</view>
<!-- 权益快览 -->
<view class="benefits-preview">
<view class="section-title">我的权益</view>
<view class="benefits-grid">
<view class="benefit-card" wx:for="{{benefits}}" wx:key="benefitCode">
<icon class="benefit-icon" type="{{item.isEnabled ? 'success' : 'cancel'}}" size="24"></icon>
<text class="benefit-name">{{item.benefitName}}</text>
<text class="benefit-value">{{item.benefitValue}}</text>
</view>
</view>
</view>
</view>
6.3 组件设计
6.3.1 会员卡片组件 (components/member-card/member-card.wxml)
<view class="member-card {{memberLevel}}">
<view class="card-background">
<image class="bg-pattern" src="/images/member-bg-{{memberLevel}}.png"></image>
</view>
<view class="card-content">
<view class="member-level">
<text class="level-text">{{levelText}}</text>
<text class="level-desc">{{levelDesc}}</text>
</view>
<view class="member-info">
<text class="member-id">会员ID: {{memberId}}</text>
<text class="valid-period" wx:if="{{endTime}}">
有效期至: {{endTime}}
</text>
</view>
</view>
</view>
7. 开发脚本
7.1 小程序端脚本
7.1.1 会员绑定页面逻辑 (pages/member/bind/bind.js)
// pages/member/bind/bind.js
const app = getApp()
const api = require('../../../utils/api')
Page({
data: {
userInfo: {},
memberInfo: {},
orderId: '',
binding: false,
benefits: [],
memberLevelText: '普通用户'
},
onLoad(options) {
this.initPage()
this.loadMemberInfo()
this.loadBenefits()
},
// 初始化页面
initPage() {
const userInfo = app.globalData.userInfo
if (!userInfo) {
wx.redirectTo({
url: '/pages/login/login'
})
return
}
this.setData({ userInfo })
},
// 加载会员信息
async loadMemberInfo() {
try {
wx.showLoading({ title: '加载中...' })
const res = await api.getMemberInfo()
if (res.code === 200) {
const memberInfo = res.data
const memberLevelText = this.getMemberLevelText(memberInfo.memberLevel)
this.setData({
memberInfo,
memberLevelText
})
}
} catch (error) {
console.error('加载会员信息失败:', error)
wx.showToast({
title: '加载失败',
icon: 'error'
})
} finally {
wx.hideLoading()
}
},
// 加载权益信息
async loadBenefits() {
try {
const res = await api.getMemberBenefits()
if (res.code === 200) {
this.setData({
benefits: res.data.benefits
})
}
} catch (error) {
console.error('加载权益信息失败:', error)
}
},
// 订单号输入
onOrderIdInput(e) {
this.setData({
orderId: e.detail.value.trim()
})
},
// 绑定会员
async onBindMember() {
const { orderId } = this.data
if (!orderId) {
wx.showToast({
title: '请输入订单号',
icon: 'error'
})
return
}
try {
this.setData({ binding: true })
wx.showLoading({ title: '绑定中...' })
const res = await api.bindMember({
orderId: orderId
})
if (res.code === 200) {
wx.showToast({
title: '绑定成功',
icon: 'success'
})
// 刷新会员信息
setTimeout(() => {
this.loadMemberInfo()
}, 1500)
// 清空订单号
this.setData({ orderId: '' })
} else {
wx.showToast({
title: res.message || '绑定失败',
icon: 'error'
})
}
} catch (error) {
console.error('绑定会员失败:', error)
wx.showToast({
title: '绑定失败',
icon: 'error'
})
} finally {
this.setData({ binding: false })
wx.hideLoading()
}
},
// 查看更多权益
onViewMoreBenefits() {
wx.navigateTo({
url: '/pages/member/benefits/benefits'
})
},
// 获取会员等级文本
getMemberLevelText(level) {
const levelMap = {
'FREE': '普通用户',
'VIP': 'VIP会员',
'SVIP': 'SVIP会员'
}
return levelMap[level] || '普通用户'
}
})
7.1.2 API工具类 (utils/api.js)
// utils/api.js
const config = require('./config')
class ApiService {
constructor() {
this.baseUrl = config.apiBaseUrl
this.token = wx.getStorageSync('token') || ''
}
// 通用请求方法
request(options) {
return new Promise((resolve, reject) => {
wx.request({
url: this.baseUrl + options.url,
method: options.method || 'GET',
data: options.data || {},
header: {
'Content-Type': 'application/json',
'Authorization': this.token ? `Bearer ${this.token}` : '',
...options.header
},
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data)
} else if (res.statusCode === 401) {
// token过期,跳转登录
wx.removeStorageSync('token')
wx.redirectTo({
url: '/pages/login/login'
})
reject(new Error('登录已过期'))
} else {
reject(new Error(`请求失败: ${res.statusCode}`))
}
},
fail: (error) => {
reject(error)
}
})
})
}
// 用户登录
login(data) {
return this.request({
url: '/api/member/login',
method: 'POST',
data
})
}
// 获取会员信息
getMemberInfo() {
return this.request({
url: '/api/member/info'
})
}
// 绑定会员
bindMember(data) {
return this.request({
url: '/api/member/bind',
method: 'POST',
data
})
}
// 获取会员权益
getMemberBenefits(memberLevel) {
const url = memberLevel
? `/api/member/benefits?memberLevel=${memberLevel}`
: '/api/member/benefits'
return this.request({
url
})
}
// 验证权益
verifyBenefit(data) {
return this.request({
url: '/api/member/verify-benefit',
method: 'POST',
data
})
}
// 更新token
setToken(token) {
this.token = token
wx.setStorageSync('token', token)
}
}
module.exports = new ApiService()
7.2 服务端脚本
7.2.1 会员服务类 (MemberService.java)
package com.xiaozhi.plus.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xiaozhi.plus.entity.MemberInfo;
import com.xiaozhi.plus.entity.MemberBindLog;
import com.xiaozhi.plus.entity.MemberBenefitConfig;
import com.xiaozhi.plus.mapper.MemberInfoMapper;
import com.xiaozhi.plus.mapper.MemberBindLogMapper;
import com.xiaozhi.plus.mapper.MemberBenefitConfigMapper;
import com.xiaozhi.plus.service.MemberService;
import com.xiaozhi.plus.dto.MemberBindRequest;
import com.xiaozhi.plus.dto.MemberInfoResponse;
import com.xiaozhi.plus.utils.IdGenerator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Service
public class MemberServiceImpl extends ServiceImpl<MemberInfoMapper, MemberInfo> implements MemberService {
@Autowired
private MemberInfoMapper memberInfoMapper;
@Autowired
private MemberBindLogMapper memberBindLogMapper;
@Autowired
private MemberBenefitConfigMapper memberBenefitConfigMapper;
@Override
public MemberInfoResponse getMemberInfo(String openid) {
MemberInfo memberInfo = memberInfoMapper.selectOne(
new QueryWrapper<MemberInfo>()
.eq("openid", openid)
.eq("deleted", 0)
);
if (memberInfo == null) {
// 创建免费用户
memberInfo = createFreeMember(openid);
}
// 检查会员是否过期
checkMemberExpiry(memberInfo);
// 获取会员权益
List<MemberBenefitConfig> benefits = getMemberBenefits(memberInfo.getMemberLevel());
return MemberInfoResponse.builder()
.memberId(memberInfo.getMemberId())
.memberLevel(memberInfo.getMemberLevel())
.memberStatus(memberInfo.getMemberStatus())
.startTime(memberInfo.getStartTime())
.endTime(memberInfo.getEndTime())
.autoRenew(memberInfo.getAutoRenew())
.benefits(benefits)
.build();
}
@Override
@Transactional
public boolean bindMember(String openid, MemberBindRequest request) {
// 验证订单有效性
if (!validateOrder(request.getOrderId())) {
throw new RuntimeException("订单验证失败");
}
// 获取或创建会员信息
MemberInfo memberInfo = memberInfoMapper.selectOne(
new QueryWrapper<MemberInfo>()
.eq("openid", openid)
.eq("deleted", 0)
);
String oldLevel = null;
if (memberInfo == null) {
memberInfo = new MemberInfo();
memberInfo.setMemberId(IdGenerator.generateMemberId());
memberInfo.setOpenid(openid);
memberInfo.setMemberLevel("FREE");
memberInfo.setMemberStatus("ACTIVE");
memberInfo.setCreateTime(LocalDateTime.now());
} else {
oldLevel = memberInfo.getMemberLevel();
}
// 更新会员信息
memberInfo.setMemberLevel(request.getMemberLevel());
memberInfo.setMemberStatus("ACTIVE");
memberInfo.setStartTime(LocalDateTime.now());
memberInfo.setEndTime(calculateEndTime(request.getDuration(), request.getDurationUnit()));
memberInfo.setPurchaseOrderId(request.getOrderId());
memberInfo.setUpdateTime(LocalDateTime.now());
// 保存会员信息
if (oldLevel == null) {
memberInfoMapper.insert(memberInfo);
} else {
memberInfoMapper.updateById(memberInfo);
}
// 记录绑定日志
recordBindLog(memberInfo, oldLevel, "PURCHASE", request.getOrderId());
return true;
}
@Override
public List<MemberBenefitConfig> getMemberBenefits(String memberLevel) {
return memberBenefitConfigMapper.selectList(
new QueryWrapper<MemberBenefitConfig>()
.eq("member_level", memberLevel)
.eq("is_enabled", 1)
.orderByAsc("sort_order")
);
}
@Override
public boolean verifyBenefit(String openid, String benefitCode, Integer requestCount) {
MemberInfo memberInfo = memberInfoMapper.selectOne(
new QueryWrapper<MemberInfo>()
.eq("openid", openid)
.eq("deleted", 0)
);
if (memberInfo == null) {
return false;
}
// 检查会员状态
if (!"ACTIVE".equals(memberInfo.getMemberStatus())) {
return false;
}
// 检查是否过期
if (memberInfo.getEndTime() != null && memberInfo.getEndTime().isBefore(LocalDateTime.now())) {
return false;
}
// 检查权益配置
MemberBenefitConfig benefitConfig = memberBenefitConfigMapper.selectOne(
new QueryWrapper<MemberBenefitConfig>()
.eq("member_level", memberInfo.getMemberLevel())
.eq("benefit_code", benefitCode)
.eq("is_enabled", 1)
);
return benefitConfig != null;
}
private MemberInfo createFreeMember(String openid) {
MemberInfo memberInfo = new MemberInfo();
memberInfo.setMemberId(IdGenerator.generateMemberId());
memberInfo.setOpenid(openid);
memberInfo.setMemberLevel("FREE");
memberInfo.setMemberStatus("ACTIVE");
memberInfo.setCreateTime(LocalDateTime.now());
memberInfoMapper.insert(memberInfo);
return memberInfo;
}
private void checkMemberExpiry(MemberInfo memberInfo) {
if (memberInfo.getEndTime() != null &&
memberInfo.getEndTime().isBefore(LocalDateTime.now()) &&
!"EXPIRED".equals(memberInfo.getMemberStatus())) {
memberInfo.setMemberStatus("EXPIRED");
memberInfoMapper.updateById(memberInfo);
}
}
private LocalDateTime calculateEndTime(Integer duration, String durationUnit) {
LocalDateTime now = LocalDateTime.now();
switch (durationUnit.toUpperCase()) {
case "DAY":
return now.plusDays(duration);
case "MONTH":
return now.plusMonths(duration);
case "YEAR":
return now.plusYears(duration);
default:
throw new IllegalArgumentException("不支持的时间单位: " + durationUnit);
}
}
private void recordBindLog(MemberInfo memberInfo, String oldLevel, String bindSource, String orderId) {
MemberBindLog bindLog = new MemberBindLog();
bindLog.setMemberId(memberInfo.getMemberId());
bindLog.setOpenid(memberInfo.getOpenid());
bindLog.setBindType(oldLevel == null ? "BIND" : "UPGRADE");
bindLog.setOldLevel(oldLevel);
bindLog.setNewLevel(memberInfo.getMemberLevel());
bindLog.setBindSource(bindSource);
bindLog.setRelatedOrderId(orderId);
bindLog.setBindTime(LocalDateTime.now());
memberBindLogMapper.insert(bindLog);
}
private boolean validateOrder(String orderId) {
// TODO: 实现订单验证逻辑
// 1. 检查订单是否存在
// 2. 检查订单是否已支付
// 3. 检查订单是否已绑定
return true;
}
}
7.2.2 会员控制器 (MemberController.java)
package com.xiaozhi.plus.controller;
import com.xiaozhi.plus.service.MemberService;
import com.xiaozhi.plus.dto.MemberBindRequest;
import com.xiaozhi.plus.dto.MemberInfoResponse;
import com.xiaozhi.plus.dto.BenefitVerifyRequest;
import com.xiaozhi.plus.entity.MemberBenefitConfig;
import com.xiaozhi.plus.utils.Result;
import com.xiaozhi.plus.utils.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/member")
public class MemberController {
@Autowired
private MemberService memberService;
@Autowired
private JwtUtil jwtUtil;
/**
* 获取会员信息
*/
@GetMapping("/info")
public Result<MemberInfoResponse> getMemberInfo(HttpServletRequest request) {
try {
String openid = getOpenidFromToken(request);
MemberInfoResponse memberInfo = memberService.getMemberInfo(openid);
return Result.success(memberInfo);
} catch (Exception e) {
return Result.error("获取会员信息失败: " + e.getMessage());
}
}
/**
* 绑定会员
*/
@PostMapping("/bind")
public Result<String> bindMember(@RequestBody MemberBindRequest request, HttpServletRequest httpRequest) {
try {
String openid = getOpenidFromToken(httpRequest);
boolean success = memberService.bindMember(openid, request);
if (success) {
return Result.success("绑定成功");
} else {
return Result.error("绑定失败");
}
} catch (Exception e) {
return Result.error("绑定失败: " + e.getMessage());
}
}
/**
* 获取会员权益
*/
@GetMapping("/benefits")
public Result<Map<String, Object>> getMemberBenefits(
@RequestParam(required = false) String memberLevel,
HttpServletRequest request) {
try {
String targetLevel = memberLevel;
if (targetLevel == null) {
String openid = getOpenidFromToken(request);
MemberInfoResponse memberInfo = memberService.getMemberInfo(openid);
targetLevel = memberInfo.getMemberLevel();
}
List<MemberBenefitConfig> benefits = memberService.getMemberBenefits(targetLevel);
Map<String, Object> result = Map.of(
"memberLevel", targetLevel,
"benefits", benefits
);
return Result.success(result);
} catch (Exception e) {
return Result.error("获取权益信息失败: " + e.getMessage());
}
}
/**
* 验证权益
*/
@PostMapping("/verify-benefit")
public Result<Map<String, Object>> verifyBenefit(
@RequestBody BenefitVerifyRequest request,
HttpServletRequest httpRequest) {
try {
String openid = getOpenidFromToken(httpRequest);
boolean hasPermission = memberService.verifyBenefit(
openid,
request.getBenefitCode(),
request.getRequestCount()
);
Map<String, Object> result = Map.of(
"hasPermission", hasPermission,
"remainingCount", hasPermission ? "unlimited" : "0",
"resetTime", null
);
return Result.success(result);
} catch (Exception e) {
return Result.error("权益验证失败: " + e.getMessage());
}
}
private String getOpenidFromToken(HttpServletRequest request) {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
return jwtUtil.getOpenidFromToken(token);
}
throw new RuntimeException("无效的token");
}
}
8. 测试用例设计
8.1 功能测试用例
8.1.1 用户登录测试
| 测试用例ID | 测试场景 | 测试步骤 | 预期结果 |
|---|---|---|---|
| TC_LOGIN_001 | 正常登录 | 1. 点击登录按钮 2. 微信授权成功 3. 获取用户信息 |
登录成功,获取到openid和用户信息 |
| TC_LOGIN_002 | 取消授权 | 1. 点击登录按钮 2. 取消微信授权 |
登录失败,提示授权失败 |
| TC_LOGIN_003 | 网络异常 | 1. 断网状态下登录 2. 点击登录按钮 |
登录失败,提示网络异常 |
8.1.2 会员绑定测试
| 测试用例ID | 测试场景 | 测试步骤 | 预期结果 |
|---|---|---|---|
| TC_BIND_001 | 正常绑定VIP | 1. 输入有效VIP订单号 2. 点击绑定按钮 |
绑定成功,会员等级变为VIP |
| TC_BIND_002 | 正常绑定SVIP | 1. 输入有效SVIP订单号 2. 点击绑定按钮 |
绑定成功,会员等级变为SVIP |
| TC_BIND_003 | 无效订单号 | 1. 输入无效订单号 2. 点击绑定按钮 |
绑定失败,提示订单无效 |
| TC_BIND_004 | 空订单号 | 1. 不输入订单号 2. 点击绑定按钮 |
提示请输入订单号 |
| TC_BIND_005 | 重复绑定 | 1. 输入已绑定的订单号 2. 点击绑定按钮 |
绑定失败,提示订单已使用 |
8.1.3 权益验证测试
| 测试用例ID | 测试场景 | 测试步骤 | 预期结果 |
|---|---|---|---|
| TC_BENEFIT_001 | VIP权益验证 | 1. VIP用户访问高级功能 2. 系统验证权益 |
验证通过,允许访问 |
| TC_BENEFIT_002 | 免费用户权益验证 | 1. 免费用户访问VIP功能 2. 系统验证权益 |
验证失败,提示升级会员 |
| TC_BENEFIT_003 | 过期会员权益验证 | 1. 过期VIP用户访问VIP功能 2. 系统验证权益 |
验证失败,提示续费 |
8.2 接口测试用例
8.2.1 会员信息查询接口测试
// 测试脚本示例
describe('会员信息查询接口测试', () => {
test('正常查询会员信息', async () => {
const response = await request(app)
.get('/api/member/info')
.set('Authorization', 'Bearer valid_token')
.expect(200);
expect(response.body.code).toBe(200);
expect(response.body.data).toHaveProperty('memberId');
expect(response.body.data).toHaveProperty('memberLevel');
});
test('无效token查询', async () => {
const response = await request(app)
.get('/api/member/info')
.set('Authorization', 'Bearer invalid_token')
.expect(401);
expect(response.body.code).toBe(401);
});
});
8.3 性能测试用例
| 测试项目 | 测试指标 | 预期值 | 测试方法 |
|---|---|---|---|
| 接口响应时间 | 平均响应时间 | <500ms | JMeter压力测试 |
| 并发处理能力 | 并发用户数 | 1000+ | 并发访问测试 |
| 数据库查询性能 | 查询响应时间 | <100ms | SQL性能测试 |
9. 部署和运维
9.1 部署环境要求
服务器配置:
- CPU: 4核心以上
- 内存: 8GB以上
- 存储: 100GB以上SSD
- 网络: 100Mbps以上带宽
软件环境:
- 操作系统: CentOS 7.6+
- Java: JDK 1.8+
- 数据库: MySQL 8.0+
- 缓存: Redis 6.0+
- Web服务器: Nginx 1.18+
9.2 部署步骤
-
数据库初始化
mysql -u root -p < db/member_tables.sql -
应用部署
# 构建应用 mvn clean package -Dmaven.test.skip=true # 部署应用 java -jar xiaozhi-server.jar --spring.profiles.active=prod -
Nginx配置
server { listen 443 ssl; server_name api.xiaozhi.com; location /api/member/ { proxy_pass http://localhost:8091; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }
9.3 监控和日志
监控指标:
- 接口响应时间
- 错误率统计
- 数据库连接数
- 内存使用率
日志配置:
logging:
level:
com.xiaozhi.plus.service.MemberService: INFO
file:
name: /var/log/xiaozhi/member.log
max-size: 100MB
max-history: 30
10. 风险评估和应对策略
10.1 技术风险
| 风险项目 | 风险等级 | 影响描述 | 应对策略 |
|---|---|---|---|
| 微信API变更 | 中 | 接口不兼容 | 定期检查API文档,及时适配 |
| 数据库性能 | 中 | 查询缓慢 | 优化索引,使用缓存 |
| 并发安全 | 高 | 数据不一致 | 使用分布式锁,事务控制 |
10.2 业务风险
| 风险项目 | 风险等级 | 影响描述 | 应对策略 |
|---|---|---|---|
| 恶意绑定 | 高 | 订单被盗用 | 增强订单验证,限制绑定频率 |
| 权益滥用 | 中 | 超出预期使用 | 设置使用限制,监控异常 |
| 数据泄露 | 高 | 用户信息泄露 | 数据加密,访问控制 |
11. 项目计划和里程碑
11.1 开发计划
| 阶段 | 任务 | 工期 | 负责人 |
|---|---|---|---|
| 第1周 | 数据库设计和API接口开发 | 5天 | 后端开发 |
| 第2周 | 小程序页面和组件开发 | 5天 | 前端开发 |
| 第3周 | 功能联调和测试 | 5天 | 全员 |
| 第4周 | 部署上线和优化 | 5天 | 运维+开发 |
11.2 里程碑
- M1 (第1周末): 后端API开发完成
- M2 (第2周末): 前端页面开发完成
- M3 (第3周末): 功能测试通过
- M4 (第4周末): 正式上线发布
12. 附录
12.1 相关文档
12.2 联系方式
- 项目经理: [姓名] - [邮箱] - [电话]
- 技术负责人: [姓名] - [邮箱] - [电话]
- 测试负责人: [姓名] - [邮箱] - [电话]
文档结束
📋 本需求规格说明书为微信小程序会员绑定系统的完整技术方案,包含了从需求分析到部署上线的全流程内容。请各相关人员严格按照本文档执行开发和测试工作。