mirror of
https://github.com/ZSCNetSupportDept/scheduler.git
synced 2025-10-28 12:35:03 +08:00
1
This commit is contained in:
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
# 构建阶段
|
||||
FROM golang:1.23 AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 复制源码
|
||||
COPY . .
|
||||
|
||||
# 构建二进制文件
|
||||
RUN go build -o /app/scheduler
|
||||
|
||||
# 运行阶段
|
||||
FROM scratch
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 复制构建好的二进制文件
|
||||
COPY --from=builder ./scheduler ./scheduler
|
||||
|
||||
RUN mkdir -p config
|
||||
|
||||
EXPOSE 25005
|
||||
|
||||
USER nonroot:nonroot
|
||||
|
||||
CMD ["./scheduler","--config","config/config.yaml"]
|
||||
@@ -4,8 +4,8 @@
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<h1 align=center>今日值班表</h1>
|
||||
<div align=center>
|
||||
<h1 align="center">今日值班表</h1>
|
||||
<div align="center">
|
||||
<button hx-get="/api/getAssignment" hx-swap="outerHTML">点我!</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -15,19 +15,22 @@
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin:auto
|
||||
margin: auto;
|
||||
}
|
||||
th, td {
|
||||
th,
|
||||
td {
|
||||
border: 1px solid black;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
td:first-child, th:first-child {
|
||||
td:first-child,
|
||||
th:first-child {
|
||||
background-color: #b3d9f7;
|
||||
font-weight: bold;
|
||||
}
|
||||
td:nth-child(2), th:nth-child(2) {
|
||||
background-color: #55ffaa
|
||||
td:nth-child(2),
|
||||
th:nth-child(2) {
|
||||
background-color: #55ffaa;
|
||||
}
|
||||
.ZoneHead {
|
||||
display: inline-block; /* 让颜色块和文字在同一行显示 */
|
||||
@@ -59,7 +62,7 @@
|
||||
vertical-align: middle; /* 让颜色块垂直居中 */
|
||||
}
|
||||
.table_notes {
|
||||
align-items:left
|
||||
align-items: left;
|
||||
}
|
||||
</style>
|
||||
</html>
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
document.getElementById('getAssignment').addEventListener('click', function () {
|
||||
dateInput = document.getElementById('calendar').value;
|
||||
document.getElementById("getAssignment").addEventListener("click", function () {
|
||||
dateInput = document.getElementById("calendar").value;
|
||||
|
||||
if (!dateInput) {
|
||||
dateInput = getToday()
|
||||
dateInput = getToday();
|
||||
}
|
||||
|
||||
const url = `/api/getAssignment?date=${dateInput}`;
|
||||
|
||||
fetch(url)
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('网络响应失败');
|
||||
throw new Error("网络响应失败");
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
const responseDiv = document.getElementById('response');
|
||||
responseDiv.innerHTML = ''; // 清除旧内容
|
||||
.then((data) => {
|
||||
const responseDiv = document.getElementById("response");
|
||||
responseDiv.innerHTML = ""; // 清除旧内容
|
||||
|
||||
const table = document.createElement('table');
|
||||
const table = document.createElement("table");
|
||||
|
||||
data.forEach(subArray => {
|
||||
const row = document.createElement('tr');
|
||||
data.forEach((subArray) => {
|
||||
const row = document.createElement("tr");
|
||||
|
||||
subArray.forEach(item => {
|
||||
const cell = document.createElement('td');
|
||||
subArray.forEach((item) => {
|
||||
const cell = document.createElement("td");
|
||||
cell.textContent = item.Name || item.ID;
|
||||
|
||||
// 优先判断 Access 条件
|
||||
if (item.Access <= 3) {
|
||||
cell.classList.add('cell_Moderator');
|
||||
cell.classList.add("cell_Moderator");
|
||||
} else if (item.Note === 1) {
|
||||
cell.classList.add('cell_SwitchOrRepay');
|
||||
cell.classList.add("cell_SwitchOrRepay");
|
||||
} else if (item.Note === 2) {
|
||||
cell.classList.add('cell_Volunteering');
|
||||
cell.classList.add("cell_Volunteering");
|
||||
}
|
||||
|
||||
row.appendChild(cell);
|
||||
@@ -41,10 +41,10 @@ document.getElementById('getAssignment').addEventListener('click', function () {
|
||||
|
||||
table.appendChild(row);
|
||||
});
|
||||
const title =`<i><h5 align=center >${dateInput}网维值班表</h5></i>`
|
||||
const titleContainer = document.createElement('div');
|
||||
titleContainer.innerHTML = title
|
||||
responseDiv.appendChild(titleContainer)
|
||||
const title = `<i><h5 align=center >${dateInput}网维值班表</h5></i>`;
|
||||
const titleContainer = document.createElement("div");
|
||||
titleContainer.innerHTML = title;
|
||||
responseDiv.appendChild(titleContainer);
|
||||
// 插入表格
|
||||
responseDiv.appendChild(table);
|
||||
|
||||
@@ -55,13 +55,13 @@ document.getElementById('getAssignment').addEventListener('click', function () {
|
||||
<i class="table_notes"><span class="SwitchOrRepay"></span>换班/补班<br></i>
|
||||
<i class="table_notes"><span class="Volunteering"></span>蹭班<br></i>
|
||||
`;
|
||||
const legendContainer = document.createElement('div');
|
||||
const legendContainer = document.createElement("div");
|
||||
legendContainer.innerHTML = legendHTML;
|
||||
responseDiv.appendChild(legendContainer);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('请求失败:', error);
|
||||
document.getElementById('response').innerHTML = '获取任务失败,请重试。';
|
||||
.catch((error) => {
|
||||
console.error("请求失败:", error);
|
||||
document.getElementById("response").innerHTML = "获取任务失败,请重试。";
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,15 +69,8 @@ function getToday() {
|
||||
const today = new Date();
|
||||
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0'); // 月份从0开始,需要+1
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
const month = String(today.getMonth() + 1).padStart(2, "0"); // 月份从0开始,需要+1
|
||||
const day = String(today.getDate()).padStart(2, "0");
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"zsxyww.com/scheduler/config"
|
||||
"zsxyww.com/scheduler/model"
|
||||
)
|
||||
|
||||
var err error
|
||||
@@ -35,7 +34,7 @@ func connectSQLite() {
|
||||
os.Exit(1)
|
||||
}
|
||||
if config.InitDB == true {
|
||||
Main.AutoMigrate(&model.Member{}, &model.Tweak{})
|
||||
//Main.AutoMigrate(&model.Member{}, &model.Tweak{})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +44,6 @@ func connectPGSQL() {
|
||||
panic(err)
|
||||
}
|
||||
if config.InitDB == true {
|
||||
Main.AutoMigrate(&model.Member{}, &model.Tweak{})
|
||||
//Main.AutoMigrate(&model.Member{}, &model.Tweak{})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,3 +8,7 @@
|
||||
为了让实习成员熟悉每一个片区,每两周都会轮换值班每位成员负责值班的的片区,片区的轮换遵守上面的规则,也就是说,女生只会在女生片区内轮换,男生则会在全部的片区内轮换。
|
||||
|
||||
一个学期值班的周数不确定,但是一般不会少于12周,即可以轮换到每个成员。
|
||||
## 问题
|
||||
排班算法现在是试图平均分配成员,可能会优化为根据片区的单子数量加权分配
|
||||
对于每两周换片区的问题,我简单地对当值成员列表进行循环移位来解决,好像和目前的算法有点冲突,未来准备写一个测试来覆盖
|
||||
|
||||
|
||||
4
go.mod
4
go.mod
@@ -18,7 +18,7 @@ require (
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.5 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.5
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
@@ -34,7 +34,7 @@ require (
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/spf13/pflag v1.0.6
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
|
||||
@@ -3,21 +3,17 @@ package handler
|
||||
import (
|
||||
//"fmt"
|
||||
"errors"
|
||||
"github.com/gocarina/gocsv"
|
||||
//"github.com/gocarina/gocsv"
|
||||
"github.com/golang-module/carbon/v2"
|
||||
"github.com/labstack/echo/v4"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
//"os"
|
||||
//"sync"
|
||||
"zsxyww.com/scheduler/config"
|
||||
"zsxyww.com/scheduler/model"
|
||||
"zsxyww.com/scheduler/signals"
|
||||
//"zsxyww.com/scheduler/signals"
|
||||
)
|
||||
|
||||
var data *[7][]*model.Member
|
||||
var mutex sync.RWMutex //lock for data
|
||||
var err error
|
||||
|
||||
// /api/getAssignment GET 获取当日值班表
|
||||
// 接受参数date,是需要生成值班表的日期
|
||||
func GetAssignment(i echo.Context) error {
|
||||
@@ -29,51 +25,40 @@ func GetAssignment(i echo.Context) error {
|
||||
arg = carbon.Parse(date)
|
||||
}
|
||||
|
||||
if (carbon.Now().ToDateString() != signals.Table.GetLastUpdated().ToDateString()) || signals.Table.IsNeedUpdate() == true {
|
||||
|
||||
mutex.Lock()
|
||||
data, err = generateTable(arg)
|
||||
mutex.Unlock()
|
||||
data, err := generateTable(arg)
|
||||
|
||||
if err != nil {
|
||||
i.String(http.StatusInternalServerError, err.Error())
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
|
||||
//signals.Table.SetUpdated(carbon.Now())
|
||||
//测试时注释掉上面的状态更新方便调试
|
||||
}
|
||||
mutex.RLock()
|
||||
i.JSON(200, data)
|
||||
mutex.RUnlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 根据指定的时间来生成对应的值班表
|
||||
func generateTable(time carbon.Carbon) (*[7][]*model.Member, error) {
|
||||
|
||||
table := [7][]*model.Member{} //结果放入这里
|
||||
members := []*model.Member{} //包含所有成员信息的切片
|
||||
today := []*model.Member{} //今天值班的人
|
||||
female := []*model.Member{} //今天的女生
|
||||
male := []*model.Member{} //今天的男生
|
||||
week, dayOfWeek := getWorkDay(time)
|
||||
|
||||
//检查传入时间有没有问题
|
||||
//TODO:这里好像有bug(对日期是否在值班时间内的判断部分),不过不怎么影响使用
|
||||
if (week < 0) || (week > config.Default.Business.Week) {
|
||||
return nil, errors.New("日期错误,日期需要在本学期的值班日期内并且格式正确")
|
||||
}
|
||||
|
||||
// 为了实现更换值班的片区,写的一个闭包切片访问器
|
||||
// 切片访问函数,用来实现自动更换值班片区的功能
|
||||
iter := func(array []*model.Member, i int) *model.Member {
|
||||
return array[(i+week)%len(array)]
|
||||
}
|
||||
|
||||
//读取csv文件
|
||||
err := readTableData(&members)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
members = model.MemberList
|
||||
|
||||
//添加标题
|
||||
table[0] = append(table[0], &model.Member{Name: "凤翔", Access: 7})
|
||||
table[1] = append(table[1], &model.Member{Name: "朝晖", Access: 7})
|
||||
@@ -98,32 +83,32 @@ func generateTable(time carbon.Carbon) (*[7][]*model.Member, error) {
|
||||
}
|
||||
}
|
||||
|
||||
//为女生分配负责人
|
||||
for i := 0; i < len(female); i++ {
|
||||
//将所有正式女生分配到女生片区
|
||||
for i := range female {
|
||||
if a := iter(female, i); a.Access < model.FRESH { //是正式成员
|
||||
table[i%4] = append(table[i%4], a) //轮流分配到女生片区
|
||||
a.Arranged = true
|
||||
}
|
||||
}
|
||||
|
||||
//为剩下的片区分配负责人
|
||||
for i := 0; i < len(male); i++ {
|
||||
//将所有正式男生分配到所有片区(优先分配人少的片区)
|
||||
for i := range male {
|
||||
if a := iter(male, i); a.Access < model.FRESH { //是正式成员
|
||||
table[fewest(table)] = append(table[fewest(table)], a)
|
||||
a.Arranged = true
|
||||
}
|
||||
}
|
||||
|
||||
//分配剩下的所有女生到女生片区
|
||||
for i := 0; i < len(female); i++ {
|
||||
//分配剩下的所有女生到女生片区(优先分配人少的片区)
|
||||
for i := range female {
|
||||
if a := iter(female, i); a.Arranged != true { //还没有安排
|
||||
table[fewestF(table)] = append(table[fewestF(table)], a)
|
||||
a.Arranged = true
|
||||
}
|
||||
}
|
||||
|
||||
//分配剩下的所有男生
|
||||
for i := 0; i < len(male); i++ {
|
||||
//分配剩下的所有男生(优先分配人少的片区)
|
||||
for i := range male {
|
||||
if a := iter(male, i); a.Arranged == false { //还没有安排
|
||||
table[fewest(table)] = append(table[fewest(table)], a)
|
||||
a.Arranged = true
|
||||
@@ -133,24 +118,6 @@ func generateTable(time carbon.Carbon) (*[7][]*model.Member, error) {
|
||||
return &table, nil
|
||||
}
|
||||
|
||||
// 读取csv文件
|
||||
func readTableData(m *[]*model.Member) error {
|
||||
data, err := os.OpenFile(config.Default.App.File, os.O_RDWR|os.O_CREATE, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer data.Close()
|
||||
|
||||
err = gocsv.UnmarshalFile(data, m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//for index, member := range *m {
|
||||
// fmt.Printf("%v:%v\n", index, member) // for debug concerns
|
||||
//}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 找出人数最少的片区
|
||||
func fewest(a [7][]*model.Member) int {
|
||||
b := min(len(a[0]), len(a[1]), len(a[2]), len(a[3]), len(a[4]), len(a[5]), len(a[6]))
|
||||
|
||||
1
handler/init.go
Normal file
1
handler/init.go
Normal file
@@ -0,0 +1 @@
|
||||
package handler
|
||||
1
handler/tweaks.go
Normal file
1
handler/tweaks.go
Normal file
@@ -0,0 +1 @@
|
||||
package handler
|
||||
@@ -1,46 +0,0 @@
|
||||
// CRUD的基础操作
|
||||
package uo
|
||||
|
||||
import (
|
||||
"zsxyww.com/scheduler/database"
|
||||
"zsxyww.com/scheduler/model"
|
||||
)
|
||||
|
||||
// 增加一项tweak
|
||||
func AddTweak(in *model.Tweak) error {
|
||||
result := db.Main.Create(in)
|
||||
return result.Error
|
||||
|
||||
}
|
||||
|
||||
// 删除一项tweak
|
||||
func DeleteTweak(in *model.Tweak) error {
|
||||
if db.Main.Error != nil {
|
||||
return db.Main.Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 查询一些tweak,通过IssueID
|
||||
func GetTweakByIssueID(in *model.Tweak) (result []*model.Tweak, err error) {
|
||||
if db.Main.Error != nil {
|
||||
return nil, db.Main.Error
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 查询一些tweak,通过一个日期
|
||||
func GetTweakByTime(in *model.Tweak) (result []*model.Tweak, err error) {
|
||||
if db.Main.Error != nil {
|
||||
return nil, db.Main.Error
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 查询一些tweak,通过一个工号
|
||||
func GetTweakByID(in *model.Tweak) (result []*model.Tweak, err error) {
|
||||
if db.Main.Error != nil {
|
||||
return nil, db.Main.Error
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
18
main.go
18
main.go
@@ -2,10 +2,13 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gocarina/gocsv"
|
||||
"github.com/labstack/echo/v4"
|
||||
"html/template"
|
||||
"os"
|
||||
"zsxyww.com/scheduler/config"
|
||||
"zsxyww.com/scheduler/database"
|
||||
"zsxyww.com/scheduler/model"
|
||||
"zsxyww.com/scheduler/route"
|
||||
"zsxyww.com/scheduler/templates"
|
||||
)
|
||||
@@ -17,6 +20,7 @@ func main() {
|
||||
|
||||
app := echo.New()
|
||||
register(app)
|
||||
csv()
|
||||
|
||||
listenAddress := fmt.Sprintf(":%d", config.Default.App.ListenPort)
|
||||
|
||||
@@ -31,3 +35,17 @@ func register(app *echo.Echo) {
|
||||
app.Renderer = renderer
|
||||
|
||||
}
|
||||
|
||||
// 读取csv文件
|
||||
func csv() {
|
||||
data, err := os.OpenFile(config.Default.App.File, os.O_RDWR|os.O_CREATE, os.ModePerm)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer data.Close()
|
||||
|
||||
err = gocsv.UnmarshalFile(data, &model.MemberList)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,3 +20,5 @@ const GROUP = 3 //组长
|
||||
const FORMAL = 4 //正式成员
|
||||
const FRESH = 5 //实习成员
|
||||
const PRE = 6 //前成员
|
||||
|
||||
var MemberList []*Member
|
||||
|
||||
@@ -3,6 +3,7 @@ package model
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
"zsxyww.com/scheduler/database"
|
||||
)
|
||||
|
||||
// 这个结构体是供数据库使用的表结构,换班补班蹭班的记录都会以这种方式储存
|
||||
@@ -23,3 +24,40 @@ const (
|
||||
OP_ADMIN_ADD = 4
|
||||
OP_ADMIN_SUB = 5
|
||||
)
|
||||
|
||||
// 增加一项tweak
|
||||
func (t *Tweak) addTweak() error {
|
||||
result := db.Main.Create(t)
|
||||
return result.Error
|
||||
|
||||
}
|
||||
|
||||
// 删除一项tweak
|
||||
func (t *Tweak) deleteTweak() error {
|
||||
result := db.Main.Delete(t)
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// 查询一些tweak,通过IssueID
|
||||
func (t *Tweak) getTweakByIssueID() (result []*Tweak, err error) {
|
||||
if db.Main.Error != nil {
|
||||
return nil, db.Main.Error
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 查询一些tweak,通过一个日期
|
||||
func (t *Tweak) getTweakByTime() (result []*Tweak, err error) {
|
||||
if db.Main.Error != nil {
|
||||
return nil, db.Main.Error
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 查询一些tweak,通过一个工号
|
||||
func (t *Tweak) getTweakByID() (result []*Tweak, err error) {
|
||||
if db.Main.Error != nil {
|
||||
return nil, db.Main.Error
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user