全栈项目(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()
		}
	}
}
相关推荐
passerby606113 分钟前
完成前端时间处理的另一块版图
前端·github·web components
KYGALYX15 分钟前
服务异步通信
开发语言·后端·微服务·ruby
掘了21 分钟前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅24 分钟前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅1 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
爬山算法1 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
崔庆才丨静觅1 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment1 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅1 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊2 小时前
jwt介绍
前端