【慧游鲁博】【8】web端·路径重定向·用户选择模式存储/统计·数据格式转换

文章目录

  • 路径重定向
    • 修改前
    • 1. 符合用户访问逻辑
    • 2. 避免路由冲突
    • 3. 统一路由控制权
    • 修改后
  • 模式选择统计
    • 核心需求
    • 一、数据库设计(Neon PostgreSQL)
    • 二、Spring Boot 后端实现
      • 项目结构
      • 依赖配置(`pom.xml`)
      • 数据库配置(`application.yml`)
      • 实体类(`ModeStats.java`)
      • 数据传输DTO(`ModeDto.java`)
      • Mapper 接口(`ModeStatsMapper.java`)
      • Controller(`ModeStatsController.java`)
      • Service接口(`ModeStatsService.java`)
      • Service实现类(`ModeStatsServiceImpl.java`)
      • 重点说明
    • 三、Uni-app 前端修改
  • 可视化用户角色分布
    • 后端修改
      • 1. 在 `ModeStatsController` 中添加新的接口
      • 2. 在 `ModeStatsService` 接口中添加方法
      • 3. 在 `ModeStatsServiceImpl` 中实现该方法
    • 前端修改
      • 1. 在 `statisticsApi.js` 中添加新的 API 方法
      • 2. 修改前端页面代码
      • 3. 修改图表标题和样式
    • 可视化数据格式转换问题解决
  • 成果展示

路径重定向

修改前

在这里插入图片描述

现在前端运行后显示http://localhost:5173/页面,然后重定向到login页面,在这个过程中,会出现鉴权错误,进行提醒,然后跳转到登录页面,基于以下原因,我们进行修改

1. 符合用户访问逻辑

  • 场景:用户首次访问应用时,通常需要先登录(尤其是需要权限的系统)。
  • 问题:如果直接显示 //dashboard,未登录用户会看到空白页或报错(因为未鉴权)。
  • 解决方案:强制重定向到 /login,确保用户从登录入口进入,逻辑更清晰。

2. 避免路由冲突

  • 如果用户未登录,直接访问 / 会先加载 MainLayout,再跳转 /dashboard,触发权限校验错误。

3. 统一路由控制权

  • 前端路由的核心原则:路由配置应集中管理跳转逻辑,而不是依赖组件内部处理(例如在 App.vue 中用 router.push)。
  • 优势:
    • 代码可维护性更高(所有路由跳转在 routes 配置中一目了然)。
    • 避免在多个组件中分散路由逻辑。

添加以下代码:

const routes = [
    {
        path: '/login',
        name: 'Login',
        component: LoginVue,
        meta: { title: '登录' }
    },
    {
        path: '/',
        redirect: '/login'  // 添加这行
    },
    {
        path: '/',
        component: MainLayout,
        redirect: '/dashboard',
        children: [
            // ...其他子路由保持不变
        ]
    }
]

修改后

在这里插入图片描述

模式选择统计

核心需求

  1. 记录模式选择次数
    • 统计 小程序端 用户选择的导览模式(普通、专业、教育)的次数。
    • 每次选择 互斥(同一时间只能记录一种模式的选择)。
    • 防止多用户并行操作导致数据不一致(确保计数器准确递增)。
  2. 数据存储
    • 使用 Neon PostgreSQL 存储统计数据。
    • 表结构包含三个计数器字段(normal_mode_count, professional_mode_count, education_mode_count)。
  3. 前后端协作
    • 前端:用户选择模式后,调用后端 API 提交选择。
    • 后端:原子性更新数据库计数器,确保并发安全。

