1805 lines
54 KiB
Markdown
1805 lines
54 KiB
Markdown
# 相亲小程序 · 完整开发规格文档
|
||
|
||
> **文档用途**:本文档供 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 API,async 支持 |
|
||
| 数据库 | 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 '模糊头像 URL(AI 生成或后端处理)',
|
||
|
||
-- 状态
|
||
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 Token(JWT)
|
||
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` 查用户,不存在则自动创建
|
||
- 返回 JWT,payload 包含 `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(手动浏览) | ai(AI推荐)
|
||
```
|
||
|
||
**响应数据(列表项,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|
|
||
|
||
### 后端 API(28 个接口)
|
||
|
||
|模块|接口数|
|
||
|---|---|
|
||
|认证(登录、刷新)|2|
|
||
|用户(获取、更新、提交审核、公开信息)|4|
|
||
|活动(列表、详情、报名、取消)|4|
|
||
|匹配(候选人、感兴趣、AI推荐、我的匹配、详情)|5|
|
||
|公告(列表、详情)|2|
|
||
|上传(头像、活动封面)|2|
|
||
|后台-看板|1|
|
||
|后台-用户管理(列表、详情、审核、封禁)|4|
|
||
|后台-活动管理(列表、创建、修改、发布)|4|
|
||
|合计|**28**|
|
||
|
||
---
|
||
|
||
_文档版本:v1.0 · 生成时间:2025年4月_
|
||
_如有接口变更,请同步更新本文档对应章节。_ |