fix(systemconfig): 首页入口展示开关与发布选项编码修复
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
||||
"hfb_sys/backend/internal/auditlog"
|
||||
"hfb_sys/backend/internal/model"
|
||||
@@ -54,6 +55,7 @@ var defaultConfigs = []defaultConfig{
|
||||
{Key: homeBannersConfigKey, Value: defaultHomeBannersConfigValue(), Description: "移动端首页轮播图 JSON 数组"},
|
||||
{Key: awRecycleConfigKey, Value: defaultAWRecycleConfigValue(), Description: "首页 AW炫彩回收入口配置 JSON(标题/副标题/展示图片)"},
|
||||
{Key: cooperationFeedbackConfigKey, Value: defaultCooperationFeedbackConfigValue(), Description: "首页 合作与反馈入口配置 JSON(标题/副标题/展示图片)"},
|
||||
{Key: homeEntriesEnabledConfigKey, Value: defaultHomeEntriesEnabledConfigValue(), Description: "首页 AW炫彩回收/合作与反馈入口总开关(关闭后 PC/移动端均隐藏)"},
|
||||
{Key: publishOptionsConfigKey, Value: defaultPublishOptionsConfigValue(), Description: "发布页选项配置 JSON"},
|
||||
{Key: salePriceConfigKey, Value: defaultSalePriceConfigValue(), Description: "内部出售定价规则 JSON"},
|
||||
{Key: DistributionSettlementHoldDaysKey, Value: defaultDistributionSettlementHoldDaysValue(), Description: "新建分销站点默认结算冷却天数(0=立即释放,最大 365)"},
|
||||
@@ -84,6 +86,7 @@ var adminVisibleConfigKeys = []string{
|
||||
"mobile.home_banners",
|
||||
"mobile.aw_recycle",
|
||||
"mobile.cooperation_feedback",
|
||||
"mobile.home_entries_enabled",
|
||||
"order.agreements",
|
||||
"order.pending_payment_timeout_minutes",
|
||||
"order.return_overdue_grace_minutes",
|
||||
@@ -288,30 +291,14 @@ func updateLegacyPublishOptions(tx *gorm.DB, item defaultConfig) error {
|
||||
if err := tx.Where("`key` = ?", item.Key).First(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if isLegacyPublishOptionsValue(row.Value) {
|
||||
row.Value = item.Value
|
||||
row.Description = item.Description
|
||||
return tx.Save(&row).Error
|
||||
}
|
||||
var options PublishOptionsDTO
|
||||
if err := json.Unmarshal([]byte(row.Value), &options); err != nil {
|
||||
row.Value = item.Value
|
||||
row.Description = item.Description
|
||||
return tx.Save(&row).Error
|
||||
}
|
||||
before := row.Value
|
||||
isOldPublishOptionsSchema := !strings.Contains(before, `"ban_record_options"`)
|
||||
normalizePublishOptions(&options)
|
||||
if isOldPublishOptionsSchema {
|
||||
applyConditionalBanEvidenceDefault(&options)
|
||||
}
|
||||
raw, err := json.Marshal(options)
|
||||
normalized, err := normalizePublishOptionsValue(row.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
normalized = item.Value
|
||||
}
|
||||
row.Value = string(raw)
|
||||
changed := row.Value != normalized || row.Description != item.Description
|
||||
row.Value = normalized
|
||||
row.Description = item.Description
|
||||
if row.Value == before {
|
||||
if !changed {
|
||||
return nil
|
||||
}
|
||||
return tx.Save(&row).Error
|
||||
@@ -333,41 +320,164 @@ func applyConditionalBanEvidenceDefault(options *PublishOptionsDTO) {
|
||||
}
|
||||
}
|
||||
|
||||
func isLegacyPublishOptionsValue(value string) bool {
|
||||
legacyMarkers := []string{
|
||||
`"kit5"`,
|
||||
"5级全装套",
|
||||
"6级子弹",
|
||||
"6头数量",
|
||||
"6甲数量",
|
||||
"0.26元/发",
|
||||
"0.8元/发",
|
||||
"çº",
|
||||
"å¼",
|
||||
"å…",
|
||||
"å¤",
|
||||
"ä½",
|
||||
"é«",
|
||||
"ï¼",
|
||||
func normalizePublishOptionsValue(value string) (string, error) {
|
||||
value = repairMojibakeJSON(value)
|
||||
var options PublishOptionsDTO
|
||||
if err := json.Unmarshal([]byte(value), &options); err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, marker := range legacyMarkers {
|
||||
if strings.Contains(value, marker) {
|
||||
var fields map[string]json.RawMessage
|
||||
if err := json.Unmarshal([]byte(value), &fields); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if options.SchemaVersion < publishOptionsSchemaVersion {
|
||||
if hasLegacyPublishQuantityItem(options.QuantityItems) {
|
||||
options = DefaultPublishOptions()
|
||||
} else {
|
||||
normalizePublishOptions(&options)
|
||||
if _, ok := fields["ban_record_options"]; !ok {
|
||||
applyConditionalBanEvidenceDefault(&options)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
normalizePublishOptions(&options)
|
||||
}
|
||||
options.SchemaVersion = publishOptionsSchemaVersion
|
||||
raw, err := json.Marshal(options)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(raw), nil
|
||||
}
|
||||
|
||||
func hasLegacyPublishQuantityItem(items []PublishQuantityItem) bool {
|
||||
for _, item := range items {
|
||||
if item.Key == "kit5" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isMojibakeText(value string) bool {
|
||||
mojibakeMarkers := []string{"å", "æ", "ç", "è", "é", "ä", "ï¼", "ã€", "â"}
|
||||
for _, marker := range mojibakeMarkers {
|
||||
if strings.Contains(value, marker) {
|
||||
// repairMojibakeJSON 先按整段 JSON 还原;整段不是纯乱码时,再逐个字符串字段尝试还原。
|
||||
func repairMojibakeJSON(value string) string {
|
||||
if decoded, ok := decodeCP1252Mojibake(value); ok {
|
||||
return decoded
|
||||
}
|
||||
var root any
|
||||
if err := json.Unmarshal([]byte(value), &root); err != nil {
|
||||
return value
|
||||
}
|
||||
repaired, changed := repairMojibakeValue(root)
|
||||
if !changed {
|
||||
return value
|
||||
}
|
||||
raw, err := json.Marshal(repaired)
|
||||
if err != nil {
|
||||
return value
|
||||
}
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
func repairMojibakeValue(value any) (any, bool) {
|
||||
switch current := value.(type) {
|
||||
case string:
|
||||
if decoded, ok := decodeCP1252Mojibake(current); ok {
|
||||
return decoded, true
|
||||
}
|
||||
return current, false
|
||||
case []any:
|
||||
changed := false
|
||||
for index := range current {
|
||||
next, updated := repairMojibakeValue(current[index])
|
||||
if updated {
|
||||
current[index] = next
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return current, changed
|
||||
case map[string]any:
|
||||
changed := false
|
||||
for key, item := range current {
|
||||
next, updated := repairMojibakeValue(item)
|
||||
if updated {
|
||||
current[key] = next
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return current, changed
|
||||
default:
|
||||
return value, false
|
||||
}
|
||||
}
|
||||
|
||||
// decodeCP1252Mojibake 把「UTF-8 字节被当作 CP1252/Latin-1 字符」的文本还原为原始 UTF-8。
|
||||
// 普通中文包含 CP1252 无法表示的字符,会直接判定失败;纯 ASCII 也不会误判。
|
||||
func decodeCP1252Mojibake(value string) (string, bool) {
|
||||
if value == "" {
|
||||
return "", false
|
||||
}
|
||||
raw := make([]byte, 0, len(value))
|
||||
sawNonASCII := false
|
||||
for _, char := range value {
|
||||
next, ok := cp1252Byte(char)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
if next >= 0x80 {
|
||||
sawNonASCII = true
|
||||
}
|
||||
raw = append(raw, next)
|
||||
}
|
||||
if !sawNonASCII || !utf8.Valid(raw) {
|
||||
return "", false
|
||||
}
|
||||
decoded := string(raw)
|
||||
if decoded == value {
|
||||
return "", false
|
||||
}
|
||||
// 只接受能还原出汉字的候选,避免把正常的西文重音字符误判为乱码。
|
||||
if !containsHan(decoded) {
|
||||
return "", false
|
||||
}
|
||||
return decoded, true
|
||||
}
|
||||
|
||||
func containsHan(value string) bool {
|
||||
for _, char := range value {
|
||||
if char >= 0x4E00 && char <= 0x9FFF {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// cp1252HighRunes 是 CP1252 中 0x80-0x9F 对应的 Unicode 字符;
|
||||
// 其余可映射字符与 Latin-1 一致。
|
||||
var cp1252HighRunes = [32]rune{
|
||||
0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021,
|
||||
0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008D, 0x017D, 0x008F,
|
||||
0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014,
|
||||
0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x009D, 0x017E, 0x0178,
|
||||
}
|
||||
|
||||
func cp1252Byte(char rune) (byte, bool) {
|
||||
if char <= 0xFF {
|
||||
return byte(char), true
|
||||
}
|
||||
for offset, candidate := range cp1252HighRunes {
|
||||
if char == candidate {
|
||||
return byte(0x80 + offset), true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func isMojibakeText(value string) bool {
|
||||
_, ok := decodeCP1252Mojibake(value)
|
||||
return ok
|
||||
}
|
||||
|
||||
func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizID uint64, meta AuditMeta, detail map[string]any) error {
|
||||
return auditlog.Append(tx, auditlog.Entry{
|
||||
ActorType: "admin",
|
||||
|
||||
Reference in New Issue
Block a user