feat: 新增检测单关联、基础数据优化与入库单升级

1. 新增出库单检测单号字段并添加索引支持溯源
2. 优化基础数据页面物料类型展示与选择逻辑
3. 升级入库单导入功能,支持自动生成单号并返回导入结果
4. 优化接驳台查询排序方式
5. 修复字符串拼接空格问题
This commit is contained in:
SunYF
2026-09-21 15:28:14 +08:00
parent 0e769f3813
commit 12ca493c61
70 changed files with 25782 additions and 119 deletions
+106 -2
View File
@@ -13,15 +13,102 @@ import (
"bj_power_wms/ent/inspectionrecord"
"bj_power_wms/ent/inventory"
"bj_power_wms/ent/material"
"bj_power_wms/ent/outboundorder"
"bj_power_wms/internal/svc"
"github.com/google/uuid"
"github.com/zeromicro/go-zero/core/logx"
)
// 物料品类:4=其他(辅料/工装/试验设备),与 schema/material.go 的 item_type 保持一致
const invItemOther = 4
// nextDailyNo 生成日期+流水号,格式:IB/OB + YYYYMMDD + -NNN(如 IB20260921-001)。
// 同一天内流水号递增,跨天重置为 001。并发冲突时自动重试最多 5 次。
// 注意:历史旧单号(IB+UUID 片段)不受影响,仅新生成的用新格式。
func nextDailyNo(ctx *svc.ServiceContext, prefix string) (string, error) {
const maxRetry = 5
var lastErr error
for attempt := 0; attempt < maxRetry; attempt++ {
no, err := genDailyNoOnce(ctx, prefix)
if err != nil {
lastErr = err
time.Sleep(20 * time.Millisecond * time.Duration(attempt+1))
continue
}
// 校验唯一性——如果刚好撞号,重试
if prefix == "IB" {
if _, e := ctx.EntClient.InboundOrder.Query().Where(inboundorder.InboundNoEQ(no)).Only(ctx0()); e == nil {
lastErr = fmt.Errorf("单号 %s 已存在,重试中", no)
time.Sleep(20 * time.Millisecond * time.Duration(attempt+1))
continue
}
} else if prefix == "OB" {
if _, e := ctx.EntClient.OutboundOrder.Query().Where(outboundorder.OutboundNoEQ(no)).Only(ctx0()); e == nil {
lastErr = fmt.Errorf("单号 %s 已存在,重试中", no)
time.Sleep(20 * time.Millisecond * time.Duration(attempt+1))
continue
}
}
return no, nil
}
if lastErr != nil {
return "", fmt.Errorf("生成单号失败(重试 %d 次仍冲突): %w", maxRetry, lastErr)
}
return "", fmt.Errorf("生成单号失败(未知错误)")
}
func genDailyNoOnce(ctx *svc.ServiceContext, prefix string) (string, error) {
now := time.Now()
datePart := now.Format("20060102")
prefixDate := prefix + datePart + "-"
var maxSeq int
var rows []string
var err error
if prefix == "IB" {
rows, err = ctx.EntClient.InboundOrder.Query().
Where(inboundorder.InboundNoHasPrefix(prefixDate)).
Select(inboundorder.FieldInboundNo).
Strings(ctx0())
} else if prefix == "OB" {
rows, err = ctx.EntClient.OutboundOrder.Query().
Where(outboundorder.OutboundNoHasPrefix(prefixDate)).
Select(outboundorder.FieldOutboundNo).
Strings(ctx0())
} else {
return "", fmt.Errorf("未知前缀: %s", prefix)
}
if err != nil {
return "", err
}
for _, n := range rows {
suffix := strings.TrimPrefix(n, prefixDate)
if s, e := strconv.Atoi(suffix); e == nil && s > maxSeq {
maxSeq = s
}
}
seq := maxSeq + 1
return fmt.Sprintf("%s%03d", prefixDate, seq), nil
}
// validateUniqueNo 校验单据号唯一(InboundNo 或 OutboundNo 不能重复)。
func validateUniqueNo(ctx *svc.ServiceContext, kind, no string) error {
if no == "" {
return nil // 空值表示走自动生成
}
switch kind {
case "IB":
if _, e := ctx.EntClient.InboundOrder.Query().Where(inboundorder.InboundNoEQ(no)).Only(ctx0()); e == nil {
return fmt.Errorf("入库单号 %s 已存在", no)
}
case "OB":
if _, e := ctx.EntClient.OutboundOrder.Query().Where(outboundorder.OutboundNoEQ(no)).Only(ctx0()); e == nil {
return fmt.Errorf("出库单号 %s 已存在", no)
}
}
return nil
}
// validateInboundQuality 入库质量门禁:该物料/批次/SN 若已被判定"不合格",拒绝入库。
// 依据客户诉求《问题记录》L135:「不合格不入库、不上产线,仅合格品入库存数量」。
func validateInboundQuality(ctx *svc.ServiceContext, materialCode, batchNo string, snList []string) error {
@@ -58,6 +145,7 @@ func validateInboundQuality(ctx *svc.ServiceContext, materialCode, batchNo strin
func createInboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
InboundNo string `json:"inboundNo"` // 可选:留空自动生成;填了必须唯一(如 IB20260921-001
InboundType string `json:"inboundType"` // purchase/semi/finished/return/other
MaterialCode string `json:"materialCode"`
BatchNo string `json:"batchNo"`
@@ -117,7 +205,20 @@ func createInboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return
}
inboundNo := "IB" + uuid.NewString()[:12]
inboundNo := strings.TrimSpace(req.InboundNo)
if inboundNo != "" {
if err := validateUniqueNo(ctx, "IB", inboundNo); err != nil {
fail(w, http.StatusBadRequest, err.Error())
return
}
} else {
no, err := nextDailyNo(ctx, "IB")
if err != nil {
fail(w, http.StatusInternalServerError, "生成入库单号失败: "+err.Error())
return
}
inboundNo = no
}
// 质量状态自动带出(客户诉求 问题记录 L129:入库记录增加 合格/不合格 列)
// 按检测单号查最近一次检验结论:合格→合格;不合格且部分入库→部分;不合格→不合格;无记录→空(未关联)
@@ -348,6 +449,9 @@ func applyInboundFilters(q *ent.InboundOrderQuery, r *http.Request) *ent.Inbound
// 2026-09-08 用户明确:禁止"关键字"多字段混搜,各字段独立筛选
q = q.Where(inboundorder.MaterialNameContainsFold(v))
}
if v := r.URL.Query().Get("inspectionNo"); v != "" {
q = q.Where(inboundorder.InspectionNoContainsFold(v))
}
// 单据状态:有效 / 已作废(作废单保留可查,但不参与库存)
if v := r.URL.Query().Get("voided"); v == "true" {
q = q.Where(inboundorder.VoidedEQ(true))