全栈项目(golang+vue)门诊系统

全栈项目(golang+vue)门诊系统

接一项目 主要实现的功能是,处理牙医门诊的预约管理。拖拖拽拽就能管理客户的预约。最近在练手golang, 就使用golang自己实现一遍后端接口,并加入自己用的登录验证, 图片存储使用的七牛存储。为了除去敏感信息,现在展示出来的前端只保留预约面板的主逻辑,其他页面简单重写实现 。(正经门诊不可能用迪迦做背景板吧🥲)

登录页

预约面板(vue-grid-layout)

项目预约使用的的是vue-grid-layout,插件文档Vue Grid Layout -️ 适用Vue.js的栅格布局系统

前端(vite + vue3 + pinia + element-plus + vue-grid-layout)

主要使用的插件

  • unplugin-auto-import/vite 按需自动导入api
  • unplugin-vue-router/vite 根据文件自动生成路由
  • vite-plugin-vue-layouts 搭配unplugin-vue-router/vite
  • vite-plugin-html 修改html

主要使用的函数

接口请求封装

js 复制代码
// request.js
import axios from 'axios'
import { ElMessage, ElLoading } from 'element-plus'
import { useAdminStore } from '@/store/admin'

import { to } from '@/utils'

const baseUrl = import.meta.env.VITE_APP_PROXY_URL

let loading = null

const instance = axios.create({
  baseURL: baseUrl,
  timeout: 60 * 1000,
})

// 请求拦截
instance.interceptors.request.use(
  (config) => {
    loading = ElLoading.service({
      lock: true,
      loading: false,
    })
    return config
  },
  (error) => {
    return Promise.reject(error)
  },
)
// 相应拦截
instance.interceptors.response.use(
  (response) => {
    const { status, data, request } = response
    loading.close()
    if (status !== 200) {
      return Promise.reject(status)
    } else {
      if (data.code !== 0) {
        ElMessage({
          message: `${data.msg}`,
          type: 'error',
          grouping: true,
          offset: 200,
        })
        return Promise.reject(data)
      } else {
        return Promise.resolve(data.data)
      }
    }
  },
  (error) => {
    loading.close()
    return Promise.reject(error)
  },
)

export const request = (req) => {
  const adminStore = useAdminStore()
  const token = adminStore.token
  let data = {}
  if (req?.method === 'get') {
    data = { params: data }
  } else {
    data = { data: data }
  }
  let headers = {}
  if (req.headers) {
    headers = req.reqders
  }
  if (token) {
    headers = {
      ...req.headers,
      'x-token': token,
    }
  }

  const good = {
    method: 'post',
    url: req.url,
    headers,
    ...data,
  }

  return to(instance(good))
}

async await 接口处理, 避免promise面条链式调用

js 复制代码
export const to = (promise) => promise.then((res) => [res, null]).catch((err) => [null, err])
举例说明
const onfetchClinicUserLogin = async () => {
const [resOrder,errOrder] = await  onApiOrder()
const [resDetail,errDetail] = await  onApiDetail({id:resOrder.id})
}

预约面板处理,需要支持随意拖动到固定位置,改变大小。 开发使用的是vue3-grid-layout-next, 以支持vue3

userStore 为使用pinia,组件间通信使用全局store,避免数据多级传递混乱

js 复制代码
<grid-layout
  v-if="userStore.layout.length"
  v-model:layout="userStore.layout"
  :margin="[0, 0]"
  :col-num="getColNum()"
  :row-height="30"
  :is-draggable="true"
  :is-resizable="true"
  :responsive="false"
  :vertical-compact="false"
  :prevent-collision="true"
  :use-css-transforms="true"
>
  <grid-item
    v-for="item in userStore.layout"
    v-show="isShowOrder(item.data)"
    :key="item.i"
    :x="item.x"
    :y="item.y"
    :w="item.w"
    :h="item.h"
    :i="item.i"
    :max-w="item.w"
    :min-w="item.w"
    @moved="onMoved"
    @move="onMove"
    @resized="onResize"
  >
    <ComShowPopover :data="item.data"></ComShowPopover>
  </grid-item>
</grid-layout>

后端(gin + gorm + gin + postgressql + redis + qiniu )

主要使用的插件

  • gin api服务
  • chai2010/webp 图片转webp 节省空间大小
  • go-redis/redis redis
  • uuid/v5 数据库id使用uuid
  • golang-jwt/jwt/v4 登录
  • gorm PostgreSQL
  • viper 环境配置

主要使用到的函数

gorm创建数据库

gorm创建表,gen生成crud,完美

go 复制代码
package main

import (
	"fmt"
	"goo/config"
	"goo/table/abc"
	"goo/table/clinic"

	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	"gorm.io/gorm/schema"
)

