316 lines
8.8 KiB
Go
316 lines
8.8 KiB
Go
package hfb
|
||
|
||
import (
|
||
"fmt"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
var (
|
||
numberPattern = regexp.MustCompile(`[-+]?\d+(?:\.\d+)?`)
|
||
onlineTimePattern = regexp.MustCompile(`^\D*(\d{1,2})(?:\s*[::点.]\s*(\d{1,2}))?\D+(\d{1,2})(?:\s*[::点.]\s*(\d{1,2}))?\D*$`)
|
||
)
|
||
|
||
func ParseUploadText(text string) (ParsedUpload, error) {
|
||
values := make(map[string]string)
|
||
for lineNo, line := range strings.Split(text, "\n") {
|
||
line = strings.TrimSpace(line)
|
||
if line == "" {
|
||
continue
|
||
}
|
||
key, value, ok := splitKeyValue(line)
|
||
if !ok {
|
||
return ParsedUpload{}, fmt.Errorf("第 %d 行格式不正确,应为“字段:值”", lineNo+1)
|
||
}
|
||
key = normalizeKey(key)
|
||
if key == "" {
|
||
continue
|
||
}
|
||
values[key] = strings.TrimSpace(value)
|
||
}
|
||
|
||
account := ExternalAccountData{}
|
||
var err error
|
||
account.LoginMethod = firstValue(values, "上号方式", "登录方式")
|
||
account.Rank = firstValue(values, "段位")
|
||
account.Level, err = parseRequiredInt(values, "等级")
|
||
if err != nil {
|
||
return ParsedUpload{}, err
|
||
}
|
||
account.SafeSlots, err = parseRequiredInt(values, "保险格数", "保险箱格数")
|
||
if err != nil {
|
||
return ParsedUpload{}, err
|
||
}
|
||
account.DailyLossM, err = parseRequiredFloat(values, "每日消耗")
|
||
if err != nil {
|
||
return ParsedUpload{}, err
|
||
}
|
||
account.SecretKD, err = parseOptionalFloat(values, "绝密KD", "绝密kd")
|
||
if err != nil {
|
||
return ParsedUpload{}, err
|
||
}
|
||
account.Deposit, err = parseRequiredFloat(values, "押金")
|
||
if err != nil {
|
||
return ParsedUpload{}, err
|
||
}
|
||
banRaw := firstValue(values, "封禁记录")
|
||
account.BanRecord = normalizeBanFlag(banRaw)
|
||
account.CommonRegion = firstValue(values, "常用地区", "常用区域")
|
||
account.ContactPhone = firstValue(values, "联系电话", "手机号", "电话")
|
||
account.OwnerOnlineTime = normalizeOwnerOnlineTime(firstValue(values, "号主在线时间", "在线时间"))
|
||
account.Remark = composeRemark(firstValue(values, "备注", "说明"), banRaw)
|
||
|
||
account.Currency.HafuCoin, err = parseRequiredFloat(values, "哈夫币纯币", "哈夫币")
|
||
if err != nil {
|
||
return ParsedUpload{}, err
|
||
}
|
||
account.Currency.RecycleRatio, err = parseRequiredRatio(values, "回收比例")
|
||
if err != nil {
|
||
return ParsedUpload{}, err
|
||
}
|
||
account.Currency.RecycleRent, err = parseRequiredFloat(values, "回收租金")
|
||
if err != nil {
|
||
return ParsedUpload{}, err
|
||
}
|
||
|
||
account.DailyConsumption.Stamina, err = parseRequiredInt(values, "体力")
|
||
if err != nil {
|
||
return ParsedUpload{}, err
|
||
}
|
||
account.DailyConsumption.Weight, err = parseRequiredInt(values, "负重")
|
||
if err != nil {
|
||
return ParsedUpload{}, err
|
||
}
|
||
account.Inventory.AWMBullets, err = parseOptionalInt(values, "AWM子弹数量", "AWM子弹")
|
||
if err != nil {
|
||
return ParsedUpload{}, err
|
||
}
|
||
account.Inventory.Level6Helmets, err = parseOptionalInt(values, "6头数量", "六头数量", "6头")
|
||
if err != nil {
|
||
return ParsedUpload{}, err
|
||
}
|
||
account.Inventory.Level6Armor, err = parseOptionalInt(values, "6甲数量", "六甲数量", "6甲")
|
||
if err != nil {
|
||
return ParsedUpload{}, err
|
||
}
|
||
account.Inventory.Skins = parseSkins(firstValue(values, "皮肤"))
|
||
|
||
if strings.TrimSpace(account.LoginMethod) == "" {
|
||
return ParsedUpload{}, fmt.Errorf("缺少必填字段:上号方式")
|
||
}
|
||
if strings.TrimSpace(account.Rank) == "" {
|
||
return ParsedUpload{}, fmt.Errorf("缺少必填字段:段位")
|
||
}
|
||
|
||
return ParsedUpload{
|
||
UploaderName: firstValue(values, "上传人", "上传者", "上传账号", "上传管理员"),
|
||
Account: account,
|
||
}, nil
|
||
}
|
||
|
||
func splitKeyValue(line string) (string, string, bool) {
|
||
fullWidthIdx := strings.Index(line, ":")
|
||
asciiIdx := strings.Index(line, ":")
|
||
if fullWidthIdx >= 0 && (asciiIdx < 0 || fullWidthIdx < asciiIdx) {
|
||
idx := fullWidthIdx
|
||
return line[:idx], line[idx+len(":"):], true
|
||
}
|
||
if asciiIdx >= 0 {
|
||
idx := asciiIdx
|
||
return line[:idx], line[idx+1:], true
|
||
}
|
||
return "", "", false
|
||
}
|
||
|
||
func normalizeKey(key string) string {
|
||
return strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(key), " ", ""))
|
||
}
|
||
|
||
func firstValue(values map[string]string, labels ...string) string {
|
||
for _, label := range labels {
|
||
if value, ok := values[normalizeKey(label)]; ok {
|
||
return strings.TrimSpace(value)
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func optionalDefault(value, fallback string) string {
|
||
if strings.TrimSpace(value) == "" {
|
||
return fallback
|
||
}
|
||
return strings.TrimSpace(value)
|
||
}
|
||
|
||
// normalizeBanFlag 只输出“有/无”,具体封禁历史放到备注。
|
||
func normalizeBanFlag(value string) string {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" || isNoBanRecord(value) {
|
||
return "无"
|
||
}
|
||
return "有"
|
||
}
|
||
|
||
func isNoBanRecord(value string) bool {
|
||
value = strings.TrimSpace(value)
|
||
switch value {
|
||
case "无", "无封禁", "无封禁记录", "否", "没有", "none", "None", "NONE":
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
// composeRemark 合并原文备注与封禁历史详情。
|
||
func composeRemark(remark, banRaw string) string {
|
||
remark = strings.TrimSpace(remark)
|
||
banDetail := strings.TrimSpace(banRaw)
|
||
if banDetail == "" || isNoBanRecord(banDetail) {
|
||
return remark
|
||
}
|
||
// 如果原文已经是“有/无”,不当作详情追加
|
||
if banDetail == "有" || banDetail == "是" || banDetail == "有封禁" || banDetail == "有封禁记录" {
|
||
return remark
|
||
}
|
||
banLine := "封禁历史:" + banDetail
|
||
if remark == "" {
|
||
return banLine
|
||
}
|
||
return remark + "\n" + banLine
|
||
}
|
||
|
||
func parseRequiredInt(values map[string]string, labels ...string) (int, error) {
|
||
value := firstValue(values, labels...)
|
||
if value == "" {
|
||
return 0, fmt.Errorf("缺少必填字段:%s", labels[0])
|
||
}
|
||
return parseIntLabel(labels[0], value)
|
||
}
|
||
|
||
func parseOptionalInt(values map[string]string, labels ...string) (int, error) {
|
||
value := firstValue(values, labels...)
|
||
if value == "" {
|
||
return 0, nil
|
||
}
|
||
return parseIntLabel(labels[0], value)
|
||
}
|
||
|
||
func parseIntLabel(label, value string) (int, error) {
|
||
number, err := parseFloatText(value, false)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("%s 数字格式不正确:%s", label, value)
|
||
}
|
||
return int(number), nil
|
||
}
|
||
|
||
func parseRequiredFloat(values map[string]string, labels ...string) (float64, error) {
|
||
value := firstValue(values, labels...)
|
||
if value == "" {
|
||
return 0, fmt.Errorf("缺少必填字段:%s", labels[0])
|
||
}
|
||
number, err := parseFloatText(value, false)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("%s 数字格式不正确:%s", labels[0], value)
|
||
}
|
||
return number, nil
|
||
}
|
||
|
||
func parseOptionalFloat(values map[string]string, labels ...string) (float64, error) {
|
||
value := firstValue(values, labels...)
|
||
if value == "" {
|
||
return 0, nil
|
||
}
|
||
number, err := parseFloatText(value, false)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("%s 数字格式不正确:%s", labels[0], value)
|
||
}
|
||
return number, nil
|
||
}
|
||
|
||
func parseRequiredRatio(values map[string]string, labels ...string) (float64, error) {
|
||
value := firstValue(values, labels...)
|
||
if value == "" {
|
||
return 0, fmt.Errorf("缺少必填字段:%s", labels[0])
|
||
}
|
||
number, err := parseFloatText(value, true)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("%s 数字格式不正确:%s", labels[0], value)
|
||
}
|
||
return number, nil
|
||
}
|
||
|
||
func parseFloatText(value string, preferLast bool) (float64, error) {
|
||
value = strings.TrimSpace(strings.ReplaceAll(value, ",", ""))
|
||
matches := numberPattern.FindAllString(value, -1)
|
||
if len(matches) == 0 {
|
||
return 0, strconv.ErrSyntax
|
||
}
|
||
pick := matches[0]
|
||
if preferLast {
|
||
pick = matches[len(matches)-1]
|
||
}
|
||
return strconv.ParseFloat(pick, 64)
|
||
}
|
||
|
||
func normalizeOwnerOnlineTime(value string) string {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" {
|
||
return ""
|
||
}
|
||
start, end, ok := parseOwnerOnlineTimeRange(value)
|
||
if !ok {
|
||
return value
|
||
}
|
||
return start + " 至 " + end
|
||
}
|
||
|
||
func parseOwnerOnlineTimeRange(value string) (string, string, bool) {
|
||
matches := onlineTimePattern.FindStringSubmatch(strings.TrimSpace(value))
|
||
if len(matches) != 5 {
|
||
return "", "", false
|
||
}
|
||
start, ok := normalizeTimePart(matches[1], matches[2])
|
||
if !ok {
|
||
return "", "", false
|
||
}
|
||
end, ok := normalizeTimePart(matches[3], matches[4])
|
||
if !ok {
|
||
return "", "", false
|
||
}
|
||
return start, end, true
|
||
}
|
||
|
||
func normalizeTimePart(hourText, minuteText string) (string, bool) {
|
||
hour, err := strconv.Atoi(strings.TrimSpace(hourText))
|
||
if err != nil || hour < 0 || hour > 23 {
|
||
return "", false
|
||
}
|
||
minute := 0
|
||
if strings.TrimSpace(minuteText) != "" {
|
||
minute, err = strconv.Atoi(strings.TrimSpace(minuteText))
|
||
if err != nil || minute < 0 || minute > 59 {
|
||
return "", false
|
||
}
|
||
}
|
||
return fmt.Sprintf("%02d:%02d", hour, minute), true
|
||
}
|
||
|
||
func parseSkins(value string) []string {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" || value == "无" || strings.EqualFold(value, "none") {
|
||
return nil
|
||
}
|
||
parts := strings.FieldsFunc(value, func(r rune) bool {
|
||
return r == ',' || r == ',' || r == '、' || r == ';' || r == ';'
|
||
})
|
||
skins := make([]string, 0, len(parts))
|
||
for _, part := range parts {
|
||
part = strings.TrimSpace(part)
|
||
if part != "" && part != "无" {
|
||
skins = append(skins, part)
|
||
}
|
||
}
|
||
return skins
|
||
}
|