Skip to content

Commit 9f8a8cd

Browse files
author
techfunway
committed
发版:v0.3.1(预览版:月薪折算日薪、按工地统计、关闭出口补齐)
本次为预览版,GitHub 侧发行版标记为预发行(prerelease)。 新增功能 - 工人档案支持填月薪,按记工当月实际天数自动折算日薪,2 月与大月都算得准 - 统计新增「按工地」,看每个工地记了多少工、应发多少、借支多少、还剩多少没结 改进 - 月薪优先于日薪,记工弹窗会提示当前是月薪工并标注折算金额 - 多天连记的月薪按「月薪 × 天数 ÷ 当月天数」一次算清,记满整月正好等于月薪 - 工资条详情页补返回入口,班组与结算弹窗补「取消」按钮 优化 - 所有弹窗与确认框都能用 ESC 关闭,点暗处也能关掉确认框 - 关闭类按钮触控区统一到 44×44,手机端不再难点 - 浮层叠加时 ESC 只关最上面一层,不会连带关掉下面的弹窗 修复 - 多个二级界面关闭按钮点了没反应:确认框、设置安全问题、恢复数据、飞牛登录确认框等 - 保存记录时点「取消」没反应,现在与右上角关闭按钮行为一致 - 员工账号未绑定工人档案时可能看到老板的全部数据 - 导出的表格里以 = + - @ 开头的内容不会再被当成公式执行
1 parent 80daeb5 commit 9f8a8cd

38 files changed

Lines changed: 1289 additions & 106 deletions

‎CHANGELOG.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,37 @@
11
# 更新日志
22

3+
## v0.3.1(2026-10-20,预览版)
4+
5+
本预览版重点:支持月薪工按当月天数折算日薪、统计新增按工地维度,并彻底补齐各类弹窗与次级页面的关闭出口。
6+
7+
### 新增功能
8+
- **月薪工**:工人档案新增「月薪」字段,填了月薪的工人记工时按记工日期所在月的实际天数折算日薪(2 月按 28/29 天、大月按 31 天,不是固定 30 天),缺勤不记工即自然扣款
9+
- **按工地统计**:统计页新增「按工地」页签,按月份统计每个工地的记工笔数、应发工资、借支、净工资与已结算金额,末行附合计;没有归属工地的记工单独归为「未分工地」
10+
- **工资条返回入口**:工资条详情页新增返回按钮,不必再从侧边栏或浏览器后退绕回去
11+
12+
### 改进
13+
- **月薪优先于日薪**:工人同时填了月薪与日薪时以月薪折算为准,清空月薪即回退日薪;记工弹窗会显示月薪工提示条并把金额预览标注为「本次工资(月薪折算)」
14+
- **多天连记金额更准**:月薪工的连记预览改为「月薪 × 天数 ÷ 当月天数」最后只取整一次,记满整月正好等于月薪(不再每天各自四舍五入累积误差)
15+
- **班组与结算弹窗补「取消」**:新建/编辑班组、设置成员工资、按人结算、一键月结这几个弹窗原来只能靠右上角关闭,现在都有显式「取消」按钮
16+
- **飞牛包与应用简介重写**:按当前能力重新总结应用介绍,飞牛应用中心的展示文案与包内简介同步更新
17+
- **飞牛更新日志分节换行**:飞牛升级弹窗里的更新说明按【新增】【改进】【优化】【修复】四节逐条换行显示,不再挤成一整段
18+
19+
### 问题修复
20+
- **修复多个二级界面关闭按钮点了没反应**:确认框、设置安全问题弹窗、恢复数据确认弹窗、飞牛登录确认框等原本存在无法关闭的情况,现在都能正常关闭
21+
- **修复保存记录时点「取消」没反应**:记工、添加工人/工地/计件项目/借支等弹窗在保存过程中会禁用「取消」按钮,而右上角关闭按钮却仍然可用,行为不一致;现在两者行为统一
22+
- **修复浮层叠加时一次 ESC 关掉多层**:记工弹窗里叠确认框时按 ESC 会把两层一起关掉,现在只关最上面一层
23+
- **修复员工账号可能看到老板全部数据**:未绑定工人档案的员工账号此前会退回按老板范围查询,能读到全部工人、统计与考勤数据
24+
- **修复导出表格的公式注入**:记工明细、工资月报、考勤表、借支记录、结算记录等导出的 CSV 中,以 `=` `+` `-` `@` 开头的内容会被电子表格当成公式执行
25+
26+
### 体验优化
27+
- **确认框也能点暗处关闭**,并新增右上角关闭按钮,与其他弹窗保持一致
28+
- **所有弹窗与确认框都支持 ESC 键关闭**
29+
- **关闭类按钮触控区统一放大到 44×44**:赞赏弹窗、消息提示、赞赏横幅的关闭按钮在手机端从 16~28px 放大到 44px,不再难点;并补齐读屏用的无障碍标签
30+
- **手机端菜单按钮触控区补到 44×44**
31+
32+
### 安全
33+
- 员工账号未绑定工人档案时不再回退到老板的数据范围;考勤导出、考勤统计与结算预览三处改为直接拒绝未绑定的员工账号
34+
335
## v0.3.0(2026-10-17)
436

