diff --git a/backend/internal/jobs/ordertimeout/job.go b/backend/internal/jobs/ordertimeout/job.go index 7951200..6f633f0 100644 --- a/backend/internal/jobs/ordertimeout/job.go +++ b/backend/internal/jobs/ordertimeout/job.go @@ -110,7 +110,7 @@ func (j *Job) loadThresholds(ctx context.Context) (thresholds, error) { } for _, row := range rows { value, err := strconv.Atoi(row.Value) - if err != nil || value <= 0 { + if err != nil || value < 0 { continue } switch row.Key { @@ -130,6 +130,9 @@ func (j *Job) loadThresholds(ctx context.Context) (thresholds, error) { } func (j *Job) handlePendingPaymentTimeout(ctx context.Context, now time.Time, cfg thresholds) (int, error) { + if cfg.PendingPaymentTimeoutMinutes <= 0 { + return 0, nil + } var rows []model.RentalOrder err := j.db.WithContext(ctx). Where("status = ? AND created_at <= ?", "pending_payment", now.Add(-time.Duration(cfg.PendingPaymentTimeoutMinutes)*time.Minute)). @@ -189,6 +192,9 @@ func (j *Job) handlePendingPaymentTimeout(ctx context.Context, now time.Time, cf } func (j *Job) handleOwnerSubmitTimeout(ctx context.Context, now time.Time, cfg thresholds) (int, error) { + if cfg.OwnerSubmitTimeoutMinutes <= 0 { + return 0, nil + } var rows []model.RentalOrder err := j.db.WithContext(ctx). Where("status = ? AND handoff_status = ? AND created_at <= ?", "pending_handoff", "pending_owner", now.Add(-time.Duration(cfg.OwnerSubmitTimeoutMinutes)*time.Minute)). @@ -237,6 +243,9 @@ func (j *Job) handleOwnerSubmitTimeout(ctx context.Context, now time.Time, cfg t } func (j *Job) handleRenterConfirmTimeout(ctx context.Context, now time.Time, cfg thresholds) (int, error) { + if cfg.RenterConfirmTimeoutMinutes <= 0 { + return 0, nil + } var rows []model.RentalOrder err := j.db.WithContext(ctx). Where("status = ? AND handoff_status = ?", "pending_handoff", "pending_renter_confirm"). @@ -345,6 +354,9 @@ func (j *Job) handleReturnOverdue(ctx context.Context, now time.Time, cfg thresh } func (j *Job) handleOwnerReturnConfirmTimeout(ctx context.Context, now time.Time, cfg thresholds) (int, error) { + if cfg.OwnerReturnConfirmTimeoutMinutes <= 0 { + return 0, nil + } var rows []model.RentalOrder err := j.db.WithContext(ctx). Where("status = ? AND handoff_status = ? AND updated_at <= ?", "pending_checkout_confirm", "pending_owner_checkout", now.Add(-time.Duration(cfg.OwnerReturnConfirmTimeoutMinutes)*time.Minute)). diff --git a/backend/internal/modules/order/repository.go b/backend/internal/modules/order/repository.go index 0371e5c..34d4d14 100644 --- a/backend/internal/modules/order/repository.go +++ b/backend/internal/modules/order/repository.go @@ -20,7 +20,7 @@ import ( ) type Repository struct { - db *gorm.DB + db *gorm.DB chatRepo *chat.Repository } @@ -195,7 +195,7 @@ func (r *Repository) Pay(userID uint64, orderID uint64) error { return ErrOrderCannotPay } timeoutMinutes := pendingPaymentTimeoutMinutes(tx) - if order.CreatedAt.Before(time.Now().Add(-time.Duration(timeoutMinutes) * time.Minute)) { + if timeoutMinutes > 0 && order.CreatedAt.Before(time.Now().Add(-time.Duration(timeoutMinutes)*time.Minute)) { return ErrOrderCannotPay } @@ -1029,7 +1029,7 @@ func pendingPaymentTimeoutMinutes(tx *gorm.DB) int { return defaultPendingPaymentTimeoutMinutes } value, err := strconv.Atoi(row.Value) - if err != nil || value <= 0 { + if err != nil || value < 0 { return defaultPendingPaymentTimeoutMinutes } return value diff --git a/backend/internal/modules/systemconfig/repository.go b/backend/internal/modules/systemconfig/repository.go index 22d8444..95d6526 100644 --- a/backend/internal/modules/systemconfig/repository.go +++ b/backend/internal/modules/systemconfig/repository.go @@ -33,21 +33,30 @@ var defaultConfigs = []defaultConfig{ {Key: "handoff.owner_return_confirm_timeout_minutes", Value: "120", Description: "号主待确认结账超时分钟数"}, {Key: "order.pending_payment_timeout_minutes", Value: "15", Description: "订单待支付超时取消分钟数"}, {Key: "order.return_overdue_grace_minutes", Value: "10", Description: "预计截止后结账宽限分钟数"}, - {Key: "deposit.min_amount", Value: "50", Description: "发布租号最低押金"}, {Key: "listing.review_required", Value: "false", Description: "发布账号是否需要后台人工审核"}, - {Key: "risk.sms_limit_per_phone_hour", Value: "5", Description: "单手机号每小时短信验证码次数"}, - {Key: "risk.sms_limit_per_ip_hour", Value: "20", Description: "单 IP 每小时短信验证码次数"}, - {Key: "realname.required_for_order", Value: "false", Description: "下单是否必须完成实名认证"}, - {Key: "settlement.platform_fee_rate", Value: "0.00", Description: "平台抽成比例"}, - {Key: "settlement.owner_cycle_days", Value: "0", Description: "号主结算周期天数"}, - {Key: "withdraw.min_amount", Value: "100", Description: "提现最低金额预留"}, {Key: "chat.default_support_admin_id", Value: "1", Description: "订单群聊回退客服 ID(仅当无可用客服时使用)"}, + {Key: "chat.auto_welcome_message", Value: "欢迎加入订单群聊!如有任何问题,请随时沟通。", Description: "建群后自动发送的欢迎话术"}, {Key: homeAnnouncementsConfigKey, Value: defaultHomeAnnouncementsConfigValue(), Description: "移动端首页公告 JSON 数组"}, {Key: homeBannersConfigKey, Value: defaultHomeBannersConfigValue(), Description: "移动端首页轮播图 JSON 数组"}, {Key: publishOptionsConfigKey, Value: defaultPublishOptionsConfigValue(), Description: "发布页选项配置 JSON"}, {Key: salePriceConfigKey, Value: defaultSalePriceConfigValue(), Description: "内部出售定价规则 JSON"}, } +var adminVisibleConfigKeys = []string{ + "chat.auto_welcome_message", + "chat.default_support_admin_id", + "handoff.owner_return_confirm_timeout_minutes", + "handoff.owner_submit_timeout_minutes", + "handoff.renter_confirm_timeout_minutes", + "listing.publish_options", + "listing.review_required", + "listing.sale_price_config", + "mobile.home_announcements", + "mobile.home_banners", + "order.pending_payment_timeout_minutes", + "order.return_overdue_grace_minutes", +} + func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } @@ -57,7 +66,7 @@ func (r *Repository) List() ([]ConfigDTO, error) { return nil, err } var rows []model.SystemConfig - if err := r.db.Order("`key` ASC").Find(&rows).Error; err != nil { + if err := r.db.Where("`key` IN ?", adminVisibleConfigKeys).Order("`key` ASC").Find(&rows).Error; err != nil { return nil, err } items := make([]ConfigDTO, 0, len(rows)) diff --git a/backend/migrations/000002_seed.sql b/backend/migrations/000002_seed.sql index e133a5c..475b485 100644 --- a/backend/migrations/000002_seed.sql +++ b/backend/migrations/000002_seed.sql @@ -93,12 +93,6 @@ WHERE au.username IN ('kf1', 'kf2') AND r.code = 'cs'; -- ------------------------------------------- INSERT INTO system_configs (`key`, `value`, description) VALUES -('order.handoff_timeout_minutes', '60', '交接超时时间(分钟)'), -('order.return_timeout_minutes', '1440', '归还超时时间(分钟)'), -('sms.rate_limit_per_minute', '1', '短信限流:每分钟最多发送次数'), -('sms.rate_limit_per_hour', '5', '短信限流:每小时最多发送次数'), -('wallet.min_deposit', '50', '最低押金金额'), -('platform.fee_ratio', '0.1', '平台抽成比例'), ('listing.review_required', 'true', '商品是否需要审核'), ('order.pending_payment_timeout_minutes', '15', '待支付超时时间(分钟)'), ('chat.default_support_admin_id', '1', '订单群聊回退客服 ID(仅当无可用客服时使用)'), diff --git a/backend/migrations/000003_cleanup_unused_system_configs.sql b/backend/migrations/000003_cleanup_unused_system_configs.sql new file mode 100644 index 0000000..9b93ab0 --- /dev/null +++ b/backend/migrations/000003_cleanup_unused_system_configs.sql @@ -0,0 +1,17 @@ +-- 清理未接入业务逻辑的历史/预留系统配置,避免后台配置列表产生误解。 +DELETE FROM system_configs +WHERE `key` IN ( + 'deposit.min_amount', + 'platform.fee_ratio', + 'realname.required_for_order', + 'risk.sms_limit_per_ip_hour', + 'risk.sms_limit_per_phone_hour', + 'settlement.owner_cycle_days', + 'settlement.platform_fee_rate', + 'sms.rate_limit_per_hour', + 'sms.rate_limit_per_minute', + 'wallet.min_deposit', + 'withdraw.min_amount', + 'order.handoff_timeout_minutes', + 'order.return_timeout_minutes' +); diff --git a/frontend/src/utils/systemConfigOptions.ts b/frontend/src/utils/systemConfigOptions.ts new file mode 100644 index 0000000..dad46fb --- /dev/null +++ b/frontend/src/utils/systemConfigOptions.ts @@ -0,0 +1,65 @@ +export interface SystemConfigOption { + label: string + value: string +} + +const shortTimeoutOptions: SystemConfigOption[] = [ + { label: '不限时', value: '0' }, + { label: '5 分钟', value: '5' }, + { label: '10 分钟', value: '10' }, + { label: '15 分钟', value: '15' }, + { label: '30 分钟', value: '30' }, + { label: '45 分钟', value: '45' }, + { label: '60 分钟', value: '60' }, + { label: '90 分钟', value: '90' }, + { label: '120 分钟', value: '120' }, +] + +const longTimeoutOptions: SystemConfigOption[] = [ + { label: '不限时', value: '0' }, + { label: '30 分钟', value: '30' }, + { label: '60 分钟', value: '60' }, + { label: '120 分钟', value: '120' }, + { label: '180 分钟', value: '180' }, + { label: '240 分钟', value: '240' }, + { label: '360 分钟', value: '360' }, + { label: '12 小时', value: '720' }, + { label: '24 小时', value: '1440' }, + { label: '48 小时', value: '2880' }, +] + +const booleanOptions: SystemConfigOption[] = [ + { label: '开启', value: 'true' }, + { label: '关闭', value: 'false' }, +] + +export const systemConfigSelectOptions: Record = { + 'handoff.owner_submit_timeout_minutes': shortTimeoutOptions, + 'handoff.renter_confirm_timeout_minutes': shortTimeoutOptions, + 'handoff.owner_return_confirm_timeout_minutes': longTimeoutOptions, + 'order.pending_payment_timeout_minutes': shortTimeoutOptions, + 'order.return_overdue_grace_minutes': [ + { label: '无宽限', value: '0' }, + { label: '5 分钟', value: '5' }, + { label: '10 分钟', value: '10' }, + { label: '15 分钟', value: '15' }, + { label: '30 分钟', value: '30' }, + { label: '60 分钟', value: '60' }, + { label: '120 分钟', value: '120' }, + ], + 'listing.review_required': booleanOptions, + 'chat.default_support_admin_id': [ + { label: '管理员 ID 1', value: '1' }, + { label: '管理员 ID 2', value: '2' }, + { label: '管理员 ID 3', value: '3' }, + ], +} + +export function getSystemConfigSelectOptions(key: string) { + return systemConfigSelectOptions[key] || null +} + +export function formatSystemConfigSelectValue(key: string, value: string) { + const option = getSystemConfigSelectOptions(key)?.find((item) => item.value === value) + return option?.label || null +} diff --git a/frontend/src/views/admin/AdminSystemConfigsView.vue b/frontend/src/views/admin/AdminSystemConfigsView.vue index 9672952..7936428 100644 --- a/frontend/src/views/admin/AdminSystemConfigsView.vue +++ b/frontend/src/views/admin/AdminSystemConfigsView.vue @@ -18,6 +18,7 @@ import { import { fetchSystemConfigs, type SystemConfig } from '@/api/systemConfigs' import { formatDateTime } from '@/utils/time' import { safeParseJSON } from '@/utils/json' +import { formatSystemConfigSelectValue } from '@/utils/systemConfigOptions' // 导入重构后的 Dialog 子组件 import PublishOptionsDialog from './components/PublishOptionsDialog.vue' @@ -154,6 +155,10 @@ function formatConfigValue(row: SystemConfig) { if (trimmed.startsWith('{') || trimmed.startsWith('[')) { return 'JSON 配置' } + const selectedLabel = formatSystemConfigSelectValue(row.key, row.value) + if (selectedLabel) { + return selectedLabel + } return row.value } diff --git a/frontend/src/views/admin/components/GeneralConfigDialog.vue b/frontend/src/views/admin/components/GeneralConfigDialog.vue index 11b20d3..d3fc64c 100644 --- a/frontend/src/views/admin/components/GeneralConfigDialog.vue +++ b/frontend/src/views/admin/components/GeneralConfigDialog.vue @@ -3,6 +3,7 @@ import { ElMessage } from 'element-plus' import { computed, ref, watch } from 'vue' import { updateSystemConfig, type SystemConfig } from '@/api/systemConfigs' +import { getSystemConfigSelectOptions, type SystemConfigOption } from '@/utils/systemConfigOptions' const props = defineProps<{ modelValue: boolean @@ -34,6 +35,15 @@ const isStructuredConfig = computed(() => { return trimmed.startsWith('{') || trimmed.startsWith('[') }) +const selectOptions = computed(() => { + const options = getSystemConfigSelectOptions(props.config.key) + if (!options) return null + if (!value.value || options.some((item) => item.value === value.value)) { + return options + } + return [{ label: `当前值:${value.value}`, value: value.value }, ...options] +}) + async function handleSave() { submitting.value = true try { @@ -73,6 +83,16 @@ function readError(error: unknown, fallback: string) { + + + + + @@ -103,6 +123,10 @@ function readError(error: unknown, fallback: string) { width: 100%; } +.full-select { + width: 100%; +} + .desc-item { margin-top: 14px; margin-bottom: 0;