func main() {
	var db, err = gorm.Open(postgres.Open(config.SqlDsn), &gorm.Config{
		NamingStrategy: schema.NamingStrategy{
			SingularTable: true,
		},
	})
	if err != nil {
		fmt.Println("连接数据库失败:")
	}

	tables := []interface{}{
		abc.TUser{},
		abc.TShop{},
		clinic.CUser{},
		clinic.COrder{},
	}
	for _, t := range tables {
		_ = db.AutoMigrate(&t)
		// 视图 authority_menu 会被当成表来创建,引发冲突错误(更新版本的gorm似乎不会)
		// 由于 AutoMigrate() 基本无需考虑错误,因此显式忽略
	}
}

gen生成数据库相应的crud

go 复制代码
package main

import (
	"gorm.io/driver/postgres"
	"gorm.io/gen"
	"gorm.io/gorm"

	"goo/config"
)

func main() {

	db, _ := gorm.Open(postgres.Open(config.SqlDsn), &gorm.Config{})
	g := gen.NewGenerator(gen.Config{
		OutPath: "../../dal/query",
		Mode:    gen.WithDefaultQuery | gen.WithoutContext | gen.WithQueryInterface,
	})
	g.UseDB(db)
	g.ApplyBasic(g.GenerateAllTable()...)
	g.Execute()
}

数据库设计

go 复制代码
// 店铺
type CShop struct {
	global.GVA_MODEL
	Name   string `json:"name" gorm:"comment:名称"`                         // 名称
	Image  string `json:"image" gorm:"comment:图片"`                        // 图片
	Status int    `json:"status" gorm:"default:0;comment:状态 0新店未启用 1启用 "` //状态 0新店未启用 1启用
	Sort   int    `json:"sort" gorm:"default:0;comment:排序"`               //排序

	//
	Users     []CUser     `json:"users"`
}
type CUser struct {
	global.GVA_MODEL
	Name      string `json:"name" gorm:"comment:名称"`                // 名称
	OpenId    string `json:"open_id" gorm:"comment:微信id"`           // 微信id
	Email     string `json:"email" gorm:"comment:邮箱"`               // 邮箱
	Password  string `json:"password" gorm:"comment:密码"`            // 密码
	Authority string `json:"authority" gorm:"comment:权限;default:3"` // 权限

	CShopID string `json:"-"`
	CShop   CShop  `json:"-"`
}

jwt中间件

go 复制代码
package middleware

import (
	"context"
	"errors"
	"goo/global"
	"goo/response"
	"goo/utils"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v4"
)

func JWTAuth() gin.HandlerFunc {
	return func(c *gin.Context) {
		token := c.Request.Header.Get("x-token")
		if token == "" {
			response.FailWithMessage("auth", c)
			c.Abort()
			return
		} else {

			j := utils.NewJWT()
			claims, err := j.ParseToken(token)
			if err != nil {
				if errors.Is(err, utils.ErrTokenExpired) {
					response.FailWithMessage("token授权已过期", c)
					c.Abort()
					return
				}
				response.FailWithMessage(err.Error(), c)
				c.Abort()
				return
			}

			resRedis, errRedis := global.GVA_REDIS.Get(context.Background(), claims.BaseClaims.ID).Result()
			if errRedis != nil {
				response.FailWithMessage("redis时效过期, 请重新登录", c)
				c.Abort()
				return
			}

			if resRedis != token {
				response.FailWithMessage("您已在其他地方登录, 请重新登录", c)
				c.Abort()
				return
			}

			time11 := claims.ExpiresAt.Unix() - time.Now().Unix()
			if time11 < claims.BufferTime {
				ep, _ := utils.ParseDuration(global.GVA_VP.GetString("jwt.expires-time"))
				claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(ep))
				newToken, _ := j.CreateTokenByOldToken(token, *claims)
				// c.Header("new-token", newToken)
				errRedis := global.GVA_REDIS.Set(context.Background(), claims.BaseClaims.ID, newToken, ep).Err()
				if errRedis != nil {
					response.FailWithMessage("err redis set", c)
					c.Abort()
					return
				}
			}
			//  验证jwt剩余时间
			c.Set("token_user_id", claims.BaseClaims.ID)
			c.Next()
		}
	}
}
相关推荐
hakesashou2 分钟前
python交互式命令时如何清除
java·前端·python
HEX9CF21 分钟前
【CTF Web】Pikachu xss之href输出 Writeup(GET请求+反射型XSS+javascript:伪协议绕过)
开发语言·前端·javascript·安全·网络安全·ecmascript·xss
凌云行者33 分钟前
使用rust写一个Web服务器——单线程版本
服务器·前端·rust
华农第一蒟蒻1 小时前
Java中JWT(JSON Web Token)的运用
java·前端·spring boot·json·token
积水成江1 小时前
关于Generator,async 和 await的介绍
前端·javascript·vue.js
___Dream1 小时前
【黑马软件测试三】web功能测试、抓包
前端·功能测试
金灰1 小时前
CSS3练习--电商web
前端·css·css3
人生の三重奏1 小时前
前端——js补充
开发语言·前端·javascript
计算机学姐1 小时前
基于SpringBoot+Vue的高校运动会管理系统
java·vue.js·spring boot·后端·mysql·intellij-idea·mybatis
Tandy12356_1 小时前
js逆向——webpack实战案例(一)
前端·javascript·安全·webpack