合并 main 分支并融合订单列表最小化与 SQL 治理

- Auth/权限中间件取 main 演进版(错误细分、token 来源),保留 dev 的用户 cookie 与登出撤销语义
- 后台订单列表:融合 main 的首页免 COUNT 优化与关键词索引路由,保留 dev 的最小化 AdminOrderListItemDTO(结账明细仍由详情接口返回,同步调整对应集成测试)
- 保留 dev 的 token_version 冻结/解冻递增与 main 的状态变化判断及通知
- 前端:保留 dev 登出接口与 main 的 fetchMe silent 参数、SSE 登录跳转;后台订单表按 dev 最小化 DTO 移除号主到手列
This commit is contained in:
yml2213
2026-09-05 11:43:13 +08:00
294 changed files with 30171 additions and 13439 deletions
@@ -17,8 +17,10 @@ export function defaultPublishForm(): PublishForm {
login_method: '',
online_start: '',
online_end: '',
early_morning_response: '否',
can_change_game_name: '',
all_hero: '',
next_season_pass: '',
ban_record: '',
common_regions: [],
depositAmountYuan: '',
@@ -79,6 +79,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
const submitButtonText = computed(() => (isEditMode.value ? '提交修改' : '立即发布'))
const submitLoadingText = computed(() => (isEditMode.value ? '提交中...' : '发布中...'))
const showBanRecordRiskHint = false
const nextSeasonPassEnabled = computed(() => publishOptions.value.next_season_pass_enabled)
const pricing = usePricingCalculator({
form,
@@ -308,8 +309,10 @@ export function usePublishForm(options: UsePublishFormOptions) {
login_method: listing.login_platform || '',
online_start: stringValue(onlineTime.start),
online_end: stringValue(onlineTime.end),
early_morning_response: stringValue(summary.early_morning_response) || '否',
can_change_game_name: stringValue(summary.can_change_game_name),
all_hero: stringValue(summary.all_hero),
next_season_pass: stringValue(summary.next_season_pass),
ban_record: stringValue(summary.ban_record),
common_regions: stringArray(summary.common_regions),
depositAmountYuan: listing.deposit_amount_cent > 0 ? listing.deposit_amount_cent / 100 : '',
@@ -700,6 +703,9 @@ export function usePublishForm(options: UsePublishFormOptions) {
if (!dailyLossOptions.includes(pricing.dailyLossMAmount.value)) return '请选择每日损耗'
const onlineTimeError = validateOnlineTime()
if (onlineTimeError) return onlineTimeError
if (form.early_morning_response !== '是' && form.early_morning_response !== '否') {
return '请选择凌晨0-2点是否可响应上号'
}
if (pricing.hasAcceleratedSaleRatioInput()) {
const ratio = Number(form.accelerated_sale_ratio)
if (!Number.isFinite(ratio) || ratio <= 0) return '加速出售比例格式不正确'
@@ -719,6 +725,9 @@ export function usePublishForm(options: UsePublishFormOptions) {
if (pricing.banRecordOptions.value.length && !form.ban_record) return '请选择封禁记录'
if (!form.can_change_game_name) return '请选择是否可以修改游戏名'
if (!form.all_hero) return '请选择是否有全英雄'
if (nextSeasonPassEnabled.value && !form.next_season_pass) {
return '请选择下赛季是否购买通行证'
}
if (form.depositAmountYuan === '' || Number(form.depositAmountYuan) < 0) return '请填写押金'
if (!Number.isFinite(Number(form.depositAmountYuan))) return '押金格式不正确'
if (
@@ -818,8 +827,10 @@ export function usePublishForm(options: UsePublishFormOptions) {
start: form.online_start,
end: form.online_end,
},
early_morning_response: form.early_morning_response,
can_change_game_name: form.can_change_game_name,
all_hero: form.all_hero,
next_season_pass: form.next_season_pass,
ban_record: form.ban_record,
common_regions: form.common_regions,
remark: form.remark,
@@ -911,6 +922,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
passwordAndDeviceConfirmed,
canPublishAfterAgreements,
showBanRecordRiskHint,
nextSeasonPassEnabled,
isScanOnlyServer,
effectiveLoginMethodOptions,
fileInput,
@@ -88,6 +88,10 @@
font-size: 16px;
}
.early-morning-response-field :deep(.van-field__label) {
width: 128px;
}
.hint-label,
.backend-hint-target.has-hint,
.upload-title.has-hint {
@@ -27,6 +27,7 @@ const {
passwordAndDeviceConfirmed,
canPublishAfterAgreements,
showBanRecordRiskHint,
nextSeasonPassEnabled,
fileInput,
activeUploadKey,
form,
@@ -121,6 +122,8 @@ function selectRadio<T extends string>(value: T, setter: (value: T) => void) {
const canChangeGameNameOptions = ['是', '否']
const allHeroOptions = ['是', '否']
const nextSeasonPassOptions = ['购买', '不购买']
const earlyMorningResponseOptions = ['是', '否']
// 在线时间:通栏自定义块 + 底部时间选择,避免 van-field 窄列截断原生 time
const onlineTimePickerVisible = ref(false)
@@ -439,6 +442,28 @@ const screenshotGuides: Record<string, string> = {
</template>
</van-field>
<van-field
v-if="nextSeasonPassEnabled"
label="下赛季是否购买通行证"
required
class="publish-field"
>
<template #input>
<div class="radio-group">
<button
v-for="opt in nextSeasonPassOptions"
:key="opt"
type="button"
class="radio-btn"
:class="{ active: form.next_season_pass === opt }"
@click="selectRadio(opt, value => (form.next_season_pass = value))"
>
{{ opt }}
</button>
</div>
</template>
</van-field>
<div class="online-time-block">
<div class="online-time-head">
<div class="online-time-title">
@@ -481,6 +506,27 @@ const screenshotGuides: Record<string, string> = {
请填写能百分百联系上您的时间。该时段联系不上可能影响上架或扣款,请预留扫码与冻结人脸时间。
</p>
<van-field
label="凌晨0-2点可响应上号"
required
class="publish-field early-morning-response-field"
>
<template #input>
<div class="radio-group">
<button
v-for="opt in earlyMorningResponseOptions"
:key="opt"
type="button"
class="radio-btn"
:class="{ active: form.early_morning_response === opt }"
@click="selectRadio(opt, value => (form.early_morning_response = value))"
>
{{ opt }}
</button>
</div>
</template>
</van-field>
<van-popup v-model:show="onlineTimePickerVisible" position="bottom" round teleport="body">
<van-time-picker
v-model="onlineTimePickerValue"
@@ -263,7 +263,9 @@ async function handleOffline() {
<h2>{{ getListingTitle(listing) }}</h2>
<p class="sub">{{ getListingSubtitle(listing) }}</p>
<div class="tag-row">
<van-tag plain type="primary" size="medium">{{ getServerRegion(listing) || '—' }}</van-tag>
<van-tag plain type="primary" size="medium">{{
getServerRegion(listing) || '—'
}}</van-tag>
<van-tag v-if="getLoginMethod(listing)" plain type="primary" size="medium">
{{ getLoginMethod(listing) }}
</van-tag>
@@ -360,7 +362,9 @@ async function handleOffline() {
<div class="bottom-spacer" />
<footer class="action-bar">
<van-button v-if="canEdit(listing)" plain round class="btn" @click="goEdit">编辑</van-button>
<van-button v-if="canEdit(listing)" plain round class="btn" @click="goEdit"
>编辑</van-button
>
<van-button
v-if="canSubmit(listing)"
type="primary"
@@ -298,13 +298,7 @@ function isPendingReview(row: Listing) {
<!-- 卡片底:操作 -->
<div class="card-footer">
<van-button
size="small"
plain
round
class="action-btn"
@click="goDetail(item)"
>
<van-button size="small" plain round class="action-btn" @click="goDetail(item)">
详情
</van-button>
<van-button
@@ -376,6 +376,10 @@
align-items: start;
}
.early-morning-response-row {
grid-template-columns: 150px minmax(0, 1fr);
}
.online-time-body {
display: flex;
flex-direction: column;
@@ -28,6 +28,7 @@ const {
passwordAndDeviceConfirmed,
canPublishAfterAgreements,
showBanRecordRiskHint,
nextSeasonPassEnabled,
fileInput,
activeUploadKey,
form,
@@ -120,6 +121,8 @@ const activeAgreementContent = computed(() => activeAgreement.value?.content ||
const canChangeGameNameOptions = ['是', '否']
const allHeroOptions = ['是', '否']
const nextSeasonPassOptions = ['购买', '不购买']
const earlyMorningResponseOptions = ['是', '否']
// 截图槽位的指导示例图(仅展示、点击放大,不占用上传数量)
const screenshotGuides: Record<string, string> = {
@@ -355,6 +358,15 @@ function selectDailyLoss(value: string | number) {
/>
</div>
<div v-if="nextSeasonPassEnabled" class="field-row panel-field-row">
<label>下赛季是否购买通行证<span>*</span></label>
<OptionChips
:options="nextSeasonPassOptions"
:model-value="form.next_season_pass"
@select="form.next_season_pass = String($event)"
/>
</div>
<div class="field-row panel-field-row online-time-row">
<label>在线时间<span>*</span></label>
<div class="online-time-body">
@@ -396,6 +408,15 @@ function selectDailyLoss(value: string | number) {
</div>
</div>
<div class="field-row panel-field-row early-morning-response-row">
<label>凌晨0-2点可响应上号<span>*</span></label>
<OptionChips
:options="earlyMorningResponseOptions"
:model-value="form.early_morning_response"
@select="form.early_morning_response = String($event)"
/>
</div>
<div v-if="banRecordOptions.length" class="field-row panel-field-row">
<label>封禁记录<span>*</span></label>
<OptionChips
@@ -127,7 +127,10 @@ const accountRows = computed(() => {
{ label: '负重等级', value: readAssetString(listing.value, 'load_level') || '--' },
{ label: '绝密KD', value: secretKD > 0 ? formatAssetNumber(secretKD) : '--' },
{ label: '纯币', value: formatHafCoinM(getCoinWan(listing.value)) },
{ label: '日损耗', value: getDailyLoss(listing.value) ? `${getDailyLoss(listing.value)}/天` : '--' },
{
label: '日损耗',
value: getDailyLoss(listing.value) ? `${getDailyLoss(listing.value)}/天` : '--',
},
{ label: 'M单价', value: formatRatio(listing.value) },
{ label: '方便上号', value: getOnlineTimeText(listing.value) || '--' },
{ label: '预计可租', value: formatEstimatedRentalDuration(listing.value) },
@@ -207,19 +210,37 @@ async function handleOffline() {
<section v-loading="loading" class="seller-detail-page">
<header class="page-head">
<div>
<el-button text :icon="ArrowLeft" @click="router.push('/seller/listings')">返回列表</el-button>
<el-button text :icon="ArrowLeft" @click="router.push('/seller/listings')"
>返回列表</el-button
>
<h1>我的商品详情</h1>
<p>仅本人可查看完整发布信息;他人无法通过此入口访问。</p>
</div>
<div v-if="listing" class="head-actions">
<el-button :icon="CopyDocument" @click="copyCode">复制编号</el-button>
<el-button v-if="canEdit(listing)" :icon="Edit" @click="router.push(`/seller/listings/${listing.id}/edit`)">
<el-button
v-if="canEdit(listing)"
:icon="Edit"
@click="router.push(`/seller/listings/${listing.id}/edit`)"
>
编辑
</el-button>
<el-button v-if="canSubmit(listing)" type="primary" plain :loading="acting" @click="handleSubmit">
<el-button
v-if="canSubmit(listing)"
type="primary"
plain
:loading="acting"
@click="handleSubmit"
>
提审
</el-button>
<el-button v-if="canOffline(listing)" type="danger" plain :loading="acting" @click="handleOffline">
<el-button
v-if="canOffline(listing)"
type="danger"
plain
:loading="acting"
@click="handleOffline"
>
下架
</el-button>
</div>