537
本次更新重点:飞牛上普通注册账号可以正常登录、班组协作更强、考勤表可导出,同时解决手机端卡顿与弹窗关闭等一批反馈。

‎VERSION‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v0.3.0
1+
v0.3.1

‎app.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"appname": "techfunway-worklog",
33
"display_name": "工记",
4-
"version": "0.3.0",
5-
"desc": "记工记账应用 — 按天/按时/计件记工,多种角色的多班组协作与成员单价,考勤日历与考勤导出,工资自动计算,借支与结算管理,一键生成工资条,支持飞牛 NAS 账号登录",
4+
"version": "0.3.1",
5+
"desc": "记工记账应用 — 按天/按时/计件记工,日薪、时薪、月薪或计件单价自动带出、工资实时计算;老板/组长/员工三种角色,支持多个班组与成员单价;考勤日历按月汇总、任意日期可点进记工并导出考勤表;借支抵扣、单人结算与一键月结、工资条导出;可按工地统计工钱支出,数据导出与备份齐全;支持飞牛 NAS 账号登录",
66
"author": "TechFunWay",
77
"website": "https://techfunway.wycto.cn",
88
"port": 8909,

‎docker-compose.v0.3.1.yml‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# 部署文件(随发行产物分发)
2+
# 本文件由发行脚本复制到 release/<版本>/ 目录,镜像版本号自动替换为当前版本。
3+
#
4+
# 用法:
5+
# cd release/<版本>
6+
# docker compose up -d
7+
#
8+
# 镜像说明:
9+
# - 本机已构建:make build-docker 生成 techfunways/worklog:<版本>
10+
# - 远程部署:需先将对应版本镜像推送到镜像仓库后再拉取
11+
#
12+
# 升级方式:
13+
# 切换到新版本发行目录后执行 docker compose up -d;
14+
# 数据(SQLite/上传文件/日志/配置)持久化在 named volume 中,升级不丢数据。
15+
services:
16+
techfunway-worklog:
17+
image: techfunways/worklog:v0.3.1
18+
container_name: techfunway-worklog
19+
ports:
20+
- "8909:8909"
21+
volumes:
22+
# 数据库、上传文件、日志和配置均持久化在此卷中。
23+
- techfunway-worklog-data:/app/data
24+
environment:
25+
- ENV=release
26+
- DATA_DIR=/app/data
27+
command: ["-data-dir", "/app/data", "-web-dir", "./static/dist"]
28+
restart: unless-stopped
29+
30+
volumes:
31+
techfunway-worklog-data:

