docs(deployment): 更新部署手册和业务架构说明
- 更新MES和WMS服务端口配置说明 - 详细说明产线工位设备配置(扫码枪+拧紧枪) - 明确AGV配送和接驳台的模拟模式与真实对接方案 - 更新WMS中AGV配送默认模拟模式说明 - 完善厂内物流(AGV/接驳台)的业务架构约束 - 修订临时文档为详细的项目问题和解决方案规划
This commit is contained in:
@@ -353,6 +353,24 @@ func deleteAttachmentHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
fail(w, http.StatusNotFound, "附件不存在")
|
||||
return
|
||||
}
|
||||
// 删除保护(需求1):关键业务附件禁止删除,避免质量/工艺记录丢失。
|
||||
// 1) 工艺流程(PROCESS_PDF)、流程卡(PROCESS_CARD):全程禁删(工艺依据,需永久留存)。
|
||||
// 2) 检验照片/报告(INSPECTION_*):未超本地保留年数禁删(质量追溯期内不可删)。
|
||||
switch rec.FileType {
|
||||
case FileTypeProcessPDF, FileTypeProcessCard:
|
||||
fail(w, http.StatusForbidden, "工艺流程 / 流程卡为关键工艺文件,禁止删除")
|
||||
return
|
||||
case FileTypeInspectionPhoto, FileTypeInspectionPDF:
|
||||
years := ctx.Config.Attachment.LocalRetentionYears
|
||||
if years <= 0 {
|
||||
years = 1
|
||||
}
|
||||
cutYear := timeNow().Year() - years
|
||||
if time.Unix(rec.CreatedAt, 0).Year() >= cutYear {
|
||||
fail(w, http.StatusForbidden, fmt.Sprintf("检验记录在质量追溯保留期(%d 年)内,禁止删除", years))
|
||||
return
|
||||
}
|
||||
}
|
||||
if _, err := ctx.EntClient.Attachment.UpdateOneID(int(req.ID)).
|
||||
SetDeleted(true).SetDeletedAt(timeNow().Unix()).
|
||||
SetNillableDeletedBy(strPtr(r.Header.Get("X-Username"))).
|
||||
@@ -593,3 +611,115 @@ func checkDiskOnce(ctx *svc.ServiceContext) {
|
||||
logx.Infof("[磁盘监控] 正常:剩余 %d%%(%s)", 100-st.UsedPercent, st.RootDir)
|
||||
}
|
||||
}
|
||||
|
||||
// purgeDeletedHandler 物理清理已逻辑删除的附件(需求2):
|
||||
// 扫描 deleted=true 的索引,删除对应磁盘文件(仅当文件未被归档到 ArchiveDir 时),
|
||||
// 释放本地空间;索引保留(deleted 标记不变)以便追溯。默认 dryRun 预检,返回将清理数量与释放字节。
|
||||
// POST /api/attachments/purge-deleted body: { dryRun }
|
||||
func purgeDeletedHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
DryRun bool `json:"dryRun"`
|
||||
}
|
||||
_ = parseJSON(r, &req)
|
||||
root := attRoot(ctx)
|
||||
recs, err := ctx.EntClient.Attachment.Query().
|
||||
Where(attachment.DeletedEQ(true)).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
removed, freed, missing, failed := 0, int64(0), 0, 0
|
||||
for _, rec := range recs {
|
||||
// 已归档的文件已移至 ArchiveDir,不在本地,跳过
|
||||
if rec.Archived {
|
||||
missing++
|
||||
continue
|
||||
}
|
||||
abs, okp := safeAbsPath(root, rec.FilePath)
|
||||
if !okp {
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
info, statErr := os.Stat(abs)
|
||||
if statErr != nil {
|
||||
missing++ // 文件已不存在(已清理或丢失),仅计数
|
||||
continue
|
||||
}
|
||||
size := info.Size()
|
||||
if req.DryRun {
|
||||
removed++
|
||||
freed += size
|
||||
continue
|
||||
}
|
||||
if err := os.Remove(abs); err != nil {
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
removed++
|
||||
freed += size
|
||||
}
|
||||
if !req.DryRun {
|
||||
ctx.EventLog.Write(ctx0(), "attachment.purge", r.Header.Get("X-Username"), "attachment", "purge",
|
||||
fmt.Sprintf("物理清理已删附件:删除%d 释放%.2fMB 缺失%d 失败%d", removed, float64(freed)/1024/1024, missing, failed),
|
||||
map[string]any{"removed": removed, "freed": freed, "missing": missing, "failed": failed})
|
||||
}
|
||||
ok(w, map[string]any{"removed": removed, "freed": freed, "missing": missing, "failed": failed, "dryRun": req.DryRun})
|
||||
}
|
||||
}
|
||||
|
||||
// cleanLogsHandler 日志清理(需求2,仅管理员):清理日志目录(Config.Log.Path)下
|
||||
// 早于 N 天(默认 30)的 *.log 文件。与 logdaily 自动滚动清理互补,提供手动「立即清理」入口。
|
||||
// 默认 dryRun 预检。POST /api/attachments/clean-logs body: { days, dryRun }
|
||||
func cleanLogsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Days int `json:"days"`
|
||||
DryRun bool `json:"dryRun"`
|
||||
}
|
||||
_ = parseJSON(r, &req)
|
||||
days := req.Days
|
||||
if days <= 0 {
|
||||
days = 30 // 默认保留 30 天
|
||||
}
|
||||
dir := strings.TrimSpace(ctx.Config.Log.Path)
|
||||
if dir == "" {
|
||||
dir = "logs"
|
||||
}
|
||||
cutoff := timeNow().AddDate(0, 0, -days)
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, "读取日志目录失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
removed, failed := 0, 0
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".log") {
|
||||
continue
|
||||
}
|
||||
info, ierr := e.Info()
|
||||
if ierr != nil {
|
||||
continue
|
||||
}
|
||||
// 按文件名日期(base-2006-01-02.log)或修改时间判定,早于 cutoff 才清
|
||||
if !info.ModTime().Before(cutoff) {
|
||||
continue
|
||||
}
|
||||
if req.DryRun {
|
||||
removed++
|
||||
continue
|
||||
}
|
||||
if err := os.Remove(filepath.Join(dir, e.Name())); err != nil {
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
removed++
|
||||
}
|
||||
if !req.DryRun {
|
||||
ctx.EventLog.Write(ctx0(), "system.cleanlog", r.Header.Get("X-Username"), "log", dir,
|
||||
fmt.Sprintf("清理日志:删除%d 个早于%d天的日志文件,失败%d", removed, days, failed),
|
||||
map[string]any{"removed": removed, "failed": failed, "days": days})
|
||||
}
|
||||
ok(w, map[string]any{"removed": removed, "failed": failed, "days": days, "dir": dir, "dryRun": req.DryRun})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user