package logic import ( "database/sql" "encoding/json" "fmt" "math" "strconv" "strings" "muyu-apiserver/model" "muyu-apiserver/rpc/inventory/pb" ) func nullString(v string) sql.NullString { return sql.NullString{String: v, Valid: v != ""} } func nullableJSON(v string) sql.NullString { v = strings.TrimSpace(v) return sql.NullString{String: v, Valid: v != ""} } func parseDecimalString(v string) float64 { n, _ := strconv.ParseFloat(strings.TrimSpace(v), 64) return n } type yarnRatioPayload struct { Warp []yarnRatioItem `json:"warp"` Weft []yarnRatioItem `json:"weft"` } type yarnRatioItem struct { YarnID string `json:"yarn_id"` YarnId string `json:"yarnId"` Ratio float64 `json:"ratio"` } func (i yarnRatioItem) yarnID() string { if i.YarnID != "" { return i.YarnID } return i.YarnId } func validateProductBatchInput(batch *pb.ProductBatchInput) error { if batch == nil { return nil } return validateYarnRatio(batch.YarnRatio) } func validateYarnRatio(raw string) error { raw = strings.TrimSpace(raw) if raw == "" { return nil } var payload yarnRatioPayload if err := json.Unmarshal([]byte(raw), &payload); err != nil { return fmt.Errorf("纱线配比格式无效") } if err := validateYarnRatioSide("经线", payload.Warp); err != nil { return err } return validateYarnRatioSide("纬线", payload.Weft) } func validateYarnRatioSide(label string, items []yarnRatioItem) error { if len(items) == 0 { return nil } total := 0 for _, item := range items { if strings.TrimSpace(item.yarnID()) == "" { return fmt.Errorf("%s纱线不能为空", label) } if item.Ratio < 1 || item.Ratio > 10 || math.Trunc(item.Ratio) != item.Ratio { return fmt.Errorf("%s使用配比必须为 1-10 的整数", label) } total += int(item.Ratio) } if total != 10 { return fmt.Errorf("%s配比合计必须为 10,当前为 %d", label, total) } return nil } func batchInputToModelFields(batch *pb.ProductBatchInput, target *model.InvProductBatch) error { if batch == nil { target.ProductionProcess = nullString(defaultProcess) return nil } if err := validateProductBatchInput(batch); err != nil { return err } target.YarnRatio = nullableJSON(batch.YarnRatio) target.WarpWeightGM = parseDecimalString(batch.WarpWeightGM) target.WeftWeightGM = parseDecimalString(batch.WeftWeightGM) process := strings.TrimSpace(batch.ProductionProcess) if process == "" { process = defaultProcess } target.ProductionProcess = nullString(process) target.Remark = nullString(batch.Remark) return nil } func batchToPb(batch *model.InvProductBatch) *pb.ProductBatchInfo { return &pb.ProductBatchInfo{ BatchId: batch.BatchId, ProductId: batch.ProductId, BatchNo: batch.BatchNo, YarnRatio: batch.YarnRatio.String, WarpWeightGM: fmt.Sprintf("%.4f", batch.WarpWeightGM), WeftWeightGM: fmt.Sprintf("%.4f", batch.WeftWeightGM), ProductionProcess: batch.ProductionProcess.String, Remark: batch.Remark.String, Status: batch.Status, CreatedAt: batch.CreatedAt.Format("2006-01-02 15:04:05"), UpdatedAt: batch.UpdatedAt.Format("2006-01-02 15:04:05"), } } func productToPb(product *model.InvProduct) *pb.ProductInfo { return &pb.ProductInfo{ ProductId: product.ProductId, ProductName: product.ProductName, ImageUrl: product.ImageUrl, Spec: product.Spec, Color: product.Color, SalesPrice: fmt.Sprintf("%.2f", product.SalesPrice), Remark: product.Remark.String, Status: product.Status, CreatedAt: product.CreatedAt.Format("2006-01-02 15:04:05"), UpdatedAt: product.UpdatedAt.Format("2006-01-02 15:04:05"), ColorNo: product.ColorNo, ProductCode: product.ProductCode, } }