Compare commits

33 Commits

Author SHA1 Message Date
Linus Torvalds
81e8a77580 把高报修单情况下的红色调深一点 2026-03-15 11:47:31 +08:00
Linus Torvalds
5223d1127f 修改微信回复的一些消息 2026-03-15 11:47:10 +08:00
Linus Torvalds
e6ce1bfecf 微调一下用户看工单的提示 2026-03-12 14:38:29 +08:00
Linus Torvalds
8dd650a16c 提醒网维值班成员不要使用改日修 2026-03-11 17:53:50 +08:00
Linus Torvalds
9240e987a5 修改一下工单状态流转的合法逻辑 2026-03-11 16:37:09 +08:00
Linus Torvalds
d2cde59e85 完善一下按照优先级排序时的逻辑
按照优先级排序,相同按照写好的逻辑按照状态排序。
2026-03-11 16:33:25 +08:00
Linus Torvalds
15f18bcf83 修改一下报修浏览界面的提示 2026-03-11 15:38:54 +08:00
Linus Torvalds
fbe20025b8 200字报修字数 2026-03-11 15:38:37 +08:00
Linus Torvalds
ff552afa43 增添工单后的跳转逻辑 2026-03-11 15:38:00 +08:00
Linus Torvalds
0e735d2f2f 修改一处错误 2026-03-11 15:37:46 +08:00
Linus Torvalds
7173fd4a01 改善一下返回错误时的语言 2026-03-04 07:37:13 +08:00
Linus Torvalds
772091667e 暂时改成只能交一个报修吧 2026-03-04 07:36:24 +08:00
Linus Torvalds
ba28c41c7f 完善了一下报修攻略 2026-03-04 07:27:46 +08:00
Linus Torvalds
9f62470cba 把一些业务的东西提取出来,修改到环境变量里面 2026-03-02 18:41:16 +08:00
Linus Torvalds
2597eeffb7 改一下slash的规则.. 2026-03-01 11:59:46 +08:00
Linus Torvalds
7583614745 也许是缓存问题?改一下。。 2026-03-01 11:33:13 +08:00
Linus Torvalds
f9bc8e2730 继续修改SPA 2026-03-01 07:53:54 +08:00
Linus Torvalds
3c1af23eb1 改成SPA构建模式试试看吧?? 2026-03-01 07:33:39 +08:00
Linus Torvalds
5be21f5598 好像有点问题......我看看 2026-03-01 07:18:54 +08:00
Linus Torvalds
cad4191c0d 完善登录成功时的错误处理,另外format下 2026-03-01 07:14:07 +08:00
Linus Torvalds
828752bf77 修改在开发模式下获取环境的行为 2026-03-01 06:57:21 +08:00
Linus Torvalds
33e5d4ca56 修改并完善了CheckAndGetJWT()的调用情况 2026-03-01 06:47:51 +08:00
Linus Torvalds
62d2375775 更新.gitignore 2026-03-01 05:56:48 +08:00
Linus Torvalds
03b455e7e9 试试不用SvelteKit的.env能不能解决问题 2026-03-01 05:53:45 +08:00
Linus Torvalds
979c3fc24f 修改一处表述不清楚 2026-03-01 05:33:36 +08:00
Linus Torvalds
0f44d8f4e4 继续尝试修复500错误 2026-03-01 05:27:37 +08:00
Linus Torvalds
87f7fb6c01 更改一下逻辑,好像有问题 2026-03-01 04:35:22 +08:00
Linus Torvalds
2ffac40054 改进日志记录:不记录前端JS代码的GET请求,完善JSON日志记录的格式 2026-03-01 04:26:58 +08:00
Linus Torvalds
119328ce8f ~typo 2026-03-01 03:54:50 +08:00
Linus Torvalds
f0d8ad461c 使得管理层才能调用panic接口 2026-03-01 03:37:44 +08:00
Linus Torvalds
2480046316 为前端增加Gzip压缩 2026-03-01 03:37:24 +08:00
Linus Torvalds
bf32cc5372 更新Makefile,README.md等等。。。 2026-03-01 03:37:06 +08:00
Linus Torvalds
a42d5f6134 尝试修复加载错误的问题 2026-03-01 01:05:23 +08:00
32 changed files with 298 additions and 220 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
artifacts
.vscode

