docs(deployment): 更新部署手册和业务架构说明

- 更新MES和WMS服务端口配置说明
- 详细说明产线工位设备配置(扫码枪+拧紧枪)
- 明确AGV配送和接驳台的模拟模式与真实对接方案
- 更新WMS中AGV配送默认模拟模式说明
- 完善厂内物流(AGV/接驳台)的业务架构约束
- 修订临时文档为详细的项目问题和解决方案规划
This commit is contained in:
SunYF
2026-09-20 14:52:36 +08:00
parent 5a33028003
commit f35e9b5b75
57 changed files with 6096 additions and 1000 deletions
+130
View File
@@ -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})
}
}