一、数据库设计(Neon PostgreSQL)

  1. 创建统计表

    CREATE TABLE mode_selection_stats (
        id SERIAL PRIMARY KEY,
        normal_mode_count INT DEFAULT 0,
        professional_mode_count INT DEFAULT 0,
        education_mode_count INT DEFAULT 0,
        updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
    
  2. 初始化数据(插入一行初始记录):

    INSERT INTO mode_selection_stats (normal_mode_count, professional_mode_count, education_mode_count)
    VALUES (0, 0, 0);
    

二、Spring Boot 后端实现

项目结构

src/main/java/com/museum/
├── pojo/ModeStats.java          # 实体类
├── mapper/ModeStatsMapper.java  # MyBatis Mapper
├── service/                     
│   ├── ModeStatsService.java      # 服务接口
│   └── impl/ModeStatsServiceImpl.java # 服务实现
└── controller/ModeStatsController.java # 控制器

依赖配置(pom.xml

这个之前已经添加过了,之前没有添加过这些依赖的需要配置一下

<dependencies>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-jpaartifactId>
    dependency>
    <dependency>
        <groupId>org.postgresqlgroupId>
        <artifactId>postgresqlartifactId>
        <scope>runtimescope>
    dependency>
dependencies>

数据库配置(application.yml

这个之前也是已经添加过了,之前没有添加过这些依赖的需要配置一下

spring:
  datasource:
    url: jdbc:postgresql://your-neon-host:5432/your-db?sslmode=require
    username: your-username
    password: your-password
  jpa:
    hibernate:
      ddl-auto: none  # 禁止自动建表

实体类(ModeStats.java

package com.museum.pojo;

import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("mode_selection_stats") // 指定表名
public class ModeStats {

    @TableId(type = IdType.AUTO) // 自增主键
    private Long id;

    // 普通模式选择次数
    @TableField("normal_mode_count")
    private Integer normalModeCount = 0;

    // 专业模式选择次数
    @TableField("professional_mode_count")
    private Integer professionalModeCount = 0;

    // 教育模式选择次数
    @TableField("education_mode_count")
    private Integer educationModeCount = 0;

    // 最后更新时间
    @TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE) // 自动填充
    private LocalDateTime updatedAt;
}

数据传输DTO(ModeDto.java

package com.museum.dto;

import lombok.Data;

@Data  // Lombok 注解,自动生成 getter/setter
public class ModeDto {
    private String mode;  // 必须与前端 JSON 的字段名一致
}

采用DTO:

  • 直接使用 Entity(如 ModeStats)作为接口参数/返回值时,会暴露数据库表结构,存在安全隐患(如敏感字段泄露)。
  • 定义专用的数据传输对象,仅暴露必要的字段,隐藏底层模型细节。
  • 直接在 Controller 中使用 @RequestParamMap 接收参数时,校验逻辑分散且难以维护
  • 前端可能传递 JSON、Form Data 或 URL 参数,DTO 统一通过 @RequestBody 接收 JSON,或通过 @ModelAttribute 接收表单数据,适配不同协议

Mapper 接口(ModeStatsMapper.java

使用原子操作,实现互斥

package com.museum.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.museum.pojo.ModeStats;
import org.apache.ibatis.annotations.Update;

public interface ModeStatsMapper extends BaseMapper<ModeStats> {

    // 原子操作:普通模式计数器+1
    @Update("UPDATE mode_selection_stats SET normal_mode_count = normal_mode_count + 1, updated_at = NOW() WHERE id = 1")
    int incrementNormalMode();

    // 专业模式计数器+1
    @Update("UPDATE mode_selection_stats SET professional_mode_count = professional_mode_count + 1, updated_at = NOW() WHERE id = 1")
    int incrementProfessionalMode();

    // 教育模式计数器+1
    @Update("UPDATE mode_selection_stats SET education_mode_count = education_mode_count + 1, updated_at = NOW() WHERE id = 1")
    int incrementEducationMode();
}

Controller(ModeStatsController.java

package com.museum.controller;

import com.museum.pojo.ModeStats;
import com.museum.service.ModeStatsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/mode-stats")
public class ModeStatsController {

    @Autowired
    private ModeStatsService modeStatsService;

    @PostMapping("/update")
    public String updateModeStats(@RequestParam String mode) {
        boolean success = modeStatsService.updateModeStats(mode);
        return success ? "更新成功" : "无效的模式类型";
    }

    @GetMapping("/current")
    public ModeStats getModeStats() {
        return modeStatsService.getModeStats();
    }
}

Service接口(ModeStatsService.java

package com.museum.service;

import com.museum.pojo.ModeStats;

public interface ModeStatsService {
    // 更新模式选择次数
    boolean updateModeStats(String mode);
    
    // 获取当前统计数据
    ModeStats getModeStats();
}

Service实现类(ModeStatsServiceImpl.java

package com.museum.service.impl;

import com.museum.mapper.ModeStatsMapper;
import com.museum.pojo.ModeStats;
import com.museum.service.ModeStatsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class ModeStatsServiceImpl implements ModeStatsService {

    @Autowired
    private ModeStatsMapper modeStatsMapper;

    @Override
    @Transactional
    public boolean updateModeStats(String mode) {
        try {
            switch (mode) {
                case "normal":
                    modeStatsMapper.incrementNormalMode();
                    break;
                case "professional":
                    modeStatsMapper.incrementProfessionalMode();
                    break;
                case "education":
                    modeStatsMapper.incrementEducationMode();
                    break;
                default:
                    return false;
            }
            return true;
        } catch (Exception e) {
            throw new RuntimeException("更新模式统计失败", e);
        }
    }

    @Override
    public ModeStats getModeStats() {
        return modeStatsMapper.selectById(1L); // 默认查询id=1的记录
    }
}

重点说明

  1. 事务控制
    • Service 实现类中使用 @Transactional 保证计数器更新的原子性。
  2. 异常处理
    • Service层捕获异常并转换为业务异常(如 RuntimeException)。
  3. MyBatis-Plus 简化操作
    • 直接使用 modeStatsMapper.selectById(1L) 查询数据。
  4. RESTful 接口设计
    • POST /api/mode-stats/update 更新统计
    • GET /api/mode-stats/current 获取当前数据

三、Uni-app 前端修改

  1. 调整 confirmSelection 方法

    async confirmSelection() {
        if (!this.selectedMode) return;
        
        uni.showLoading({ title: '提交中...' });
        
        try {
            // 调用 Spring Boot 后端 API
            const response = await uni.request({
                url: 'https://your-springboot-domain/api/update-mode-stats',
                method: 'POST',
                data: { mode: this.selectedMode },
                header: { 'Content-Type': 'application/json' }
            });
            
            // 本地存储并跳转
            uni.setStorageSync('selectedMode', this.selectedMode);
            uni.redirectTo({ url: '/pages/ai_tour_guide/ai_tour_guide' });
            
        } catch (err) {
            uni.showToast({ title: '提交失败: ' + err.message, icon: 'none' });
        } finally {
            uni.hideLoading();
        }
    }
    
  2. 测试接口

    • 使用 Postman 测试 POST /api/update-mode-stats?mode=normal
    • 使用微信小程序实践测试
    • 检查数据库计数器是否递增(测试成功)。

可视化用户角色分布

后端修改

1. 在 ModeStatsController 中添加新的接口

@GetMapping("/distribution")
    public Map<String, Integer> getModeDistribution() {
       return modeStatsService.getModeDistribution();
    }

2. 在 ModeStatsService 接口中添加方法

Map<String, Integer> getModeDistribution();

3. 在 ModeStatsServiceImpl 中实现该方法

@Override
public Map<String, Integer> getModeDistribution() {
    ModeStats stats = getModeStats();
    Map<String, Integer> distribution = new HashMap<>();
    distribution.put("普通模式", stats.getNormalModeCount());
    distribution.put("专业模式", stats.getProfessionalModeCount());
    distribution.put("教育模式", stats.getEducationModeCount());
    return distribution;
}

前端修改

1. 在 statisticsApi.js 中添加新的 API 方法

// 获取模式分布数据
getModeDistribution: (params) => {
  return request({
    url: '/api/mode-stats/distribution',
    method: 'get',
    params
  })
}

2. 修改前端页面代码

// 获取用户模式分布
const fetchUserRoles = async () => {
  loading.userRoles = true;
  try {
    const result = await statisticsApi.getModeDistribution(dateRangeParams.value);
    // 将后端返回的数据格式转换为前端图表需要的格式
    chartData.userRoles = Object.entries(result).map(([name, value]) => ({
      name,
      value
    }));
  } catch (error) {
    console.error('获取用户模式分布失败', error);
    ElMessage.error('获取用户模式分布失败');
  } finally {
    loading.userRoles = false;
  }
};

3. 修改图表标题和样式


<chart-card title="用户模式分布" :loading="loading.userRoles" :refreshable="true" @refresh="refreshUserRoles">
  <pie-chart :data="chartData.userRoles" height="300px" :doughnut="true"
    :colors="['#1e88e5', '#26a69a', '#ffc107']" />
chart-card>

可视化数据格式转换问题解决

  • 后端返回的是 Map 格式(如 {"普通模式":2,"教育模式":0,"专业模式":0}

    {
      普通模式: 2,
      教育模式: 0,
      专业模式: 0
    }
    
  • 前端 pie-chart 组件需要的是 Array<{name: string, value: number}> 格式

  • 前端 错误地进入了 catch,导致数据无法正确渲染到饼图

    [
      { name: "普通模式", value: 2 },
      { name: "教育模式", value: 0 },
      { name: "专业模式", value: 0 }
    ]
    

修改代码进行数据处理:

// 如果错误对象本身就是数据(如API返回非200但带数据)
if (error && typeof error === "object" && !Array.isArray(error)) {
  chartData.userRoles = Object.entries(error).map(([name, value]) => ({
    name,
    value: Number(value) || 0,
  }));
  console.warn("成功:", chartData.userRoles);
}

成果展示

【慧游鲁博】【8】web端·路径重定向·用户选择模式存储/统计·数据格式转换_第1张图片

你可能感兴趣的:(创新实训个人记录,前端,vue,postgresql,数据可视化)