135
Makefile
View File

@@ -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
FRONT_DIR := front
back-dependcies:
cd $(BACK)/src && go mod tidy
.PHONY: help
help:
@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)'
front-dependcies:
cd $(FRONT) && IBM_TELEMETRY_DISABLED='true';npm install
.PHONY: dev build test clean install doctor
mkdir1:
mkdir -p $(PWD)/artifacts
dev:
@bash -c 'set -euo pipefail; \
$(MAKE) dev-front & pf=$$!; \
$(MAKE) dev-back & pb=$$!; \
trap "kill $$pf $$pb 2>/dev/null || true" INT TERM EXIT; \
wait $$pf $$pb'
mkdir-front:
mkdir -p $(PWD)/artifacts/FrontEndBuild
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
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
front: mkdir-front front-dependcies
cd $(FRONT) && IBM_TELEMETRY_DISABLED='true';npm run build && cp -r $(FRONT)/build/* $(PWD)/artifacts/FrontEndBuild

View File

@@ -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`,是硬编码在程序里的,所以要套上一层反代。
后端可以自己托管前端文件夹,也可以在反代那里就把请求拦截下来,这个看怎么部署,根据你的需要,和喜好自行决定即可。

View File

@@ -15,7 +15,7 @@ import (
"math/rand"
)
// 在每天值班结束的时候,自动取消(改日修)预约在今天但是状态今天没有更新的工单
// 在每天值班结束的时候,自动取消预约在今天但是状态今天没有更新的工单
func scheduledAutoCancel() {
go func() {
var first = true

View File

@@ -118,3 +118,9 @@ type TicketOverviewResponse struct {
commonMember
CountByBlock map[sqlc.WtsBlock]int64 `json:"count_by_block,omitempty"`
}
// Used by various APIs......
type GenericResponse struct {
commonMember
Store any `json:"Store"`
}

View File

@@ -6,15 +6,15 @@ import "errors"
// 多个逻辑可能共享这些错误定义
var (
//注册时,数据库中不存在该学号对应的记录(data.students)
ErrNoStudentRecord = errors.New("抱歉,您输入的姓名或学号有误,如果确信所输入信息没有问题,请联系我们的工作人员。")
ErrNoStudentRecord = errors.New("您输入的姓名或学号有误,如果确信所输入信息没有问题,请联系我们的工作人员。")
//注册时,所提供的学号-姓名不匹配数据库记录
ErrSidNameNotMatch = errors.New("抱歉,您输入的姓名或学号有误,如果确信所输入信息没有问题,请联系我们的工作人员。")
ErrSidNameNotMatch = errors.New("您输入的姓名或学号有误,如果确信所输入信息没有问题,请联系我们的工作人员。")
//注册时,提供的学号-姓名所对应的用户已经注册了
ErrUserAlreadyRegistered = errors.New("您已经注册了。如果您确信您还没有注册,请联系我们的工作人员。")
//该联系电话号码已在数据库中被使用注册
ErrPhoneUsed = errors.New("抱歉,您所使用的联系电话已经被登记,请换一个不一样的电话号码。")
ErrPhoneUsed = errors.New("您所使用的联系电话已经被登记,请换一个不一样的电话号码。")
//注册时,该微信号已被使用注册
ErrWxUsed = errors.New("抱歉,您的微信已经注册过了,一个微信只能注册一个账号。")
ErrWxUsed = errors.New("您的微信已经注册过了,一个微信只能注册一个账号。")
// 真的。。。会出现这种错误吗?
ErrDataInconsistent = errors.New("创建用户时数据库返回数据与请求数据不一致,请联系我们的技术团队。")
// 根据OpenID查不到用户
@@ -23,8 +23,8 @@ var (
ErrAppointTimeInvalid = errors.New("请填写有效的预约时间")
// 故障发生时间晚于现在了
ErrOccurAtTimeInvalid = errors.New("请填写有效的故障发生时间")
// 不允许用户创建太多没有关闭的工单,设置成3个,有需要可以改
ErrTicketTooMuch = errors.New("抱歉,您当前还有正在活跃的报修,无法创建新报修")
// 不允许用户创建太多没有关闭的工单,设置成~3~个,有需要可以改
ErrTicketTooMuch = errors.New("您当前还有正在活跃的报修,无法创建新报修")
// 根据工单ID查不到工单
ErrNoSuchTicket = errors.New("无法找到对应的工单")
// 根据网维成员ID查不到网维成员

View File

@@ -54,7 +54,7 @@ func NewTicket(c *hutil.WtsCtx, op string, r hutil.NewTicketRequest) hutil.NewTi
}
}
//fmt.Println("未结工单数量:", count)
if count >= 3 {
if count >= 1 {
return hutil.NewWtsErr(ErrTicketTooMuch, nil)
}

View File

@@ -99,7 +99,7 @@ var freshTo = map[sqlc.WtsStatus]bool{
}
var NoPresentTo = map[sqlc.WtsStatus]bool{
sqlc.WtsStatusFresh: false,
sqlc.WtsStatusFresh: true,
sqlc.WtsStatusDelay: true,
sqlc.WtsStatusScheduled: true,
sqlc.WtsStatusEscalated: true,
@@ -108,7 +108,7 @@ var NoPresentTo = map[sqlc.WtsStatus]bool{
}
var ScheduledTo = map[sqlc.WtsStatus]bool{
sqlc.WtsStatusFresh: false,
sqlc.WtsStatusFresh: true,
sqlc.WtsStatusDelay: true,
sqlc.WtsStatusScheduled: true,
sqlc.WtsStatusEscalated: true,
@@ -120,7 +120,7 @@ var EscalatedTo = map[sqlc.WtsStatus]bool{
sqlc.WtsStatusFresh: false,
sqlc.WtsStatusDelay: false,
sqlc.WtsStatusScheduled: true,
sqlc.WtsStatusEscalated: true,
sqlc.WtsStatusEscalated: false,
sqlc.WtsStatusCanceled: true,
sqlc.WtsStatusSolved: true,
}

View File

@@ -45,7 +45,7 @@ func (i *Ctx) handleWXEvent(m *message.MixMessage) string {
// 用户关注时发送欢迎文本
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 {
// 不知道为什么view事件也会被送到这里如果不处理的话会在log里面出现有点烦对用户体验倒是没什么影响
@@ -67,7 +67,7 @@ func (i *Ctx) handleWXTxtMsg(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)
}

View File

@@ -3,8 +3,10 @@ package handler
import (
"context"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
. "zsxyww.com/wts/handler/handlerUtilities"
hutil "zsxyww.com/wts/handler/handlerUtilities"
)
// GET: /br-debug/testdb
@@ -36,5 +38,16 @@ func Hello(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")
}

View File

@@ -1,6 +1,8 @@
package server
import (
"strings"
"github.com/golang-jwt/jwt/v5"
echojwt "github.com/labstack/echo-jwt/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.Secure())
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 {
@@ -67,8 +90,8 @@ var human = middleware.LoggerConfig{
}
var json = middleware.LoggerConfig{
Skipper: middleware.DefaultSkipper,
Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}",` +
Skipper: NotLogFrontEndJSFiles,
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}",` +
`"status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}"` +
`,"bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n",
@@ -76,10 +99,14 @@ var json = middleware.LoggerConfig{
}
var human2 = middleware.LoggerConfig{
Skipper: middleware.DefaultSkipper,
Skipper: NotLogFrontEndJSFiles,
Format: `time=${time_custom} level=INFO msg=HTTP请求已完成 ` +
`uri="${method} ${uri}" from=${remote_ip} user_agent="${user_agent}" ` +
`id=${id} respond=${status} latency=${latency_human} error(if do exist)=${error} ` +
`bytes_in=${bytes_in} bytes_out=${bytes_out} ` + "\n",
CustomTimeFormat: "2006-01-02T15:04:05.000+00:00",
}
func NotLogFrontEndJSFiles(i echo.Context) bool {
return strings.HasPrefix(i.Request().URL.Path, "/_app")
}

View File

@@ -1,4 +1,4 @@
import { PUBLIC_BR_URL } from '$env/static/public';
import { BACKEND } from '$lib/env/env';
import { CheckAndGetJWT } from './jwt';
import axios from 'axios';
import type {
@@ -23,7 +23,7 @@ import type {
RegisterReq
} from './types/apiRequest';
const br = PUBLIC_BR_URL;
const br = BACKEND;
export const api = axios.create({
baseURL: br,

View File

@@ -141,6 +141,11 @@
</DatePicker>
{/if}
{#if r.new_status === 'delay'}
<br/>
<p class='text-red-500'>如果没人在,请电联用户问一下哪天有空,然后选择”已预约“,预约到那天。<br/>如果预约单没人,你可以直接取消报修。<br/>总之,尽量不要使用改日修。</p>
{/if}
<br />
<Select
labelText="工单新故障类型"

6
front/src/lib/env/businesses.ts vendored Normal file
View 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
View File

@@ -0,0 +1,2 @@
export const BACKEND = 'https://wwbx.davisye.cn';
export const AUTH_REDIRECT = 'https://wwbx.davisye.cn/api/v3p/wx/auth';

View File

@@ -3,9 +3,9 @@ import type { WtsAccess } from './types/enum';
import { docCookies } from '$lib/vendor/docCookie';
import { TheLastPage } from './states/theLastPage.svelte';
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { browser } from '$app/environment';
//TODO: 关于JWT以及前端的权限检查我觉得这里还是太粗糙了或许应该重新设计吗
export interface WtsJWT {
openid: string;
sid: string;
@@ -31,7 +31,7 @@ export function CheckAndGetJWT(tx: 'raw' | 'parsed'): WtsJWT | string | null {
if (!browser) {
return {
access: 'user',
name: '请刷新页面'
name: '请重新登录'
} as WtsJWT;
}
let token: string;
@@ -88,8 +88,8 @@ export function GetJWTFromCookie(): boolean {
export function Guard(a: (subject: WtsAccess) => boolean) {
let jwt = CheckAndGetJWT('parsed');
if (!jwt) {
TheLastPage.Write(page.url.pathname);
if (!jwt || jwt.name === '请重新登录') {
TheLastPage.Write(window.location.pathname);
goto('/login');
return;
}

View File

@@ -137,6 +137,15 @@ export const StatusMap: Record<WtsStatus, string> = {
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 const PriorityMap: Record<WtsPriority, string> = {
@@ -148,6 +157,15 @@ export const PriorityMap: Record<WtsPriority, string> = {
least: '最低'
};
export const PriorityOrder: Record<WtsPriority, number> = {
highest: 1,
assigned: 2,
mainline: 3,
normal: 4,
'in-passing': 5,
least: 6
};
export type WtsCategory =
| 'first-install'
| 'low-speed'

View File

@@ -6,4 +6,6 @@ export const load = (async () => {
export const prerender = true;
export const trailingSlash = 'always';

View File

@@ -4,6 +4,7 @@
import group from '$lib/assets/picture/girl-using-computer.svg';
import help from '$lib/assets/picture/help.svg';
import outlook from '$lib/assets/picture/two-people-credit-card.svg';
import { SUPPORT_QQ } from '$lib/env/businesses';
</script>
<h1 style="font-size: 25px">欢迎使用ZSC网维报修系统</h1>
@@ -62,7 +63,7 @@
如果有其它问题您可以加入我们的网络支持QQ群我们的工作人员会详细解答任何问题。
</p>
<p style="display: inline-block; margin: 0;">
群号:<strong>123123123</strong>
群号:<strong>{SUPPORT_QQ}</strong>
</p>
</div>
<img

View File

@@ -106,7 +106,7 @@
title: '提交成功',
timeout: 3000
});
setTimeout(() => goto('/repair'), 3900);
setTimeout(() => goto('/admin'), 3900);
} catch (e: any) {
notLoading = true;
const errMsg = e.response?.data?.msg || e.message || '未知错误';

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { Accordion, AccordionItem } from 'carbon-components-svelte';
import RetroCard from '$lib/components/RetroCard.svelte';
import { CHIEF_PHONE, SUPPORT_QQ } from '$lib/env/businesses';
</script>
<h1>校园网使用攻略</h1>
@@ -17,33 +18,50 @@
<Accordion size="xl">
<AccordionItem title="电脑获取不到IP地址">
<p>
Natural Language Classifier uses advanced natural language processing and machine learning
techniques to create custom classification models. Users train their data and the service
predicts the appropriate category for the inputted text.
检查网线有没有插好,换一根网线的话能否获取?如果使用转接器的话,也尝试换一个转接器。<br />
如果通过Wi-Fi联网检查路由器“WAN”插口的网线或者光纤是否接触良好重启路由器试试。<br />
如果上述方法都没有效果,请找我们报修。
</p>
</AccordionItem>
<AccordionItem title="忘记了账号密码">
<p>
Analyze text to extract meta-data from content such as concepts, entities, emotion,
relations, sentiment and more.
根据校园卡运营商的不同,您可以这样做:<br />
如果是电信,默认的密码是`A1234567`。改过的密码可以在中国电信APP或者致电`10000`重置。<br />
同理联通也可以在APP和`10001`重置,没有默认密码。<br />
移动情况有点儿复杂如果您忘了网页登录界面的那个密码界面上有一个找回密码功能可供使用。如果您没有绑定邮箱因而无法找回的话请提交报修让我们来解决。如果是忘记系统中和校园卡一起绑定的密码可以在中国移动APP找回或者致电`10086`询问。<br
/>
如果遇到没办法或者不会的情况,您可以提交报修,我们会竭诚解决您的问题。
</p>
</AccordionItem>
<AccordionItem title="Wi-Fi没法用了">
<p>
Translate text, documents, and websites from one language to another. Create industry or
region-specific translations via the service's customization capability.
尝试重启路由器拔下电源过1分钟后重新插上去。检查宿舍内有没有过多的无线耳机键盘或者鼠标等在干扰信号都关掉试一试。<br
/>
如果可以使用有线网但是只有Wi-Fi没法用最好提交报修让我们检查。<br />
总之,这种情况建议向我们提交报修,我们会认真解决您的问题。
</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.
首先,建议最好不要主动升级客户端,除非没法用了。如果客户端出现故障,有可能是最近升级的原因,可以看看电脑下载文件夹里面有无以前的客户端安装包,删除新的再安装这个旧的试一试。<br
/>
有的时候可能是电信服务器出现大规模故障,这个时候请耐心等待。<br />
无论什么情况,您都可以向我们提交报修。
</p>
</AccordionItem>
<AccordionItem title="出现169.254开头的IP">
<p>
Translate text, documents, and websites from one language to another. Create industry or
region-specific translations via the service's customization capability.
您可以先确认是不是自己的电脑有问题,比如开启手机热点看看电脑是否连得上。<br />
有线网的话,把电脑带到其他同学连接正常的床位那里连一下,如果能连上就证明不是您电脑的问题。<br
/>
如果不是您电脑的问题,请向我们提交报修。有的时候这是运营商的大规模故障,这时请耐心等待,我们的工作人员会尽可能的跟进,向您解释最新的情况。
</p>
</AccordionItem>
<AccordionItem title="为什么晚上断网? ">
<p>
按学校管理要求为了同学们保持规律的生活作息在每周日每周四的晚上2330起限制校园网连接每周五每周六不断网。<strong
>网维仅传达断网的通知,无决定是否断网的权力。</strong
>
</p>
</AccordionItem>
</Accordion>
@@ -58,27 +76,27 @@
<Accordion size="xl">
<AccordionItem title="去哪里办校园网?">
<p>
Natural Language Classifier uses advanced natural language processing and machine learning
techniques to create custom classification models. Users train their data and the service
predicts the appropriate category for the inputted text.
虽然可能有学长学姐上门推销,不过还是建议自己去营业厅办:<br />
电信在学校北门外面,出门向右拐,第一个门面就是。<br />
联通和正常家宽一样的流程。<br />
移动一般在开学季固定在9栋那里开放业务受理处不过那里不是常年开放的。<br />
总之办完了,你会得到一张手机卡,这就是你的校园卡了,把他填到网维报修系统的“校园网账号”那里即可。
</p>
</AccordionItem>
<AccordionItem title="我该办什么套餐?">
<p>
Analyze text to extract meta-data from content such as concepts, entities, emotion,
relations, sentiment and more.
<strong>首先要声明:网维是一个中立的组织,我们不参与任何运营商的营销和推广。</strong><br />
其次,你必须住在香晖苑才能办联通的卡,因为他们只在香晖苑服务。<br />
如果可以的话,最好不要跟那些上门推销的人办卡,自己去营业厅咨询。因为每年这个时候很混乱,会有很多骗子混杂在正常推销的学长学姐中,每年都有新生上当受骗。另外也并不是每一位学长学姐都有道德的,我们确实也遇到过那种满嘴吹牛,只想快点骗你办理拿提成的那种人,营业厅这种情况一般比较少。<br
/>
每年运营商的套餐活动都略有不同,每个人的情况也不一样。所以就像我们所说的,自己去营业厅看看什么套餐是适合自己的,办什么套餐没有一个标准的最优解。
</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>
</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.
办完了套餐一般很快会有工程师或者我们网维的成员联系您,为您安装宽带,装完了一般会告诉你如何使用。<br
/>
如果办完套餐长期没有人来安装,或是没有人告诉您如何使用校园网的话,请提交报修,我们会为您安装宽带,而且手把手教您如何使用校园网。
</p>
</AccordionItem>
</Accordion>
@@ -92,33 +110,41 @@
<Accordion size="xl">
<AccordionItem title="如何报修我的网络故障?">
<p>
Natural Language Classifier uses advanced natural language processing and machine learning
techniques to create custom classification models. Users train their data and the service
predicts the appropriate category for the inputted text.
点击<a href="/repair/new">这里</a
>来提交新报修。如果您遇上了任何问题可以加入QQ群{SUPPORT_QQ}来反馈。<br />
您也可以尝试在我们微信公众号的聊天界面留言,我们看到会回复。
</p>
</AccordionItem>
<AccordionItem title="我报修了,大概多久能上门?">
<p>
Analyze text to extract meta-data from content such as concepts, entities, emotion,
relations, sentiment and more.
在当天1630之前的报修通常能够在当天内上门解决在1800之后的一般报修通常要等到第二天才能上门。我们每天固定1630到1800上班。<br
/>
预约报修会在预约当天的16301800上门报修。根据这个时间您在对应日期当天的1630到1800之间需要本人在宿舍。<br
/>
特殊情况下,时间可能会延迟(详见下一条问题)。
</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.
发生这种情况我们非常抱歉首先您需要知道的一点是我们的值班时间固定是每天的下午16301800,您在这段时间内需要本人在宿舍,如果您本人不在宿舍,我们是没有权力为您维修的。<br
/>
在每学期开学时尤其是暑假回来后的910月是我们维修的高峰期由于我们的人手实在有限在这段时间我们可能无法在一个半小时内处理当天的所有报修。发生这种情况请耐心等待尝试预约报修。我们会认真处理每一单报修您可以放心。
</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.
如果问题在我们解决之后不久复发了,您可以重新提交一个新的报修单。<br />
如果网维成员没有解决您的问题,并且没有向您解释原因就离开的话,您可以选择投诉(详见下一条问题)。我们对此感到非常抱歉。<br
/>
有时候可能是我们的沟通没有传达到位,因为许多网络的问题确实是我们无法解决的,例如运营商的大规模故障,墙壁内部的线路故障等......网维会力所能及地为您提供服务,在不可抗力情况下会为您做好解释。
</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.
您可以选择:<br />
1.加入支持QQ群{SUPPORT_QQ},私聊群管理进行投诉, <br />
2.拨打科长电话{CHIEF_PHONE || '(暂无)'}进行投诉。<br />
为您维修的网维成员的名字可以在报修详情页找到。
</p>
</AccordionItem>
</Accordion>

View File

@@ -5,19 +5,20 @@
import { dev } from '$app/environment';
import { env } from '$env/dynamic/public';
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 { goto } from '$app/navigation';
function gotoAuthAPI() {
if (!env.PUBLIC_JWT) {
console.log('未找到PUBLIC_JWT');
}
if (dev && env.PUBLIC_JWT) {
docCookies.setItem('jwt', env.PUBLIC_JWT, Infinity, '/');
goto('/login/success');
if (dev) {
if (env.PUBLIC_JWT) {
docCookies.setItem('jwt', env.PUBLIC_JWT, Infinity, '/');
goto('/login/success');
} else {
console.error('未找到PUBLIC_JWT请在测试环境中添加该环境变量');
}
} else {
window.location.href = PUBLIC_AUTH_REDIRECT;
window.location.href = AUTH_REDIRECT;
}
}
@@ -25,3 +26,5 @@
gotoAuthAPI();
});
</script>
<h1>登录中,稍等...</h1>

View File

@@ -12,23 +12,33 @@
let q: NotificationQueue;
onMount(() => {
let ok = GetJWTFromCookie();
if (!ok) {
try {
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({
kind: 'error',
title: '登录失败',
subtitle: '请查看控制台',
subtitle: '请查看控制台' + e,
timeout: 5000
});
setTimeout(() => goto(TheLastPage.Read()), 5500);
return;
}
let isRegistered = CheckAndGetJWT('parsed').access !== 'unregistered';
if (!isRegistered) {
goto('/register');
return;
}
goto(TheLastPage.Read());
});
</script>

View File

@@ -23,6 +23,7 @@
import Renew from 'carbon-icons-svelte/lib/Renew.svelte';
import { TheLastPage } from '$lib/states/theLastPage.svelte';
import { goto } from '$app/navigation';
import { SUPPORT_QQ, CHIEF_QQ, CHIEF_PHONE } from '$lib/env/businesses';
let pending = $state(true);
let info = $state({} as UserProfile);
@@ -143,11 +144,11 @@
<AccordionItem title="联系我们">
<p>如果您对网维的服务或本系统有任何意见或建议,请尽管联系我们!我们非常重视您的建议。</p>
<br />
<p>ZSC学生网络支撑QQ群:123123123</p>
<p>ZSC学生网络支撑QQ群:{SUPPORT_QQ}</p>
<br />
<p>科长QQ:</p>
<p>科长QQ:{CHIEF_QQ}</p>
<br />
<p>科长微信/电话:</p>
<p>科长微信/电话:{CHIEF_PHONE}</p>
<br />
</AccordionItem>
<AccordionItem title="关于">

View File

@@ -103,6 +103,9 @@
async function submit() {
req.who = CheckAndGetJWT('parsed').openid;
if (!req.who) {
throw new Error('未找到您的信息,请重新登录');
}
open = false;
checked = false;

View File

@@ -20,7 +20,7 @@
onMount(() => Guard(IsOperator));
onMount(() => {
name = CheckAndGetJWT('parsed').name;
name = CheckAndGetJWT('parsed')?.name || '啊?';
});
onMount(() => getTicketOverview());
@@ -174,6 +174,6 @@
}
:global(.zone-red) {
background-color: #fff1f1; /* Red 10 */
background-color: hsl(0, 100%, 92%); /* 稍微比 Red 10 深一点,这样更明显 */
}
</style>

