xiangqinxiaochengxu/相亲小程序开发详情.md

1805 lines
54 KiB
Markdown
Raw Permalink Normal View History

2026-04-17 10:49:14 +08:00
# 相亲小程序 · 完整开发规格文档
> **文档用途**:本文档供 AI 编程助手直接执行包含完整的功能清单、目录结构、数据库设计、API 接口、页面逻辑和开发注意事项。
> **技术栈**:微信小程序原生(微信开发者工具) + Python FastAPI 后端
> **阅读顺序**:先看第 1-3 节了解全局,再按模块逐节实现。
---
## 目录
1. [项目概览](https://claude.ai/chat/18b08d3b-0364-4e53-804b-61b4cf5b63d4#1-%E9%A1%B9%E7%9B%AE%E6%A6%82%E8%A7%88)
2. [目录结构](https://claude.ai/chat/18b08d3b-0364-4e53-804b-61b4cf5b63d4#2-%E7%9B%AE%E5%BD%95%E7%BB%93%E6%9E%84)
3. [数据库设计](https://claude.ai/chat/18b08d3b-0364-4e53-804b-61b4cf5b63d4#3-%E6%95%B0%E6%8D%AE%E5%BA%93%E8%AE%BE%E8%AE%A1)
4. [后端 API 规格](https://claude.ai/chat/18b08d3b-0364-4e53-804b-61b4cf5b63d4#4-%E5%90%8E%E7%AB%AF-api-%E8%A7%84%E6%A0%BC)
5. [前端页面规格](https://claude.ai/chat/18b08d3b-0364-4e53-804b-61b4cf5b63d4#5-%E5%89%8D%E7%AB%AF%E9%A1%B5%E9%9D%A2%E8%A7%84%E6%A0%BC)
6. [AI 匹配算法](https://claude.ai/chat/18b08d3b-0364-4e53-804b-61b4cf5b63d4#6-ai-%E5%8C%B9%E9%85%8D%E7%AE%97%E6%B3%95)
7. [微信能力集成](https://claude.ai/chat/18b08d3b-0364-4e53-804b-61b4cf5b63d4#7-%E5%BE%AE%E4%BF%A1%E8%83%BD%E5%8A%9B%E9%9B%86%E6%88%90)
8. [后台管理端](https://claude.ai/chat/18b08d3b-0364-4e53-804b-61b4cf5b63d4#8-%E5%90%8E%E5%8F%B0%E7%AE%A1%E7%90%86%E7%AB%AF)
9. [开发注意事项](https://claude.ai/chat/18b08d3b-0364-4e53-804b-61b4cf5b63d4#9-%E5%BC%80%E5%8F%91%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9)
10. [环境配置](https://claude.ai/chat/18b08d3b-0364-4e53-804b-61b4cf5b63d4#10-%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE)
---
## 1. 项目概览
### 1.1 产品定位
线下相亲活动组织工具 + AI 辅助匹配平台。核心流程:
```
用户注册 → 填写资料 → 提交审核 → 审核通过 → 报名活动 / 参与匹配
```
### 1.2 技术栈总览
| 层 | 技术 | 说明 |
| ----- | ------------------------- | -------------------- |
| 小程序前端 | 微信小程序原生WXML/WXSS/JS | 使用微信开发者工具开发 |
| 后端框架 | Python 3.11 + FastAPI | RESTful APIasync 支持 |
| 数据库 | MySQL 8.0 | 主数据库 |
| 缓存 | Redis 7.x | 会话、频控、热数据缓存 |
| ORM | SQLAlchemy 2.x (async) | 数据库操作 |
| 数据库迁移 | Alembic | 版本管理 |
| 文件存储 | 腾讯云 COS | 头像/活动图片存储 |
| 消息推送 | 微信订阅消息 | 审核结果、匹配成功通知 |
| 后台管理 | Vue3 + Element Plus独立项目 | 管理员操作界面 |
### 1.3 用户角色
- **普通用户**:微信登录,填写资料,报名活动,参与匹配
- **管理员**:后台审核资料,管理活动,查看数据看板
- **超级管理员**:系统配置,管理员账号管理
---
## 2. 目录结构
### 2.1 小程序前端结构
```
miniprogram/
├── app.js # 全局入口,处理登录态初始化
├── app.json # 全局配置TabBar、页面注册
├── app.wxss # 全局样式
├── project.config.json # 项目配置
├── pages/
│ ├── home/ # 首页
│ │ ├── home.js
│ │ ├── home.wxml
│ │ ├── home.wxss
│ │ └── home.json
│ ├── activity/ # 活动列表页
│ │ ├── activity.js
│ │ ├── activity.wxml
│ │ ├── activity.wxss
│ │ └── activity.json
│ ├── activity-detail/ # 活动详情+报名页
│ │ └── ...
│ ├── match/ # 匹配主页
│ │ └── ...
│ ├── profile/ # 我的页面
│ │ └── ...
│ ├── profile-edit/ # 资料填写/编辑
│ │ └── ...
│ ├── audit-status/ # 审核状态追踪页
│ │ └── ...
│ ├── my-activities/ # 我报名的活动列表
│ │ └── ...
│ ├── my-matches/ # 我的匹配记录
│ │ └── ...
│ └── announcement/ # 公告详情页
│ └── ...
├── components/
│ ├── member-card/ # 会员卡片组件(首页随机展示用)
│ ├── activity-card/ # 活动卡片组件
│ ├── match-card/ # 匹配候选人卡片组件
│ ├── audit-badge/ # 认证/审核状态徽章
│ ├── progress-bar/ # 资料完整度进度条
│ └── empty-state/ # 空状态占位组件
└── utils/
├── request.js # 统一 HTTP 请求封装(含 token 刷新)
├── auth.js # 登录态管理
├── upload.js # 图片上传到 COS 封装
└── constants.js # 全局常量API_BASE_URL 等)
```
### 2.2 后端结构
```
backend/
├── main.py # FastAPI 入口
├── requirements.txt
├── .env # 环境变量(不提交 git
├── alembic.ini
├── alembic/
│ └── versions/ # 数据库迁移文件
├── app/
│ ├── __init__.py
│ ├── config.py # 配置读取(从 .env
│ ├── database.py # 数据库连接、Session 工厂
│ ├── dependencies.py # FastAPI 依赖注入current_user 等)
│ │
│ ├── models/ # SQLAlchemy ORM 模型
│ │ ├── __init__.py
│ │ ├── user.py
│ │ ├── activity.py
│ │ ├── registration.py
│ │ ├── match.py
│ │ └── announcement.py
│ │
│ ├── schemas/ # Pydantic 请求/响应模型
│ │ ├── __init__.py
│ │ ├── user.py
│ │ ├── activity.py
│ │ ├── match.py
│ │ └── common.py
│ │
│ ├── routers/ # 路由模块
│ │ ├── __init__.py
│ │ ├── auth.py # 登录/刷新 token
│ │ ├── users.py # 用户信息
│ │ ├── activities.py # 活动相关
│ │ ├── matches.py # 匹配相关
│ │ ├── announcements.py
│ │ ├── upload.py # 文件上传
│ │ └── admin/ # 后台接口(需 admin 权限)
│ │ ├── users.py
│ │ ├── activities.py
│ │ ├── matches.py
│ │ └── dashboard.py
│ │
│ ├── services/ # 业务逻辑层
│ │ ├── auth_service.py
│ │ ├── match_service.py # AI 匹配核心算法
│ │ ├── notify_service.py # 微信订阅消息发送
│ │ └── cos_service.py # 腾讯云 COS 操作
│ │
│ └── utils/
│ ├── security.py # JWT 生成/验证
│ └── wx_api.py # 微信 API 调用封装code2session 等)
```
---
## 3. 数据库设计
> **所有表**均包含 `created_at DATETIME` 和 `updated_at DATETIME`(自动维护)。
> **字符集**utf8mb4排序规则utf8mb4_unicode_ci。
### 3.1 users 表
```sql
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
openid VARCHAR(64) NOT NULL UNIQUE COMMENT '微信 openid',
unionid VARCHAR(64) COMMENT '微信 unionid可选',
-- 基础信息(用户填写)
nickname VARCHAR(50) COMMENT '昵称',
real_name VARCHAR(20) COMMENT '真实姓名(审核可见)',
gender TINYINT COMMENT '1=男 2=女',
birth_year SMALLINT COMMENT '出生年份',
city VARCHAR(30) COMMENT '所在城市',
height SMALLINT COMMENT '身高(cm)',
education TINYINT COMMENT '1=高中 2=大专 3=本科 4=硕士 5=博士',
job_industry VARCHAR(50) COMMENT '行业',
job_company VARCHAR(100) COMMENT '单位(仅匹配成功后展示)',
income_range TINYINT COMMENT '月收入段1=<5k 2=5-10k 3=10-20k 4=20k+',
marriage_status TINYINT DEFAULT 1 COMMENT '1=未婚 2=离异 3=丧偶',
-- 性格与兴趣
personality_tags JSON COMMENT '性格标签数组,如["开朗","细心"]',
hobbies JSON COMMENT '爱好数组,如["旅游","读书","健身"]',
-- 价值观问卷答案
value_answers JSON COMMENT '价值观问卷,格式: {"q1":2,"q2":1,...}',
-- 择偶偏好(用于匹配过滤)
prefer_age_min SMALLINT COMMENT '期望对方最小出生年份(越大越年轻)',
prefer_age_max SMALLINT COMMENT '期望对方最大出生年份',
prefer_city VARCHAR(30) COMMENT '期望城市null=不限',
prefer_education TINYINT COMMENT '期望最低学历null=不限',
-- 自我介绍
self_intro TEXT COMMENT '自我介绍500字内',
-- 头像
avatar_url VARCHAR(500) COMMENT 'COS 头像 URL',
avatar_blur_url VARCHAR(500) COMMENT '模糊头像 URLAI 生成或后端处理)',
-- 状态
audit_status TINYINT DEFAULT 0 COMMENT '0=未提交 1=待审核 2=审核通过 3=审核驳回',
audit_remark VARCHAR(500) COMMENT '驳回原因',
audit_time DATETIME COMMENT '审核时间',
auditor_id INT UNSIGNED COMMENT '审核人 admin ID',
-- 账号状态
is_active TINYINT DEFAULT 1 COMMENT '1=正常 0=封禁',
ban_reason VARCHAR(200),
-- 统计
profile_completeness TINYINT DEFAULT 0 COMMENT '资料完整度 0-100',
-- 微信订阅消息
subscribe_audit TINYINT DEFAULT 0 COMMENT '是否订阅审核结果通知',
subscribe_match TINYINT DEFAULT 0 COMMENT '是否订阅匹配成功通知',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_audit_status (audit_status),
INDEX idx_gender_city (gender, city)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
### 3.2 activities 表
```sql
CREATE TABLE activities (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(100) NOT NULL COMMENT '活动标题',
description TEXT COMMENT '活动详情(富文本/Markdown',
category VARCHAR(20) COMMENT '类型outdoor/dining/culture/sport/other',
location VARCHAR(200) COMMENT '活动地点',
cover_image VARCHAR(500) COMMENT '封面图 COS URL',
start_time DATETIME NOT NULL COMMENT '活动开始时间',
end_time DATETIME NOT NULL COMMENT '活动结束时间',
signup_deadline DATETIME COMMENT '报名截止时间',
capacity_male SMALLINT DEFAULT 0 COMMENT '男性名额0=不限)',
capacity_female SMALLINT DEFAULT 0 COMMENT '女性名额0=不限)',
-- 匹配窗口
match_window_hours INT DEFAULT 48 COMMENT '活动结束后开放匹配的小时数',
-- 状态
status TINYINT DEFAULT 0 COMMENT '0=草稿 1=报名中 2=报名截止 3=进行中 4=已结束',
is_published TINYINT DEFAULT 0 COMMENT '是否在小程序展示',
-- 报名要求
require_audit TINYINT DEFAULT 1 COMMENT '是否要求资料审核通过才能报名',
created_by INT UNSIGNED COMMENT '创建人 admin ID',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_status_published (status, is_published),
INDEX idx_start_time (start_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
### 3.3 registrations 表
```sql
CREATE TABLE registrations (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
activity_id INT UNSIGNED NOT NULL,
status TINYINT DEFAULT 1 COMMENT '1=待确认 2=已确认 3=已取消 4=已签到',
checked_in_at DATETIME COMMENT '签到时间',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_activity (user_id, activity_id),
INDEX idx_activity_id (activity_id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (activity_id) REFERENCES activities(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
### 3.4 match_likes 表(单向感兴趣记录)
```sql
CREATE TABLE match_likes (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
from_user_id INT UNSIGNED NOT NULL COMMENT '发起感兴趣的用户',
to_user_id INT UNSIGNED NOT NULL COMMENT '被感兴趣的用户',
source VARCHAR(20) DEFAULT 'manual' COMMENT 'manual=手动 ai=AI推荐',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_like (from_user_id, to_user_id),
INDEX idx_to_user (to_user_id),
FOREIGN KEY (from_user_id) REFERENCES users(id),
FOREIGN KEY (to_user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
### 3.5 matches 表(双向匹配成功记录)
```sql
CREATE TABLE matches (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_a_id INT UNSIGNED NOT NULL COMMENT '较小 user_id保证唯一性',
user_b_id INT UNSIGNED NOT NULL COMMENT '较大 user_id',
match_score DECIMAL(5,2) COMMENT 'AI 匹配分 0-100',
match_type VARCHAR(20) DEFAULT 'manual' COMMENT 'manual/ai',
source_activity_id INT UNSIGNED COMMENT '来源活动(如果有)',
-- 双方是否查看了对方信息
a_viewed TINYINT DEFAULT 0,
b_viewed TINYINT DEFAULT 0,
matched_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_match (user_a_id, user_b_id),
INDEX idx_user_a (user_a_id),
INDEX idx_user_b (user_b_id),
FOREIGN KEY (user_a_id) REFERENCES users(id),
FOREIGN KEY (user_b_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
### 3.6 announcements 表
```sql
CREATE TABLE announcements (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(100) NOT NULL,
content TEXT NOT NULL COMMENT 'Markdown 内容',
is_pinned TINYINT DEFAULT 0 COMMENT '是否置顶',
is_published TINYINT DEFAULT 1,
created_by INT UNSIGNED,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
### 3.7 admins 表
```sql
CREATE TABLE admins (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(128) NOT NULL COMMENT 'bcrypt hash',
role TINYINT DEFAULT 1 COMMENT '1=普通管理员 2=超级管理员',
is_active TINYINT DEFAULT 1,
last_login DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
---
## 4. 后端 API 规格
### 4.1 通用规范
```
Base URL开发: http://localhost:8000/api/v1
Base URL生产: https://your-domain.com/api/v1
认证方式: Bearer TokenJWT
Content-Type: application/json
统一响应格式:
{
"code": 0, // 0=成功非0=错误
"message": "ok",
"data": {} // 业务数据
}
错误码约定:
1001 - token 无效或过期
1002 - 权限不足
1003 - 资源不存在
1004 - 参数校验失败
1005 - 业务逻辑错误message 中包含具体原因)
```
### 4.2 认证接口
#### POST /auth/wx-login
微信登录,通过 code 换取系统 JWT token。
**请求体:**
```json
{
"code": "微信登录临时code",
"subscribe_audit": true, // 是否订阅审核通知(首次登录时传)
"subscribe_match": true
}
```
**响应数据:**
```json
{
"access_token": "eyJ...",
"token_type": "bearer",
"expires_in": 7200,
"user_id": 123,
"audit_status": 0,
"is_new_user": true
}
```
**后端实现要点:**
- 调用微信 `code2session` 接口获取 `openid`
-`openid` 查用户,不存在则自动创建
- 返回 JWTpayload 包含 `user_id`, `openid`, `exp`
#### POST /auth/refresh
刷新 token在 access_token 快过期时调用)。
---
### 4.3 用户接口
#### GET /users/me
获取当前用户完整信息(含审核状态)。
**响应数据:**
```json
{
"id": 123,
"nickname": "小明",
"gender": 1,
"birth_year": 1995,
"city": "北京",
"height": 175,
"education": 3,
"job_industry": "互联网",
"personality_tags": ["开朗", "细心"],
"hobbies": ["旅游", "读书"],
"self_intro": "...",
"avatar_url": "https://...",
"audit_status": 2,
"audit_remark": null,
"profile_completeness": 85
}
```
#### PUT /users/me
更新当前用户信息。提交后若 `audit_status` 为 0 或 3自动置为 1待审核
**请求体(所有字段均可选,仅传需要更新的):**
```json
{
"nickname": "小明",
"gender": 1,
"birth_year": 1995,
"city": "北京",
"height": 175,
"education": 3,
"job_industry": "互联网",
"job_company": "某公司",
"income_range": 3,
"personality_tags": ["开朗"],
"hobbies": ["旅游"],
"value_answers": {"q1": 2, "q2": 1, "q3": 3, "q4": 2},
"prefer_age_min": 1990,
"prefer_age_max": 2000,
"prefer_city": "北京",
"self_intro": "..."
}
```
**后端实现要点:**
- 更新后重新计算 `profile_completeness`(见计算规则 §6
-`audit_status` 为 2已通过用户修改资料后状态重置为 1待审核
#### POST /users/submit-audit
提交资料审核申请状态从0/3变为1。需要头像已上传。
#### GET /users/public/{user_id}
获取其他用户的公开信息(根据匹配阶段返回不同字段,见信息分级规则)。
**信息分级规则(后端强制执行):**
- 未匹配:返回 nickname, birth_year范围, city, personality_tags, avatar_blur_url
- 单向感兴趣(我对他/她表示过兴趣):额外返回 avatar_url, education, hobbies
- 双向匹配成功:额外返回 job_industry, job_company, self_intro, income_range
---
### 4.4 活动接口
#### GET /activities
获取活动列表。
**Query 参数:**
```
status: 1=报名中 4=已结束 (可选,不传返回所有已发布)
page: 页码默认1
page_size: 每页数量默认10
```
**响应数据(列表项):**
```json
{
"id": 1,
"title": "春日户外约会",
"category": "outdoor",
"cover_image": "https://...",
"start_time": "2025-04-20T14:00:00",
"end_time": "2025-04-20T18:00:00",
"signup_deadline": "2025-04-19T23:59:59",
"location": "北京朝阳公园",
"status": 1,
"capacity_male": 20,
"capacity_female": 20,
"registered_male": 15,
"registered_female": 12,
"is_registered": false, // 当前用户是否已报名
"can_register": true // 是否可以报名(考虑审核状态、名额等)
}
```
#### GET /activities/{id}
获取活动详情包含已报名人员的模糊头像列表最多展示8个异性
#### POST /activities/{id}/register
报名活动。
**后端校验(按顺序检查,失败立即返回对应错误):**
1. 活动是否存在且已发布
2. 活动报名是否未截止(`signup_deadline > now`
3. 活动 `require_audit=1` 时,用户 `audit_status` 须为 2
4. 对应性别名额是否未满
5. 用户是否已报名过该活动(幂等处理:已报名则返回成功)
**成功响应:**
```json
{
"registration_id": 456,
"status": 1,
"message": "报名成功,等待确认"
}
```
#### DELETE /activities/{id}/register
取消报名活动开始前24小时内不可取消
---
### 4.5 匹配接口
#### GET /matches/candidates
获取当日推荐候选人列表异性每天最多返回10条Redis 缓存当日推荐列表)。
**Query 参数:**
```
source: manual手动浏览 | aiAI推荐
```
**响应数据列表项Level 1 信息):**
```json
{
"user_id": 456,
"nickname": "小花",
"birth_year_range": "1993-1997", // 模糊处理5年范围
"city": "北京",
"personality_tags": ["温柔", "活泼"],
"avatar_blur_url": "https://...", // 模糊头像
"match_score": 87.5, // AI 推荐时返回
"match_reasons": ["同样喜欢旅游", "生活节奏相近"] // AI 推荐时返回
}
```
**后端实现要点:**
- 过滤已点过"不感兴趣"的用户
- 过滤已互相匹配成功的用户
- 今日已展示过的用户不重复出现Redis `SET` 记录)
#### POST /matches/like
对某用户表示感兴趣(触发双向检查)。
**请求体:**
```json
{
"to_user_id": 456,
"source": "manual"
}
```
**响应数据:**
```json
{
"is_mutual": true, // 是否产生了双向匹配
"match_id": 789, // 匹配成功时返回 match_id
"match_info": { // 匹配成功时返回对方 Level 2 信息
"nickname": "小花",
"avatar_url": "https://...",
"education": 3,
"hobbies": ["旅游"]
}
}
```
**后端实现要点:**
1. 写入 `match_likes`
2. 查询反向是否存在 `match_likes``from_user_id=to_user_id, to_user_id=from_user_id`
3. 若存在,在 `matches` 表创建记录user_a_id 取较小值)
4. 匹配成功后发送微信订阅消息给双方(异步任务)
#### POST /matches/ai-suggest
触发 AI 推荐返回得分最高的候选人列表最多5个
**响应同 /matches/candidates但包含详细 match_reasons。**
#### GET /matches/my
获取我的匹配成功列表。
**响应数据(列表项):**
```json
{
"match_id": 789,
"matched_at": "2025-04-15T20:30:00",
"other_user": {
"user_id": 456,
"nickname": "小花",
"avatar_url": "https://...", // 匹配成功后展示真实头像
"education": 3,
"city": "北京",
"hobbies": ["旅游"]
}
}
```
#### GET /matches/{match_id}/detail
获取匹配详情Level 3 完整信息,仅匹配双方可访问)。
---
### 4.6 公告接口
#### GET /announcements
获取公告列表(置顶的排在前面)。
```
page, page_size 参数同活动接口
```
#### GET /announcements/{id}
获取公告详情。
---
### 4.7 上传接口
#### POST /upload/avatar
上传用户头像。
**请求**multipart/form-data字段名 `file`,支持 jpg/png最大 5MB。
**后端处理:**
1. 接收文件
2. 压缩到 800x800 以内(使用 Pillow
3. 上传到腾讯云 COS路径`avatars/{user_id}/{timestamp}.jpg`
4. 生成模糊版本Pillow 高斯模糊radius=15上传到 COS
5. 更新 users 表 `avatar_url``avatar_blur_url`
**响应:**
```json
{
"avatar_url": "https://cos.../avatars/123/1713200000.jpg",
"avatar_blur_url": "https://cos.../avatars/123/1713200000_blur.jpg"
}
```
#### POST /upload/activity-cover
上传活动封面图(需 admin token
---
### 4.8 后台管理接口
所有 `/admin/*` 接口需要 `admin` 角色的 JWT token。
#### POST /admin/auth/login
管理员账号密码登录,返回 JWT。
#### GET /admin/dashboard
数据看板数据。
**响应数据:**
```json
{
"total_users": 1250,
"active_users_7d": 380,
"pending_audit": 23,
"total_activities": 45,
"ongoing_activities": 3,
"total_matches": 890,
"match_rate": 0.312,
"user_trend": [
{"date": "2025-04-01", "count": 12},
...
],
"match_trend": [...]
}
```
#### GET /admin/users
用户列表,支持搜索和过滤。
```
Query: keyword(昵称/真实姓名), audit_status, gender, page, page_size
```
#### GET /admin/users/{id}
获取用户完整信息(含 job_company 等敏感字段)。
#### PUT /admin/users/{id}/audit
审核用户资料。
**请求体:**
```json
{
"audit_status": 2, // 2=通过 3=驳回
"audit_remark": "照片不清晰,请重新上传" // 驳回时必填
}
```
**后端操作:**
1. 更新 `audit_status`, `audit_remark`, `audit_time`, `auditor_id`
2. 若用户订阅了审核通知,发送微信订阅消息
#### PUT /admin/users/{id}/ban
封禁/解封用户。
#### GET /admin/activities
活动列表(含草稿)。
#### POST /admin/activities
创建活动。
**请求体(完整字段):**
```json
{
"title": "春日约会",
"description": "Markdown 内容",
"category": "outdoor",
"location": "北京朝阳公园",
"cover_image": "https://...",
"start_time": "2025-04-20T14:00:00",
"end_time": "2025-04-20T18:00:00",
"signup_deadline": "2025-04-19T23:59:59",
"capacity_male": 20,
"capacity_female": 20,
"match_window_hours": 48,
"require_audit": true,
"is_published": false
}
```
#### PUT /admin/activities/{id}
修改活动(已结束的活动不可修改)。
#### PUT /admin/activities/{id}/publish
发布/下架活动(`is_published` 切换)。
#### GET /admin/matches
匹配记录列表支持按用户ID、日期过滤。
#### GET /admin/system/config
获取系统配置AI 权重、每日推荐上限等)。
#### PUT /admin/system/config
更新系统配置。
---
## 5. 前端页面规格
### 5.1 app.json 配置
```json
{
"pages": [
"pages/home/home",
"pages/activity/activity",
"pages/match/match",
"pages/profile/profile",
"pages/activity-detail/activity-detail",
"pages/profile-edit/profile-edit",
"pages/audit-status/audit-status",
"pages/my-activities/my-activities",
"pages/my-matches/my-matches",
"pages/announcement/announcement"
],
"tabBar": {
"color": "#999999",
"selectedColor": "#E8593C",
"borderStyle": "white",
"list": [
{
"pagePath": "pages/home/home",
"text": "首页",
"iconPath": "assets/icons/home.png",
"selectedIconPath": "assets/icons/home-active.png"
},
{
"pagePath": "pages/activity/activity",
"text": "活动",
"iconPath": "assets/icons/activity.png",
"selectedIconPath": "assets/icons/activity-active.png"
},
{
"pagePath": "pages/match/match",
"text": "匹配",
"iconPath": "assets/icons/match.png",
"selectedIconPath": "assets/icons/match-active.png"
},
{
"pagePath": "pages/profile/profile",
"text": "我的",
"iconPath": "assets/icons/profile.png",
"selectedIconPath": "assets/icons/profile-active.png"
}
]
},
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "相遇",
"navigationBarTextStyle": "black"
}
}
```
### 5.2 app.js 全局初始化逻辑
```javascript
// app.js 核心逻辑(伪代码描述)
App({
globalData: {
userInfo: null,
token: null,
baseUrl: 'https://your-domain.com/api/v1'
},
onLaunch() {
// 1. 检查本地 token
const token = wx.getStorageSync('token');
if (token) {
this.globalData.token = token;
// 2. 验证 token 有效性(调用 /users/me
// 3. 若失败则重新登录
} else {
this.wxLogin();
}
},
async wxLogin() {
// 1. wx.login() 获取 code
// 2. 调用后端 /auth/wx-login
// 3. 存储 token 到 globalData 和 storage
// 4. 若是新用户,跳转到资料填写页
}
});
```
### 5.3 首页home
**功能模块:**
1. **活动预告轮播**(头部 swiper
- 展示 `status=1`(报名中)的已发布活动
- 每张卡片显示:封面图、标题、时间、剩余名额(「还剩 N 个名额」)
- 点击跳转到活动详情页
2. **公告栏**(轮播或单行滚动)
- 展示最新置顶公告标题
- 点击跳转公告详情页
3. **会员展示区**(随机卡片列表)
- 调用 `GET /matches/candidates?source=manual`
- 展示 Level 1 信息:模糊头像、昵称、城市、性格标签
- 点击卡片跳转到对应用户的公开页(弹出半屏 modal
**页面加载逻辑:**
```
onLoad:
1. 检查登录态,未登录则调用 app.wxLogin()
2. 并发请求GET /activities?status=1&page_size=5 和 GET /announcements?page_size=3
3. 展示轮播和公告
onPullDownRefresh:
刷新活动和会员列表
```
### 5.4 活动页activity
**功能模块:**
1. **分类 Tab**:全部 / 户外 / 餐饮 / 文化 / 体育
2. **活动卡片列表**(上拉加载更多)
3. **活动状态标签**:报名中(绿)/ 即将截止(橙)/ 已结束(灰)
**活动详情页activity-detail**
1. 封面图(全宽展示)
2. 活动基本信息(时间、地点、人数)
3. 名额进度条:`男 15/20 · 女 12/20`
4. 已报名人员预览模糊头像列表最多8个异性
5. 活动详情内容(富文本渲染)
6. **底部报名按钮**,状态:
- 未审核通过:「完善资料后可报名」→ 点击跳转资料页
- 可报名:「立即报名」
- 已报名:「已报名 ✓」
- 名额已满:「名额已满」
- 已截止:「报名已截止」
### 5.5 匹配页match
**布局:**
1. **顶部 AI 匹配按钮**(醒目展示)
2. **今日推荐列表**(卡片式,每张展示 Level 1 信息)
3. 每张卡片底部:「感兴趣 ♥」和「跳过」按钮
**AI 匹配按钮逻辑:**
```
点击 → 调用 POST /matches/ai-suggest
→ 展示匹配结果(含匹配分数和共同点)
→ 用户可逐个查看并点击感兴趣
```
**点击「感兴趣」逻辑:**
```
1. 调用 POST /matches/like
2. 若 is_mutual=true
- 弹出「匹配成功!」动画弹窗
- 展示对方 Level 2 信息
- 提示「可在「我的匹配」中查看完整信息」
3. 若 is_mutual=false
- 卡片消失,显示「已发送喜欢」
```
**用户信息弹窗(点击卡片展开):**
- 展示 Level 1 信息(未点感兴趣前)
- 展示 Level 2 信息(已点感兴趣后)
### 5.6 我的页面profile
**布局(从上到下):**
1. **用户卡片**:头像 + 昵称 + 审核状态徽章(待审核/已通过/未提交)
2. **资料完整度进度条**`85% 完整,继续完善提升匹配率 →`
3. **功能菜单列表**
- 编辑资料 → `/profile-edit`
- 审核状态 → `/audit-status`
- 我的活动 → `/my-activities`
- 匹配记录 → `/my-matches`
- 隐私设置(暂时展示,功能后续迭代)
**审核状态页audit-status展示**
```
状态为 0未提交: 引导填写资料并提交
状态为 1待审核: 展示进度提示「预计 4 小时内完成审核」
状态为 2已通过: 绿色勾,展示通过时间
状态为 3驳回 : 红色提示,展示驳回原因,「重新提交」按钮
```
### 5.7 资料填写页profile-edit
**分步骤填写Step Indicator 展示进度):**
- Step 1基本信息性别、出生年、城市、身高、学历
- Step 2职业信息行业、公司名称
- Step 3性格与兴趣标签多选爱好最多选8个
- Step 4价值观问卷4道场景题单选
- Step 5择偶偏好年龄范围、城市、学历要求
- Step 6头像上传 + 自我介绍
**每步保存逻辑:**
- 每步完成后立即调用 `PUT /users/me` 保存(非全部填完才保存)
- 允许用户跳步,但提交审核时校验必填字段
**头像上传:**
```
wx.chooseMedia → 压缩 → 上传到后端 /upload/avatar
→ 后端处理并存 COS → 返回 URL → 本地展示
```
**价值观问卷题目4 题):**
```
Q1. 周末你更倾向于?
A. 宅在家休息看剧 B. 约三五好友聚餐 C. 户外运动或旅行
Q2. 婚后与父母同住这件事,你的态度是?
A. 希望独立居住 B. 可以接受偶尔同住 C. 愿意长期同住
Q3. 对于两人的经济管理方式,你倾向于?
A. 各自管理各自的 B. 合并统一管理 C. 部分共用部分独立
Q4. 关于要孩子这件事?
A. 明确想要孩子 B. 顺其自然 C. 目前不想要
```
---
## 6. AI 匹配算法
### 6.1 完整实现match_service.py
```python
# app/services/match_service.py
from typing import List, Dict
import math
from app.models.user import User
# 各维度权重(可从数据库 system_config 表读取,实现动态调参)
WEIGHTS = {
"age": 0.20,
"hobbies": 0.18,
"values": 0.18,
"education": 0.14,
"activity": 0.12, # 用户活跃度
"location": 0.10,
"height": 0.08,
}
def calculate_match_score(user_a: User, user_b: User) -> Dict:
"""
计算两个用户的匹配分数0-100
user_a 是发起匹配的用户user_b 是候选人。
返回: {"score": 87.5, "reasons": ["同样喜欢旅游"]}
"""
scores = {}
reasons = []
# 1. 年龄契合度(基于对方的择偶偏好)
age_score = _age_score(user_a, user_b)
scores["age"] = age_score
# 2. 兴趣重叠度Jaccard 相似度)
hobby_score, hobby_overlap = _hobby_score(user_a, user_b)
scores["hobbies"] = hobby_score
if hobby_overlap:
reasons.append(f"都喜欢{hobby_overlap[0]}")
# 3. 价值观相似度(问卷答案加权欧氏距离)
value_score, value_reason = _value_score(user_a, user_b)
scores["values"] = value_score
if value_reason:
reasons.append(value_reason)
# 4. 学历背景(符合对方偏好加分)
scores["education"] = _education_score(user_a, user_b)
# 5. 活跃度(最近 7 天有登录记录)
scores["activity"] = _activity_score(user_b)
# 6. 地理位置(同城 100同省 60跨省 20
location_score = _location_score(user_a, user_b)
scores["location"] = location_score
if location_score == 100:
reasons.append("同城")
# 7. 身高偏好
scores["height"] = _height_score(user_a, user_b)
# 加权求和
total = sum(scores[dim] * WEIGHTS[dim] for dim in scores)
return {
"score": round(total, 1),
"reasons": reasons[:3] # 最多返回3条理由
}
def _age_score(user_a: User, user_b: User) -> float:
"""检查 user_b 的年龄是否在 user_a 的择偶年龄偏好范围内"""
if not user_b.birth_year:
return 50.0
if user_a.prefer_age_min and user_b.birth_year < user_a.prefer_age_min:
return 0.0
if user_a.prefer_age_max and user_b.birth_year > user_a.prefer_age_max:
return 0.0
# 在偏好范围内,越接近中间值分越高
if user_a.prefer_age_min and user_a.prefer_age_max:
mid = (user_a.prefer_age_min + user_a.prefer_age_max) / 2
diff = abs(user_b.birth_year - mid)
half_range = (user_a.prefer_age_max - user_a.prefer_age_min) / 2
return max(0, 100 - (diff / half_range) * 50)
return 80.0
def _hobby_score(user_a: User, user_b: User):
"""Jaccard 相似度计算兴趣重叠"""
hobbies_a = set(user_a.hobbies or [])
hobbies_b = set(user_b.hobbies or [])
if not hobbies_a or not hobbies_b:
return 30.0, []
intersection = hobbies_a & hobbies_b
union = hobbies_a | hobbies_b
jaccard = len(intersection) / len(union)
return round(jaccard * 100, 1), list(intersection)
def _value_score(user_a: User, user_b: User):
"""价值观问卷:对应题目答案相同则加分"""
answers_a = user_a.value_answers or {}
answers_b = user_b.value_answers or {}
if not answers_a or not answers_b:
return 50.0, None
total_q = 4 # 总题目数
matched = 0
for q in ["q1", "q2", "q3", "q4"]:
if answers_a.get(q) == answers_b.get(q):
matched += 1
score = (matched / total_q) * 100
reason = None
if matched >= 3:
reason = "生活节奏相近"
elif matched >= 2:
reason = "价值观较为一致"
return round(score, 1), reason
def _education_score(user_a: User, user_b: User) -> float:
"""user_b 的学历是否达到 user_a 的偏好"""
if not user_a.prefer_education:
return 80.0
if not user_b.education:
return 40.0
if user_b.education >= user_a.prefer_education:
return 100.0
# 每差一级扣 25 分
diff = user_a.prefer_education - user_b.education
return max(0, 100 - diff * 25)
def _activity_score(user_b: User) -> float:
"""基于最近活跃时间评分"""
from datetime import datetime, timedelta
if not user_b.updated_at:
return 30.0
days_inactive = (datetime.now() - user_b.updated_at).days
if days_inactive <= 3:
return 100.0
elif days_inactive <= 7:
return 80.0
elif days_inactive <= 30:
return 50.0
return 20.0
def _location_score(user_a: User, user_b: User) -> float:
if not user_a.city or not user_b.city:
return 50.0
if user_a.city == user_b.city:
return 100.0
# 简单实现:同省判断(实际可接入城市数据库)
# 此处以城市名前两字作为省份简单判断
if user_a.city[:2] == user_b.city[:2]:
return 60.0
return 20.0
def _height_score(user_a: User, user_b: User) -> float:
"""简单身高偏好:女性用户对男性 175+ 额外加分"""
if not user_b.height:
return 50.0
if user_a.gender == 2 and user_b.gender == 1: # 女看男
if user_b.height >= 175:
return 100.0
elif user_b.height >= 170:
return 70.0
return 40.0
return 70.0 # 其他情况默认中等分
def get_ai_candidates(
current_user: User,
all_candidates: List[User],
limit: int = 5,
diversity_ratio: float = 0.3
) -> List[Dict]:
"""
获取 AI 推荐候选人列表。
diversity_ratio: 多样性候选人比例(避免推荐列表同质化)
"""
scored = []
for candidate in all_candidates:
result = calculate_match_score(current_user, candidate)
scored.append({
"user": candidate,
"score": result["score"],
"reasons": result["reasons"]
})
scored.sort(key=lambda x: x["score"], reverse=True)
# 主列表:高分候选
main_count = math.ceil(limit * (1 - diversity_ratio))
main_list = scored[:main_count]
# 多样性候选:从 main_count+1 到 main_count+10 中随机取
import random
diverse_pool = scored[main_count:main_count + 10]
diverse_count = limit - main_count
diverse_list = random.sample(diverse_pool, min(diverse_count, len(diverse_pool)))
return main_list + diverse_list
def calculate_profile_completeness(user: User) -> int:
"""
计算用户资料完整度0-100
各字段权重不同,越重要的字段权重越高。
"""
fields = {
"nickname": 5,
"gender": 5,
"birth_year": 5,
"city": 5,
"height": 3,
"education": 5,
"job_industry": 5,
"personality_tags": 8, # 至少3个
"hobbies": 8, # 至少3个
"value_answers": 10, # 4题全答完
"avatar_url": 15, # 头像权重最高
"self_intro": 8,
"prefer_age_min": 3,
"prefer_city": 3,
"prefer_education": 3,
"job_company": 4,
"income_range": 5,
}
total_weight = sum(fields.values())
earned = 0
for field, weight in fields.items():
val = getattr(user, field, None)
if field == "personality_tags":
if val and len(val) >= 3:
earned += weight
elif val and len(val) >= 1:
earned += weight * 0.5
elif field == "hobbies":
if val and len(val) >= 3:
earned += weight
elif val and len(val) >= 1:
earned += weight * 0.5
elif field == "value_answers":
if val and len(val) >= 4:
earned += weight
elif val and len(val) >= 2:
earned += weight * 0.5
elif val:
earned += weight
return round((earned / total_weight) * 100)
```
---
## 7. 微信能力集成
### 7.1 登录流程(完整实现)
```python
# app/utils/wx_api.py
import httpx
from app.config import settings
async def code2session(code: str) -> dict:
"""
调用微信 code2session 接口获取 openid
返回: {"openid": "xxx", "session_key": "xxx"}
"""
url = "https://api.weixin.qq.com/sns/jscode2session"
params = {
"appid": settings.WX_APPID,
"secret": settings.WX_SECRET,
"js_code": code,
"grant_type": "authorization_code"
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
data = resp.json()
if "errcode" in data and data["errcode"] != 0:
raise ValueError(f"微信登录失败: {data.get('errmsg')}")
return data
```
### 7.2 订阅消息发送
```python
# app/services/notify_service.py
import httpx
from app.config import settings
# 需在微信公众平台申请的订阅消息模板 ID
TEMPLATE_AUDIT_RESULT = "your_audit_template_id" # 审核结果通知模板
TEMPLATE_MATCH_SUCCESS = "your_match_template_id" # 匹配成功通知模板
async def get_access_token() -> str:
"""获取微信接口调用凭证(需加 Redis 缓存,有效期 2 小时)"""
url = "https://api.weixin.qq.com/cgi-bin/token"
params = {
"grant_type": "client_credential",
"appid": settings.WX_APPID,
"secret": settings.WX_SECRET
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
return resp.json()["access_token"]
async def send_audit_result(openid: str, passed: bool, remark: str = ""):
"""发送审核结果订阅消息"""
access_token = await get_access_token()
url = f"https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={access_token}"
payload = {
"touser": openid,
"template_id": TEMPLATE_AUDIT_RESULT,
"miniprogram_state": "formal",
"lang": "zh_CN",
"data": {
"thing1": {"value": "资料审核结果通知"},
"phrase2": {"value": "审核通过" if passed else "审核未通过"},
"thing3": {"value": remark or ("恭喜,您的资料已通过审核!" if passed else "请根据提示修改后重新提交")},
}
}
async with httpx.AsyncClient() as client:
await client.post(url, json=payload)
async def send_match_success(openid: str, other_nickname: str):
"""发送匹配成功订阅消息"""
access_token = await get_access_token()
url = f"https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={access_token}"
payload = {
"touser": openid,
"template_id": TEMPLATE_MATCH_SUCCESS,
"page": "pages/my-matches/my-matches", # 点击消息跳转页
"miniprogram_state": "formal",
"data": {
"name3": {"value": other_nickname},
"thing4": {"value": "你们互相感兴趣,匹配成功啦!快去查看吧"},
}
}
async with httpx.AsyncClient() as client:
await client.post(url, json=payload)
```
### 7.3 前端请求封装
```javascript
// utils/request.js
const app = getApp();
const request = (options) => {
return new Promise((resolve, reject) => {
const token = wx.getStorageSync('token');
wx.request({
url: `${app.globalData.baseUrl}${options.url}`,
method: options.method || 'GET',
data: options.data || {},
header: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : ''
},
success(res) {
if (res.statusCode === 401) {
// Token 失效,重新登录
wx.removeStorageSync('token');
app.wxLogin();
reject(new Error('登录已过期'));
return;
}
if (res.data.code !== 0) {
wx.showToast({ title: res.data.message || '请求失败', icon: 'none' });
reject(res.data);
return;
}
resolve(res.data.data);
},
fail(err) {
wx.showToast({ title: '网络异常,请重试', icon: 'none' });
reject(err);
}
});
});
};
export default request;
```
---
## 8. 后台管理端
> 后台管理使用独立的 Vue3 + Element Plus 项目(非小程序)。以下是关键页面规格。
### 8.1 页面列表
|路由|页面|核心功能|
|---|---|---|
|`/login`|管理员登录|账号密码登录|
|`/dashboard`|数据看板|用户趋势、匹配率、审核积压量图表|
|`/users`|用户管理|搜索、筛选、查看详情|
|`/users/audit`|待审核队列|重点页面,快速审核|
|`/activities`|活动列表|增删改查、发布/下架|
|`/activities/new`|创建活动|表单|
|`/matches`|匹配记录|查看所有匹配,可按用户筛选|
|`/announcements`|公告管理|增删改查|
|`/settings`|系统设置|AI 权重调参|
### 8.2 待审核队列页面逻辑
这是管理员最高频使用的页面,需优化操作效率:
1. 列表展示待审核用户(按提交时间升序,越早越靠前)
2. 显示已等待时长(超过 4 小时标红提示 SLA 警告)
3. 点击用户展开侧边栏,显示完整资料和头像
4. 侧边栏操作:**通过** 按钮(绿色)+ **驳回** 按钮(红色,点击后弹出输入框填写原因)
5. 操作完成后自动加载下一条
---
## 9. 开发注意事项
### 9.1 微信小程序开发注意事项
```
【安全域名配置】
- 微信公众平台 → 开发管理 → 开发设置 → 服务器域名
- request 合法域名:填入后端服务器域名(必须 HTTPS
- uploadFile 合法域名:填入腾讯云 COS 域名
- 开发阶段可在微信开发者工具中勾选「不校验合法域名」
【登录流程注意】
- wx.login() 返回的 code 有效期为 5 分钟,只能使用一次
- 不要在前端存储 session_key只存系统颁发的 JWT token
- JWT 推荐有效期 7 天,结合 refresh token 机制
【图片上传注意】
- wx.chooseMedia 替代已废弃的 wx.chooseImage基础库 2.10.0+
- 上传前在前端压缩quality 参数),减少上传时间
- 上传到后端后由后端再次压缩,避免大图存入 COS
【分包加载(后续优化)】
- 当小程序代码超过 1.5MB 时需分包
- 匹配相关页面可放入子包
【wx.request 并发限制】
- 微信限制同时最多 10 个并发请求
- 首页多接口并发请求时注意控制数量
```
### 9.2 后端 FastAPI 开发注意事项
```
【异步数据库操作】
- 使用 SQLAlchemy 2.x async 引擎,配合 asyncmy 驱动
- pip install sqlalchemy[asyncio] asyncmy
- 所有数据库操作使用 async with AsyncSession 上下文
【JWT 配置】
- SECRET_KEY 使用 openssl rand -hex 32 生成,存入 .env
- 算法推荐 HS256
- payload 中仅存 user_id不存敏感信息
【CORS 配置】
- 本地开发allow_origins=["*"]
- 生产环境allow_origins=["https://your-admin-domain.com"]
- 微信小程序请求不受 CORS 限制,但后台管理 Vue 项目会受影响
【频率限制】
- 匹配接口(/matches/like同一用户每分钟最多 20 次Redis 计数
- 登录接口:同一 IP 每分钟最多 10 次
【图片处理】
- pip install Pillow
- 头像模糊处理ImageFilter.GaussianBlur(radius=15)
- 上传前校验文件类型(检查文件头 magic bytes而非仅看后缀名
【双向匹配并发安全】
- 两个用户同时互相点感兴趣时可能产生竞态条件
- matches 表有 UNIQUE KEY (user_a_id, user_b_id),用数据库唯一约束保证
- 插入时使用 INSERT IGNORE 或捕获 IntegrityError
【订阅消息异步发送】
- 不要在请求链路内同步发送微信消息(影响响应速度)
- 使用 FastAPI BackgroundTasks 异步发送
- 或接入 Celery + Redis 任务队列(生产环境推荐)
```
### 9.3 数据库注意事项
```
【JSON 字段使用】
- MySQL 5.7+ 支持 JSON 类型
- personality_tags、hobbies、value_answers 使用 JSON 字段
- SQLAlchemy 中使用 JSON 类型映射
【时区处理】
- MySQL 服务器时区设置为 Asia/Shanghai
- 所有 DATETIME 字段存储本地时间(简化处理)
- 前端展示时注意格式化
【索引设计】
- 高频查询users(gender, city, audit_status),建复合索引
- match_likes(from_user_id, to_user_id) 已有 UNIQUE 约束(自带索引)
- activities(status, is_published, start_time) 建复合索引
【Alembic 迁移】
- 每次修改 models 后运行alembic revision --autogenerate -m "描述"
- 应用迁移alembic upgrade head
- 永远不要在生产环境直接修改表结构
```
### 9.4 腾讯云 COS 配置
```python
# app/services/cos_service.py
from qcloud_cos import CosConfig, CosS3Client
from app.config import settings
def get_cos_client():
config = CosConfig(
Region=settings.COS_REGION,
SecretId=settings.COS_SECRET_ID,
SecretKey=settings.COS_SECRET_KEY
)
return CosS3Client(config)
def upload_file(local_path: str, cos_key: str) -> str:
"""上传文件到 COS返回公开访问 URL"""
client = get_cos_client()
client.upload_file(
Bucket=settings.COS_BUCKET,
LocalFilePath=local_path,
Key=cos_key,
)
return f"https://{settings.COS_BUCKET}.cos.{settings.COS_REGION}.myqcloud.com/{cos_key}"
# pip install cos-python-sdk-v5
```
---
## 10. 环境配置
### 10.1 .env 文件(后端)
```env
# 微信小程序
WX_APPID=your_wx_appid
WX_SECRET=your_wx_app_secret
# JWT
JWT_SECRET_KEY=your_super_secret_key_generated_by_openssl
JWT_ALGORITHM=HS256
JWT_EXPIRE_HOURS=168 # 7天
# 数据库
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=matchmaking
DB_USER=root
DB_PASSWORD=your_db_password
# Redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_DB=0
# 腾讯云 COS
COS_REGION=ap-beijing
COS_BUCKET=your-bucket-name
COS_SECRET_ID=your_secret_id
COS_SECRET_KEY=your_secret_key
# 微信订阅消息模板 ID
WX_TEMPLATE_AUDIT=your_audit_template_id
WX_TEMPLATE_MATCH=your_match_template_id
# 系统配置
DAILY_RECOMMEND_LIMIT=10 # 每日推荐上限
AUDIT_SLA_HOURS=4 # 审核 SLA 小时数
```
### 10.2 requirements.txt
```
fastapi==0.111.0
uvicorn[standard]==0.29.0
sqlalchemy[asyncio]==2.0.30
asyncmy==0.2.9
alembic==1.13.1
pydantic==2.7.1
pydantic-settings==2.2.1
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
httpx==0.27.0
Pillow==10.3.0
python-multipart==0.0.9
redis==5.0.4
cos-python-sdk-v5==1.9.30
python-dotenv==1.0.1
```
### 10.3 启动命令
```bash
# 安装依赖
pip install -r requirements.txt
# 初始化数据库(首次)
alembic upgrade head
# 创建初始管理员账号(运行一次)
python scripts/create_admin.py --username admin --password your_password
# 启动开发服务器
uvicorn main:app --reload --host 0.0.0.0 --port 8000
# 生产环境启动(多 worker
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
```
### 10.4 main.py 入口
```python
# main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.routers import auth, users, activities, matches, announcements, upload
from app.routers.admin import users as admin_users, activities as admin_activities
from app.routers.admin import matches as admin_matches, dashboard
app = FastAPI(title="相亲小程序 API", version="1.0.0")
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 生产环境改为具体域名
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 注册路由
app.include_router(auth.router, prefix="/api/v1/auth", tags=["认证"])
app.include_router(users.router, prefix="/api/v1/users", tags=["用户"])
app.include_router(activities.router, prefix="/api/v1/activities", tags=["活动"])
app.include_router(matches.router, prefix="/api/v1/matches", tags=["匹配"])
app.include_router(announcements.router, prefix="/api/v1/announcements", tags=["公告"])
app.include_router(upload.router, prefix="/api/v1/upload", tags=["上传"])
# 后台路由
app.include_router(dashboard.router, prefix="/api/v1/admin/dashboard", tags=["后台-看板"])
app.include_router(admin_users.router, prefix="/api/v1/admin/users", tags=["后台-用户"])
app.include_router(admin_activities.router, prefix="/api/v1/admin/activities", tags=["后台-活动"])
app.include_router(admin_matches.router, prefix="/api/v1/admin/matches", tags=["后台-匹配"])
@app.get("/health")
async def health_check():
return {"status": "ok"}
```
---
## 附录功能清单总览MVP 版本)
### 小程序前端9 个页面)
|页面|功能|状态|
|---|---|---|
|首页|活动预告轮播、公告、会员展示|MVP|
|活动列表|分类浏览、上拉加载|MVP|
|活动详情|详情展示、报名/取消报名|MVP|
|匹配页|候选人浏览、感兴趣/跳过、AI推荐|MVP|
|我的|个人信息、功能入口|MVP|
|资料编辑|6步填写、头像上传|MVP|
|审核状态|状态展示、驳回原因|MVP|
|我的活动|报名活动列表|MVP|
|我的匹配|匹配成功列表、查看详情|MVP|
### 后端 API28 个接口)
|模块|接口数|
|---|---|
|认证(登录、刷新)|2|
|用户(获取、更新、提交审核、公开信息)|4|
|活动(列表、详情、报名、取消)|4|
|匹配候选人、感兴趣、AI推荐、我的匹配、详情|5|
|公告(列表、详情)|2|
|上传(头像、活动封面)|2|
|后台-看板|1|
|后台-用户管理(列表、详情、审核、封禁)|4|
|后台-活动管理(列表、创建、修改、发布)|4|
|合计|**28**|
---
_文档版本v1.0 · 生成时间2025年4月_
_如有接口变更请同步更新本文档对应章节。_