增加渠道信息

This commit is contained in:
yml2213
2026-07-25 14:59:56 +08:00
parent edbe8ca227
commit 660c56db44
8 changed files with 292 additions and 29 deletions
+21 -14
View File
@@ -37,18 +37,20 @@ type App struct {
} }
type DefaultSettings struct { type DefaultSettings struct {
Server string `json:"server"` Server string `json:"server"`
Uploader string `json:"uploader"` Uploader string `json:"uploader"`
Secret string `json:"secret"` Secret string `json:"secret"`
Timeout string `json:"timeout"` Timeout string `json:"timeout"`
SourceChannel string `json:"sourceChannel"`
} }
type UploadForm struct { type UploadForm struct {
Server string `json:"server"` Server string `json:"server"`
Uploader string `json:"uploader"` Uploader string `json:"uploader"`
Secret string `json:"secret"` Secret string `json:"secret"`
Timeout string `json:"timeout"` Timeout string `json:"timeout"`
Data string `json:"data"` SourceChannel string `json:"sourceChannel"`
Data string `json:"data"`
} }
func NewApp() *App { func NewApp() *App {
@@ -61,10 +63,11 @@ func (a *App) startup(ctx context.Context) {
func (a *App) DefaultSettings() DefaultSettings { func (a *App) DefaultSettings() DefaultSettings {
return DefaultSettings{ return DefaultSettings{
Server: envDefault("HFB_SERVER", "http://127.0.0.1:8080"), Server: envDefault("HFB_SERVER", "http://127.0.0.1:8080"),
Uploader: strings.TrimSpace(os.Getenv("HFB_UPLOADER_NAME")), Uploader: strings.TrimSpace(os.Getenv("HFB_UPLOADER_NAME")),
Secret: strings.TrimSpace(os.Getenv("HFB_UPLOAD_SECRET")), Secret: strings.TrimSpace(os.Getenv("HFB_UPLOAD_SECRET")),
Timeout: "15s", Timeout: "15s",
SourceChannel: strings.TrimSpace(os.Getenv("HFB_SOURCE_CHANNEL")),
} }
} }
@@ -125,7 +128,11 @@ func buildUploadRequest(form UploadForm) (hfb.ExternalUploadRequest, error) {
if uploaderName == "" { if uploaderName == "" {
return hfb.ExternalUploadRequest{}, fmt.Errorf("缺少上传人名称:请展开“连接配置”填写上传人,或在文本中增加“上传人:姓名”") return hfb.ExternalUploadRequest{}, fmt.Errorf("缺少上传人名称:请展开“连接配置”填写上传人,或在文本中增加“上传人:姓名”")
} }
return hfb.NewExternalUploadRequest(uploaderName, parsed.Account, time.Now()), nil sourceChannel := strings.TrimSpace(form.SourceChannel)
if sourceChannel == "" {
return hfb.ExternalUploadRequest{}, fmt.Errorf("请选择订单来源渠道")
}
return hfb.NewExternalUploadRequest(uploaderName, sourceChannel, parsed.Account, time.Now()), nil
} }
func parseTimeout(raw string) (time.Duration, error) { func parseTimeout(raw string) (time.Duration, error) {
+77 -1
View File
@@ -2,6 +2,8 @@ import './style.css';
import { ClipboardGetText } from '../wailsjs/runtime/runtime.js'; import { ClipboardGetText } from '../wailsjs/runtime/runtime.js';
const storageKey = 'hfb-upload-settings'; const storageKey = 'hfb-upload-settings';
const customChannelValue = '__custom__';
const sourceChannelOptions = ['咸鱼', '淘宝', '京东', 'QQ', '微信'];
const app = document.querySelector('#app'); const app = document.querySelector('#app');
@@ -37,6 +39,26 @@ app.innerHTML = `
</div> </div>
</details> </details>
<section class="channel-panel" aria-labelledby="sourceChannelTitle">
<span class="channel-title" id="sourceChannelTitle">来源渠道</span>
<div class="channel-options">
${sourceChannelOptions
.map(
(item) => `
<label class="channel-choice">
<input type="radio" name="sourceChannelOption" value="${item}" />
<span>${item}</span>
</label>`
)
.join('')}
<label class="channel-choice">
<input type="radio" name="sourceChannelOption" value="${customChannelValue}" />
<span>自定义</span>
</label>
<input id="customChannel" class="custom-channel" autocomplete="off" placeholder="输入自定义渠道" hidden />
</div>
</section>
<section class="actions"> <section class="actions">
<label class="button secondary"> <label class="button secondary">
打开文件 打开文件
@@ -65,6 +87,8 @@ const elements = {
status: document.querySelector('#status'), status: document.querySelector('#status'),
server: document.querySelector('#server'), server: document.querySelector('#server'),
uploader: document.querySelector('#uploader'), uploader: document.querySelector('#uploader'),
channelOptions: [...document.querySelectorAll('input[name="sourceChannelOption"]')],
customChannel: document.querySelector('#customChannel'),
secret: document.querySelector('#secret'), secret: document.querySelector('#secret'),
timeout: document.querySelector('#timeout'), timeout: document.querySelector('#timeout'),
data: document.querySelector('#data'), data: document.querySelector('#data'),
@@ -100,6 +124,7 @@ function currentSettings() {
return { return {
server: elements.server.value.trim(), server: elements.server.value.trim(),
uploader: elements.uploader.value.trim(), uploader: elements.uploader.value.trim(),
sourceChannel: selectedSourceChannel(),
secret: elements.secret.value.trim(), secret: elements.secret.value.trim(),
timeout: elements.timeout.value.trim() || '15s', timeout: elements.timeout.value.trim() || '15s',
}; };
@@ -116,6 +141,47 @@ function collectForm() {
}; };
} }
function selectedSourceChannel() {
const selected = elements.channelOptions.find((input) => input.checked);
if (!selected) {
return '';
}
if (selected.value === customChannelValue) {
return elements.customChannel.value.trim();
}
return selected.value.trim();
}
function applySourceChannelSetting(value) {
const sourceChannel = String(value || '').trim();
if (sourceChannelOptions.includes(sourceChannel)) {
setSelectedChannelOption(sourceChannel);
elements.customChannel.value = '';
} else if (sourceChannel === '') {
setSelectedChannelOption('');
elements.customChannel.value = '';
} else {
setSelectedChannelOption(customChannelValue);
elements.customChannel.value = sourceChannel;
}
syncCustomChannel();
}
function setSelectedChannelOption(value) {
elements.channelOptions.forEach((input) => {
input.checked = input.value === value;
});
}
function syncCustomChannel() {
const selected = elements.channelOptions.find((input) => input.checked);
const isCustom = selected?.value === customChannelValue;
elements.customChannel.hidden = !isCustom;
if (!isCustom) {
elements.customChannel.value = '';
}
}
async function readClipboardText() { async function readClipboardText() {
try { try {
const text = await ClipboardGetText(); const text = await ClipboardGetText();
@@ -142,13 +208,23 @@ async function init() {
const settings = { ...defaults, ...saved }; const settings = { ...defaults, ...saved };
elements.server.value = settings.server || 'http://127.0.0.1:8080'; elements.server.value = settings.server || 'http://127.0.0.1:8080';
elements.uploader.value = settings.uploader || ''; elements.uploader.value = settings.uploader || '';
applySourceChannelSetting(settings.sourceChannel || '');
elements.secret.value = settings.secret || ''; elements.secret.value = settings.secret || '';
elements.timeout.value = settings.timeout || '15s'; elements.timeout.value = settings.timeout || '15s';
elements.data.placeholder = await api.SampleText(); elements.data.placeholder = await api.SampleText();
[elements.server, elements.uploader, elements.secret, elements.timeout].forEach((input) => { [elements.server, elements.uploader, elements.secret, elements.timeout, elements.customChannel].forEach((input) => {
input.addEventListener('input', saveSettings); input.addEventListener('input', saveSettings);
}); });
elements.channelOptions.forEach((input) => {
input.addEventListener('change', () => {
syncCustomChannel();
saveSettings();
if (input.value === customChannelValue) {
elements.customChannel.focus();
}
});
});
} }
elements.fileInput.addEventListener('change', async (event) => { elements.fileInput.addEventListener('change', async (event) => {
+97 -1
View File
@@ -33,7 +33,7 @@ textarea {
margin: 0 auto; margin: 0 auto;
padding: 22px 0; padding: 22px 0;
display: grid; display: grid;
grid-template-rows: auto auto auto minmax(0, 1fr); grid-template-rows: auto auto auto auto minmax(0, 1fr);
gap: 14px; gap: 14px;
overflow: hidden; overflow: hidden;
} }
@@ -131,6 +131,78 @@ textarea:focus {
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12); box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
} }
.channel-panel {
display: flex;
align-items: center;
gap: 14px;
min-width: 0;
}
.channel-title {
flex: 0 0 auto;
font-weight: 700;
color: #374151;
}
.channel-options {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
min-width: 0;
}
.channel-choice {
position: relative;
display: inline-flex;
}
.channel-choice input {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.channel-choice span {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 76px;
height: 42px;
margin: 0;
padding: 0 18px;
border: 1px solid #d7dce5;
border-radius: 8px;
background: #ffffff;
color: #374151;
font-weight: 700;
cursor: pointer;
user-select: none;
}
.channel-choice input:checked + span {
border-color: #2563eb;
background: #2563eb;
color: #ffffff;
box-shadow: 0 8px 16px rgba(37, 99, 235, 0.18);
}
.channel-choice input:focus-visible + span {
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.16);
}
.custom-channel {
width: min(220px, 100%);
}
.custom-channel[hidden] {
display: none;
}
.actions { .actions {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -242,6 +314,30 @@ textarea:focus {
margin-top: 12px; margin-top: 12px;
} }
.channel-panel {
align-items: flex-start;
flex-direction: column;
gap: 8px;
}
.channel-options {
width: 100%;
}
.channel-choice {
flex: 1 1 calc(33.333% - 8px);
}
.channel-choice span {
width: 100%;
min-width: 0;
padding: 0 10px;
}
.custom-channel {
flex: 1 1 100%;
}
.workspace { .workspace {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
+4
View File
@@ -5,6 +5,7 @@ export namespace main {
uploader: string; uploader: string;
secret: string; secret: string;
timeout: string; timeout: string;
sourceChannel: string;
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
return new DefaultSettings(source); return new DefaultSettings(source);
@@ -16,6 +17,7 @@ export namespace main {
this.uploader = source["uploader"]; this.uploader = source["uploader"];
this.secret = source["secret"]; this.secret = source["secret"];
this.timeout = source["timeout"]; this.timeout = source["timeout"];
this.sourceChannel = source["sourceChannel"];
} }
} }
export class UploadForm { export class UploadForm {
@@ -23,6 +25,7 @@ export namespace main {
uploader: string; uploader: string;
secret: string; secret: string;
timeout: string; timeout: string;
sourceChannel: string;
data: string; data: string;
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
@@ -35,6 +38,7 @@ export namespace main {
this.uploader = source["uploader"]; this.uploader = source["uploader"];
this.secret = source["secret"]; this.secret = source["secret"];
this.timeout = source["timeout"]; this.timeout = source["timeout"];
this.sourceChannel = source["sourceChannel"];
this.data = source["data"]; this.data = source["data"];
} }
} }
+54 -4
View File
@@ -7,7 +7,10 @@ import (
"strings" "strings"
) )
var numberPattern = regexp.MustCompile(`[-+]?\d+(?:\.\d+)?`) 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) { func ParseUploadText(text string) (ParsedUpload, error) {
values := make(map[string]string) values := make(map[string]string)
@@ -55,7 +58,7 @@ func ParseUploadText(text string) (ParsedUpload, error) {
account.BanRecord = normalizeBanFlag(banRaw) account.BanRecord = normalizeBanFlag(banRaw)
account.CommonRegion = firstValue(values, "常用地区", "常用区域") account.CommonRegion = firstValue(values, "常用地区", "常用区域")
account.ContactPhone = firstValue(values, "联系电话", "手机号", "电话") account.ContactPhone = firstValue(values, "联系电话", "手机号", "电话")
account.OwnerOnlineTime = firstValue(values, "号主在线时间", "在线时间") account.OwnerOnlineTime = normalizeOwnerOnlineTime(firstValue(values, "号主在线时间", "在线时间"))
account.Remark = composeRemark(firstValue(values, "备注", "说明"), banRaw) account.Remark = composeRemark(firstValue(values, "备注", "说明"), banRaw)
account.Currency.HafuCoin, err = parseRequiredFloat(values, "哈夫币纯币", "哈夫币") account.Currency.HafuCoin, err = parseRequiredFloat(values, "哈夫币纯币", "哈夫币")
@@ -107,10 +110,14 @@ func ParseUploadText(text string) (ParsedUpload, error) {
} }
func splitKeyValue(line string) (string, string, bool) { func splitKeyValue(line string) (string, string, bool) {
if idx := strings.Index(line, ""); idx >= 0 { fullWidthIdx := strings.Index(line, "")
asciiIdx := strings.Index(line, ":")
if fullWidthIdx >= 0 && (asciiIdx < 0 || fullWidthIdx < asciiIdx) {
idx := fullWidthIdx
return line[:idx], line[idx+len(""):], true return line[:idx], line[idx+len(""):], true
} }
if idx := strings.Index(line, ":"); idx >= 0 { if asciiIdx >= 0 {
idx := asciiIdx
return line[:idx], line[idx+1:], true return line[:idx], line[idx+1:], true
} }
return "", "", false return "", "", false
@@ -246,6 +253,49 @@ func parseFloatText(value string, preferLast bool) (float64, error) {
return strconv.ParseFloat(pick, 64) 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 { func parseSkins(value string) []string {
value = strings.TrimSpace(value) value = strings.TrimSpace(value)
if value == "" || value == "无" || strings.EqualFold(value, "none") { if value == "" || value == "无" || strings.EqualFold(value, "none") {
+29 -1
View File
@@ -1,6 +1,9 @@
package hfb package hfb
import "testing" import (
"strings"
"testing"
)
const sampleText = `上号方式:QQ扫码 const sampleText = `上号方式:QQ扫码
哈夫币纯币:156 哈夫币纯币:156
@@ -60,6 +63,31 @@ func TestParseUploadTextASCIIDelimiterKeepsRatioValue(t *testing.T) {
} }
} }
func TestParseUploadTextNormalizesOwnerOnlineTimeFormats(t *testing.T) {
tests := []struct {
name string
line string
want string
}{
{name: "至", line: "号主在线时间:08:00 至 23:00", want: "08:00 至 23:00"},
{name: "短横线和全角冒号", line: "号主在线时间:8:00-100", want: "08:00 至 01:00"},
{name: "英文字段冒号", line: "号主在线时间:8:00-100", want: "08:00 至 01:00"},
{name: "点和到", line: "在线时间:8点到1点", want: "08:00 至 01:00"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
text := strings.Replace(sampleText, "号主在线时间:11:00 至 00:00", tt.line, 1)
parsed, err := ParseUploadText(text)
if err != nil {
t.Fatalf("ParseUploadText() error = %v", err)
}
if parsed.Account.OwnerOnlineTime != tt.want {
t.Fatalf("OwnerOnlineTime = %q, want %q", parsed.Account.OwnerOnlineTime, tt.want)
}
})
}
}
func TestParseUploadTextMissingRequiredField(t *testing.T) { func TestParseUploadTextMissingRequiredField(t *testing.T) {
_, err := ParseUploadText("上号方式:QQ扫码") _, err := ParseUploadText("上号方式:QQ扫码")
if err == nil { if err == nil {
+9 -7
View File
@@ -11,9 +11,10 @@ type ParsedUpload struct {
} }
type ExternalUploadRequest struct { type ExternalUploadRequest struct {
UploadTime int64 `json:"uploadTime"` UploadTime int64 `json:"uploadTime"`
UploaderName string `json:"uploaderName"` UploaderName string `json:"uploaderName"`
Data ExternalAccountData `json:"data"` SourceChannel string `json:"sourceChannel"`
Data ExternalAccountData `json:"data"`
} }
type ExternalAccountData struct { type ExternalAccountData struct {
@@ -52,11 +53,12 @@ type ExternalUploadInventory struct {
Skins []string `json:"skins"` Skins []string `json:"skins"`
} }
func NewExternalUploadRequest(uploaderName string, account ExternalAccountData, now time.Time) ExternalUploadRequest { func NewExternalUploadRequest(uploaderName string, sourceChannel string, account ExternalAccountData, now time.Time) ExternalUploadRequest {
return ExternalUploadRequest{ return ExternalUploadRequest{
UploadTime: now.Unix(), UploadTime: now.Unix(),
UploaderName: uploaderName, UploaderName: uploaderName,
Data: account, SourceChannel: sourceChannel,
Data: account,
} }
} }
+1 -1
View File
@@ -36,7 +36,7 @@ func TestUploadSendsSignedRequest(t *testing.T) {
defer server.Close() defer server.Close()
client := Client{Server: server.URL, Secret: secret, Timeout: time.Second} client := Client{Server: server.URL, Secret: secret, Timeout: time.Second}
req := NewExternalUploadRequest("张三", mustSampleAccount(t), time.Unix(100, 0)) req := NewExternalUploadRequest("张三", "淘宝", mustSampleAccount(t), time.Unix(100, 0))
resp, err := client.Upload(context.Background(), req) resp, err := client.Upload(context.Background(), req)
if err != nil { if err != nil {
t.Fatalf("Upload() error = %v", err) t.Fatalf("Upload() error = %v", err)