View File

@@ -31,7 +31,7 @@
import type { WtsStatus, WtsPriority, WtsCategory, WtsISP } from '$lib/types/enum';
onMount(() => Guard(IsOperator));
onMount(() => setTimeout(() => goto('/op/ticket_search'), 500));//暂时的权宜之计
//onMount(() => setTimeout(() => goto('/op/ticket_search'), 500));//暂时的权宜之计
let req = $state(criteria.r as FilterTicketsReq);
@@ -78,7 +78,7 @@
}
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).filter(
(status) => status !== 'solved' && status !== 'canceled'
@@ -112,7 +112,7 @@
'delay',
'escalated'
] as const satisfies readonly WtsStatus[];
const statusOptions: readonly WtsStatus[] = IsAdmin(CheckAndGetJWT('parsed').access)
const statusOptions: readonly WtsStatus[] = IsAdmin(CheckAndGetJWT('parsed')?.access)
? statusOptionsAdmin
: statusOptionsUser;
@@ -191,7 +191,7 @@
<br />
<p>选择您需要检索报修工单的条件</p>
{#if IsAdmin(CheckAndGetJWT('parsed').access)}
{#if IsAdmin(CheckAndGetJWT('parsed')?.access)}
<br />
<RadioButtonGroup id="scope" legendText="范围" bind:selected={req.scope} required={true}>
<RadioButton labelText="只看活跃的" value="active" />
@@ -254,7 +254,7 @@
<Column sm={2} md={2} lg={4}
><Checkbox value="escalated" labelText={StatusMap['escalated']} /></Column
>
{#if IsAdmin(CheckAndGetJWT('parsed').access)}
{#if IsAdmin(CheckAndGetJWT('parsed')?.access)}
<Column sm={2} md={2} lg={4}
><Checkbox value="solved" labelText={StatusMap['solved']} /></Column
>

View File

@@ -4,7 +4,14 @@
let { data }: PageProps = $props();
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 { Button, NotificationQueue } from 'carbon-components-svelte';
import Return from 'carbon-icons-svelte/lib/Return.svelte';
@@ -55,6 +62,16 @@
if (criteria._order === 'oldest') {
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) {
tickets = tickets.filter((t) => getFloorFromRoom(t?.issuer?.room) === criteria._floor);
}

View File

@@ -12,6 +12,7 @@
import type { Ticket } from '$lib/types/apiResponse';
import { GetTicket } from '$lib/api';
import { NotificationQueue } from 'carbon-components-svelte';
import { SUPPORT_QQ } from '$lib/env/businesses';
let q: NotificationQueue;
@@ -21,7 +22,7 @@
async function fetchTickets() {
try {
let res = await GetTicket(CheckAndGetJWT('parsed').openid);
let res = await GetTicket(CheckAndGetJWT('parsed')?.openid);
if (!res.success) {
throw new Error(res.msg || '获取报修记录失败');
}
@@ -51,7 +52,7 @@
<hr />
<br />
<p>
这里将显示您提交的所有报修记录,由于各种原因,我们可能只会显示您最近几个报修单。点击单子可展开详情。
这里将显示您提交的所有报修记录,点击单子可展开详情。有任何问题请加入QQ群<strong>{SUPPORT_QQ}</strong>询问
</p>
<br />
<div

View File

@@ -65,8 +65,8 @@
occurAt.assert(!r.occur_at || IsRFC3339(r.occur_at), '请输入正确的故障发生时间');
appointedAt.assert(!r.appointed_at || IsRFC3339(r.appointed_at), '请输入正确的预约时间');
description.assert(r.description && r.description.length > 0, '请填写故障描述');
description.assert(r.description.length <= 100, '字数太多了,请控制在100字以内');
notes.assert(!r.notes || r.notes.length <= 100, '字数太多了...请控制在100字以内');
description.assert(r.description.length <= 200, '字数太多了,请控制在200字以内');
notes.assert(!r.notes || r.notes.length <= 200, '字数太多了...请控制在200字以内');
if (r.category == undefined) {
r.category = 'others';
}
@@ -89,7 +89,7 @@
}
async function submit() {
let issuerSID = CheckAndGetJWT('parsed').sid;
let issuerSID = CheckAndGetJWT('parsed')?.sid;
r.issuer_sid = issuerSID;
try {
notLoading = false;
@@ -101,9 +101,9 @@
q.add({
kind: 'success',
title: '提交成功',
timeout: 3000
timeout: 1000
});
setTimeout(() => goto('/repair'), 3900);
setTimeout(() => goto('/repair'), 1500);
} catch (e: any) {
notLoading = true;
const errMsg = e.response?.data?.msg || e.message || '未知错误';
@@ -182,7 +182,7 @@
<DatePicker datePickerType="single" on:change={onAppointDateChange}>
<DatePickerInput
labelText="预约我们上门维修的日期"
placeholder="当天4:30~6:00您本人需要在宿舍"
placeholder="当天下午4:30~6:00您需要在宿舍"
invalid={appointedAt.notOK}
invalidText={appointedAt.txt}
/>

View File

@@ -9,7 +9,10 @@ const config = {
// for more information about preprocessors
preprocess: [vitePreprocess(), mdsvex(), optimizeImports(), optimizeCss()],
kit: { adapter: adapter() },
kit: { adapter: adapter({
fallback: 'index2.html',
strict: false
}) },
extensions: ['.svelte', '.svx']
};

View File

@@ -1,14 +1,12 @@
# 开发目标
- Makefile (现在AI写的那个不太好手动写一个)
- 改善日志输出,好像不是完全结构化的
- 在不确定用户学号时的工单添加机制(和按照楼层筛选兼容)
- 可以增加一个定时脚本,每晚将当日预约的单子取消
- 数据库备份/恢复/迁移脚本
- 反代配置,数据库备份/恢复/迁移脚本systemd配置...
- 把前端放在反代里或者用gzip压缩
- 缓存微信Token应该不着急每日2000次感觉用不完