552 lines
18 KiB
Go
552 lines
18 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"encoding/json"
|
||
"fmt"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
"unicode"
|
||
|
||
"cmautobuy/admin/model"
|
||
"cmautobuy/admin/repository"
|
||
"cmautobuy/admin/syb"
|
||
)
|
||
|
||
// InnerCodeDetailReader 只开放读取已知货运单详情的能力,供规划和回写前核验复用。
|
||
type InnerCodeDetailReader interface {
|
||
DetailListByStock(context.Context, []int64) ([]syb.StockDetail, error)
|
||
}
|
||
|
||
// InnerCodePlanReader 是规划阶段需要的顺运宝只读能力。
|
||
// 货运单定位必须直接查询远端,不能依赖是否已同步进本地 syb_orders。
|
||
type InnerCodePlanReader interface {
|
||
InnerCodeDetailReader
|
||
ListByOrderNumber(context.Context, string) ([]syb.StockRow, error)
|
||
}
|
||
|
||
const innerCodeReadBatchSize = 100
|
||
|
||
// InnerCodePlanResult 是一次规划的逐状态统计。
|
||
type InnerCodePlanResult struct {
|
||
Total int
|
||
Ready int
|
||
AlreadyFilled int
|
||
Skipped int
|
||
Failed int
|
||
}
|
||
|
||
// PlanInnerCodeRecords 对某个业务日期选中的可重规划记录读取最新远端详情并原子保存计划。
|
||
// 本函数绝不调用删除或更新顺运宝接口。
|
||
func PlanInnerCodeRecords(ctx context.Context, db *sql.DB, reader InnerCodePlanReader, businessDate string, selectedIDs []int64) (*InnerCodePlanResult, error) {
|
||
if _, err := time.Parse("2006-01-02", businessDate); err != nil {
|
||
return nil, fmt.Errorf("业务日期格式应为 YYYY-MM-DD")
|
||
}
|
||
selectedIDs = uniquePositiveInnerCodeIDs(selectedIDs)
|
||
if len(selectedIDs) == 0 {
|
||
return nil, fmt.Errorf("没有选中可匹配记录")
|
||
}
|
||
records, err := repository.ListInnerCodeRecordsForPlanning(db, businessDate, selectedIDs)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(records) != len(selectedIDs) {
|
||
return nil, fmt.Errorf("部分选中记录不存在、业务日期不一致或状态已变化,请刷新后重新勾选")
|
||
}
|
||
result := &InnerCodePlanResult{Total: len(records)}
|
||
|
||
orders := uniqueInnerCodeOrders(records)
|
||
contextRecords, err := repository.ListInnerCodePlanningContext(db, businessDate, orders)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
selectedSet := make(map[int64]bool, len(records))
|
||
for _, record := range records {
|
||
selectedSet[record.ID] = true
|
||
}
|
||
reservedDetails := innerCodeReservedDetails(contextRecords, selectedSet)
|
||
stockIDsByOrder, err := readInnerCodeStockIDs(ctx, reader, orders)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
requested := uniqueInnerCodeStockIDs(records, stockIDsByOrder)
|
||
detailsByID, err := readInnerCodeDetails(ctx, reader, requested)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
plans := planInnerCodeRowsWithReserved(records, stockIDsByOrder, detailsByID, reservedDetails)
|
||
for _, plan := range plans {
|
||
switch plan.Status {
|
||
case model.InnerCodeReady:
|
||
result.Ready++
|
||
case model.InnerCodeAlreadyFilled:
|
||
result.AlreadyFilled++
|
||
case model.InnerCodeFailed:
|
||
result.Failed++
|
||
default:
|
||
result.Skipped++
|
||
}
|
||
}
|
||
if err := repository.SaveInnerCodePlans(db, plans, model.NowISO()); err != nil {
|
||
return nil, err
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// readInnerCodeDetails 只限制单次顺运宝请求大小,不限制一次规划的总选择数量。
|
||
func readInnerCodeDetails(ctx context.Context, reader InnerCodeDetailReader, requested []int64) (map[int64]syb.StockDetail, error) {
|
||
detailsByID := make(map[int64]syb.StockDetail, len(requested))
|
||
for start := 0; start < len(requested); start += innerCodeReadBatchSize {
|
||
end := start + innerCodeReadBatchSize
|
||
if end > len(requested) {
|
||
end = len(requested)
|
||
}
|
||
batch := requested[start:end]
|
||
details, err := reader.DetailListByStock(ctx, batch)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("读取顺运宝最新商品详情失败,本次规划未保存: %w", err)
|
||
}
|
||
for _, detail := range details {
|
||
if _, duplicate := detailsByID[detail.ID]; duplicate {
|
||
return nil, fmt.Errorf("顺运宝重复返回货运单 id=%d,本次规划未保存", detail.ID)
|
||
}
|
||
detailsByID[detail.ID] = detail
|
||
}
|
||
for _, id := range batch {
|
||
if _, exists := detailsByID[id]; !exists {
|
||
return nil, fmt.Errorf("顺运宝响应缺少货运单 id=%d,本次规划未保存", id)
|
||
}
|
||
}
|
||
}
|
||
return detailsByID, nil
|
||
}
|
||
|
||
func uniqueInnerCodeOrders(records []model.InnerCodeRecord) []string {
|
||
seen := make(map[string]bool, len(records))
|
||
result := make([]string, 0, len(records))
|
||
for _, record := range records {
|
||
if !seen[record.OrderNumber] {
|
||
seen[record.OrderNumber] = true
|
||
result = append(result, record.OrderNumber)
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
// readInnerCodeStockIDs 对勾选记录的货运单号逐个直查顺运宝。
|
||
// 零命中和多命中交给逐行规划生成明确状态;网络或协议错误会中止整批,
|
||
// 避免保存一半新计划、一半旧计划。
|
||
func readInnerCodeStockIDs(ctx context.Context, reader InnerCodePlanReader, orders []string) (map[string][]int64, error) {
|
||
result := make(map[string][]int64, len(orders))
|
||
for _, order := range orders {
|
||
rows, err := reader.ListByOrderNumber(ctx, order)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("查询顺运宝货运单 %q 失败,本次规划未保存: %w", order, err)
|
||
}
|
||
ids := make([]int64, 0, len(rows))
|
||
seen := make(map[int64]bool, len(rows))
|
||
for _, row := range rows {
|
||
if row.Code != order {
|
||
return nil, fmt.Errorf("查询顺运宝货运单 %q 返回不一致的 code %q,本次规划未保存", order, row.Code)
|
||
}
|
||
if row.ID <= 0 {
|
||
return nil, fmt.Errorf("查询顺运宝货运单 %q 返回非法 id=%d,本次规划未保存", order, row.ID)
|
||
}
|
||
if !seen[row.ID] {
|
||
seen[row.ID] = true
|
||
ids = append(ids, row.ID)
|
||
}
|
||
}
|
||
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
|
||
result[order] = ids
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func uniqueInnerCodeStockIDs(records []model.InnerCodeRecord, byOrder map[string][]int64) []int64 {
|
||
seen := make(map[int64]bool)
|
||
result := make([]int64, 0)
|
||
for _, record := range records {
|
||
ids := byOrder[record.OrderNumber]
|
||
if len(ids) != 1 || seen[ids[0]] {
|
||
continue
|
||
}
|
||
seen[ids[0]] = true
|
||
result = append(result, ids[0])
|
||
}
|
||
sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
|
||
return result
|
||
}
|
||
|
||
func innerCodeReservedDetails(records []model.InnerCodeRecord, selected map[int64]bool) map[string]map[int64]bool {
|
||
reserved := make(map[string]map[int64]bool)
|
||
for _, record := range records {
|
||
if selected[record.ID] || record.DetailID <= 0 || !innerCodeStatusReservesDetail(record.Status) {
|
||
continue
|
||
}
|
||
if reserved[record.OrderNumber] == nil {
|
||
reserved[record.OrderNumber] = make(map[int64]bool)
|
||
}
|
||
reserved[record.OrderNumber][record.DetailID] = true
|
||
}
|
||
return reserved
|
||
}
|
||
|
||
func innerCodeStatusReservesDetail(status model.InnerCodeStatus) bool {
|
||
switch status {
|
||
case model.InnerCodeReady, model.InnerCodeQueued, model.InnerCodeApplying, model.InnerCodeUpdated,
|
||
model.InnerCodeAlreadyFilled, model.InnerCodeNeedsCheck:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func planInnerCodeRows(records []model.InnerCodeRecord, stockIDsByOrder map[string][]int64, detailsByID map[int64]syb.StockDetail) []model.InnerCodeRecord {
|
||
return planInnerCodeRowsWithReserved(records, stockIDsByOrder, detailsByID, nil)
|
||
}
|
||
|
||
func planInnerCodeRowsWithReserved(records []model.InnerCodeRecord, stockIDsByOrder map[string][]int64, detailsByID map[int64]syb.StockDetail, reserved map[string]map[int64]bool) []model.InnerCodeRecord {
|
||
used := make(map[string]map[int64]bool, len(reserved))
|
||
for orderNumber, detailIDs := range reserved {
|
||
used[orderNumber] = make(map[int64]bool, len(detailIDs))
|
||
for detailID := range detailIDs {
|
||
used[orderNumber][detailID] = true
|
||
}
|
||
}
|
||
plans := make([]model.InnerCodeRecord, 0, len(records))
|
||
for _, record := range records {
|
||
plan := record
|
||
plan.StockID, plan.DetailID = 0, 0
|
||
plan.SybSpec, plan.SybSKU, plan.SybVariationSKU = "", "", ""
|
||
plan.PurchasePlatform, plan.PurchaseCode, plan.RemoteInnerCode = "", "", ""
|
||
if strings.TrimSpace(record.SpecRaw) == "" {
|
||
plans = append(plans, innerCodeSkippedPlan(plan, "Excel 清洗后规格为空,跳过"))
|
||
continue
|
||
}
|
||
stockIDs := stockIDsByOrder[record.OrderNumber]
|
||
if len(stockIDs) == 0 {
|
||
plan.Status = model.InnerCodeFailed
|
||
plan.ResultMessage = "顺运宝未找到货运单"
|
||
plans = append(plans, plan)
|
||
continue
|
||
}
|
||
if len(stockIDs) > 1 {
|
||
plans = append(plans, innerCodeSkippedPlan(plan, "同一订单号命中多张顺运宝货运单,不能自动选择"))
|
||
continue
|
||
}
|
||
stockID := stockIDs[0]
|
||
stock := detailsByID[stockID]
|
||
if used[record.OrderNumber] == nil {
|
||
used[record.OrderNumber] = make(map[int64]bool)
|
||
}
|
||
eligible := make([]syb.DetailItem, 0, len(stock.Details))
|
||
availableBeforeReservation := 0
|
||
for _, item := range stock.Details {
|
||
if innerCodeRawText(item.Raw["purchasePlatform"]) == "" && innerCodeRawText(item.Raw["purchaseCode"]) == "" {
|
||
availableBeforeReservation++
|
||
if used[record.OrderNumber][item.ID] {
|
||
continue
|
||
}
|
||
eligible = append(eligible, item)
|
||
}
|
||
}
|
||
if len(eligible) == 0 {
|
||
message := "没有采购平台和采购单号都为空的顺运宝商品"
|
||
if availableBeforeReservation > 0 {
|
||
message = "符合条件的顺运宝商品已被同订单其他记录占用"
|
||
}
|
||
plans = append(plans, innerCodeSkippedPlan(plan, message))
|
||
continue
|
||
}
|
||
specMatches := matchInnerCodeSpecItems(record.SpecRaw, eligible)
|
||
matches, evidenceReason := matchInnerCodeItemsWithEvidence(record.Stall, record.SourceSKURaw, specMatches)
|
||
if evidenceReason != "" {
|
||
plans = append(plans, innerCodeSkippedPlan(plan, evidenceReason))
|
||
continue
|
||
}
|
||
if len(matches) == 0 {
|
||
reason := "候选商品中没有相同规格"
|
||
if len(specMatches) > 0 {
|
||
reason = "规格能匹配,但档口及货号与候选商品不一致"
|
||
}
|
||
plans = append(plans, innerCodeSkippedPlan(plan, reason))
|
||
continue
|
||
}
|
||
if len(matches) > 1 {
|
||
plans = append(plans, innerCodeSkippedPlan(plan, "同一订单存在多条相同规格候选商品,不能自动选择"))
|
||
continue
|
||
}
|
||
matched := matches[0]
|
||
codes, codeErr := splitInnerCodes(record.InnerCode)
|
||
if codeErr != nil || len(codes) != record.SourceDuplicateCount {
|
||
plans = append(plans, innerCodeSkippedPlan(plan,
|
||
fmt.Sprintf("单件入库码数量与 Excel 源行数不一致(入库码 %d 个,源行 %d 行)", len(codes), record.SourceDuplicateCount)))
|
||
continue
|
||
}
|
||
if matched.ProductQty != len(codes) {
|
||
plans = append(plans, innerCodeSkippedPlan(plan,
|
||
fmt.Sprintf("顺运宝商品数量为 %d,单件入库码为 %d 个,数量不一致", matched.ProductQty, len(codes))))
|
||
continue
|
||
}
|
||
used[record.OrderNumber][matched.ID] = true
|
||
plan.StockID = stockID
|
||
plan.DetailID = matched.ID
|
||
plan.SybSpec = matched.ProductSpec
|
||
plan.SybSKU = innerCodeRawText(matched.Raw["sku"])
|
||
plan.SybVariationSKU = innerCodeRawText(matched.Raw["variationSku"])
|
||
plan.PurchasePlatform = innerCodeRawText(matched.Raw["purchasePlatform"])
|
||
plan.PurchaseCode = innerCodeRawText(matched.Raw["purchaseCode"])
|
||
plan.RemoteInnerCode = innerCodeRawText(matched.Raw["innerExpCode"])
|
||
_, missing, remoteErr := planInnerCodeRemoteItems(plan, stock, codes)
|
||
if remoteErr != nil {
|
||
plan.Status = model.InnerCodeSkipped
|
||
plan.ResultMessage = remoteErr.Error()
|
||
} else if len(missing) == 0 {
|
||
plan.Status = model.InnerCodeAlreadyFilled
|
||
plan.ResultMessage = fmt.Sprintf("远端已存在全部 %d 个单件入库码,无需重复写入", len(codes))
|
||
} else {
|
||
plan.Status = model.InnerCodeReady
|
||
plan.ResultMessage = "唯一匹配,等待操作员确认回写"
|
||
}
|
||
plans = append(plans, plan)
|
||
}
|
||
return plans
|
||
}
|
||
|
||
func innerCodeSkippedPlan(plan model.InnerCodeRecord, message string) model.InnerCodeRecord {
|
||
plan.Status = model.InnerCodeSkipped
|
||
plan.ResultMessage = message
|
||
return plan
|
||
}
|
||
|
||
func matchInnerCodeItems(spec, stall string, eligible []syb.DetailItem) []syb.DetailItem {
|
||
return filterInnerCodeItemsByStall(matchInnerCodeSpecItems(spec, eligible), stall)
|
||
}
|
||
|
||
func matchInnerCodeSpecItems(spec string, eligible []syb.DetailItem) []syb.DetailItem {
|
||
matches := make([]syb.DetailItem, 0)
|
||
for _, item := range eligible {
|
||
if item.ProductSpec == spec {
|
||
matches = append(matches, item)
|
||
}
|
||
}
|
||
if len(matches) == 0 {
|
||
key := NormalizeInnerCodeSpecKey(spec)
|
||
if key == "" {
|
||
return nil
|
||
}
|
||
for _, item := range eligible {
|
||
if NormalizeInnerCodeSpecKey(item.ProductSpec) == key {
|
||
matches = append(matches, item)
|
||
}
|
||
}
|
||
}
|
||
return matches
|
||
}
|
||
|
||
func matchInnerCodeItemsWithEvidence(stall, sourceSKURaw string, specMatches []syb.DetailItem) ([]syb.DetailItem, string) {
|
||
sourceSKU := normalizeInnerCodeSourceSKU(sourceSKURaw)
|
||
if sourceSKU != "" {
|
||
sourceMatches := make([]syb.DetailItem, 0)
|
||
for _, item := range specMatches {
|
||
if normalizeInnerCodeSourceSKU(innerCodeRawText(item.Raw["sku"])) == sourceSKU ||
|
||
normalizeInnerCodeSourceSKU(innerCodeRawText(item.Raw["variationSku"])) == sourceSKU {
|
||
sourceMatches = append(sourceMatches, item)
|
||
}
|
||
}
|
||
if len(sourceMatches) > 1 {
|
||
return nil, "原始 SKU 候选重复,不能自动选择"
|
||
}
|
||
if len(sourceMatches) == 1 {
|
||
stallMatches := filterInnerCodeItemsByStallStrict(specMatches, stall)
|
||
if len(stallMatches) > 1 {
|
||
return nil, "档口货号候选重复,不能自动选择"
|
||
}
|
||
if len(stallMatches) == 1 && stallMatches[0].ID != sourceMatches[0].ID {
|
||
return nil, "原始 SKU 与档口货号冲突,不能自动选择"
|
||
}
|
||
return sourceMatches, ""
|
||
}
|
||
}
|
||
return filterInnerCodeItemsByStall(specMatches, stall), ""
|
||
}
|
||
|
||
func filterInnerCodeItemsByStall(items []syb.DetailItem, stall string) []syb.DetailItem {
|
||
stall = strings.TrimSpace(stall)
|
||
if stall == "" {
|
||
return items
|
||
}
|
||
matched := make([]syb.DetailItem, 0)
|
||
for _, item := range items {
|
||
if innerCodeStallMatches(stall, item) {
|
||
matched = append(matched, item)
|
||
}
|
||
}
|
||
if len(matched) > 0 {
|
||
return matched
|
||
}
|
||
for _, item := range items {
|
||
if innerCodeRawText(item.Raw["sku"]) == "" && innerCodeRawText(item.Raw["variationSku"]) == "" {
|
||
matched = append(matched, item)
|
||
}
|
||
}
|
||
return matched
|
||
}
|
||
|
||
func filterInnerCodeItemsByStallStrict(items []syb.DetailItem, stall string) []syb.DetailItem {
|
||
stall = strings.TrimSpace(stall)
|
||
if stall == "" {
|
||
return nil
|
||
}
|
||
matched := make([]syb.DetailItem, 0)
|
||
for _, item := range items {
|
||
if innerCodeStallMatches(stall, item) {
|
||
matched = append(matched, item)
|
||
}
|
||
}
|
||
return matched
|
||
}
|
||
|
||
func innerCodeStallMatches(stall string, item syb.DetailItem) bool {
|
||
sku := innerCodeRawText(item.Raw["sku"])
|
||
variation := innerCodeRawText(item.Raw["variationSku"])
|
||
blob := sku + " " + variation + " " + item.ProductSpec
|
||
if strings.Contains(blob, stall) {
|
||
return true
|
||
}
|
||
name, article, hasArticle := splitInnerCodeStall(stall)
|
||
if !hasArticle {
|
||
return false
|
||
}
|
||
if article == "" {
|
||
return name != "" && (strings.Contains(sku, name) || strings.Contains(variation, name))
|
||
}
|
||
|
||
// 纯数字货号容易在不同档口重复。只有候选也包含同一档口名称时,
|
||
// 才允许把 067 与 67 视为同一个货号;不能只凭短数字自动选商品。
|
||
if isInnerCodeNumericArticle(article) {
|
||
nameMatches := name != "" && (strings.Contains(sku, name) || strings.Contains(variation, name))
|
||
if !nameMatches {
|
||
return false
|
||
}
|
||
return innerCodeTextHasNumericArticle(sku, article) ||
|
||
innerCodeTextHasNumericArticle(variation, article) ||
|
||
innerCodeProductSpecStartsWithArticle(item.ProductSpec, article, true)
|
||
}
|
||
|
||
return innerCodeTextHasExactArticle(sku, article) ||
|
||
innerCodeTextHasExactArticle(variation, article) ||
|
||
innerCodeProductSpecStartsWithArticle(item.ProductSpec, article, false)
|
||
}
|
||
|
||
// splitInnerCodeStall 从“档口名称#货号”取最后一个 #,避免档口名称本身含 # 时截错。
|
||
func splitInnerCodeStall(stall string) (name, article string, ok bool) {
|
||
stall = strings.TrimSpace(stall)
|
||
separator := strings.LastIndex(stall, "#")
|
||
if separator < 0 {
|
||
return stall, "", false
|
||
}
|
||
return strings.TrimSpace(stall[:separator]), strings.TrimSpace(stall[separator+1:]), true
|
||
}
|
||
|
||
func isInnerCodeNumericArticle(article string) bool {
|
||
if article == "" {
|
||
return false
|
||
}
|
||
for _, char := range article {
|
||
if !unicode.IsDigit(char) {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
func innerCodeTextHasNumericArticle(text, article string) bool {
|
||
target := normalizeInnerCodeNumericArticle(article)
|
||
for _, token := range innerCodeArticleTokens(text) {
|
||
if isInnerCodeNumericArticle(token) && normalizeInnerCodeNumericArticle(token) == target {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func normalizeInnerCodeNumericArticle(article string) string {
|
||
normalized := strings.TrimLeft(article, "0")
|
||
if normalized == "" {
|
||
return "0"
|
||
}
|
||
return normalized
|
||
}
|
||
|
||
func innerCodeTextHasExactArticle(text, article string) bool {
|
||
for _, token := range innerCodeArticleTokens(text) {
|
||
if token == article {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func innerCodeProductSpecStartsWithArticle(productSpec, article string, numeric bool) bool {
|
||
productSpec = strings.TrimSpace(productSpec)
|
||
separator := strings.IndexAny(productSpec, " ,,")
|
||
if separator <= 0 {
|
||
return false
|
||
}
|
||
prefix := strings.TrimSpace(productSpec[:separator])
|
||
if numeric {
|
||
return isInnerCodeNumericArticle(prefix) &&
|
||
normalizeInnerCodeNumericArticle(prefix) == normalizeInnerCodeNumericArticle(article)
|
||
}
|
||
return prefix == article
|
||
}
|
||
|
||
// innerCodeArticleTokens 只把连续字母或数字视为货号候选。
|
||
// `【】`、`#`、横线、空格等标点自然成为边界,因此能识别 `067【档口】`,
|
||
// 又不会把 `PDD256437` 中间的数字误认为独立货号。
|
||
func innerCodeArticleTokens(text string) []string {
|
||
tokens := make([]string, 0)
|
||
start := -1
|
||
runes := []rune(text)
|
||
for index, char := range runes {
|
||
if unicode.IsLetter(char) || unicode.IsDigit(char) {
|
||
if start < 0 {
|
||
start = index
|
||
}
|
||
continue
|
||
}
|
||
if start >= 0 {
|
||
tokens = append(tokens, string(runes[start:index]))
|
||
start = -1
|
||
}
|
||
}
|
||
if start >= 0 {
|
||
tokens = append(tokens, string(runes[start:]))
|
||
}
|
||
return tokens
|
||
}
|
||
|
||
func innerCodeRawText(value any) string {
|
||
switch typed := value.(type) {
|
||
case nil:
|
||
return ""
|
||
case string:
|
||
return strings.TrimSpace(typed)
|
||
case json.Number:
|
||
return strings.TrimSpace(typed.String())
|
||
case float64:
|
||
if typed == float64(int64(typed)) {
|
||
return strconv.FormatInt(int64(typed), 10)
|
||
}
|
||
return strconv.FormatFloat(typed, 'f', -1, 64)
|
||
default:
|
||
return strings.TrimSpace(fmt.Sprint(typed))
|
||
}
|
||
}
|