‎fnpack/manifest‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
appname = techfunway-worklog
2-
version = 0.3.0
2+
version = 0.3.1
33
display_name = 工记
4-
desc = 记工记账应用 — 老板/组长/员工三角色班组协作,支持多班组与成员单价,考勤日历与导出,按天/按时/计件记工,工资自动计算,借支与结算,一键生成工资条
4+
desc = 面向班组、包工头与个体用工的记工记账应用:按天/按时/计件记工,日薪、时薪、月薪或计件单价自动带出并实时算工资,支持半天与多天连记;老板/组长/员工三种角色,可建多个班组并给每个成员设单价;考勤日历按月汇总,任意日期可点进记工并导出考勤表;借支抵扣、单人结算与一键月结、工资条可导出图片或文字;可按工地统计工钱支出,数据导出与备份齐全,支持飞牛 NAS 账号登录。
55
source = thirdparty
66
platform = x86
77
maintainer = 科技智趣坊
@@ -15,4 +15,26 @@ desktop_applaunchname = techfunway-worklog.main
1515
service_port = 8909
1616
checkport = false
1717
disable_authorization_path = false
18-
changelog = """v0.3.0:【新增】支持创建多个班组并随时切换;在我的班组里可给每个成员设置日薪/时薪,记工时自动带出;管理员可在用户管理里把已有账号直接加入自己的班组;考勤表新增「导出考勤表」,按工人逐日导出并带每人合计。【改进】飞牛上普通注册账号不再必须绑定飞牛账号,登录后刷新、重进应用都不掉线;注册成功直接进入控制台;局域网直连端口也能正常下载各类导出文件。【优化】弹窗关闭按钮触控区加大到 44×44 且长表单滚动时吸顶,记工/添加工人/工地/计件项目/借支补「取消」按钮;手机端用户管理改为卡片列表,操作第一屏可见;登录页与主界面去掉手机端高耗能特效,输入不再卡顿闪屏。【修复】修复弹窗右上角 X 无法关闭、换账号后沿用上一个账号角色、考勤表空白日期无法点击记工等问题。"""
18+
changelog = """
19+
v0.3.1 更新
20+
21+
【新增】
22+
- 工人档案支持填月薪,按记工当月实际天数自动折算日薪,2 月与大月都算得准
23+
- 统计新增「按工地」,看每个工地记了多少工、应发多少、借支多少、还剩多少没结
24+
25+
【改进】
26+
- 月薪优先于日薪,记工弹窗会提示当前是月薪工并标注折算金额
27+
- 多天连记的月薪按「月薪 × 天数 ÷ 当月天数」一次算清,记满整月正好等于月薪
28+
- 工资条详情页补返回入口,班组与结算弹窗补「取消」按钮
29+
30+
【优化】
31+
- 所有弹窗与确认框都能用 ESC 关闭,点暗处也能关掉确认框
32+
- 关闭类按钮触控区统一到 44×44,手机端不再难点
33+
- 浮层叠加时 ESC 只关最上面一层,不会连带关掉下面的弹窗
34+
35+
【修复】
36+
- 多个二级界面关闭按钮点了没反应:确认框、设置安全问题、恢复数据、飞牛登录确认框等
37+
- 保存记录时点「取消」没反应,现在与右上角关闭按钮行为一致
38+
- 员工账号未绑定工人档案时可能看到老板的全部数据
39+
- 导出的表格里以 = + - @ 开头的内容不会再被当成公式执行
40+
"""

‎server/worklog/amount.go‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"math"
66
"math/big"
77
"strings"
8+
"time"
89

910
"github.com/gin-gonic/gin"
1011
"smallgo/server/response"
@@ -76,9 +77,37 @@ var methodLabels = map[string]string{
7677
"cash": "现金",
7778
"wechat": "微信",
7879
"alipay": "支付宝",
80+
"bank": "银行转账",
7981
"other": "其他",
8082
}
8183

