forked from wts/wts
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81e8a77580 | ||
|
|
5223d1127f | ||
|
|
e6ce1bfecf | ||
|
|
8dd650a16c | ||
|
|
9240e987a5 | ||
|
|
d2cde59e85 | ||
|
|
15f18bcf83 | ||
|
|
fbe20025b8 | ||
|
|
ff552afa43 | ||
|
|
0e735d2f2f | ||
|
|
7173fd4a01 | ||
|
|
772091667e | ||
|
|
ba28c41c7f | ||
|
|
9f62470cba | ||
|
|
2597eeffb7 | ||
|
|
7583614745 | ||
|
|
f9bc8e2730 | ||
|
|
3c1af23eb1 | ||
|
|
5be21f5598 | ||
|
|
cad4191c0d | ||
|
|
828752bf77 | ||
|
|
33e5d4ca56 | ||
|
|
62d2375775 | ||
|
|
03b455e7e9 | ||
|
|
979c3fc24f | ||
|
|
0f44d8f4e4 | ||
|
|
87f7fb6c01 | ||
|
|
2ffac40054 | ||
|
|
119328ce8f | ||
|
|
f0d8ad461c | ||
|
|
2480046316 | ||
|
|
bf32cc5372 | ||
|
|
a42d5f6134 |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
artifacts
|
||||||
|
.vscode
|
||||||
135
Makefile
135
Makefile
@@ -1,127 +1,26 @@
|
|||||||
|
PWD= $(shell pwd)
|
||||||
|
BACK = $(PWD)/back
|
||||||
|
FRONT = $(PWD)/front
|
||||||
|
|
||||||
SHELL := /bin/bash
|
.PHONY: back front mkdir1 mkdir-front back-dependcies front-dependcies
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
all: back front
|
||||||
|
|
||||||
BACK_DIR := back
|
back-dependcies:
|
||||||
FRONT_DIR := front
|
cd $(BACK)/src && go mod tidy
|
||||||
|
|
||||||
.PHONY: help
|
front-dependcies:
|
||||||
help:
|
cd $(FRONT) && IBM_TELEMETRY_DISABLED='true';npm install
|
||||||
@printf '%s\n' \
|
|
||||||
'Usage:' \
|
|
||||||
' make <target>' \
|
|
||||||
'' \
|
|
||||||
'Common targets:' \
|
|
||||||
' dev Run front+back dev servers' \
|
|
||||||
' build Build front+back' \
|
|
||||||
' test Run front+back tests' \
|
|
||||||
' clean Remove build artifacts' \
|
|
||||||
'' \
|
|
||||||
'Backend targets:' \
|
|
||||||
' back-build Build server+tool binaries' \
|
|
||||||
' back-dev Build and run server (dev config)' \
|
|
||||||
' back-run Run server (dev config)' \
|
|
||||||
' back-tool Build tool binary' \
|
|
||||||
' back-run-tool Run tool (dev config)' \
|
|
||||||
' back-test go test ./... (in back/src)' \
|
|
||||||
' back-fmt gofmt ./... (in back/src)' \
|
|
||||||
'' \
|
|
||||||
'Frontend targets:' \
|
|
||||||
' front-install npm ci (in front)' \
|
|
||||||
' front-dev npm run dev (in front)' \
|
|
||||||
' front-build npm run build (in front)' \
|
|
||||||
' front-preview npm run preview (in front)' \
|
|
||||||
' front-check npm run check (in front)' \
|
|
||||||
' front-lint npm run lint (in front)' \
|
|
||||||
' front-format npm run format (in front)' \
|
|
||||||
' front-test npm test (in front)'
|
|
||||||
|
|
||||||
.PHONY: dev build test clean install doctor
|
mkdir1:
|
||||||
|
mkdir -p $(PWD)/artifacts
|
||||||
|
|
||||||
dev:
|
mkdir-front:
|
||||||
@bash -c 'set -euo pipefail; \
|
mkdir -p $(PWD)/artifacts/FrontEndBuild
|
||||||
$(MAKE) dev-front & pf=$$!; \
|
|
||||||
$(MAKE) dev-back & pb=$$!; \
|
|
||||||
trap "kill $$pf $$pb 2>/dev/null || true" INT TERM EXIT; \
|
|
||||||
wait $$pf $$pb'
|
|
||||||
|
|
||||||
build: back-build front-build
|
|
||||||
|
|
||||||
test: back-test front-test
|
back: mkdir1 back-dependcies
|
||||||
|
cd $(BACK) && make server && cp $(BACK)/build/wts $(PWD)/artifacts/wts
|
||||||
|
|
||||||
clean: back-clean front-clean
|
front: mkdir-front front-dependcies
|
||||||
|
cd $(FRONT) && IBM_TELEMETRY_DISABLED='true';npm run build && cp -r $(FRONT)/build/* $(PWD)/artifacts/FrontEndBuild
|
||||||
install: back-install front-install
|
|
||||||
|
|
||||||
doctor:
|
|
||||||
@command -v go >/dev/null 2>&1 && go version || echo 'go: not found'
|
|
||||||
@command -v node >/dev/null 2>&1 && node --version || echo 'node: not found'
|
|
||||||
@command -v npm >/dev/null 2>&1 && npm --version || echo 'npm: not found'
|
|
||||||
|
|
||||||
## Backend
|
|
||||||
.PHONY: dev-back back-dev back-build back-server back-tool back-run back-run-tool back-test back-fmt back-clean back-install
|
|
||||||
|
|
||||||
dev-back: back-dev
|
|
||||||
|
|
||||||
back-dev:
|
|
||||||
@$(MAKE) -C $(BACK_DIR) dev
|
|
||||||
|
|
||||||
back-build:
|
|
||||||
@$(MAKE) -C $(BACK_DIR) build-all
|
|
||||||
|
|
||||||
back-server:
|
|
||||||
@$(MAKE) -C $(BACK_DIR) server
|
|
||||||
|
|
||||||
back-tool:
|
|
||||||
@$(MAKE) -C $(BACK_DIR) tool
|
|
||||||
|
|
||||||
back-run:
|
|
||||||
@$(MAKE) -C $(BACK_DIR) start-server
|
|
||||||
|
|
||||||
back-run-tool:
|
|
||||||
@$(MAKE) -C $(BACK_DIR) start-tool
|
|
||||||
|
|
||||||
back-test:
|
|
||||||
@cd $(BACK_DIR)/src && go test ./...
|
|
||||||
|
|
||||||
back-fmt:
|
|
||||||
@cd $(BACK_DIR)/src && gofmt -w ./
|
|
||||||
|
|
||||||
back-clean:
|
|
||||||
@rm -f $(BACK_DIR)/build/wts $(BACK_DIR)/build/wtstool
|
|
||||||
|
|
||||||
back-install:
|
|
||||||
@cd $(BACK_DIR)/src && go mod download
|
|
||||||
|
|
||||||
## Frontend
|
|
||||||
.PHONY: dev-front front-install front-dev front-build front-preview front-check front-lint front-format front-test front-clean
|
|
||||||
|
|
||||||
dev-front: front-dev
|
|
||||||
|
|
||||||
front-install:
|
|
||||||
@npm --prefix $(FRONT_DIR) ci
|
|
||||||
|
|
||||||
front-dev:
|
|
||||||
@npm --prefix $(FRONT_DIR) run dev
|
|
||||||
|
|
||||||
front-build:
|
|
||||||
@npm --prefix $(FRONT_DIR) run build
|
|
||||||
|
|
||||||
front-preview:
|
|
||||||
@npm --prefix $(FRONT_DIR) run preview
|
|
||||||
|
|
||||||
front-check:
|
|
||||||
@npm --prefix $(FRONT_DIR) run check
|
|
||||||
|
|
||||||
front-lint:
|
|
||||||
@npm --prefix $(FRONT_DIR) run lint
|
|
||||||
|
|
||||||
front-format:
|
|
||||||
@npm --prefix $(FRONT_DIR) run format
|
|
||||||
|
|
||||||
front-test:
|
|
||||||
@npm --prefix $(FRONT_DIR) test
|
|
||||||
|
|
||||||
front-clean:
|
|
||||||
@rm -rf $(FRONT_DIR)/build $(FRONT_DIR)/.svelte-kit $(FRONT_DIR)/.vite
|
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -1,2 +1,37 @@
|
|||||||
# 中山学院网维报修系统
|
# 中山学院网维报修系统
|
||||||
> 第三版
|
> 第三版
|
||||||
|
|
||||||
|
|
||||||
|
## 介绍
|
||||||
|
用于电子科技大学中山学院信息中心网络维护科的工单报修系统,依托于微信服务号提供服务。
|
||||||
|
|
||||||
|
|
||||||
|
技术栈:
|
||||||
|
前端:Svelte/SvelteKit
|
||||||
|
UI:[Carbon Componenets Svelte](https://svelte.carbondesignsystem.com/)
|
||||||
|
HTTP通信:Axios
|
||||||
|
|
||||||
|
后端:Go
|
||||||
|
HTTP通信&杂项:Echo
|
||||||
|
数据库:PostgreSQL([sqlc](https://sqlc.dev))
|
||||||
|
|
||||||
|
## 构建与部署
|
||||||
|
依赖:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm
|
||||||
|
|
||||||
|
Go >= 1.24.6
|
||||||
|
|
||||||
|
GNU make
|
||||||
|
|
||||||
|
```
|
||||||
|
在项目根目录下执行`make`,在`artifacts`里面查看后端可执行文件和前端素材文件夹,之后随便你怎么部署。
|
||||||
|
|
||||||
|
配置文件请查看`back/doc`下的示例文件,或者找开发组组长要一份生产环境下的。
|
||||||
|
|
||||||
|
这里附赠了systemd unit文件,想这样部署的话可以参考。
|
||||||
|
|
||||||
|
另外注意一下后端程序只监听`127.0.0.1`,是硬编码在程序里的,所以要套上一层反代。
|
||||||
|
|
||||||
|
后端可以自己托管前端文件夹,也可以在反代那里就把请求拦截下来,这个看怎么部署,根据你的需要,和喜好自行决定即可。
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 在每天值班结束的时候,自动取消(改日修)预约在今天但是状态今天没有更新的工单
|
// 在每天值班结束的时候,自动取消预约在今天但是状态今天没有更新的工单
|
||||||
func scheduledAutoCancel() {
|
func scheduledAutoCancel() {
|
||||||
go func() {
|
go func() {
|
||||||
var first = true
|
var first = true
|
||||||
|
|||||||
@@ -118,3 +118,9 @@ type TicketOverviewResponse struct {
|
|||||||
commonMember
|
commonMember
|
||||||
CountByBlock map[sqlc.WtsBlock]int64 `json:"count_by_block,omitempty"`
|
CountByBlock map[sqlc.WtsBlock]int64 `json:"count_by_block,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used by various APIs......
|
||||||
|
type GenericResponse struct {
|
||||||
|
commonMember
|
||||||
|
Store any `json:"Store"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ import "errors"
|
|||||||
// 多个逻辑可能共享这些错误定义
|
// 多个逻辑可能共享这些错误定义
|
||||||
var (
|
var (
|
||||||
//注册时,数据库中不存在该学号对应的记录(data.students)
|
//注册时,数据库中不存在该学号对应的记录(data.students)
|
||||||
ErrNoStudentRecord = errors.New("抱歉,您输入的姓名或学号有误,如果确信所输入信息没有问题,请联系我们的工作人员。")
|
ErrNoStudentRecord = errors.New("您输入的姓名或学号有误,如果确信所输入信息没有问题,请联系我们的工作人员。")
|
||||||
//注册时,所提供的学号-姓名不匹配数据库记录
|
//注册时,所提供的学号-姓名不匹配数据库记录
|
||||||
ErrSidNameNotMatch = errors.New("抱歉,您输入的姓名或学号有误,如果确信所输入信息没有问题,请联系我们的工作人员。")
|
ErrSidNameNotMatch = errors.New("您输入的姓名或学号有误,如果确信所输入信息没有问题,请联系我们的工作人员。")
|
||||||
//注册时,提供的学号-姓名所对应的用户已经注册了
|
//注册时,提供的学号-姓名所对应的用户已经注册了
|
||||||
ErrUserAlreadyRegistered = errors.New("您已经注册了。如果您确信您还没有注册,请联系我们的工作人员。")
|
ErrUserAlreadyRegistered = errors.New("您已经注册了。如果您确信您还没有注册,请联系我们的工作人员。")
|
||||||
//该联系电话号码已在数据库中被使用注册
|
//该联系电话号码已在数据库中被使用注册
|
||||||
ErrPhoneUsed = errors.New("抱歉,您所使用的联系电话已经被登记,请换一个不一样的电话号码。")
|
ErrPhoneUsed = errors.New("您所使用的联系电话已经被登记,请换一个不一样的电话号码。")
|
||||||
//注册时,该微信号已被使用注册
|
//注册时,该微信号已被使用注册
|
||||||
ErrWxUsed = errors.New("抱歉,您的微信已经注册过了,一个微信只能注册一个账号。")
|
ErrWxUsed = errors.New("您的微信已经注册过了,一个微信只能注册一个账号。")
|
||||||
// 真的。。。会出现这种错误吗?
|
// 真的。。。会出现这种错误吗?
|
||||||
ErrDataInconsistent = errors.New("创建用户时数据库返回数据与请求数据不一致,请联系我们的技术团队。")
|
ErrDataInconsistent = errors.New("创建用户时数据库返回数据与请求数据不一致,请联系我们的技术团队。")
|
||||||
// 根据OpenID查不到用户
|
// 根据OpenID查不到用户
|
||||||
@@ -23,8 +23,8 @@ var (
|
|||||||
ErrAppointTimeInvalid = errors.New("请填写有效的预约时间")
|
ErrAppointTimeInvalid = errors.New("请填写有效的预约时间")
|
||||||
// 故障发生时间晚于现在了
|
// 故障发生时间晚于现在了
|
||||||
ErrOccurAtTimeInvalid = errors.New("请填写有效的故障发生时间")
|
ErrOccurAtTimeInvalid = errors.New("请填写有效的故障发生时间")
|
||||||
// 不允许用户创建太多没有关闭的工单,设置成3个,有需要可以改
|
// 不允许用户创建太多没有关闭的工单,设置成~3~个,有需要可以改
|
||||||
ErrTicketTooMuch = errors.New("抱歉,您当前还有正在活跃的报修,无法创建新报修")
|
ErrTicketTooMuch = errors.New("您当前还有正在活跃的报修,无法创建新报修")
|
||||||
// 根据工单ID查不到工单
|
// 根据工单ID查不到工单
|
||||||
ErrNoSuchTicket = errors.New("无法找到对应的工单")
|
ErrNoSuchTicket = errors.New("无法找到对应的工单")
|
||||||
// 根据网维成员ID查不到网维成员
|
// 根据网维成员ID查不到网维成员
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func NewTicket(c *hutil.WtsCtx, op string, r hutil.NewTicketRequest) hutil.NewTi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
//fmt.Println("未结工单数量:", count)
|
//fmt.Println("未结工单数量:", count)
|
||||||
if count >= 3 {
|
if count >= 1 {
|
||||||
return hutil.NewWtsErr(ErrTicketTooMuch, nil)
|
return hutil.NewWtsErr(ErrTicketTooMuch, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ var freshTo = map[sqlc.WtsStatus]bool{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var NoPresentTo = map[sqlc.WtsStatus]bool{
|
var NoPresentTo = map[sqlc.WtsStatus]bool{
|
||||||
sqlc.WtsStatusFresh: false,
|
sqlc.WtsStatusFresh: true,
|
||||||
sqlc.WtsStatusDelay: true,
|
sqlc.WtsStatusDelay: true,
|
||||||
sqlc.WtsStatusScheduled: true,
|
sqlc.WtsStatusScheduled: true,
|
||||||
sqlc.WtsStatusEscalated: true,
|
sqlc.WtsStatusEscalated: true,
|
||||||
@@ -108,7 +108,7 @@ var NoPresentTo = map[sqlc.WtsStatus]bool{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ScheduledTo = map[sqlc.WtsStatus]bool{
|
var ScheduledTo = map[sqlc.WtsStatus]bool{
|
||||||
sqlc.WtsStatusFresh: false,
|
sqlc.WtsStatusFresh: true,
|
||||||
sqlc.WtsStatusDelay: true,
|
sqlc.WtsStatusDelay: true,
|
||||||
sqlc.WtsStatusScheduled: true,
|
sqlc.WtsStatusScheduled: true,
|
||||||
sqlc.WtsStatusEscalated: true,
|
sqlc.WtsStatusEscalated: true,
|
||||||
@@ -120,7 +120,7 @@ var EscalatedTo = map[sqlc.WtsStatus]bool{
|
|||||||
sqlc.WtsStatusFresh: false,
|
sqlc.WtsStatusFresh: false,
|
||||||
sqlc.WtsStatusDelay: false,
|
sqlc.WtsStatusDelay: false,
|
||||||
sqlc.WtsStatusScheduled: true,
|
sqlc.WtsStatusScheduled: true,
|
||||||
sqlc.WtsStatusEscalated: true,
|
sqlc.WtsStatusEscalated: false,
|
||||||
sqlc.WtsStatusCanceled: true,
|
sqlc.WtsStatusCanceled: true,
|
||||||
sqlc.WtsStatusSolved: true,
|
sqlc.WtsStatusSolved: true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func (i *Ctx) handleWXEvent(m *message.MixMessage) string {
|
|||||||
|
|
||||||
// 用户关注时发送欢迎文本
|
// 用户关注时发送欢迎文本
|
||||||
if m.Event == message.EventSubscribe {
|
if m.Event == message.EventSubscribe {
|
||||||
return "同学你好,欢迎使用网维报修系统~\n\n建议先看看使用攻略呢:https://wts.zsxyww.com/self-service/usage\n"
|
return "同学你好,欢迎使用网维报修系统~\n\n建议先看看使用攻略呢:https://wwbx.davisye.cn/help\n"
|
||||||
}
|
}
|
||||||
if m.Event == message.EventView {
|
if m.Event == message.EventView {
|
||||||
// 不知道为什么,view事件也会被送到这里,如果不处理的话会在log里面出现,有点烦,对用户体验倒是没什么影响
|
// 不知道为什么,view事件也会被送到这里,如果不处理的话会在log里面出现,有点烦,对用户体验倒是没什么影响
|
||||||
@@ -67,7 +67,7 @@ func (i *Ctx) handleWXTxtMsg(m *message.MixMessage) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *Ctx) handleNormalMsg(m *message.MixMessage) string {
|
func (i *Ctx) handleNormalMsg(m *message.MixMessage) string {
|
||||||
return "聊天功能正在开发中"
|
return "您好,系统暂时不支持自动回复哦~点击<a href=\"https://wwbx.davisye.cn\">这里</a>进入系统界面进行报修吧~"
|
||||||
//return i.superEasyNLPProgram(m)
|
//return i.superEasyNLPProgram(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
jwt "github.com/golang-jwt/jwt/v5"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
. "zsxyww.com/wts/handler/handlerUtilities"
|
. "zsxyww.com/wts/handler/handlerUtilities"
|
||||||
|
hutil "zsxyww.com/wts/handler/handlerUtilities"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GET: /br-debug/testdb
|
// GET: /br-debug/testdb
|
||||||
@@ -36,5 +38,16 @@ func Hello(i echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Panic(i echo.Context) error {
|
func Panic(i echo.Context) error {
|
||||||
|
c := i.(*WtsCtx)
|
||||||
|
var res hutil.GenericResponse
|
||||||
|
//校验权限
|
||||||
|
if !c.Cfg.Debug.SkipJWTAuth {
|
||||||
|
if !hutil.IsAdmin(i.Get("jwt").(*jwt.Token).Claims.(*hutil.WtsJWT).Access) {
|
||||||
|
res.Success = false
|
||||||
|
res.ErrType = hutil.ErrAuth
|
||||||
|
res.Msg = "only developers can access this API"
|
||||||
|
return i.JSON(403, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
panic("this is a test panic")
|
panic("this is a test panic")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
echojwt "github.com/labstack/echo-jwt/v4"
|
echojwt "github.com/labstack/echo-jwt/v4"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
@@ -24,6 +26,27 @@ func middlewareRegister(app *echo.Echo, cfg *config.Config) {
|
|||||||
app.Use(middleware.Recover())
|
app.Use(middleware.Recover())
|
||||||
app.Use(middleware.Secure())
|
app.Use(middleware.Secure())
|
||||||
app.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20.0)))
|
app.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20.0)))
|
||||||
|
|
||||||
|
app.Use(middleware.GzipWithConfig(middleware.GzipConfig{
|
||||||
|
Level: 5,
|
||||||
|
Skipper: func(c echo.Context) bool {
|
||||||
|
return strings.HasPrefix(c.Request().URL.Path, "/api")
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
app.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
path := c.Request().URL.Path
|
||||||
|
// 对 SvelteKit 编译的静态不变资源开启永久缓存
|
||||||
|
if strings.HasPrefix(path, "/_app/immutable/") {
|
||||||
|
c.Response().Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||||
|
} else if !strings.HasPrefix(path, "/api/") {
|
||||||
|
// 对 HTML 文件和其它路由(非 API 接口)强制不缓存,确保每次拿到最新的 JS 引用
|
||||||
|
c.Response().Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
}
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func customContext(next echo.HandlerFunc) echo.HandlerFunc {
|
func customContext(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
@@ -67,8 +90,8 @@ var human = middleware.LoggerConfig{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var json = middleware.LoggerConfig{
|
var json = middleware.LoggerConfig{
|
||||||
Skipper: middleware.DefaultSkipper,
|
Skipper: NotLogFrontEndJSFiles,
|
||||||
Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}",` +
|
Format: `{"time":"${time_rfc3339_nano}","level":"INFO","msg":"HTTP请求已完成","id":"${id}","remote_ip":"${remote_ip}",` +
|
||||||
`"host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}",` +
|
`"host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}",` +
|
||||||
`"status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}"` +
|
`"status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}"` +
|
||||||
`,"bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n",
|
`,"bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n",
|
||||||
@@ -76,10 +99,14 @@ var json = middleware.LoggerConfig{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var human2 = middleware.LoggerConfig{
|
var human2 = middleware.LoggerConfig{
|
||||||
Skipper: middleware.DefaultSkipper,
|
Skipper: NotLogFrontEndJSFiles,
|
||||||
Format: `time=${time_custom} level=INFO msg=HTTP请求已完成 ` +
|
Format: `time=${time_custom} level=INFO msg=HTTP请求已完成 ` +
|
||||||
`uri="${method} ${uri}" from=${remote_ip} user_agent="${user_agent}" ` +
|
`uri="${method} ${uri}" from=${remote_ip} user_agent="${user_agent}" ` +
|
||||||
`id=${id} respond=${status} latency=${latency_human} error(if do exist)=${error} ` +
|
`id=${id} respond=${status} latency=${latency_human} error(if do exist)=${error} ` +
|
||||||
`bytes_in=${bytes_in} bytes_out=${bytes_out} ` + "\n",
|
`bytes_in=${bytes_in} bytes_out=${bytes_out} ` + "\n",
|
||||||
CustomTimeFormat: "2006-01-02T15:04:05.000+00:00",
|
CustomTimeFormat: "2006-01-02T15:04:05.000+00:00",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NotLogFrontEndJSFiles(i echo.Context) bool {
|
||||||
|
return strings.HasPrefix(i.Request().URL.Path, "/_app")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PUBLIC_BR_URL } from '$env/static/public';
|
import { BACKEND } from '$lib/env/env';
|
||||||
import { CheckAndGetJWT } from './jwt';
|
import { CheckAndGetJWT } from './jwt';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type {
|
import type {
|
||||||
@@ -23,7 +23,7 @@ import type {
|
|||||||
RegisterReq
|
RegisterReq
|
||||||
} from './types/apiRequest';
|
} from './types/apiRequest';
|
||||||
|
|
||||||
const br = PUBLIC_BR_URL;
|
const br = BACKEND;
|
||||||
|
|
||||||
export const api = axios.create({
|
export const api = axios.create({
|
||||||
baseURL: br,
|
baseURL: br,
|
||||||
|
|||||||
@@ -141,6 +141,11 @@
|
|||||||
</DatePicker>
|
</DatePicker>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if r.new_status === 'delay'}
|
||||||
|
<br/>
|
||||||
|
<p class='text-red-500'>如果没人在,请电联用户问一下哪天有空,然后选择”已预约“,预约到那天。<br/>如果预约单没人,你可以直接取消报修。<br/>总之,尽量不要使用改日修。</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
<Select
|
<Select
|
||||||
labelText="工单新故障类型"
|
labelText="工单新故障类型"
|
||||||
|
|||||||
6
front/src/lib/env/businesses.ts
vendored
Normal file
6
front/src/lib/env/businesses.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
//网络支持群的群号
|
||||||
|
export const SUPPORT_QQ = '1090225031';
|
||||||
|
//科长QQ
|
||||||
|
export const CHIEF_QQ = '';
|
||||||
|
//科长电话/微信
|
||||||
|
export const CHIEF_PHONE = '';
|
||||||
2
front/src/lib/env/env.ts
vendored
Normal file
2
front/src/lib/env/env.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const BACKEND = 'https://wwbx.davisye.cn';
|
||||||
|
export const AUTH_REDIRECT = 'https://wwbx.davisye.cn/api/v3p/wx/auth';
|
||||||
@@ -3,9 +3,9 @@ import type { WtsAccess } from './types/enum';
|
|||||||
import { docCookies } from '$lib/vendor/docCookie';
|
import { docCookies } from '$lib/vendor/docCookie';
|
||||||
import { TheLastPage } from './states/theLastPage.svelte';
|
import { TheLastPage } from './states/theLastPage.svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/state';
|
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
|
//TODO: 关于JWT,以及前端的权限检查,我觉得这里还是太粗糙了,或许应该重新设计吗?
|
||||||
export interface WtsJWT {
|
export interface WtsJWT {
|
||||||
openid: string;
|
openid: string;
|
||||||
sid: string;
|
sid: string;
|
||||||
@@ -31,7 +31,7 @@ export function CheckAndGetJWT(tx: 'raw' | 'parsed'): WtsJWT | string | null {
|
|||||||
if (!browser) {
|
if (!browser) {
|
||||||
return {
|
return {
|
||||||
access: 'user',
|
access: 'user',
|
||||||
name: '请刷新页面'
|
name: '请重新登录'
|
||||||
} as WtsJWT;
|
} as WtsJWT;
|
||||||
}
|
}
|
||||||
let token: string;
|
let token: string;
|
||||||
@@ -88,8 +88,8 @@ export function GetJWTFromCookie(): boolean {
|
|||||||
|
|
||||||
export function Guard(a: (subject: WtsAccess) => boolean) {
|
export function Guard(a: (subject: WtsAccess) => boolean) {
|
||||||
let jwt = CheckAndGetJWT('parsed');
|
let jwt = CheckAndGetJWT('parsed');
|
||||||
if (!jwt) {
|
if (!jwt || jwt.name === '请重新登录') {
|
||||||
TheLastPage.Write(page.url.pathname);
|
TheLastPage.Write(window.location.pathname);
|
||||||
goto('/login');
|
goto('/login');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,15 @@ export const StatusMap: Record<WtsStatus, string> = {
|
|||||||
canceled: '已取消'
|
canceled: '已取消'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const StatusOrder: Record<WtsStatus, number> = {
|
||||||
|
scheduled: 1,
|
||||||
|
fresh: 2,
|
||||||
|
delay: 3,
|
||||||
|
escalated: 4,
|
||||||
|
solved: 5,
|
||||||
|
canceled: 6
|
||||||
|
}
|
||||||
|
|
||||||
export type WtsPriority = 'highest' | 'assigned' | 'mainline' | 'normal' | 'in-passing' | 'least';
|
export type WtsPriority = 'highest' | 'assigned' | 'mainline' | 'normal' | 'in-passing' | 'least';
|
||||||
|
|
||||||
export const PriorityMap: Record<WtsPriority, string> = {
|
export const PriorityMap: Record<WtsPriority, string> = {
|
||||||
@@ -148,6 +157,15 @@ export const PriorityMap: Record<WtsPriority, string> = {
|
|||||||
least: '最低'
|
least: '最低'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PriorityOrder: Record<WtsPriority, number> = {
|
||||||
|
highest: 1,
|
||||||
|
assigned: 2,
|
||||||
|
mainline: 3,
|
||||||
|
normal: 4,
|
||||||
|
'in-passing': 5,
|
||||||
|
least: 6
|
||||||
|
};
|
||||||
|
|
||||||
export type WtsCategory =
|
export type WtsCategory =
|
||||||
| 'first-install'
|
| 'first-install'
|
||||||
| 'low-speed'
|
| 'low-speed'
|
||||||
|
|||||||
@@ -6,4 +6,6 @@ export const load = (async () => {
|
|||||||
|
|
||||||
export const prerender = true;
|
export const prerender = true;
|
||||||
|
|
||||||
|
|
||||||
export const trailingSlash = 'always';
|
export const trailingSlash = 'always';
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import group from '$lib/assets/picture/girl-using-computer.svg';
|
import group from '$lib/assets/picture/girl-using-computer.svg';
|
||||||
import help from '$lib/assets/picture/help.svg';
|
import help from '$lib/assets/picture/help.svg';
|
||||||
import outlook from '$lib/assets/picture/two-people-credit-card.svg';
|
import outlook from '$lib/assets/picture/two-people-credit-card.svg';
|
||||||
|
import { SUPPORT_QQ } from '$lib/env/businesses';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1 style="font-size: 25px">欢迎使用ZSC网维报修系统</h1>
|
<h1 style="font-size: 25px">欢迎使用ZSC网维报修系统</h1>
|
||||||
@@ -62,7 +63,7 @@
|
|||||||
如果有其它问题,您可以加入我们的网络支持QQ群,我们的工作人员会详细解答任何问题。
|
如果有其它问题,您可以加入我们的网络支持QQ群,我们的工作人员会详细解答任何问题。
|
||||||
</p>
|
</p>
|
||||||
<p style="display: inline-block; margin: 0;">
|
<p style="display: inline-block; margin: 0;">
|
||||||
群号:<strong>123123123</strong>
|
群号:<strong>{SUPPORT_QQ}</strong>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
title: '提交成功',
|
title: '提交成功',
|
||||||
timeout: 3000
|
timeout: 3000
|
||||||
});
|
});
|
||||||
setTimeout(() => goto('/repair'), 3900);
|
setTimeout(() => goto('/admin'), 3900);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
notLoading = true;
|
notLoading = true;
|
||||||
const errMsg = e.response?.data?.msg || e.message || '未知错误';
|
const errMsg = e.response?.data?.msg || e.message || '未知错误';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Accordion, AccordionItem } from 'carbon-components-svelte';
|
import { Accordion, AccordionItem } from 'carbon-components-svelte';
|
||||||
import RetroCard from '$lib/components/RetroCard.svelte';
|
import RetroCard from '$lib/components/RetroCard.svelte';
|
||||||
|
import { CHIEF_PHONE, SUPPORT_QQ } from '$lib/env/businesses';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1>校园网使用攻略</h1>
|
<h1>校园网使用攻略</h1>
|
||||||
@@ -17,33 +18,50 @@
|
|||||||
<Accordion size="xl">
|
<Accordion size="xl">
|
||||||
<AccordionItem title="电脑获取不到IP地址">
|
<AccordionItem title="电脑获取不到IP地址">
|
||||||
<p>
|
<p>
|
||||||
Natural Language Classifier uses advanced natural language processing and machine learning
|
检查网线有没有插好,换一根网线的话能否获取?如果使用转接器的话,也尝试换一个转接器。<br />
|
||||||
techniques to create custom classification models. Users train their data and the service
|
如果通过Wi-Fi联网,检查路由器“WAN”插口的网线或者光纤是否接触良好,重启路由器试试。<br />
|
||||||
predicts the appropriate category for the inputted text.
|
如果上述方法都没有效果,请找我们报修。
|
||||||
</p>
|
</p>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem title="忘记了账号密码">
|
<AccordionItem title="忘记了账号密码">
|
||||||
<p>
|
<p>
|
||||||
Analyze text to extract meta-data from content such as concepts, entities, emotion,
|
根据校园卡运营商的不同,您可以这样做:<br />
|
||||||
relations, sentiment and more.
|
如果是电信,默认的密码是`A1234567`。改过的密码可以在中国电信APP或者致电`10000`重置。<br />
|
||||||
|
同理联通也可以在APP和`10001`重置,没有默认密码。<br />
|
||||||
|
移动情况有点儿复杂,如果您忘了网页登录界面的那个密码,界面上有一个找回密码功能可供使用。如果您没有绑定邮箱因而无法找回的话,请提交报修让我们来解决。如果是忘记系统中和校园卡一起绑定的密码,可以在中国移动APP找回或者致电`10086`询问。<br
|
||||||
|
/>
|
||||||
|
如果遇到没办法或者不会的情况,您可以提交报修,我们会竭诚解决您的问题。
|
||||||
</p>
|
</p>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem title="Wi-Fi没法用了">
|
<AccordionItem title="Wi-Fi没法用了">
|
||||||
<p>
|
<p>
|
||||||
Translate text, documents, and websites from one language to another. Create industry or
|
尝试重启路由器,拔下电源,过1分钟后重新插上去。检查宿舍内有没有过多的无线耳机,键盘或者鼠标等在干扰信号,都关掉试一试。<br
|
||||||
region-specific translations via the service's customization capability.
|
/>
|
||||||
|
如果可以使用有线网,但是只有Wi-Fi没法用,最好提交报修让我们检查。<br />
|
||||||
|
总之,这种情况建议向我们提交报修,我们会认真解决您的问题。
|
||||||
</p>
|
</p>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem title="电信客户端故障">
|
<AccordionItem title="电信客户端故障">
|
||||||
<p>
|
<p>
|
||||||
Translate text, documents, and websites from one language to another. Create industry or
|
首先,建议最好不要主动升级客户端,除非没法用了。如果客户端出现故障,有可能是最近升级的原因,可以看看电脑下载文件夹里面有无以前的客户端安装包,删除新的再安装这个旧的试一试。<br
|
||||||
region-specific translations via the service's customization capability.
|
/>
|
||||||
|
有的时候可能是电信服务器出现大规模故障,这个时候请耐心等待。<br />
|
||||||
|
无论什么情况,您都可以向我们提交报修。
|
||||||
</p>
|
</p>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem title="出现169.254开头的IP">
|
<AccordionItem title="出现169.254开头的IP">
|
||||||
<p>
|
<p>
|
||||||
Translate text, documents, and websites from one language to another. Create industry or
|
您可以先确认是不是自己的电脑有问题,比如开启手机热点看看电脑是否连得上。<br />
|
||||||
region-specific translations via the service's customization capability.
|
有线网的话,把电脑带到其他同学连接正常的床位那里连一下,如果能连上就证明不是您电脑的问题。<br
|
||||||
|
/>
|
||||||
|
如果不是您电脑的问题,请向我们提交报修。有的时候这是运营商的大规模故障,这时请耐心等待,我们的工作人员会尽可能的跟进,向您解释最新的情况。
|
||||||
|
</p>
|
||||||
|
</AccordionItem>
|
||||||
|
<AccordionItem title="为什么晚上断网? ">
|
||||||
|
<p>
|
||||||
|
按学校管理要求,为了同学们保持规律的生活作息,在每周日~每周四的晚上23:30起限制校园网连接,每周五~每周六不断网。<strong
|
||||||
|
>网维仅传达断网的通知,无决定是否断网的权力。</strong
|
||||||
|
>
|
||||||
</p>
|
</p>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
@@ -58,27 +76,27 @@
|
|||||||
<Accordion size="xl">
|
<Accordion size="xl">
|
||||||
<AccordionItem title="去哪里办校园网?">
|
<AccordionItem title="去哪里办校园网?">
|
||||||
<p>
|
<p>
|
||||||
Natural Language Classifier uses advanced natural language processing and machine learning
|
虽然可能有学长学姐上门推销,不过还是建议自己去营业厅办:<br />
|
||||||
techniques to create custom classification models. Users train their data and the service
|
电信在学校北门外面,出门向右拐,第一个门面就是。<br />
|
||||||
predicts the appropriate category for the inputted text.
|
联通和正常家宽一样的流程。<br />
|
||||||
|
移动一般在开学季固定在9栋那里开放业务受理处,不过那里不是常年开放的。<br />
|
||||||
|
总之办完了,你会得到一张手机卡,这就是你的校园卡了,把他填到网维报修系统的“校园网账号”那里即可。
|
||||||
</p>
|
</p>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem title="我该办什么套餐?">
|
<AccordionItem title="我该办什么套餐?">
|
||||||
<p>
|
<p>
|
||||||
Analyze text to extract meta-data from content such as concepts, entities, emotion,
|
<strong>首先要声明:网维是一个中立的组织,我们不参与任何运营商的营销和推广。</strong><br />
|
||||||
relations, sentiment and more.
|
其次,你必须住在香晖苑才能办联通的卡,因为他们只在香晖苑服务。<br />
|
||||||
|
如果可以的话,最好不要跟那些上门推销的人办卡,自己去营业厅咨询。因为每年这个时候很混乱,会有很多骗子混杂在正常推销的学长学姐中,每年都有新生上当受骗。另外也并不是每一位学长学姐都有道德的,我们确实也遇到过那种满嘴吹牛,只想快点骗你办理拿提成的那种人,营业厅这种情况一般比较少。<br
|
||||||
|
/>
|
||||||
|
每年运营商的套餐活动都略有不同,每个人的情况也不一样。所以就像我们所说的,自己去营业厅看看什么套餐是适合自己的,办什么套餐没有一个标准的最优解。
|
||||||
</p>
|
</p>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem title="如何把我的电脑手机连接到校园网?">
|
<AccordionItem title="如何把我的电脑手机连接到校园网?">
|
||||||
<p>
|
<p>
|
||||||
Translate text, documents, and websites from one language to another. Create industry or
|
办完了套餐一般很快会有工程师或者我们网维的成员联系您,为您安装宽带,装完了一般会告诉你如何使用。<br
|
||||||
region-specific translations via the service's customization capability.
|
/>
|
||||||
</p>
|
如果办完套餐长期没有人来安装,或是没有人告诉您如何使用校园网的话,请提交报修,我们会为您安装宽带,而且手把手教您如何使用校园网。
|
||||||
</AccordionItem>
|
|
||||||
<AccordionItem title="我该如何登录校园网?">
|
|
||||||
<p>
|
|
||||||
Translate text, documents, and websites from one language to another. Create industry or
|
|
||||||
region-specific translations via the service's customization capability.
|
|
||||||
</p>
|
</p>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
@@ -92,33 +110,41 @@
|
|||||||
<Accordion size="xl">
|
<Accordion size="xl">
|
||||||
<AccordionItem title="如何报修我的网络故障?">
|
<AccordionItem title="如何报修我的网络故障?">
|
||||||
<p>
|
<p>
|
||||||
Natural Language Classifier uses advanced natural language processing and machine learning
|
点击<a href="/repair/new">这里</a
|
||||||
techniques to create custom classification models. Users train their data and the service
|
>来提交新报修。如果您遇上了任何问题,可以加入QQ群:{SUPPORT_QQ}来反馈。<br />
|
||||||
predicts the appropriate category for the inputted text.
|
您也可以尝试在我们微信公众号的聊天界面留言,我们看到会回复。
|
||||||
</p>
|
</p>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem title="我报修了,大概多久能上门?">
|
<AccordionItem title="我报修了,大概多久能上门?">
|
||||||
<p>
|
<p>
|
||||||
Analyze text to extract meta-data from content such as concepts, entities, emotion,
|
在当天16:30之前的报修,通常能够在当天内上门解决,在18:00之后的一般报修,通常要等到第二天才能上门。我们每天固定16:30到18:00上班。<br
|
||||||
relations, sentiment and more.
|
/>
|
||||||
|
预约报修会在预约当天的16:30~18:00上门报修。根据这个时间,您在对应日期当天的16:30到18:00之间需要本人在宿舍。<br
|
||||||
|
/>
|
||||||
|
特殊情况下,时间可能会延迟(详见下一条问题)。
|
||||||
</p>
|
</p>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem title="怎么一直没人来?">
|
<AccordionItem title="怎么一直没人来?">
|
||||||
<p>
|
<p>
|
||||||
Translate text, documents, and websites from one language to another. Create industry or
|
发生这种情况,我们非常抱歉!首先您需要知道的一点是:我们的值班时间固定是每天的下午16:30~18:00,您在这段时间内需要本人在宿舍,如果您本人不在宿舍,我们是没有权力为您维修的。<br
|
||||||
region-specific translations via the service's customization capability.
|
/>
|
||||||
|
在每学期开学时(尤其是暑假回来后的9~10月)是我们维修的高峰期,由于我们的人手实在有限,在这段时间我们可能无法在一个半小时内处理当天的所有报修。发生这种情况,请耐心等待,尝试预约报修。我们会认真处理每一单报修,您可以放心。
|
||||||
</p>
|
</p>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem title="你们的人没把问题解决就跑了!">
|
<AccordionItem title="你们的人没把问题解决就跑了!">
|
||||||
<p>
|
<p>
|
||||||
Translate text, documents, and websites from one language to another. Create industry or
|
如果问题在我们解决之后不久复发了,您可以重新提交一个新的报修单。<br />
|
||||||
region-specific translations via the service's customization capability.
|
如果网维成员没有解决您的问题,并且没有向您解释原因就离开的话,您可以选择投诉(详见下一条问题)。我们对此感到非常抱歉。<br
|
||||||
|
/>
|
||||||
|
有时候可能是我们的沟通没有传达到位,因为许多网络的问题确实是我们无法解决的,例如运营商的大规模故障,墙壁内部的线路故障等......网维会力所能及地为您提供服务,在不可抗力情况下会为您做好解释。
|
||||||
</p>
|
</p>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem title="我应该如何投诉网维成员?">
|
<AccordionItem title="我应该如何投诉网维成员?">
|
||||||
<p>
|
<p>
|
||||||
Translate text, documents, and websites from one language to another. Create industry or
|
您可以选择:<br />
|
||||||
region-specific translations via the service's customization capability.
|
1.加入支持QQ群:{SUPPORT_QQ},私聊群管理进行投诉, <br />
|
||||||
|
2.拨打科长电话{CHIEF_PHONE || '(暂无)'}进行投诉。<br />
|
||||||
|
为您维修的网维成员的名字可以在报修详情页找到。
|
||||||
</p>
|
</p>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|||||||
@@ -5,19 +5,20 @@
|
|||||||
import { dev } from '$app/environment';
|
import { dev } from '$app/environment';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
import { docCookies } from '$lib/vendor/docCookie';
|
import { docCookies } from '$lib/vendor/docCookie';
|
||||||
import { PUBLIC_AUTH_REDIRECT } from '$env/static/public';
|
import { AUTH_REDIRECT } from '$lib/env/env';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
function gotoAuthAPI() {
|
function gotoAuthAPI() {
|
||||||
if (!env.PUBLIC_JWT) {
|
if (dev) {
|
||||||
console.log('未找到PUBLIC_JWT');
|
if (env.PUBLIC_JWT) {
|
||||||
}
|
docCookies.setItem('jwt', env.PUBLIC_JWT, Infinity, '/');
|
||||||
if (dev && env.PUBLIC_JWT) {
|
goto('/login/success');
|
||||||
docCookies.setItem('jwt', env.PUBLIC_JWT, Infinity, '/');
|
} else {
|
||||||
goto('/login/success');
|
console.error('未找到PUBLIC_JWT,请在测试环境中添加该环境变量');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
window.location.href = PUBLIC_AUTH_REDIRECT;
|
window.location.href = AUTH_REDIRECT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,3 +26,5 @@
|
|||||||
gotoAuthAPI();
|
gotoAuthAPI();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<h1>登录中,稍等...</h1>
|
||||||
|
|||||||
@@ -12,23 +12,33 @@
|
|||||||
let q: NotificationQueue;
|
let q: NotificationQueue;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
let ok = GetJWTFromCookie();
|
try {
|
||||||
if (!ok) {
|
let ok = GetJWTFromCookie();
|
||||||
|
if (!ok) {
|
||||||
|
q.add({
|
||||||
|
kind: 'error',
|
||||||
|
title: '登录失败',
|
||||||
|
subtitle: '请查看控制台',
|
||||||
|
timeout: 5000
|
||||||
|
});
|
||||||
|
setTimeout(() => goto(TheLastPage.Read()), 5500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let isRegistered = CheckAndGetJWT('parsed').access !== 'unregistered';
|
||||||
|
if (!isRegistered) {
|
||||||
|
goto('/register');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
goto(TheLastPage.Read());
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
q.add({
|
q.add({
|
||||||
kind: 'error',
|
kind: 'error',
|
||||||
title: '登录失败',
|
title: '登录失败',
|
||||||
subtitle: '请查看控制台',
|
subtitle: '请查看控制台' + e,
|
||||||
timeout: 5000
|
timeout: 5000
|
||||||
});
|
});
|
||||||
setTimeout(() => goto(TheLastPage.Read()), 5500);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
let isRegistered = CheckAndGetJWT('parsed').access !== 'unregistered';
|
|
||||||
if (!isRegistered) {
|
|
||||||
goto('/register');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
goto(TheLastPage.Read());
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
import Renew from 'carbon-icons-svelte/lib/Renew.svelte';
|
import Renew from 'carbon-icons-svelte/lib/Renew.svelte';
|
||||||
import { TheLastPage } from '$lib/states/theLastPage.svelte';
|
import { TheLastPage } from '$lib/states/theLastPage.svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { SUPPORT_QQ, CHIEF_QQ, CHIEF_PHONE } from '$lib/env/businesses';
|
||||||
|
|
||||||
let pending = $state(true);
|
let pending = $state(true);
|
||||||
let info = $state({} as UserProfile);
|
let info = $state({} as UserProfile);
|
||||||
@@ -143,11 +144,11 @@
|
|||||||
<AccordionItem title="联系我们">
|
<AccordionItem title="联系我们">
|
||||||
<p>如果您对网维的服务或本系统有任何意见或建议,请尽管联系我们!我们非常重视您的建议。</p>
|
<p>如果您对网维的服务或本系统有任何意见或建议,请尽管联系我们!我们非常重视您的建议。</p>
|
||||||
<br />
|
<br />
|
||||||
<p>ZSC学生网络支撑QQ群:123123123</p>
|
<p>ZSC学生网络支撑QQ群:{SUPPORT_QQ}</p>
|
||||||
<br />
|
<br />
|
||||||
<p>科长QQ:</p>
|
<p>科长QQ:{CHIEF_QQ}</p>
|
||||||
<br />
|
<br />
|
||||||
<p>科长微信/电话:</p>
|
<p>科长微信/电话:{CHIEF_PHONE}</p>
|
||||||
<br />
|
<br />
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem title="关于">
|
<AccordionItem title="关于">
|
||||||
|
|||||||
@@ -103,6 +103,9 @@
|
|||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
req.who = CheckAndGetJWT('parsed').openid;
|
req.who = CheckAndGetJWT('parsed').openid;
|
||||||
|
if (!req.who) {
|
||||||
|
throw new Error('未找到您的信息,请重新登录');
|
||||||
|
}
|
||||||
|
|
||||||
open = false;
|
open = false;
|
||||||
checked = false;
|
checked = false;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
onMount(() => Guard(IsOperator));
|
onMount(() => Guard(IsOperator));
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
name = CheckAndGetJWT('parsed').name;
|
name = CheckAndGetJWT('parsed')?.name || '啊?';
|
||||||
});
|
});
|
||||||
onMount(() => getTicketOverview());
|
onMount(() => getTicketOverview());
|
||||||
|
|
||||||
@@ -174,6 +174,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:global(.zone-red) {
|
:global(.zone-red) {
|
||||||
background-color: #fff1f1; /* Red 10 */
|
background-color: hsl(0, 100%, 92%); /* 稍微比 Red 10 深一点,这样更明显 */
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
import type { WtsStatus, WtsPriority, WtsCategory, WtsISP } from '$lib/types/enum';
|
import type { WtsStatus, WtsPriority, WtsCategory, WtsISP } from '$lib/types/enum';
|
||||||
|
|
||||||
onMount(() => Guard(IsOperator));
|
onMount(() => Guard(IsOperator));
|
||||||
onMount(() => setTimeout(() => goto('/op/ticket_search'), 500));//暂时的权宜之计
|
//onMount(() => setTimeout(() => goto('/op/ticket_search'), 500));//暂时的权宜之计
|
||||||
|
|
||||||
let req = $state(criteria.r as FilterTicketsReq);
|
let req = $state(criteria.r as FilterTicketsReq);
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allZones = Object.keys(ZoneMap) as WtsZone[];
|
const allZones = Object.keys(ZoneMap) as WtsZone[];
|
||||||
const allStatuses = IsAdmin(CheckAndGetJWT('parsed').access)
|
const allStatuses = IsAdmin(CheckAndGetJWT('parsed')?.access)
|
||||||
? (Object.keys(StatusMap) as WtsStatus[])
|
? (Object.keys(StatusMap) as WtsStatus[])
|
||||||
: (Object.keys(StatusMap).filter(
|
: (Object.keys(StatusMap).filter(
|
||||||
(status) => status !== 'solved' && status !== 'canceled'
|
(status) => status !== 'solved' && status !== 'canceled'
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
'delay',
|
'delay',
|
||||||
'escalated'
|
'escalated'
|
||||||
] as const satisfies readonly WtsStatus[];
|
] as const satisfies readonly WtsStatus[];
|
||||||
const statusOptions: readonly WtsStatus[] = IsAdmin(CheckAndGetJWT('parsed').access)
|
const statusOptions: readonly WtsStatus[] = IsAdmin(CheckAndGetJWT('parsed')?.access)
|
||||||
? statusOptionsAdmin
|
? statusOptionsAdmin
|
||||||
: statusOptionsUser;
|
: statusOptionsUser;
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@
|
|||||||
<br />
|
<br />
|
||||||
<p>选择您需要检索报修工单的条件</p>
|
<p>选择您需要检索报修工单的条件</p>
|
||||||
|
|
||||||
{#if IsAdmin(CheckAndGetJWT('parsed').access)}
|
{#if IsAdmin(CheckAndGetJWT('parsed')?.access)}
|
||||||
<br />
|
<br />
|
||||||
<RadioButtonGroup id="scope" legendText="范围" bind:selected={req.scope} required={true}>
|
<RadioButtonGroup id="scope" legendText="范围" bind:selected={req.scope} required={true}>
|
||||||
<RadioButton labelText="只看活跃的" value="active" />
|
<RadioButton labelText="只看活跃的" value="active" />
|
||||||
@@ -254,7 +254,7 @@
|
|||||||
<Column sm={2} md={2} lg={4}
|
<Column sm={2} md={2} lg={4}
|
||||||
><Checkbox value="escalated" labelText={StatusMap['escalated']} /></Column
|
><Checkbox value="escalated" labelText={StatusMap['escalated']} /></Column
|
||||||
>
|
>
|
||||||
{#if IsAdmin(CheckAndGetJWT('parsed').access)}
|
{#if IsAdmin(CheckAndGetJWT('parsed')?.access)}
|
||||||
<Column sm={2} md={2} lg={4}
|
<Column sm={2} md={2} lg={4}
|
||||||
><Checkbox value="solved" labelText={StatusMap['solved']} /></Column
|
><Checkbox value="solved" labelText={StatusMap['solved']} /></Column
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -4,7 +4,14 @@
|
|||||||
let { data }: PageProps = $props();
|
let { data }: PageProps = $props();
|
||||||
|
|
||||||
import { CheckAndGetJWT, Guard } from '$lib/jwt';
|
import { CheckAndGetJWT, Guard } from '$lib/jwt';
|
||||||
import { IsAdmin, IsOperator, PriorityMap, CategoryMap } from '$lib/types/enum';
|
import {
|
||||||
|
IsAdmin,
|
||||||
|
IsOperator,
|
||||||
|
PriorityMap,
|
||||||
|
CategoryMap,
|
||||||
|
PriorityOrder,
|
||||||
|
StatusOrder
|
||||||
|
} from '$lib/types/enum';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { Button, NotificationQueue } from 'carbon-components-svelte';
|
import { Button, NotificationQueue } from 'carbon-components-svelte';
|
||||||
import Return from 'carbon-icons-svelte/lib/Return.svelte';
|
import Return from 'carbon-icons-svelte/lib/Return.svelte';
|
||||||
@@ -55,6 +62,16 @@
|
|||||||
if (criteria._order === 'oldest') {
|
if (criteria._order === 'oldest') {
|
||||||
tickets = [...tickets].sort((a, b) => toMs(a.submitted_at) - toMs(b.submitted_at));
|
tickets = [...tickets].sort((a, b) => toMs(a.submitted_at) - toMs(b.submitted_at));
|
||||||
}
|
}
|
||||||
|
if (criteria._order === 'priority') {
|
||||||
|
tickets = [...tickets].sort((a, b) => {
|
||||||
|
const priorityDiff = PriorityOrder[a.priority] - PriorityOrder[b.priority];
|
||||||
|
if (priorityDiff !== 0) {
|
||||||
|
return priorityDiff;
|
||||||
|
}
|
||||||
|
|
||||||
|
return StatusOrder[a.status] - StatusOrder[b.status];
|
||||||
|
});
|
||||||
|
}
|
||||||
if (criteria._floor !== null && criteria._floor !== undefined && criteria._floor !== 0) {
|
if (criteria._floor !== null && criteria._floor !== undefined && criteria._floor !== 0) {
|
||||||
tickets = tickets.filter((t) => getFloorFromRoom(t?.issuer?.room) === criteria._floor);
|
tickets = tickets.filter((t) => getFloorFromRoom(t?.issuer?.room) === criteria._floor);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
import type { Ticket } from '$lib/types/apiResponse';
|
import type { Ticket } from '$lib/types/apiResponse';
|
||||||
import { GetTicket } from '$lib/api';
|
import { GetTicket } from '$lib/api';
|
||||||
import { NotificationQueue } from 'carbon-components-svelte';
|
import { NotificationQueue } from 'carbon-components-svelte';
|
||||||
|
import { SUPPORT_QQ } from '$lib/env/businesses';
|
||||||
|
|
||||||
let q: NotificationQueue;
|
let q: NotificationQueue;
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
|
|
||||||
async function fetchTickets() {
|
async function fetchTickets() {
|
||||||
try {
|
try {
|
||||||
let res = await GetTicket(CheckAndGetJWT('parsed').openid);
|
let res = await GetTicket(CheckAndGetJWT('parsed')?.openid);
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
throw new Error(res.msg || '获取报修记录失败');
|
throw new Error(res.msg || '获取报修记录失败');
|
||||||
}
|
}
|
||||||
@@ -51,7 +52,7 @@
|
|||||||
<hr />
|
<hr />
|
||||||
<br />
|
<br />
|
||||||
<p>
|
<p>
|
||||||
这里将显示您提交的所有报修记录,由于各种原因,我们可能只会显示您最近几个报修单。点击单子可展开详情。
|
这里将显示您提交的所有报修记录,点击单子可展开详情。有任何问题请加入QQ群<strong>{SUPPORT_QQ}</strong>询问。
|
||||||
</p>
|
</p>
|
||||||
<br />
|
<br />
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -65,8 +65,8 @@
|
|||||||
occurAt.assert(!r.occur_at || IsRFC3339(r.occur_at), '请输入正确的故障发生时间');
|
occurAt.assert(!r.occur_at || IsRFC3339(r.occur_at), '请输入正确的故障发生时间');
|
||||||
appointedAt.assert(!r.appointed_at || IsRFC3339(r.appointed_at), '请输入正确的预约时间');
|
appointedAt.assert(!r.appointed_at || IsRFC3339(r.appointed_at), '请输入正确的预约时间');
|
||||||
description.assert(r.description && r.description.length > 0, '请填写故障描述');
|
description.assert(r.description && r.description.length > 0, '请填写故障描述');
|
||||||
description.assert(r.description.length <= 100, '字数太多了,请控制在100字以内');
|
description.assert(r.description.length <= 200, '字数太多了,请控制在200字以内');
|
||||||
notes.assert(!r.notes || r.notes.length <= 100, '字数太多了...请控制在100字以内');
|
notes.assert(!r.notes || r.notes.length <= 200, '字数太多了...请控制在200字以内');
|
||||||
if (r.category == undefined) {
|
if (r.category == undefined) {
|
||||||
r.category = 'others';
|
r.category = 'others';
|
||||||
}
|
}
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
let issuerSID = CheckAndGetJWT('parsed').sid;
|
let issuerSID = CheckAndGetJWT('parsed')?.sid;
|
||||||
r.issuer_sid = issuerSID;
|
r.issuer_sid = issuerSID;
|
||||||
try {
|
try {
|
||||||
notLoading = false;
|
notLoading = false;
|
||||||
@@ -101,9 +101,9 @@
|
|||||||
q.add({
|
q.add({
|
||||||
kind: 'success',
|
kind: 'success',
|
||||||
title: '提交成功',
|
title: '提交成功',
|
||||||
timeout: 3000
|
timeout: 1000
|
||||||
});
|
});
|
||||||
setTimeout(() => goto('/repair'), 3900);
|
setTimeout(() => goto('/repair'), 1500);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
notLoading = true;
|
notLoading = true;
|
||||||
const errMsg = e.response?.data?.msg || e.message || '未知错误';
|
const errMsg = e.response?.data?.msg || e.message || '未知错误';
|
||||||
@@ -182,7 +182,7 @@
|
|||||||
<DatePicker datePickerType="single" on:change={onAppointDateChange}>
|
<DatePicker datePickerType="single" on:change={onAppointDateChange}>
|
||||||
<DatePickerInput
|
<DatePickerInput
|
||||||
labelText="预约我们上门维修的日期"
|
labelText="预约我们上门维修的日期"
|
||||||
placeholder="当天4:30~6:00您本人需要在宿舍"
|
placeholder="当天下午4:30~6:00您需要在宿舍"
|
||||||
invalid={appointedAt.notOK}
|
invalid={appointedAt.notOK}
|
||||||
invalidText={appointedAt.txt}
|
invalidText={appointedAt.txt}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ const config = {
|
|||||||
// for more information about preprocessors
|
// for more information about preprocessors
|
||||||
preprocess: [vitePreprocess(), mdsvex(), optimizeImports(), optimizeCss()],
|
preprocess: [vitePreprocess(), mdsvex(), optimizeImports(), optimizeCss()],
|
||||||
|
|
||||||
kit: { adapter: adapter() },
|
kit: { adapter: adapter({
|
||||||
|
fallback: 'index2.html',
|
||||||
|
strict: false
|
||||||
|
}) },
|
||||||
extensions: ['.svelte', '.svx']
|
extensions: ['.svelte', '.svx']
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
6
todos.md
6
todos.md
@@ -1,14 +1,12 @@
|
|||||||
# 开发目标
|
# 开发目标
|
||||||
|
|
||||||
- Makefile (现在AI写的那个不太好,手动写一个)
|
|
||||||
|
|
||||||
- 改善日志输出,好像不是完全结构化的
|
- 改善日志输出,好像不是完全结构化的
|
||||||
|
|
||||||
- 在不确定用户学号时的工单添加机制(和按照楼层筛选兼容)
|
- 在不确定用户学号时的工单添加机制(和按照楼层筛选兼容)
|
||||||
|
|
||||||
- 可以增加一个定时脚本,每晚将当日预约的单子取消
|
- 数据库备份/恢复/迁移脚本
|
||||||
|
|
||||||
- 反代配置,数据库备份/恢复/迁移脚本,systemd配置...
|
- 把前端放在反代里或者用gzip压缩
|
||||||
|
|
||||||
- 缓存微信Token(应该不着急,每日2000次感觉用不完)
|
- 缓存微信Token(应该不着急,每日2000次感觉用不完)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user