一、系统概述
专家预测系统是体育直播平台核心商业化功能之一,在 “东莞梦幻网络科技” 开发的体育直播系统中,专家功能是平台商业化的重要组成部分,允许普通用户申请成为“专家”,在平台内发布赛事预测内容,其他用户可付费查看,平台从中抽成。系统需支持:
模块 | 功能描述 |
---|---|
专家认证 | 用户申请专家,后台审核通过后赋予专家身份 |
内容发布 | 专家发布赛事分析与预测文章,设置是否收费 |
付费阅读 | 普通用户购买后才能查看专家付费内容 |
数据统计 | 实时统计专家收益、预测胜率、粉丝量 |
下文将详细介绍该模块的核心功能设计与技术实现方案,包括专家审核流程、预测内容管理机制、付费系统集成、胜率与收益统计逻辑等关键技术点。
二、数据库设计(MySQL)
-- 用户表(简化)
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50),
is_expert TINYINT DEFAULT 0, -- 是否为专家
expert_status ENUM('pending', 'approved', 'rejected') DEFAULT 'pending'
);
-- 专家文章表
CREATE TABLE expert_articles (
id INT PRIMARY KEY AUTO_INCREMENT,
expert_id INT,
title VARCHAR(100),
content TEXT,
is_paid TINYINT DEFAULT 0,
price DECIMAL(10,2) DEFAULT 0.00,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 用户购买记录
CREATE TABLE article_purchases (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
article_id INT,
purchased_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 预测结果记录表(用于计算胜率)
CREATE TABLE expert_predictions (
id INT PRIMARY KEY AUTO_INCREMENT,
article_id INT,
result ENUM('win', 'lose', 'pending') DEFAULT 'pending'
);
AI 代码解读
三、后台管理端(PHP + ThinkPHP)
1. 用户申请专家接口(用户端发起)
// 控制器:ExpertController.php
public function applyExpert() {
$userId = session('user_id');
Db::name('users')->where('id', $userId)->update([
'expert_status' => 'pending'
]);
return json(['code' => 1, 'msg' => '申请已提交,等待审核']);
}
AI 代码解读
2. 后台审核专家申请
// 控制器:AdminExpertController.php
public function auditExpert($userId, $status) {
if (!in_array($status, ['approved', 'rejected'])) {
return json(['code' => 0, 'msg' => '非法状态']);
}
Db::name('users')->where('id', $userId)->update([
'is_expert' => $status == 'approved' ? 1 : 0,
'expert_status' => $status
]);
return json(['code' => 1, 'msg' => '审核完成']);
}
AI 代码解读
四、专家内容发布功能
1. 专家发布文章
public function publishArticle(Request $request) {
$expertId = session('user_id');
// 验证专家身份
$user = Db::name('users')->find($expertId);
if (!$user || !$user['is_expert']) {
return json(['code' => 0, 'msg' => '非专家用户']);
}
// 获取内容
$title = $request->post('title');
$content = $request->post('content');
$isPaid = $request->post('is_paid', 0);
$price = $request->post('price', 0.00);
Db::name('expert_articles')->insert([
'expert_id' => $expertId,
'title' => $title,
'content' => $content,
'is_paid' => $isPaid,
'price' => $price,
'created_at' => date('Y-m-d H:i:s')
]);
return json(['code' => 1, 'msg' => '发布成功']);
}
AI 代码解读
五、用户付费查看机制
1. 前端判断是否已购买
public function getArticle($articleId) {
$userId = session('user_id');
$article = Db::name('expert_articles')->find($articleId);
if ($article['is_paid']) {
$hasPurchased = Db::name('article_purchases')
->where(['user_id' => $userId, 'article_id' => $articleId])
->count();
if (!$hasPurchased) {
return json(['code' => 2, 'msg' => '请先付费购买']);
}
}
return json(['code' => 1, 'data' => $article]);
}
AI 代码解读
2. 用户付费接口
public function buyArticle($articleId) {
$userId = session('user_id');
// 假设余额足够,直接扣费逻辑略
Db::name('article_purchases')->insert([
'user_id' => $userId,
'article_id' => $articleId
]);
return json(['code' => 1, 'msg' => '购买成功']);
}
AI 代码解读
六、专家胜率与收益统计
1. 胜率统计逻辑(后台或API)
public function getExpertStats($expertId) {
$articles = Db::name('expert_articles')->where('expert_id', $expertId)->column('id');
if (!$articles) return json(['code' => 1, 'data' => ['win_rate' => '0%', 'income' => 0]]);
$total = Db::name('expert_predictions')->whereIn('article_id', $articles)->count();
$wins = Db::name('expert_predictions')->whereIn('article_id', $articles)->where('result', 'win')->count();
$income = Db::name('article_purchases')
->whereIn('article_id', $articles)
->count() * Db::name('expert_articles')->whereIn('id', $articles)->sum('price');
return json([
'code' => 1,
'data' => [
'win_rate' => $total ? round($wins / $total * 100, 2) . '%' : '0%',
'income' => $income
]
]);
}
AI 代码解读
七、Java Android客户端实现
专家预测发布Activity CreatePredictionActivity.java:
public class CreatePredictionActivity extends AppCompatActivity {
private EditText titleEditText;
private EditText contentEditText;
private EditText priceEditText;
private SwitchCompat freeSwitch;
private Spinner matchSpinner;
private Button submitButton;
private ApiService apiService;
private List<Match> matches = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_create_prediction);
apiService = RetrofitClient.getApiService();
titleEditText = findViewById(R.id.titleEditText);
contentEditText = findViewById(R.id.contentEditText);
priceEditText = findViewById(R.id.priceEditText);
freeSwitch = findViewById(R.id.freeSwitch);
matchSpinner = findViewById(R.id.matchSpinner);
submitButton = findViewById(R.id.submitButton);
freeSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
priceEditText.setEnabled(!isChecked);
if (isChecked) {
priceEditText.setText("0");
}
});
loadMatches();
submitButton.setOnClickListener(v -> submitPrediction());
}
private void loadMatches() {
apiService.getUpcomingMatches().enqueue(new Callback<List<Match>>() {
@Override
public void onResponse(Call<List<Match>> call, Response<List<Match>> response) {
if (response.isSuccessful() && response.body() != null) {
matches = response.body();
ArrayAdapter<Match> adapter = new ArrayAdapter<>(
CreatePredictionActivity.this,
android.R.layout.simple_spinner_item,
matches
);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
matchSpinner.setAdapter(adapter);
}
}
@Override
public void onFailure(Call<List<Match>> call, Throwable t) {
Toast.makeText(CreatePredictionActivity.this, "加载赛事失败", Toast.LENGTH_SHORT).show();
}
});
}
private void submitPrediction() {
String title = titleEditText.getText().toString().trim();
String content = contentEditText.getText().toString().trim();
String priceStr = priceEditText.getText().toString().trim();
boolean isFree = freeSwitch.isChecked();
Match selectedMatch = (Match) matchSpinner.getSelectedItem();
if (title.isEmpty() || content.isEmpty()) {
Toast.makeText(this, "请填写标题和内容", Toast.LENGTH_SHORT).show();
return;
}
double price = 0;
if (!isFree) {
try {
price = Double.parseDouble(priceStr);
if (price <= 0) {
Toast.makeText(this, "价格必须大于0", Toast.LENGTH_SHORT).show();
return;
}
} catch (NumberFormatException e) {
Toast.makeText(this, "请输入有效的价格", Toast.LENGTH_SHORT).show();
return;
}
}
CreatePredictionRequest request = new CreatePredictionRequest();
request.setTitle(title);
request.setContent(content);
request.setPrice(price);
request.setFree(isFree);
if (selectedMatch != null) {
request.setMatchId(selectedMatch.getId());
}
apiService.createPrediction(request).enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(Call<ApiResponse> call, Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().isSuccess()) {
Toast.makeText(CreatePredictionActivity.this, "预测发布成功", Toast.LENGTH_SHORT).show();
finish();
} else {
Toast.makeText(CreatePredictionActivity.this, "预测发布失败", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ApiResponse> call, Throwable t) {
Toast.makeText(CreatePredictionActivity.this, "网络错误", Toast.LENGTH_SHORT).show();
}
});
}
}
AI 代码解读
八、Vue.js H5移动端实现 - 付费阅读组件
PredictionDetail.vue:
<template>
<div class="prediction-detail">
<div v-if="loading" class="loading">加载中...</div>
<div v-else>
<h1>{
{ prediction.title }}</h1>
<div class="expert-info">
<img :src="prediction.expert.avatar" class="avatar">
<span>{
{ prediction.expert.name }}</span>
<span>胜率: {
{ prediction.expert.winRate }}%</span>
</div>
<div v-if="hasPurchased || prediction.isFree" class="content">
<div v-html="prediction.content"></div>
<div v-if="prediction.match" class="match-info">
<h3>相关赛事: {
{ prediction.match.name }}</h3>
<p>时间: {
{ prediction.match.time }}</p>
</div>
</div>
<div v-else class="pay-wall">
<p>此内容需要付费查看</p>
<p>价格: {
{ prediction.price }}元</p>
<button @click="purchase" :disabled="purchasing">
{
{ purchasing ? '购买中...' : '立即购买' }}
</button>
</div>
<div class="stats">
<span>浏览: {
{ prediction.viewCount }}</span>
<span>购买: {
{ prediction.purchaseCount }}</span>
</div>
</div>
</div>
</template>
<script>
import {
getPredictionDetail, purchasePrediction } from '@/api/expert';
export default {
data() {
return {
prediction: {
},
loading: true,
hasPurchased: false,
purchasing: false
};
},
async created() {
const id = this.$route.params.id;
try {
const response = await getPredictionDetail(id);
this.prediction = response.data;
this.hasPurchased = response.data.hasPurchased;
this.loading = false;
// 记录浏览
this.recordView(id);
} catch (error) {
console.error(error);
this.$toast.error('加载预测详情失败');
}
},
methods: {
async purchase() {
if (this.purchasing) return;
this.purchasing = true;
try {
const response = await purchasePrediction(this.prediction.id);
if (response.success) {
this.hasPurchased = true;
this.prediction.purchaseCount += 1;
this.$toast.success('购买成功');
} else {
this.$toast.error(response.message || '购买失败');
}
} catch (error) {
console.error(error);
this.$toast.error('网络错误');
} finally {
this.purchasing = false;
}
},
async recordView(id) {
try {
await recordPredictionView(id);
} catch (error) {
console.error('记录浏览失败', error);
}
}
}
};
</script>
<style scoped>
.prediction-detail {
padding: 15px;
}
.expert-info {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 10px;
}
.pay-wall {
text-align: center;
padding: 30px 0;
background: #f5f5f5;
border-radius: 5px;
margin: 20px 0;
}
.pay-wall button {
background: #ff6b00;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
}
.stats {
margin-top: 20px;
color: #999;
font-size: 14px;
}
.stats span {
margin-right: 15px;
}
</style>
AI 代码解读