一、功能概述
为保障体育直播平台内容的专业性与可信度,“东莞梦幻网络科技” 体育直播系统引入“专家认证”机制,对申请成为解说嘉宾、分析师或主播的用户进行严格身份审核。通过认证的专家将获得“专家标识”并享有专属权限,如发布赛事预测方案(可设置收费)、专家动态、专家视频、发起专家资讯等。
二、数据库设计 (MySQL)
-- 用户基础表
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
password VARCHAR(255) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
phone VARCHAR(20),
avatar VARCHAR(255),
status TINYINT DEFAULT 1 COMMENT '1-正常, 0-禁用',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 专家申请表
CREATE TABLE expert_applications (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
real_name VARCHAR(50) NOT NULL,
id_card VARCHAR(20) NOT NULL,
id_card_front VARCHAR(255) NOT NULL COMMENT '身份证正面照片URL',
id_card_back VARCHAR(255) NOT NULL COMMENT '身份证反面照片URL',
qualification_cert VARCHAR(255) COMMENT '专业资质证明URL',
work_proof VARCHAR(255) COMMENT '工作经历证明URL',
introduction TEXT COMMENT '个人简介',
status TINYINT DEFAULT 0 COMMENT '0-待审核, 1-初审通过, 2-复审通过, 3-驳回',
reject_reason VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 专家信息表
CREATE TABLE experts (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
expert_level TINYINT DEFAULT 1 COMMENT '专家等级',
hit_rate DECIMAL(5,2) DEFAULT 0 COMMENT '预测命中率',
follower_count INT DEFAULT 0 COMMENT '粉丝数',
is_featured BOOLEAN DEFAULT FALSE COMMENT '是否推荐专家',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 专家内容表
CREATE TABLE expert_contents (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
expert_id BIGINT NOT NULL,
content_type TINYINT NOT NULL COMMENT '1-赛事预测, 2-分析文章, 3-短视频, 4-动态',
title VARCHAR(255) NOT NULL,
content TEXT,
video_url VARCHAR(255),
images TEXT COMMENT '图片URL, 多个用逗号分隔',
is_paid BOOLEAN DEFAULT FALSE,
price DECIMAL(10,2) DEFAULT 0,
view_count INT DEFAULT 0,
like_count INT DEFAULT 0,
comment_count INT DEFAULT 0,
is_verified BOOLEAN DEFAULT FALSE COMMENT '是否审核通过',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (expert_id) REFERENCES experts(id)
);
-- 审核日志表
CREATE TABLE audit_logs (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
application_id BIGINT NOT NULL,
operator_id BIGINT COMMENT '操作人ID',
operation_type TINYINT NOT NULL COMMENT '1-初审通过, 2-复审通过, 3-驳回',
remark VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (application_id) REFERENCES expert_applications(id)
);
三、PHP 管理端实现
1、专家审核控制器
<?php
namespace app\admin\controller;
use think\Controller;
use think\Request;
use app\admin\model\ExpertApplication;
use app\admin\model\AuditLog;
use app\admin\service\ExpertService;
class ExpertAudit extends Controller
{
// 初审列表
public function initialList()
{
$status = input('status', 0);
$list = ExpertApplication::where('status', $status)
->order('created_at', 'desc')
->paginate(10);
return view('initial_list', ['list' => $list]);
}
// 复审列表
public function reviewList()
{
$list = ExpertApplication::where('status', 1)
->order('created_at', 'desc')
->paginate(10);
return view('review_list', ['list' => $list]);
}
// 初审操作
public function initialAudit()
{
$id = input('id');
$action = input('action'); // pass/reject
$reason = input('reason', '');
$application = ExpertApplication::get($id);
if (!$application) {
return json(['code' => 404, 'msg' => '申请不存在']);
}
if ($action == 'pass') {
$application->status = 1;
$application->save();
// 记录审核日志
AuditLog::create([
'application_id' => $id,
'operator_id' => session('admin_id'),
'operation_type' => 1,
'remark' => '初审通过'
]);
return json(['code' => 200, 'msg' => '初审通过成功']);
} else {
$application->status = 3;
$application->reject_reason = $reason;
$application->save();
AuditLog::create([
'application_id' => $id,
'operator_id' => session('admin_id'),
'operation_type' => 3,
'remark' => $reason
]);
// TODO: 发送通知给用户
return json(['code' => 200, 'msg' => '已驳回申请']);
}
}
// 复审操作
public function reviewAudit()
{
$id = input('id');
$action = input('action'); // pass/reject
$reason = input('reason', '');
$application = ExpertApplication::get($id);
if (!$application || $application->status != 1) {
return json(['code' => 400, 'msg' => '申请状态不正确']);
}
if ($action == 'pass') {
// 调用专家服务处理认证通过逻辑
$service = new ExpertService();
$result = $service->approveExpert($application);
if ($result) {
AuditLog::create([
'application_id' => $id,
'operator_id' => session('admin_id'),
'operation_type' => 2,
'remark' => '复审通过,专家认证成功'
]);
return json(['code' => 200, 'msg' => '专家认证成功']);
} else {
return json(['code' => 500, 'msg' => '专家认证失败']);
}
} else {
$application->status = 3;
$application->reject_reason = $reason;
$application->save();
AuditLog::create([
'application_id' => $id,
'operator_id' => session('admin_id'),
'operation_type' => 3,
'remark' => $reason
]);
// TODO: 发送通知给用户
return json(['code' => 200, 'msg' => '已驳回申请']);
}
}
// 专家管理列表
public function expertList()
{
$list = Expert::with('user')
->order('created_at', 'desc')
->paginate(10);
return view('expert_list', ['list' => $list]);
}
// 撤销专家资格
public function revokeExpert()
{
$id = input('id');
$reason = input('reason', '');
$service = new ExpertService();
$result = $service->revokeExpert($id, $reason);
if ($result) {
return json(['code' => 200, 'msg' => '撤销专家资格成功']);
} else {
return json(['code' => 500, 'msg' => '撤销专家资格失败']);
}
}
}
2、专家服务类
<?php
namespace app\admin\service;
use think\Db;
use app\admin\model\ExpertApplication;
use app\admin\model\Expert;
use app\admin\model\User;
class ExpertService
{
// 批准专家申请
public function approveExpert(ExpertApplication $application)
{
Db::startTrans();
try {
// 更新用户角色为专家
User::where('id', $application->user_id)
->update(['role' => 'expert']);
// 创建专家记录
Expert::create([
'user_id' => $application->user_id,
'created_at' => time()
]);
// 更新申请状态
$application->status = 2; // 复审通过
$application->save();
// TODO: 发送专家认证通过通知
// $this->sendExpertApprovedNotification($application->user_id);
Db::commit();
return true;
} catch (\Exception $e) {
Db::rollback();
return false;
}
}
// 撤销专家资格
public function revokeExpert($expertId, $reason)
{
Db::startTrans();
try {
// 获取专家信息
$expert = Expert::get($expertId);
if (!$expert) {
throw new \Exception('专家不存在');
}
// 更新用户角色
User::where('id', $expert->user_id)
->update(['role' => 'user']);
// 删除专家记录
$expert->delete();
// TODO: 发送撤销通知
// $this->sendExpertRevokedNotification($expert->user_id, $reason);
Db::commit();
return true;
} catch (\Exception $e) {
Db::rollback();
return false;
}
}
// 自动初审逻辑
public function autoInitialReview($applicationId)
{
$application = ExpertApplication::get($applicationId);
if (!$application) {
return false;
}
// 调用实名认证接口
$realNameVerified = $this->verifyRealName(
$application->real_name,
$application->id_card
);
// 检查材料完整性
$materialsComplete = $this->checkMaterialsComplete($application);
// 检查违规内容
$noViolation = $this->checkViolation($application);
if ($realNameVerified && $materialsComplete && $noViolation) {
$application->status = 1; // 初审通过
$application->save();
AuditLog::create([
'application_id' => $applicationId,
'operation_type' => 1,
'remark' => '系统自动初审通过'
]);
return true;
} else {
$application->status = 3; // 驳回
$application->reject_reason = '自动审核未通过';
$application->save();
AuditLog::create([
'application_id' => $applicationId,
'operation_type' => 3,
'remark' => '系统自动初审驳回'
]);
return false;
}
}
// 实名认证接口调用
private function verifyRealName($name, $idCard)
{
// TODO: 调用第三方实名认证接口
// 这里简化处理,实际应调用公安部门接口
return true;
}
// 检查材料完整性
private function checkMaterialsComplete($application)
{
return !empty($application->id_card_front)
&& !empty($application->id_card_back)
&& (!empty($application->qualification_cert) || !empty($application->work_proof));
}
// 检查违规内容
private function checkViolation($application)
{
// TODO: 使用关键词过滤和图片识别技术
// 这里简化处理
$bannedWords = ['赌博', '诈骗', '色情'];
foreach ($bannedWords as $word) {
if (strpos($application->introduction, $word) !== false) {
return false;
}
}
return true;
}
}
四、Java Android 客户端实现
1、专家认证API接口
// ExpertApiService.java
public interface ExpertApiService {
// 提交专家申请
@Multipart
@POST("expert/apply")
Call<BaseResponse> submitExpertApplication(
@Part("real_name") RequestBody realName,
@Part("id_card") RequestBody idCard,
@Part MultipartBody.Part idCardFront,
@Part MultipartBody.Part idCardBack,
@Part MultipartBody.Part qualificationCert,
@Part MultipartBody.Part workProof,
@Part("introduction") RequestBody introduction
);
// 获取申请状态
@GET("expert/application/status")
Call<BaseResponse<ExpertApplicationStatus>> getApplicationStatus();
// 获取专家主页信息
@GET("expert/profile/{expertId}")
Call<BaseResponse<ExpertProfile>> getExpertProfile(
@Path("expertId") long expertId
);
// 获取专家内容列表
@GET("expert/contents")
Call<BaseResponse<List<ExpertContent>>> getExpertContents(
@Query("expertId") long expertId,
@Query("type") int type,
@Query("page") int page,
@Query("size") int size
);
}
// 数据模型
public class ExpertApplicationStatus {
private int status; // 0-待审核, 1-初审通过, 2-复审通过, 3-驳回
private String rejectReason;
private Date applyTime;
// getters & setters
}
public class ExpertProfile {
private long expertId;
private String nickname;
private String avatar;
private String title;
private String introduction;
private int followerCount;
private double hitRate;
private boolean isFollowed;
// getters & setters
}
public class ExpertContent {
private long id;
private int type; // 1-赛事预测, 2-分析文章, 3-短视频, 4-动态
private String title;
private String summary;
private String coverImage;
private boolean isPaid;
private double price;
private int viewCount;
private Date createTime;
// getters & setters
}
2、专家申请Activity
// ExpertApplyActivity.java
public class ExpertApplyActivity extends AppCompatActivity {
private static final int REQUEST_PICK_ID_FRONT = 1;
private static final int REQUEST_PICK_ID_BACK = 2;
private static final int REQUEST_PICK_QUALIFICATION = 3;
private static final int REQUEST_PICK_WORK_PROOF = 4;
private EditText etRealName, etIdCard, etIntroduction;
private ImageView ivIdCardFront, ivIdCardBack, ivQualification, ivWorkProof;
private Button btnSubmit;
private Uri idCardFrontUri, idCardBackUri, qualificationUri, workProofUri;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_expert_apply);
initViews();
setupListeners();
}
private void initViews() {
etRealName = findViewById(R.id.et_real_name);
etIdCard = findViewById(R.id.et_id_card);
etIntroduction = findViewById(R.id.et_introduction);
ivIdCardFront = findViewById(R.id.iv_id_card_front);
ivIdCardBack = findViewById(R.id.iv_id_card_back);
ivQualification = findViewById(R.id.iv_qualification);
ivWorkProof = findViewById(R.id.iv_work_proof);
btnSubmit = findViewById(R.id.btn_submit);
}
private void setupListeners() {
ivIdCardFront.setOnClickListener(v -> pickImage(REQUEST_PICK_ID_FRONT));
ivIdCardBack.setOnClickListener(v -> pickImage(REQUEST_PICK_ID_BACK));
ivQualification.setOnClickListener(v -> pickImage(REQUEST_PICK_QUALIFICATION));
ivWorkProof.setOnClickListener(v -> pickImage(REQUEST_PICK_WORK_PROOF));
btnSubmit.setOnClickListener(v -> submitApplication());
}
private void pickImage(int requestCode) {
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
startActivityForResult(intent, requestCode);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK && data != null) {
Uri uri = data.getData();
switch (requestCode) {
case REQUEST_PICK_ID_FRONT:
idCardFrontUri = uri;
ivIdCardFront.setImageURI(uri);
break;
case REQUEST_PICK_ID_BACK:
idCardBackUri = uri;
ivIdCardBack.setImageURI(uri);
break;
case REQUEST_PICK_QUALIFICATION:
qualificationUri = uri;
ivQualification.setImageURI(uri);
break;
case REQUEST_PICK_WORK_PROOF:
workProofUri = uri;
ivWorkProof.setImageURI(uri);
break;
}
}
}
private void submitApplication() {
String realName = etRealName.getText().toString().trim();
String idCard = etIdCard.getText().toString().trim();
String introduction = etIntroduction.getText().toString().trim();
if (realName.isEmpty() || idCard.isEmpty() || idCardFrontUri == null || idCardBackUri == null) {
Toast.makeText(this, "请填写完整信息并上传身份证正反面", Toast.LENGTH_SHORT).show();
return;
}
if (qualificationUri == null && workProofUri == null) {
Toast.makeText(this, "请至少上传一种资质证明或工作证明", Toast.LENGTH_SHORT).show();
return;
}
// 创建请求体
RequestBody realNameBody = RequestBody.create(realName, MediaType.parse("text/plain"));
RequestBody idCardBody = RequestBody.create(idCard, MediaType.parse("text/plain"));
RequestBody introBody = RequestBody.create(introduction, MediaType.parse("text/plain"));
MultipartBody.Part frontPart = prepareFilePart("id_card_front", idCardFrontUri);
MultipartBody.Part backPart = prepareFilePart("id_card_back", idCardBackUri);
MultipartBody.Part qualPart = qualificationUri != null ?
prepareFilePart("qualification_cert", qualificationUri) : null;
MultipartBody.Part workPart = workProofUri != null ?
prepareFilePart("work_proof", workProofUri) : null;
// 显示加载对话框
ProgressDialog progressDialog = new ProgressDialog(this);
progressDialog.setMessage("提交中...");
progressDialog.setCancelable(false);
progressDialog.show();
// 调用API
ExpertApiService apiService = RetrofitClient.getInstance().create(ExpertApiService.class);
Call<BaseResponse> call = apiService.submitExpertApplication(
realNameBody, idCardBody, frontPart, backPart, qualPart, workPart, introBody
);
call.enqueue(new Callback<BaseResponse>() {
@Override
public void onResponse(Call<BaseResponse> call, Response<BaseResponse> response) {
progressDialog.dismiss();
if (response.isSuccessful() && response.body() != null) {
if (response.body().isSuccess()) {
Toast.makeText(ExpertApplyActivity.this, "提交成功,请等待审核", Toast.LENGTH_SHORT).show();
finish();
} else {
Toast.makeText(ExpertApplyActivity.this, response.body().getMessage(), Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(ExpertApplyActivity.this, "提交失败", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<BaseResponse> call, Throwable t) {
progressDialog.dismiss();
Toast.makeText(ExpertApplyActivity.this, "网络错误: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
private MultipartBody.Part prepareFilePart(String partName, Uri fileUri) {
File file = new File(getRealPathFromURI(fileUri));
RequestBody requestFile = RequestBody.create(file, MediaType.parse("image/*"));
return MultipartBody.Part.createFormData(partName, file.getName(), requestFile);
}
private String getRealPathFromURI(Uri contentUri) {
String[] proj = {
MediaStore.Images.Media.DATA};
Cursor cursor = getContentResolver().query(contentUri, proj, null, null, null);
if (cursor == null) return null;
int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
cursor.moveToFirst();
String path = cursor.getString(column_index);
cursor.close();
return path;
}
}
五、Vue.js PC网页端实现
1、专家认证组件
<template>
<div class="expert-apply-container">
<el-card class="box-card">
<div slot="header" class="clearfix">
<h2>专家认证申请</h2>
</div>
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-form-item label="真实姓名" prop="realName">
<el-input v-model="form.realName" placeholder="请输入真实姓名"></el-input>
</el-form-item>
<el-form-item label="身份证号" prop="idCard">
<el-input v-model="form.idCard" placeholder="请输入身份证号码"></el-input>
</el-form-item>
<el-form-item label="身份证正面" prop="idCardFront">
<el-upload
action="#"
:auto-upload="false"
:on-change="handleIdCardFrontChange"
:show-file-list="false"
accept="image/*">
<img v-if="form.idCardFront" :src="form.idCardFront" class="id-card-img">
<el-button v-else size="small" type="primary">点击上传</el-button>
</el-upload>
</el-form-item>
<el-form-item label="身份证反面" prop="idCardBack">
<el-upload
action="#"
:auto-upload="false"
:on-change="handleIdCardBackChange"
:show-file-list="false"
accept="image/*">
<img v-if="form.idCardBack" :src="form.idCardBack" class="id-card-img">
<el-button v-else size="small" type="primary">点击上传</el-button>
</el-upload>
</el-form-item>
<el-form-item label="专业资质证明">
<el-upload
action="#"
:auto-upload="false"
:on-change="handleQualificationChange"
:show-file-list="false"
accept="image/*">
<img v-if="form.qualification" :src="form.qualification" class="id-card-img">
<el-button v-else size="small" type="primary">点击上传</el-button>
</el-upload>
<p class="tip">如体育从业证书、解说资格证书等</p>
</el-form-item>
<el-form-item label="工作经历证明">
<el-upload
action="#"
:auto-upload="false"
:on-change="handleWorkProofChange"
:show-file-list="false"
accept="image/*">
<img v-if="form.workProof" :src="form.workProof" class="id-card-img">
<el-button v-else size="small" type="primary">点击上传</el-button>
</el-upload>
<p class="tip">如工作证明、往期解说截图等</p>
</el-form-item>
<el-form-item label="个人简介" prop="introduction">
<el-input
type="textarea"
:rows="5"
v-model="form.introduction"
placeholder="请简要介绍您的专业背景、解说经验等">
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm" :loading="submitting">提交申请</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
import {
submitExpertApplication } from '@/api/expert';
import {
uploadImage } from '@/api/upload';
export default {
name: 'ExpertApply',
data() {
return {
submitting: false,
form: {
realName: '',
idCard: '',
idCardFront: '',
idCardBack: '',
qualification: '',
workProof: '',
introduction: ''
},
rules: {
realName: [
{
required: true, message: '请输入真实姓名', trigger: 'blur' }
],
idCard: [
{
required: true, message: '请输入身份证号码', trigger: 'blur' },
{
pattern: /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/, message: '身份证格式不正确' }
],
idCardFront: [
{
required: true, message: '请上传身份证正面照片', trigger: 'change' }
],
idCardBack: [
{
required: true, message: '请上传身份证反面照片', trigger: 'change' }
],
introduction: [
{
required: true, message: '请输入个人简介', trigger: 'blur' },
{
min: 50, message: '至少输入50个字符', trigger: 'blur' }
]
}
};
},
methods: {
handleIdCardFrontChange(file) {
this.getBase64(file.raw).then(base64 => {
this.form.idCardFront = base64;
});
},
handleIdCardBackChange(file) {
this.getBase64(file.raw).then(base64 => {
this.form.idCardBack = base64;
});
},
handleQualificationChange(file) {
this.getBase64(file.raw).then(base64 => {
this.form.qualification = base64;
});
},
handleWorkProofChange(file) {
this.getBase64(file.raw).then(base64 => {
this.form.workProof = base64;
});
},
getBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
},
async submitForm() {
this.$refs.form.validate(async valid => {
if (!valid) {
return false;
}
if (!this.form.qualification && !this.form.workProof) {
this.$message.warning('请至少上传一种资质证明或工作证明');
return;
}
this.submitting = true;
try {
// 上传图片并获取URL
const uploadTasks = [];
if (this.form.idCardFront) {
uploadTasks.push(uploadImage(this.form.idCardFront).then(url => {
this.form.idCardFront = url;
}));
}
if (this.form.idCardBack) {
uploadTasks.push(uploadImage(this.form.idCardBack).then(url => {
this.form.idCardBack = url;
}));
}
if (this.form.qualification) {
uploadTasks.push(uploadImage(this.form.qualification).then(url => {
this.form.qualification = url;
}));
}
if (this.form.workProof) {
uploadTasks.push(uploadImage(this.form.workProof).then(url => {
this.form.workProof = url;
}));
}
await Promise.all(uploadTasks);
// 提交申请
const response = await submitExpertApplication({
real_name: this.form.realName,
id_card: this.form.idCard,
id_card_front: this.form.idCardFront,
id_card_back: this.form.idCardBack,
qualification_cert: this.form.qualification,
work_proof: this.form.workProof,
introduction: this.form.introduction
});
if (response.code === 200) {
this.$message.success('提交成功,请等待审核');
this.$router.push('/user/center');
} else {
this.$message.error(response.message || '提交失败');
}
} catch (error) {
console.error('提交失败:', error);
this.$message.error('提交失败');
} finally {
this.submitting = false;
}
});
}
}
};
</script>
<style scoped>
.expert-apply-container {
max-width: 800px;
margin: 20px auto;
}
.id-card-img {
max-width: 300px;
max-height: 200px;
display: block;
margin-bottom: 10px;
}
.tip {
font-size: 12px;
color: #999;
margin-top: 5px;
}
</style>