84+
// dailyFromMonthly 月薪折算日薪:月薪 ÷ 当月实际天数,四舍五入到分。
85+
// 缺勤一天扣的钱 = 这里的折算日薪 × 缺勤天数(记工时缺勤日不记工即自然扣款)。
86+
func dailyFromMonthly(monthlyCents int64, date string) int64 {
87+
if monthlyCents <= 0 {
88+
return 0
89+
}
90+
days := daysInMonthOf(date)
91+
if days <= 0 {
92+
days = 30
93+
}
94+
return int64(math.Round(float64(monthlyCents) / float64(days)))
95+
}
96+
97+
// daysInMonthOf 解析 YYYY-MM-DD 所在月份的实际天数;解析失败返回 0。
98+
func daysInMonthOf(date string) int {
99+
t, err := time.Parse("2006-01-02", date)
100+
if err != nil {
101+
return 0
102+
}
103+
return daysInMonth(t.Year(), int(t.Month()))
104+
}
105+
106+
// daysInMonth 某年某月的实际天数。
107+
func daysInMonth(year, month int) int {
108+
return time.Date(year, time.Month(month)+1, 0, 0, 0, 0, 0, time.Local).Day()
109+
}
110+
82111
func centsToYuanString(cents int64) string {
83112
neg := ""
84113
if cents < 0 {

‎server/worklog/crud.go‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ func updateWorker(db *gorm.DB) gin.HandlerFunc {
110110
item.Note = in.Note
111111
item.DailyWageCents = in.DailyWageCents
112112
item.HourlyWageCents = in.HourlyWageCents
113+
item.MonthlyWageCents = in.MonthlyWageCents
113114
if in.Status == "active" || in.Status == "archived" {
114115
item.Status = in.Status
115116
}
@@ -433,14 +434,19 @@ func validateRecordInput(db *gorm.DB, uid uint, in *recordInput, rec *WorkRecord
433434
}
434435
}
435436

436-
// 单价:未传则带默认值(工人日薪/时薪或计件项目单价)
437+
// 单价:未传则带默认值(工人日薪/时薪或计件项目单价)。
438+
// 月薪工(填了月薪)点工按当月实际天数折算日薪,缺勤不记工即自然扣款。
437439
price := int64(0)
438440
if in.UnitPriceCents != nil {
439441
price = *in.UnitPriceCents
440442
} else {
441443
switch in.Type {
442444
case TypeDay:
443-
price = worker.DailyWageCents
445+
if worker.MonthlyWageCents > 0 {
446+
price = dailyFromMonthly(worker.MonthlyWageCents, in.Date)
447+
} else {
448+
price = worker.DailyWageCents
449+
}
444450
case TypeHour:
445451
price = worker.HourlyWageCents
446452
case TypePiece:

‎server/worklog/csv_export.go‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// csv_export.go — CSV 导出的公式注入防护。
2+
//
3+
// 为什么需要:电子表格(Excel / WPS / Numbers)会把以 = + - @ 开头的单元格
4+
// 当成公式执行。工记导出的工人姓名、备注、工地名都是用户录入内容,原样写进
5+
// CSV 就构成 CSV 公式注入——包工头把工资表转给会计、或导出后转存时可能触发
6+
// =HYPERLINK(...) 一类外链请求。
7+
//
8+
// 为什么做成包装函数而不是在每处 WriteCSV 前手工转义:本仓库有 5 处导出
9+
// (记工明细、工资月报、考勤表、借支记录、结算记录),逐个改必然漏;包装
10+
// 函数把「安全」变成默认路径,新增导出只要调 writeCSV 就自带防护。
11+
//
12+
// 与 collect 的实现口径一致(collect/server/collect/export.go 的 csvCell),
13+
// 家族统一做法:命中危险首字符时前置一个单引号,电子表格会按文本处理。
14+
package worklog
15+
16+
import (
17+
"github.com/gin-gonic/gin"
18+
19+
"smallgo/server/utils"
20+
)
21+
22+
// csvCell 公式注入防护:电子表格会把 = + - @ 开头的单元格当公式执行。
23+
func csvCell(s string) string {
24+
if s == "" {
25+
return s
26+
}
27+
switch s[0] {
28+
case '=', '+', '-', '@':
29+
return "'" + s
30+
}
31+
return s
32+
}
33+
34+
// writeCSV 先做公式注入防护再交给框架的 WriteCSV(负责 BOM 与下载头)。
35+
func writeCSV(c *gin.Context, filename string, rows [][]string) {
36+
safe := make([][]string, len(rows))
37+
for i, row := range rows {
38+
cells := make([]string, len(row))
39+
for j, cell := range row {
40+
cells[j] = csvCell(cell)
41+
}
42+
safe[i] = cells
43+
}
44+
utils.WriteCSV(c, filename, safe)
45+
}

‎server/worklog/csv_export_test.go‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package worklog
2+
3+
import (
4+
"net/http/httptest"
5+
"strings"
6+
"testing"
7+
8+
"github.com/gin-gonic/gin"
9+
)
10+
11+
// TestCSVCellFormulaInjection 锁住 CSV 公式注入防护。
12+
func TestCSVCellFormulaInjection(t *testing.T) {
13+
cases := []struct{ in, want string }{
14+
{"", ""},
15+
{"张三", "张三"},
16+
{"=1+1", "'=1+1"},
17+
{"+100", "'+100"},
18+
{"-100", "'-100"},
19+
{"@SUM(A1)", "'@SUM(A1)"},
20+
{"1+1", "1+1"},
21+
}
22+
for _, c := range cases {
23+
if got := csvCell(c.in); got != c.want {
24+
t.Errorf("csvCell(%q) = %q, want %q", c.in, got, c.want)
25+
}
26+
}
27+
}
28+
29+
// TestWriteCSVEndToEnd 走真实响应链路验证转义生效且中文不受影响。
30+
func TestWriteCSVEndToEnd(t *testing.T) {
31+
gin.SetMode(gin.TestMode)
32+
w := httptest.NewRecorder()
33+
c, _ := gin.CreateTestContext(w)
34+
35+
writeCSV(c, "attendance.csv", [][]string{
36+
{"工人", "备注"},
37+
{"=cmd|'/c calc'!A1", "@evil"},
38+
{"李四", "正常"},
39+
})
40+
41+
body := w.Body.String()
42+
if !strings.Contains(body, "'=cmd|'/c calc'!A1") {
43+
t.Fatalf("= 开头的公式未被转义,响应: %s", body)
44+
}
45+
if !strings.Contains(body, "'@evil") {
46+
t.Fatalf("@ 开头的公式未被转义,响应: %s", body)
47+
}
48+
if !strings.Contains(body, "李四") || !strings.Contains(body, "正常") {
49+
t.Fatalf("普通中文被破坏,响应: %s", body)
50+
}
51+
}

‎server/worklog/export.go‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func exportRecordsCSV(db *gorm.DB) gin.HandlerFunc {
4545
r.ProjectName, pieceLabel(r), status, r.Note,
4646
})
4747
}
48-
utils.WriteCSV(c, utils.ExportFilename("记工明细", "csv"), rows)
48+
writeCSV(c, utils.ExportFilename("记工明细", "csv"), rows)
4949
}
5050
}
5151

