全栈项目(golang+vue)门诊系统
接一项目 主要实现的功能是,处理牙医门诊的预约管理。拖拖拽拽就能管理客户的预约。最近在练手golang, 就使用golang自己实现一遍后端接口,并加入自己用的登录验证, 图片存储使用的七牛存储。为了除去敏感信息,现在展示出来的前端只保留预约面板的主逻辑,其他页面简单重写实现 。(正经门诊不可能用迪迦做背景板吧🥲)
- 测试环境:clinic-test.cooog.com
- 正式环境:clinic.cooog.com
登录页
预约面板(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()
}
}
}