ITPub博客

首页 > 应用开发 > IT综合 > 多端知识付费小程序源码|在线教育考试答题源码|完整前后端开源可二开

多端知识付费小程序源码|在线教育考试答题源码|完整前后端开源可二开

原创 IT综合 作者:gkreds 时间:2025-12-23 12:10:14 0 删除 编辑

  本文详细介绍一套完整的多端知识付费小程序解决方案,该系统集成了在线教育、考试答题、付费课程等核心功能,采用前后端分离架构,支持微信小程序、H5等多端适配,代码完全开源并支持二次开发。文章将从技术架构、核心功能、代码实现、部署方案等方面进行全面解析。

  源码:zx.xcxyms.top

  一、技术架构设计

  1.1整体架构概述

  本系统采用微服务架构设计,前后端完全分离,支持高并发、高可用的在线教育场景需求。   ┌─────────────────────────────────────────────────────┐

  │客户端层│
  │┌─────────┐┌─────────┐┌─────────┐│
  ││微信小程序││H5页面││管理后台││
  │└─────────┘└─────────┘└─────────┘│
  └─────────────────────────────────────────────────────┘
  ┌─────────────────────────────────────────────────────┐
  │网关层│
  │┌─────────────────┐│
  ││API网关/Nginx││
  │└─────────────────┘│
  └─────────────────────────────────────────────────────┘
  ┌─────────────────────────────────────────────────────┐
  │微服务层│
  │┌─────────┐┌─────────┐┌─────────┐┌─────────┐│
  ││用户服务││课程服务││考试服务││支付服务││
  │└─────────┘└─────────┘└─────────┘└─────────┘│
  └─────────────────────────────────────────────────────┘
  ┌─────────────────────────────────────────────────────┐
  │基础设施层│
  │┌─────────┐┌─────────┐┌─────────┐│
  ││MySQL││Redis││MinIO││
  │└─────────┘└─────────┘└─────────┘│
  └─────────────────────────────────────────────────────┘

  1.2技术栈选型

  •后端技术栈:

  •框架:Spring Boot 2.7+Spring Cloud Alibaba

  •认证授权:Spring Security+JWT

  •数据库:MySQL 8.0+Redis 7.0

  •消息队列:RocketMQ

  •文件存储:MinIO(兼容S3协议)

  •服务注册与发现:Nacos

  •配置中心:Nacos Config

  •服务监控:Spring Boot Admin+Prometheus

  •前端技术栈:

  •小程序端:Uni-app+Vue 3

  •管理后台:Vue 3+Element Plus

  •状态管理:Pinia

  •构建工具:Vite

  •包管理:pnpm

  二、核心功能模块详解

  2.1知识付费模块

  2.1.1课程体系设计

  //课程实体类设计
  Entity
  Table(name="course")
  Data
  public class Course{
  Id
  GeneratedValue(strategy=GenerationType.IDENTITY)
  private Long id;
  private String title;//课程标题
  private String coverImage;//封面图
  private String description;//课程描述
  Enumerated(EnumType.STRING)
  private CourseStatus status;//课程状态
  Enumerated(EnumType.STRING)
  private CourseType type;//课程类型(免费/付费)
  private BigDecimal price;//价格
  private BigDecimal discountPrice;//折扣价
  ManyToOne
  JoinColumn(name="teacher_id")
  private User teacher;//讲师
  OneToMany(mappedBy="course",cascade=CascadeType.ALL)
  private List<Chapter>chapters;//章节列表
  private Integer sortOrder;//排序
  private LocalDateTime createTime;
  private LocalDateTime updateTime;
  }
  //章节实体
  Entity
  Table(name="chapter")
  Data
  public class Chapter{
  Id
  GeneratedValue(strategy=GenerationType.IDENTITY)
  private Long id;
  private String title;
  private String description;
  private Integer sortOrder;
  ManyToOne
  JoinColumn(name="course_id")
  private Course course;
  OneToMany(mappedBy="chapter",cascade=CascadeType.ALL)
  private List<Lesson>lessons;
  }
  //课时实体
  Entity
  Table(name="lesson")
  Data
  public class Lesson{
  Id
  GeneratedValue(strategy=GenerationType.IDENTITY)
  private Long id;
  private String title;
  private String videoUrl;//视频地址
  private String attachmentUrl;//附件地址
  private Integer duration;//时长(秒)
  private Boolean freePreview;//是否可试看
  ManyToOne
  JoinColumn(name="chapter_id")
  private Chapter chapter;
  }

  2.1.2支付集成实现

  //支付服务接口
  Service
  Slf4j
  public class PaymentServiceImpl implements PaymentService{
  Autowired
  private WxPayService wxPayService;
  Autowired
  private CourseOrderRepository orderRepository;
  /**
  *创建课程订单
  */
  Override
  Transactional
  public CreateOrderResult createCourseOrder(CreateOrderRequest request){
  //1.验证课程信息
  Course course=courseService.getCourseById(request.getCourseId());
  if(course==null){
  throw new BusinessException("课程不存在");
  }
  //2.检查是否已购买
  if(courseOrderRepository.existsByUserIdAndCourseId(
  request.getUserId(),request.getCourseId())){
  throw new BusinessException("您已购买此课程");
  }
  //3.创建订单
  CourseOrder order=new CourseOrder();
  order.setOrderNo(generateOrderNo());
  order.setUserId(request.getUserId());
  order.setCourseId(request.getCourseId());
  order.setAmount(course.getPrice());
  order.setActualAmount(course.getDiscountPrice());
  order.setStatus(OrderStatus.PENDING);
  order.setCreateTime(LocalDateTime.now());
  order=orderRepository.save(order);
  //4.调用微信支付
  WxPayUnifiedOrderRequest wxRequest=new WxPayUnifiedOrderRequest();
  wxRequest.setOutTradeNo(order.getOrderNo());
  wxRequest.setTotalFee(order.getActualAmount()
  .multiply(BigDecimal.valueOf(100)).intValue());
  wxRequest.setBody("购买课程:"+course.getTitle());
  wxRequest.setTradeType(WxPayConstants.TradeType.JSAPI);
  wxRequest.setOpenid(request.getOpenid());
  WxPayUnifiedOrderResult wxResult=
  wxPayService.unifiedOrder(wxRequest);
  //5.返回支付参数
  return CreateOrderResult.builder()
  .orderId(order.getId())
  .orderNo(order.getOrderNo())
  .timeStamp(String.valueOf(System.currentTimeMillis()/1000))
  .nonceStr(RandomStringUtils.randomAlphanumeric(32))
  .packageStr("prepay_id="+wxResult.getPrepayId())
  .signType("RSA")
  .paySign(generatePaySign(wxResult))
  .build();
  }
  /**
  *支付回调处理
  */
  Override
  Transactional
  public void handlePayNotify(WxPayOrderNotifyResult notifyResult){
  String orderNo=notifyResult.getOutTradeNo();
  //1.查询订单
  CourseOrder order=orderRepository.findByOrderNo(orderNo)
  .orElseThrow(()->new BusinessException("订单不存在"));
  //2.验证订单状态和金额
  if(order.getStatus()!=OrderStatus.PENDING){
  log.warn("订单状态异常,订单号:{}",orderNo);
  return;
  }
  BigDecimal notifyAmount=BigDecimal.valueOf(notifyResult.getTotalFee())
  .divide(BigDecimal.valueOf(100));
  if(notifyAmount.compareTo(order.getActualAmount())!=0){
  log.error("金额不一致,订单号:{}",orderNo);
  return;
  }
  //3.更新订单状态
  order.setStatus(OrderStatus.PAID);
  order.setPayTime(LocalDateTime.now());
  order.setTransactionId(notifyResult.getTransactionId());
  orderRepository.save(order);
  //4.创建购买记录
  UserCourse userCourse=new UserCourse();
  userCourse.setUserId(order.getUserId());
  userCourse.setCourseId(order.getCourseId());
  userCourse.setOrderId(order.getId());
  userCourse.setCreateTime(LocalDateTime.now());
  userCourseRepository.save(userCourse);
  //5.发送消息通知
  rocketMQTemplate.convertAndSend("course-purchase-topic",
  new PurchaseMessage(order.getUserId(),order.getCourseId()));
  }
  }

  2.2在线考试模块

  2.2.1题库管理系统

 //题目实体设计
  Entity
  Table(name="question")
  Inheritance(strategy=InheritanceType.SINGLE_TABLE)
  DiscriminatorColumn(name="type",discriminatorType=DiscriminatorType.STRING)
  Data
  public abstract class Question{
  Id
  GeneratedValue(strategy=GenerationType.IDENTITY)
  private Long id;
  private String content;//题目内容
  private String analysis;//答案解析
  Enumerated(EnumType.STRING)
  private DifficultyLevel difficulty;//难度级别
  ManyToOne
  JoinColumn(name="category_id")
  private QuestionCategory category;//分类
  ManyToMany
  JoinTable(name="question_tag_relation",
  joinColumns=JoinColumn(name="question_id"),
  inverseJoinColumns=JoinColumn(name="tag_id"))
  private Set<QuestionTag>tags;//标签
  private Integer score;//分值
  private LocalDateTime createTime;
  }
  //单选题实体
  Entity
  DiscriminatorValue("SINGLE_CHOICE")
  Data
  EqualsAndHashCode(callSuper=true)
  public class SingleChoiceQuestion extends Question{
  ElementCollection
  CollectionTable(name="question_option",
  joinColumns=JoinColumn(name="question_id"))
  OrderColumn(name="option_order")
  private List<QuestionOption>options;//选项列表
  private Integer correctOptionIndex;//正确答案索引
  }
  //多选题实体
  Entity
  DiscriminatorValue("MULTIPLE_CHOICE")
  Data
  EqualsAndHashCode(callSuper=true)
  public class MultipleChoiceQuestion extends Question{
  ElementCollection
  CollectionTable(name="question_option",
  joinColumns=JoinColumn(name="question_id"))
  OrderColumn(name="option_order")
  private List<QuestionOption>options;
  ElementCollection
  CollectionTable(name="question_correct_option",
  joinColumns=JoinColumn(name="question_id"))
  private Set<Integer>correctOptionIndexes;//多个正确答案索引
  }
  //试卷实体
  Entity
  Table(name="exam_paper")
  Data
  public class ExamPaper{
  Id
  GeneratedValue(strategy=GenerationType.IDENTITY)
  private Long id;
  private String title;//试卷标题
  private String description;//试卷描述
  Enumerated(EnumType.STRING)
  private ExamType type;//考试类型
  private Integer totalScore;//总分
  private Integer timeLimit;//时间限制(分钟)
  private Integer passScore;//及格分数
  OneToMany(mappedBy="examPaper",cascade=CascadeType.ALL)
  OrderBy("sortOrder ASC")
  private List<ExamPaperQuestion>questions;
  private LocalDateTime createTime;
  private LocalDateTime updateTime;
  }

  2.2.2考试引擎实现

  //考试服务核心类
  Service
  Slf4j
  public class ExamServiceImpl implements ExamService{
  Autowired
  private ExamRecordRepository examRecordRepository;
  Autowired
  private QuestionRepository questionRepository;
  Autowired
  private RedisTemplate<String,Object>redisTemplate;
  /**
  *开始考试
  */
  Override
  Transactional
  public ExamStartResult startExam(StartExamRequest request){
  Long userId=SecurityUtils.getCurrentUserId();
  Long paperId=request.getPaperId();
  //1.检查考试次数限制
  Integer todayCount=examRecordRepository.countTodayExams(userId,paperId);
  Integer maxAttempts=examPaperService.getMaxAttempts(paperId);
  if(todayCount>=maxAttempts){
  throw new BusinessException("今日考试次数已达上限");
  }
  //2.生成考试记录
  ExamRecord record=new ExamRecord();
  record.setUserId(userId);
  record.setExamPaperId(paperId);
  record.setStartTime(LocalDateTime.now());
  record.setStatus(ExamStatus.IN_PROGRESS);
  record.setExamCode(generateExamCode());
  record=examRecordRepository.save(record);
  //3.从题库中随机抽题
  ExamPaper paper=examPaperRepository.findById(paperId)
  .orElseThrow(()->new BusinessException("试卷不存在"));
  List<ExamQuestionVO>questions=generateExamQuestions(paper);
  //4.将题目和答案缓存到Redis
  String examKey="exam:"+record.getId();
  Map<String,Object>examData=new HashMap<>();
  examData.put("questions",questions);
  examData.put("startTime",record.getStartTime().toString());
  examData.put("paperId",paperId);
  redisTemplate.opsForHash().putAll(examKey,examData);
  //设置过期时间=考试时间+10分钟缓冲
  redisTemplate.expire(examKey,
  paper.getTimeLimit()+10,TimeUnit.MINUTES);
  //5.返回考试信息
  return ExamStartResult.builder()
  .examRecordId(record.getId())
  .examCode(record.getExamCode())
  .questions(questions)
  .timeLimit(paper.getTimeLimit())
  .startTime(record.getStartTime())
  .totalScore(paper.getTotalScore())
  .build();
  }
  /**
  *提交答案
  */
  Override
  Transactional
  public ExamSubmitResult submitAnswer(SubmitAnswerRequest request){
  Long examRecordId=request.getExamRecordId();
  Long userId=SecurityUtils.getCurrentUserId();
  //1.验证考试记录
  ExamRecord record=examRecordRepository.findByIdAndUserId(
  examRecordId,userId)
  .orElseThrow(()->new BusinessException("考试记录不存在"));
  if(record.getStatus()!=ExamStatus.IN_PROGRESS){
  throw new BusinessException("考试已结束");
  }
  //2.验证考试时间
  ExamPaper paper=examPaperRepository.findById(record.getExamPaperId())
  .orElseThrow(()->new BusinessException("试卷不存在"));
  LocalDateTime now=LocalDateTime.now();
  long duration=ChronoUnit.MINUTES.between(
  record.getStartTime(),now);
  if(duration>paper.getTimeLimit()){
  throw new BusinessException("考试时间已结束");
  }
  //3.从Redis获取题目信息
  String examKey="exam:"+examRecordId;
  Map<Object,Object>examData=redisTemplate.opsForHash()
  .entries(examKey);
  if(examData.isEmpty()){
  throw new BusinessException("考试信息已过期");
  }
  //4.保存答案
  ExamAnswer answer=new ExamAnswer();
  answer.setExamRecordId(examRecordId);
  answer.setQuestionId(request.getQuestionId());
  answer.setUserAnswer(request.getAnswer());
  answer.setSubmitTime(now);
  examAnswerRepository.save(answer);
  //5.更新Redis中的答案
  String answerKey="answer:"+request.getQuestionId();
  redisTemplate.opsForHash().put(examKey,answerKey,request.getAnswer());
  return ExamSubmitResult.builder()
  .success(true)
  .message("答案提交成功")
  .build();
  }
  /**
  *自动阅卷
  */
  private ExamResult autoGrade(Long examRecordId){
  //1.获取所有题目和用户答案
  List<ExamAnswer>answers=examAnswerRepository
  .findByExamRecordId(examRecordId);
  //2.批量获取题目信息
  List<Long>questionIds=answers.stream()
  .map(ExamAnswer::getQuestionId)
  .collect(Collectors.toList());
  Map<Long,Question>questionMap=questionRepository
  .findAllById(questionIds).stream()
  .collect(Collectors.toMap(Question::getId,q->q));
  //3.计算得分
  Integer totalScore=0;
  Integer correctCount=0;
  List<QuestionResult>questionResults=new ArrayList<>();
  for(ExamAnswer answer:answers){
  Question question=questionMap.get(answer.getQuestionId());
  if(question==null)continue;
  QuestionResult result=gradeSingleQuestion(
  question,answer.getUserAnswer());
  questionResults.add(result);
  if(result.isCorrect()){
  correctCount++;
  totalScore+=result.getScore();
  }
  }
  //4.生成考试结果
  return ExamResult.builder()
  .totalScore(totalScore)
  .correctCount(correctCount)
  .totalQuestions(answers.size())
  .questionResults(questionResults)
  .build();
  }
  }

  2.3多端方案

  2.3.1 Uni-app多端配置

  //manifest.json配置文件
  {
  "name":"知识付费小程序",
  "appid":"__UNI__APPID",
  "description":"多端知识付费解决方案",
  "versionName":"1.0.0",
  "versionCode":"100",
  /*小程序特有配置*/
  "mp-weixin":{
  "appid":"wx-your-appid",
  "setting":{
  "urlCheck":false,
  "es6":true,
  "postcss":true,
  "minified":true
  },
  "usingComponents":true,
  "permission":{
  "scope.userLocation":{
  "desc":"获取地理位置用于附近课程推荐"
  }
  }
  },
  /*H5配置*/
  "h5":{
  "template":"index.h5.html",
  "router":{
  "mode":"history",
  "base":"/"
  },
  "devServer":{
  "port":8080,
  "disableHostCheck":true,
  "proxy":{
  "/api":{
  "target":"https://api.yourdomain.com",
  "changeOrigin":true,
  "secure":false
  }
  }
  }
  },
  /*通用配置*/
  "uniStatistics":{
  "enable":true
  }
  }

  2.3.2统一API接口设计

  //api/index.ts-统一API管理
  import request from'/utils/request'
  //课程相关接口
  export const courseApi={
  //获取课程列表
  getCourseList(params:CourseListParams){
  return request.get<ApiResponse<PageResult<CourseVO>>>('/api/course/list',{params})
  },
  //获取课程详情
  getCourseDetail(id:number){
  return request.get<ApiResponse<CourseDetailVO>>(`/api/course/detail/${id}`)
  },
  //购买课程
  purchaseCourse(data:PurchaseRequest){
  return request.post<ApiResponse<PurchaseResult>>('/api/course/purchase',data)
  }
  }
  //考试相关接口
  export const examApi={
  //获取试卷列表
  getExamPaperList(params:ExamListParams){
  return request.get<ApiResponse<PageResult<ExamPaperVO>>>('/api/exam/paper/list',{params})
  },
  //开始考试
  startExam(data:StartExamRequest){
  return request.post<ApiResponse<StartExamResult>>('/api/exam/start',data)
  },
  //提交答案
  submitAnswer(data:SubmitAnswerRequest){
  return request.post<ApiResponse<SubmitAnswerResult>>('/api/exam/submit',data)
  },
  //获取考试结果
  getExamResult(examRecordId:number){
  return request.get<ApiResponse<ExamResultVO>>(`/api/exam/result/${examRecordId}`)
  }
  }
  //用户相关接口
  export const userApi={
  //微信登录
  wxLogin(code:string){
  return request.post<ApiResponse<LoginResult>>('/api/user/wx-login',{code})
  },
  //获取用户信息
  getUserInfo(){
  return request.get<ApiResponse<UserInfoVO>>('/api/user/info')
  },
  //更新用户信息
  updateUserInfo(data:UpdateUserInfoRequest){
  return request.put<ApiResponse<void>>('/api/user/info',data)
  }
  }

  三、核心功能代码实现

  3.1微服务网关配置

  #application.yml
  spring:
  application:
  name:gateway-service
  cloud:
  nacos:
  discovery:
  server-addr:${NACOS_HOST:localhost}:8848
  config:
  server-addr:${NACOS_HOST:localhost}:8848
  file-extension:yaml
  gateway:
  discovery:
  locator:
  enabled:true
  routes:
  #用户服务路由
  -id:user-service
  uri:lb://user-service
  predicates:
  -Path=/api/user/**
  filters:
  -StripPrefix=1
  -name:RequestRateLimiter
  args:
  redis-rate-limiter.replenishRate:10
  redis-rate-limiter.burstCapacity:20
  key-resolver:"#{userKeyResolver}"
  #课程服务路由
  -id:course-service
  uri:lb://course-service
  predicates:
  -Path=/api/course/**
  filters:
  -StripPrefix=1
  #考试服务路由
  -id:exam-service
  uri:lb://exam-service
  predicates:
  -Path=/api/exam/**
  filters:
  -StripPrefix=1
  #支付服务路由
  -id:payment-service
  uri:lb://payment-service
  predicates:
  -Path=/api/payment/**
  filters:
  -StripPrefix=1
  #全局过滤器配置
  default-filters:
  -DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials
  #限流配置
  redis:
  host:${REDIS_HOST:localhost}
  port:6379
  password:${REDIS_PASSWORD:}
  database:0
  #安全配置
  security:
  oauth2:
  resourceserver:
  jwt:
  issuer-uri:${JWT_ISSUER_URI:http://localhost:9000}

  3.2微信小程序页面示例

  <!--pages/course/detail.vue-->
  <template>
  <view class="course-detail">
  <!--课程封面-->
  <view class="course-cover">
  <image:src="courseInfo.coverImage"mode="widthFix"/>
  <view class="course-price"v-if="courseInfo.type==='PAID'">
  <text class="current-price">¥{{courseInfo.discountPrice}}</text>
  <text class="original-price"v-if="courseInfo.discountPrice<courseInfo.price">
  ¥{{courseInfo.price}}
  </text>
  </view>
  </view>
  <!--课程信息-->
  <view class="course-info">
  <view class="course-title">{{courseInfo.title}}</view>
  <view class="course-meta">
  <view class="meta-item">
  <uni-icons type="person"size="16"></uni-icons>
  <text>{{courseInfo.teacher.nickname}}</text>
  </view>
  <view class="meta-item">
  <uni-icons type="eye"size="16"></uni-icons>
  <text>{{courseInfo.viewCount}}人学习</text>
  </view>
  <view class="meta-item">
  <uni-icons type="time"size="16"></uni-icons>
  <text>{{formatDate(courseInfo.createTime)}}</text>
  </view>
  </view>
  </view>
  <!--课程章节-->
  <view class="course-chapters">
  <view class="section-title">课程章节</view>
  <view class="chapter-list">
  <view
  v-for="(chapter,index)in courseInfo.chapters"
  :key="chapter.id"
  class="chapter-item"
  >
  <view class="chapter-header"click="toggleChapter(index)">
  <text class="chapter-index">第{{index+1}}章</text>
  <text class="chapter-title">{{chapter.title}}</text>
  <uni-icons
  :type="expandedChapter===index?'arrowup':'arrowdown'"
  size="16"
  ></uni-icons>
  </view>
  <view
  class="lesson-list"
  v-if="expandedChapter===index"
  >
  <view
  v-for="lesson in chapter.lessons"
  :key="lesson.id"
  class="lesson-item"
  click="playLesson(lesson)"
  >
  <view class="lesson-info">
  <text class="lesson-title">{{lesson.title}}</text>
  <text class="lesson-duration">
  {{formatDuration(lesson.duration)}}
  </text>
  </view>
  <view class="lesson-status">
  <text
  v-if="lesson.freePreview"
  class="free-tag"
  >
  试看
  </text>
  <uni-icons
  v-if="isLessonFinished(lesson.id)"
  type="checkmarkempty"
  color="#07c160"
  size="20"
  ></uni-icons>
  </view>
  </view>
  </view>
  </view>
  </view>
  </view>
  <!--操作按钮-->
  <view class="action-bar"v-if="!isPurchased">
  <button
  v-if="courseInfo.type==='FREE'"
  class="action-btn free-btn"
  click="startLearning"
  >
  立即学习
  </button>
  <button
  v-else
  class="action-btn buy-btn"
  click="handlePurchase"
  >
  立即购买¥{{courseInfo.discountPrice}}
  </button>
  </view>
  </view>
  </template>
  <script setup>
  import{ref,onMounted}from'vue'
  import{onLoad}from'dcloudio/uni-app'
  import{courseApi}from'/api/course'
  import{userApi}from'/api/user'
  const courseInfo=ref({})
  const expandedChapter=ref(0)
  const isPurchased=ref(false)
  const courseId=ref('')
  onLoad((options)=>{
  courseId.value=options.id
  loadCourseDetail()
  checkPurchaseStatus()
  })
  //加载课程详情
  const loadCourseDetail=async()=>{
  try{
  const res=await courseApi.getCourseDetail(courseId.value)
  if(res.code===200){
  courseInfo.value=res.data
  }
  }catch(error){
  uni.showToast({
  title:'加载失败',
  icon:'error'
  })
  }
  }
  //检查购买状态
  const checkPurchaseStatus=async()=>{
  const res=await courseApi.checkPurchaseStatus(courseId.value)
  isPurchased.value=res.data.purchased
  }
  //切换章节展开
  const toggleChapter=(index)=>{
  expandedChapter.value=expandedChapter.value===index?-1:index
  }
  //播放课时
  const playLesson=(lesson)=>{
  if(!isPurchased.value&&!lesson.freePreview){
  uni.showModal({
  title:'提示',
  content:'此课程需要购买后才能观看',
  success:(res)=>{
  if(res.confirm){
  handlePurchase()
  }
  }
  })
  return
  }
  uni.navigateTo({
  url:`/pages/course/player?id=${lesson.id}`
  })
  }
  //处理购买
  const handlePurchase=async()=>{
  //检查登录状态
  const isLogin=await checkLogin()
  if(!isLogin){
  uni.navigateTo({
  url:'/pages/login/login'
  })
  return
  }
  //发起购买
  uni.showLoading({title:'创建订单中...'})
  try{
  const res=await courseApi.purchaseCourse({
  courseId:courseId.value
  })
  if(res.code===200){
  //调用微信支付
  const paymentResult=await requestPayment(res.data)
  if(paymentResult){
  uni.showToast({
  title:'购买成功',
  icon:'success'
  })
  isPurchased.value=true
  }
  }
  }catch(error){
  uni.showToast({
  title:error.message||'购买失败',
  icon:'error'
  })
  }finally{
  uni.hideLoading()
  }
  }
  //微信支付
  const requestPayment=(paymentParams)=>{
  return new Promise((resolve,reject)=>{
  uni.requestPayment({
  provider:'wxpay',
  timeStamp:paymentParams.timeStamp,
  nonceStr:paymentParams.nonceStr,
  package:paymentParams.packageStr,
  signType:paymentParams.signType,
  paySign:paymentParams.paySign,
  success:(res)=>{
  resolve(true)
  },
  fail:(err)=>{
  reject(new Error('支付取消或失败'))
  }
  })
  })
  }
  </script>
  <style scoped>
  .course-detail{
  padding-bottom:100rpx;
  }
  .course-cover{
  position:relative;
  width:100%;
  height:420rpx;
  }
  .course-cover image{
  width:100%;
  height:100%;
  }
  .course-price{
  position:absolute;
  bottom:20rpx;
  right:20rpx;
  background:rgba(0,0,0,0.7);
  padding:10rpx 20rpx;
  border-radius:8rpx;
  display:flex;
  align-items:center;
  }
  .current-price{
  color:#ff6b6b;
  font-size:36rpx;
  font-weight:bold;
  }
  .original-price{
  color:#999;
  font-size:24rpx;
  text-decoration:line-through;
  margin-left:10rpx;
  }
  .course-info{
  padding:30rpx;
  background:#fff;
  }
  .course-title{
  font-size:36rpx;
  font-weight:bold;
  line-height:1.4;
  margin-bottom:20rpx;
  }
  .course-meta{
  display:flex;
  flex-wrap:wrap;
  gap:20rpx;
  font-size:24rpx;
  color:#666;
  }
  .meta-item{
  display:flex;
  align-items:center;
  gap:8rpx;
  }
  .course-chapters{
  margin-top:20rpx;
  background:#fff;
  padding:30rpx;
  }
  .section-title{
  font-size:32rpx;
  font-weight:bold;
  margin-bottom:30rpx;
  padding-left:10rpx;
  border-left:6rpx solid#007aff;
  }
  .chapter-item{
  border-radius:12rpx;
  overflow:hidden;
  margin-bottom:20rpx;
  box-shadow:0 2rpx 12rpx rgba(0,0,0,0.1);
  }
  .chapter-header{
  padding:30rpx;
  background:#f8f9fa;
  display:flex;
  align-items:center;
  justify-content:space-between;
  font-size:28rpx;
  cursor:pointer;
  }
  .chapter-index{
  color:#007aff;
  font-weight:bold;
  margin-right:20rpx;
  }
  .chapter-title{
  flex:1;
  font-weight:500;
  }
  .lesson-list{
  background:#fff;
  }
  .lesson-item{
  padding:25rpx 30rpx;
  border-top:1rpx solid#eee;
  display:flex;
  align-items:center;
  justify-content:space-between;
  transition:background-color 0.3s;
  }
  .lesson-item:active{
  background-color:#f5f5f5;
  }
  .lesson-info{
  flex:1;
  }
  .lesson-title{
  display:block;
  font-size:28rpx;
  margin-bottom:10rpx;
  }
  .lesson-duration{
  font-size:24rpx;
  color:#999;
  }
  .lesson-status{
  display:flex;
  align-items:center;
  gap:15rpx;
  }
  .free-tag{
  background:#07c160;
  color:#fff;
  font-size:20rpx;
  padding:4rpx 12rpx;
  border-radius:20rpx;
  }
  .action-bar{
  position:fixed;
  bottom:0;
  left:0;
  right:0;
  background:#fff;
  padding:20rpx 30rpx;
  box-shadow:0-2rpx 12rpx rgba(0,0,0,0.1);
  display:flex;
  gap:20rpx;
  }
  .action-btn{
  flex:1;
  height:80rpx;
  line-height:80rpx;
  border-radius:40rpx;
  font-size:32rpx;
  border:none;
  }
  .free-btn{
  background:#07c160;
  color:#fff;
  }
  .buy-btn{
  background:linear-gradient(135deg,#ff6b6b,#ff8e53);
  color:#fff;
  }
  </style>

  四、部署与运维

  4.1 Docker容器化部署

  #Dockerfile for backend service

  FROM openjdk:11-jre-slim

  #设置时区

  ENV TZ=Asia/Shanghai

 RUN ln-snf/usr/share/zoneinfo/$TZ/etc/localtime&&echo$TZ>/etc/timezone
  #创建应用目录
  RUN mkdir-p/app
  WORKDIR/app
  #复制JAR文件
  COPY target/*.jar app.jar
  #创建非root用户运行
  RUN addgroup-g 1000 appuser&&
  adduser-u 1000-G appuser-D appuser
  USER appuser
  #暴露端口
  EXPOSE 8080
  #启动应用
  ENTRYPOINT["java","-jar","app.jar"]
  #docker-compose.yml
  version:'3.8'
  services:
  #MySQL数据库
  mysql:
  image:mysql:8.0
  container_name:edu-mysql
  environment:
  MYSQL_ROOT_PASSWORD:${MYSQL_ROOT_PASSWORD}
  MYSQL_DATABASE:edu_db
  MYSQL_USER:edu_user
  MYSQL_PASSWORD:${MYSQL_PASSWORD}
  ports:
  -"3306:3306"
  volumes:
  -mysql_data:/var/lib/mysql
  -./config/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
  command:
  ---default-authentication-plugin=mysql_native_password
  ---character-set-server=utf8mb4
  ---collation-server=utf8mb4_unicode_ci
  networks:
  -edu-network
  #Redis缓存
  redis:
  image:redis:7-alpine
  container_name:edu-redis
  ports:
  -"6379:6379"
  volumes:
  -redis_data:/data
  command:redis-server--requirepass${REDIS_PASSWORD}
  networks:
  -edu-network
  #Nacos服务注册中心
  nacos:
  image:nacos/nacos-server:v2.2.0
  container_name:edu-nacos
  environment:
  -MODE=standalone
  -SPRING_DATASOURCE_PLATFORM=mysql
  -MYSQL_SERVICE_HOST=mysql
  -MYSQL_SERVICE_DB_NAME=nacos
  -MYSQL_SERVICE_PORT=3306
  -MYSQL_SERVICE_USER=root
  -MYSQL_SERVICE_PASSWORD=${MYSQL_ROOT_PASSWORD}
  volumes:
  -nacos_logs:/home/nacos/logs
  ports:
  -"8848:8848"
  depends_on:
  -mysql
  networks:
  -edu-network
  #MinIO文件存储
  minio:
  image:minio/minio:latest
  container_name:edu-minio
  ports:
  -"9000:9000"
  -"9001:9001"
  environment:
  MINIO_ROOT_USER:${MINIO_ROOT_USER}
  MINIO_ROOT_PASSWORD:${MINIO_ROOT_PASSWORD}
  volumes:
  -minio_data:/data
  command:server/data--console-address":9001"
  networks:
  -edu-network
  #用户服务
  user-service:
  build:./user-service
  container_name:edu-user-service
  environment:
  -SPRING_PROFILES_ACTIVE=docker
  -EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://nacos:8848/nacos
  ports:
  -"8081:8080"
  depends_on:
  -mysql
  -redis
  -nacos
  networks:
  -edu-network
  deploy:
  replicas:2
  restart_policy:
  condition:on-failure
  #课程服务
  course-service:
  build:./course-service
  container_name:edu-course-service
  environment:
  -SPRING_PROFILES_ACTIVE=docker
  ports:
  -"8082:8080"
  depends_on:
  -mysql
  -redis
  -nacos
  -minio
  networks:
  -edu-network
  #API网关
  api-gateway:
  build:./api-gateway
  container_name:edu-api-gateway
  ports:
  -"8080:8080"
  depends_on:
  -nacos
  networks:
  -edu-network
  networks:
  edu-network:
  driver:bridge
  volumes:
  mysql_data:
  redis_data:
  nacos_logs:
  minio_data:
  4.2性能优化方案
  //使用Redis缓存优化
  Configuration
  EnableCaching
  public class CacheConfig{
  Bean
  public RedisCacheManager cacheManager(RedisConnectionFactory factory){
  RedisCacheConfiguration config=RedisCacheConfiguration.defaultCacheConfig()
  .entryTtl(Duration.ofMinutes(30))//默认缓存30分钟
  .disableCachingNullValues()
  .serializeKeysWith(RedisSerializationContext
  .SerializationPair.fromSerializer(new StringRedisSerializer()))
  .serializeValuesWith(RedisSerializationContext
  .SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
  //特殊缓存配置
  Map<String,RedisCacheConfiguration>cacheConfigs=new HashMap<>();
  cacheConfigs.put("courseDetail",
  config.entryTtl(Duration.ofHours(1)));
  cacheConfigs.put("hotCourses",
  config.entryTtl(Duration.ofMinutes(10)));
  cacheConfigs.put("userInfo",
  config.entryTtl(Duration.ofDays(1)));
  return RedisCacheManager.builder(factory)
  .cacheDefaults(config)
  .withInitialCacheConfigurations(cacheConfigs)
  .build();
  }
  }
  //数据库查询优化
  Repository
  public class CourseRepositoryImpl implements CourseCustomRepository{
  PersistenceContext
  private EntityManager entityManager;
  Override
  public Page<CourseVO>findCoursesWithCondition(CourseQuery query,Pageable pageable){
  String sql="""
  SELECT new com.edu.vo.CourseVO(
  c.id,c.title,c.coverImage,c.description,
  c.price,c.discountPrice,c.viewCount,c.studentCount,
  t.id,t.nickname,t.avatar,
  COUNT(DISTINCT l.id)as lessonCount
  )
  FROM Course c
  LEFT JOIN c.teacher t
  LEFT JOIN c.chapters ch
  LEFT JOIN ch.lessons l
  WHERE 1=1
  """;
  Map<String,Object>params=new HashMap<>();
  StringBuilder whereClause=new StringBuilder();
  if(StringUtils.hasText(query.getKeyword())){
  whereClause.append("AND(c.title LIKE:keyword OR c.description LIKE:keyword)");
  params.put("keyword","%"+query.getKeyword()+"%");
  }
  if(query.getCategoryId()!=null){
  whereClause.append("AND c.category.id=:categoryId");
  params.put("categoryId",query.getCategoryId());
  }
  if(query.getMinPrice()!=null){
  whereClause.append("AND c.discountPrice>=:minPrice");
  params.put("minPrice",query.getMinPrice());
  }
  if(query.getMaxPrice()!=null){
  whereClause.append("AND c.discountPrice<=:maxPrice");
  params.put("maxPrice",query.getMaxPrice());
  }
  sql+=whereClause.toString()+"GROUP BY c.id";
  //查询总数
  String countSql="SELECT COUNT(DISTINCT c.id)FROM Course c WHERE 1=1"+whereClause;
  Long total=(Long)entityManager.createQuery(countSql)
  .setProperties(params)
  .getSingleResult();
  //查询数据
  List<CourseVO>content=entityManager.createQuery(sql,CourseVO.class)
  .setProperties(params)
  .setFirstResult((int)pageable.getOffset())
  .setMaxResults(pageable.getPageSize())
  .getResultList();
  return new PageImpl<>(content,pageable,total);
  }
  }
  五、安全与监控
  5.1接口安全防护
  //接口限流配置
  Configuration
  public class RateLimitConfig{
  Bean(name="userKeyResolver")
  public KeyResolver userKeyResolver(){
  return exchange->{
  String userId=exchange.getRequest()
  .getHeaders()
  .getFirst("X-User-Id");
  return Mono.just(userId!=null?userId:
  exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
  };
  }
  }
  //防刷策略
  Component
  public class AntiBrushFilter implements GlobalFilter,Ordered{
  Autowired
  private RedisTemplate<String,Object>redisTemplate;
  Override
  public Mono<Void>filter(ServerWebExchange exchange,GatewayFilterChain chain){
  ServerHttpRequest request=exchange.getRequest();
  String path=request.getURI().getPath();
  String ip=getClientIp(request);
  //检查关键接口访问频率
  if(path.contains("/api/exam/submit")||
  path.contains("/api/payment/create")){
  String key="rate_limit:"+ip+":"+path;
  Long count=redisTemplate.opsForValue().increment(key,1);
  if(count!=null&&count==1){
  redisTemplate.expire(key,60,TimeUnit.SECONDS);
  }
  if(count!=null&&count>10){//限制10次/分钟
  ServerHttpResponse response=exchange.getResponse();
  response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
  return response.setComplete();
  }
  }
  return chain.filter(exchange);
  }
  Override
  public int getOrder(){
  return-1;
  }
  }
  六、二次开发指南
  6.1模块扩展示例
  //1.添加直播功能模块
  Service
  public class LiveStreamServiceImpl implements LiveStreamService{
  Autowired
  private LiveStreamRepository liveStreamRepository;
  Value("${live.push.url}")
  private String pushUrl;
  Value("${live.play.url}")
  private String playUrl;
  Override
  public LiveStreamVO createLiveStream(CreateLiveStreamRequest request){
  //生成推流地址
  String streamName="live_"+UUID.randomUUID().toString().replace("-","");
  String pushAddress=String.format("%s/%s",pushUrl,streamName);
  String playAddress=String.format("%s/%s.m3u8",playUrl,streamName);
  //创建直播记录
  LiveStream liveStream=new LiveStream();
  liveStream.setTitle(request.getTitle());
  liveStream.setDescription(request.getDescription());
  liveStream.setTeacherId(request.getTeacherId());
  liveStream.setCourseId(request.getCourseId());
  liveStream.setStreamName(streamName);
  liveStream.setPushUrl(pushAddress);
  liveStream.setPlayUrl(playAddress);
  liveStream.setStatus(LiveStatus.NOT_STARTED);
  liveStream.setStartTime(request.getStartTime());
  liveStream.setCreateTime(LocalDateTime.now());
  liveStream=liveStreamRepository.save(liveStream);
  return convertToVO(liveStream);
  }
  Override
  public void startLiveStream(Long liveId){
  LiveStream liveStream=liveStreamRepository.findById(liveId)
  .orElseThrow(()->new BusinessException("直播不存在"));
  liveStream.setStatus(LiveStatus.LIVING);
  liveStream.setActualStartTime(LocalDateTime.now());
  liveStreamRepository.save(liveStream);
  //发送开播通知
  notificationService.sendLiveStartNotification(liveStream);
  }
  Override
  public void endLiveStream(Long liveId){
  LiveStream liveStream=liveStreamRepository.findById(liveId)
  .orElseThrow(()->new BusinessException("直播不存在"));
  liveStream.setStatus(LiveStatus.ENDED);
  liveStream.setEndTime(LocalDateTime.now());
  liveStreamRepository.save(liveStream);
  //生成回放
  generateReplay(liveStream);
  }
  private void generateReplay(LiveStream liveStream){
  //调用视频处理服务生成回放
  VideoProcessRequest processRequest=new VideoProcessRequest();
  processRequest.setStreamName(liveStream.getStreamName());
  processRequest.setTitle(liveStream.getTitle()+"-回放");
  processRequest.setCourseId(liveStream.getCourseId());
  videoProcessService.processLiveReplay(processRequest);
  }
  }
  //2.添加积分系统
  Service
  Transactional
  public class PointServiceImpl implements PointService{
  Autowired
  private UserPointRepository pointRepository;
  Autowired
  private PointRuleRepository ruleRepository;
  Override
  public void addPoints(AddPointsRequest request){
  //查询积分规则
  PointRule rule=ruleRepository.findByAction(request.getAction())
  .orElseThrow(()->new BusinessException("积分规则不存在"));
  //检查重复操作
  if(rule.getUniqueAction()&&
  pointRepository.existsByUserIdAndActionAndUniqueKey(
  request.getUserId(),request.getAction(),request.getUniqueKey())){
  return;
  }
  //添加积分记录
  UserPoint point=new UserPoint();
  point.setUserId(request.getUserId());
  point.setAction(request.getAction());
  point.setPoints(rule.getPoints());
  point.setDescription(rule.getDescription());
  point.setUniqueKey(request.getUniqueKey());
  point.setCreateTime(LocalDateTime.now());
  pointRepository.save(point);
  //更新用户总积分
  userRepository.addPoints(request.getUserId(),rule.getPoints());
  //发送积分变动通知
  notificationService.sendPointChangeNotification(
  request.getUserId(),rule.getPoints(),point.getDescription());
  }
  Override
  public boolean deductPoints(DeductPointsRequest request){
  Integer currentPoints=userRepository.getUserPoints(request.getUserId());
  if(currentPoints<request.getPoints()){
  throw new BusinessException("积分不足");
  }
  UserPoint point=new UserPoint();
  point.setUserId(request.getUserId());
  point.setAction("POINTS_DEDUCT");
  point.setPoints(-request.getPoints());
  point.setDescription(request.getReason());
  point.setCreateTime(LocalDateTime.now());
  pointRepository.save(point);
  //更新用户积分
  userRepository.deductPoints(request.getUserId(),request.getPoints());
  return true;
  }
  }

  总结

  本文详细介绍了多端知识付费小程序的完整技术解决方案,涵盖了系统架构、核心功能实现、代码示例、部署方案和扩展指南。该系统具有以下特点:

  1.架构先进:采用微服务架构,支持高并发、高可用场景

  2.功能全面:集知识付费、在线教育、考试系统于一体

  3.多端支持:基于Uni-app实现一套代码多端运行

  4.安全可靠:完善的权限控制和防刷策略

  5.易于扩展:模块化设计,支持快速二次开发

  6.性能优化:多级缓存、数据库优化等性能保障措施

  该源码完全开源,开发者可以根据实际业务需求进行二次开发,快速构建自己的知识付费平台。系统采用MIT开源协议,允许商业使用和修改,为教育行业数字化转型提供了强有力的技术支撑。


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70049050/viewspace-3105961/ ,如需转载,请注明出处,否则将追究法律责任。

上一篇: 没有了~
下一篇: 没有了~
请登录后发表评论 登录
全部评论

注册时间:2025-12-23

  • 博文量
    1
  • 访问量
    821