@@ -78,7 +78,7 @@ func exportMonthlyCSV(db *gorm.DB) gin.HandlerFunc {
7878
r.ProjectName, pieceLabel(r), r.Note,
7979
})
8080
}
81-
utils.WriteCSV(c, utils.ExportFilename(fmt.Sprintf("工资月报-%d-%02d", year, month), "csv"), rows)
81+
writeCSV(c, utils.ExportFilename(fmt.Sprintf("工资月报-%d-%02d", year, month), "csv"), rows)
8282
}
8383
}
8484

@@ -101,6 +101,12 @@ func exportAttendanceCSV(db *gorm.DB) gin.HandlerFunc {
101101

102102
workerID := utils.Atoi(c.Query("worker_id"), 0)
103103
if sw, ok := ac.workerScope(); ok {
104+
// 员工只能导出自己;成员行没绑工人档案时直接拒绝,
105+
// 不能退回下面的 else 分支去导出全部在职工人。
106+
if ac.workerUnbound() {
107+
forbidden(c)
108+
return
109+
}
104110
workerID = int(sw)
105111
}
106112

@@ -231,7 +237,7 @@ func exportAttendanceCSV(db *gorm.DB) gin.HandlerFunc {
231237
centsToYuanString(totalAmount), "",
232238
})
233239
}
234-
utils.WriteCSV(c, utils.ExportFilename(fmt.Sprintf("考勤表-%d-%02d", year, month), "csv"), rows)
240+
writeCSV(c, utils.ExportFilename(fmt.Sprintf("考勤表-%d-%02d", year, month), "csv"), rows)
235241
}
236242
}
237243

@@ -252,7 +258,7 @@ func exportAdvancesCSV(db *gorm.DB) gin.HandlerFunc {
252258
}
253259
rows = append(rows, []string{a.Date, a.WorkerName, centsToYuanString(a.AmountCents), methodLabels[a.Method], status, a.Note})
254260
}
255-
utils.WriteCSV(c, utils.ExportFilename("借支记录", "csv"), rows)
261+
writeCSV(c, utils.ExportFilename("借支记录", "csv"), rows)
256262
}
257263
}
258264

@@ -273,7 +279,7 @@ func exportSettlementsCSV(db *gorm.DB) gin.HandlerFunc {
273279
s.SettledAt.Format("2006-01-02 15:04"), s.Note,
274280
})
275281
}
276-
utils.WriteCSV(c, utils.ExportFilename("结算记录", "csv"), rows)
282+
writeCSV(c, utils.ExportFilename("结算记录", "csv"), rows)
277283
}
278284
}
279285

0 commit comments

Comments
 (0)