setCloseTarget(null)}
+ >
+
e.stopPropagation()}
+ >
+
+ 确认永久闭店?
+
- 关闭后不可恢复营业,确认关闭该门店?
+ 永久闭店后不可再开门营业,确认关闭该门店?
diff --git a/apps/h5-partner/src/styles.css b/apps/h5-partner/src/styles.css
index 00136ed..b026ee8 100644
--- a/apps/h5-partner/src/styles.css
+++ b/apps/h5-partner/src/styles.css
@@ -1960,6 +1960,134 @@ nav.app-tabbar .app-tabbar-label {
color: #fff;
}
+.partner-store-filter-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.partner-store-filter-label {
+ flex-shrink: 0;
+ font-family: var(--font-label);
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--color-on-surface-variant);
+}
+
+.partner-store-filter-select {
+ flex: 1;
+ height: 40px;
+ padding: 0 12px;
+ border: 1px solid var(--color-outline-variant);
+ border-radius: var(--radius-md);
+ background: var(--color-surface-container-low);
+ font-size: 14px;
+ color: var(--color-ink-black);
+}
+
+.partner-store-filter-select:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px rgba(166, 29, 36, 0.2);
+}
+
+.partner-store-card-main {
+ flex: 1;
+ min-width: 0;
+ padding-right: 8px;
+}
+
+.partner-store-card-badge {
+ flex-shrink: 0;
+ white-space: nowrap;
+}
+
+.partner-store-card-actions--v3416 {
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.partner-store-card-actions--v3416 > .partner-store-switch {
+ flex: 1 1 auto;
+ min-width: 140px;
+}
+
+.partner-store-card-actions--v3416 > .partner-store-close-btn {
+ flex: 0 0 auto;
+}
+
+.partner-store-card-actions--v3416 > .partner-menu-icon {
+ flex: 0 0 auto;
+ margin-left: auto;
+}
+
+.partner-store-switch {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ cursor: pointer;
+ font-family: var(--font-label);
+ font-size: 12px;
+ color: var(--color-on-surface-variant);
+ user-select: none;
+}
+
+.partner-store-switch input {
+ position: absolute;
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.partner-store-switch-track {
+ position: relative;
+ width: 44px;
+ height: 24px;
+ border-radius: 999px;
+ background: var(--color-surface-container-highest);
+ transition: background 0.2s ease;
+ flex-shrink: 0;
+}
+
+.partner-store-switch-track::after {
+ content: '';
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: #fff;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
+ transition: transform 0.2s ease;
+}
+
+.partner-store-switch input:checked + .partner-store-switch-track {
+ background: var(--color-success-green);
+}
+
+.partner-store-switch input:checked + .partner-store-switch-track::after {
+ transform: translateX(20px);
+}
+
+.partner-store-switch--disabled {
+ opacity: 0.45;
+ cursor: not-allowed;
+}
+
+.partner-store-close-btn {
+ flex: 0 0 auto !important;
+ padding: 8px 12px !important;
+ border: 1px solid rgba(166, 29, 36, 0.35);
+ background: transparent;
+ color: var(--color-heritage-red);
+ font-weight: 500;
+}
+
+.partner-store-close-btn:disabled {
+ opacity: 0.45;
+ cursor: not-allowed;
+}
+
.partner-store-card {
background: var(--color-card);
border-radius: var(--radius-md);
@@ -2061,6 +2189,39 @@ nav.app-tabbar .app-tabbar-label {
margin: 0 var(--space-page) var(--space-md);
}
+.partner-onboard-cs-card {
+ text-align: center;
+}
+
+.partner-onboard-cs-qr {
+ display: block;
+ width: min(220px, 70vw);
+ height: auto;
+ margin: 0 auto 16px;
+ border-radius: 8px;
+ background: #fff;
+ border: 1px solid var(--color-surface-container);
+}
+
+.partner-onboard-cs-check {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ margin-top: 8px;
+ font-family: var(--font-label);
+ font-size: 14px;
+ color: var(--color-ink-black);
+ cursor: pointer;
+ text-align: left;
+}
+
+.partner-onboard-cs-check input {
+ width: 18px;
+ height: 18px;
+ flex-shrink: 0;
+}
+
/* ── Store detail ── */
.partner-detail-page {
padding-bottom: 96px;
diff --git a/apps/h5-partner/vite.config.ts b/apps/h5-partner/vite.config.ts
index ef72a3e..2cf42af 100644
--- a/apps/h5-partner/vite.config.ts
+++ b/apps/h5-partner/vite.config.ts
@@ -12,6 +12,7 @@ export default defineConfig({
},
},
server: {
+ host: true,
port: 5175,
proxy: {
'/api': process.env.VITE_API_TARGET ?? 'http://localhost:3010',
diff --git a/apps/h5-shop/vite.config.ts b/apps/h5-shop/vite.config.ts
index 803f41b..decaef4 100644
--- a/apps/h5-shop/vite.config.ts
+++ b/apps/h5-shop/vite.config.ts
@@ -12,6 +12,7 @@ export default defineConfig({
},
},
server: {
+ host: true,
port: 5174,
proxy: { '/api': process.env.VITE_API_TARGET ?? 'http://localhost:3010' },
},
diff --git a/apps/h5-user/vite.config.ts b/apps/h5-user/vite.config.ts
index 8fa7010..1afb2c0 100644
--- a/apps/h5-user/vite.config.ts
+++ b/apps/h5-user/vite.config.ts
@@ -12,6 +12,7 @@ export default defineConfig({
},
},
server: {
+ host: true,
proxy: {
'/api': process.env.VITE_API_TARGET ?? 'http://localhost:3010',
},
diff --git a/apps/mini-user/src/pages/store-detail/index.tsx b/apps/mini-user/src/pages/store-detail/index.tsx
index a966b67..f326b2f 100644
--- a/apps/mini-user/src/pages/store-detail/index.tsx
+++ b/apps/mini-user/src/pages/store-detail/index.tsx
@@ -108,6 +108,13 @@ function formatRedeemTime(input?: string | null) {
return formatShanghaiDateTime(input);
}
+function formatPackagePriceYuan(price: string | number) {
+ const n = typeof price === 'number' ? price : Number(price);
+ if (!Number.isFinite(n)) return '0';
+ if (Math.abs(n - Math.round(n)) < 1e-9) return String(Math.round(n));
+ return n.toFixed(2).replace(/\.?0+$/, '');
+}
+
function formatRedeemAmountYuan(amount: number | string) {
const n = typeof amount === 'number' ? amount : Number(amount);
if (!Number.isFinite(n)) return '0';
@@ -419,7 +426,12 @@ export default function StoreDetailPage() {
className="store-detail-package-list-item"
onClick={() => openPackageDetail(index)}
>
-
{pkg.name}
+
+ {pkg.name}
+
+ ¥{formatPackagePriceYuan(pkg.price)}
+
+
))}
diff --git a/apps/mini-user/src/styles/store-detail.css b/apps/mini-user/src/styles/store-detail.css
index fef3d3a..327c4fe 100644
--- a/apps/mini-user/src/styles/store-detail.css
+++ b/apps/mini-user/src/styles/store-detail.css
@@ -281,13 +281,36 @@
padding-bottom: 0;
}
-.store-detail-package-list-title {
+/* 名称在左可换行;价格始终贴该行最右侧 */
+.store-detail-package-list-row {
+ /* display: flex;
+ flex-wrap: wrap;
+ align-items: flex-start;
+ column-gap: 12px;
+ row-gap: 4px; */
display: block;
+ width: 100%;
+}
+
+.store-detail-package-list-title {
+ min-width: 0;
+ font-family: var(--font-headline);
font-size: 15px;
line-height: 1.5;
font-weight: 500;
color: var(--color-on-surface);
word-break: break-word;
+ display: inline;
+}
+
+.store-detail-package-list-price {
+ display: inline;
+ margin-left: auto;
+ font-size: 15px;
+ line-height: 1.5;
+ font-weight: 600;
+ color: var(--color-heritage-red, #a61d24);
+ text-align: right;
}
.store-detail-package-list-item:active {
@@ -315,15 +338,18 @@
padding: 8px 16px 4px 16px;
}
+/* 与门店详情套餐列表一致:名称在左可换行;价格始终贴该行最右侧 */
.store-package-detail-title-row {
display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 16px;
+ flex-wrap: wrap;
+ align-items: flex-end;
+ column-gap: 16px;
+ row-gap: 4px;
+ width: 100%;
}
.store-package-detail-title {
- flex: 1;
+ /* flex: 1 1 10em; */
min-width: 0;
font-family: var(--font-headline);
font-size: 20px;
@@ -334,13 +360,14 @@
}
.store-package-detail-price {
- flex-shrink: 0;
- font-size: 24px;
+ /* flex: 0 0 auto; */
+ margin-left: auto;
+ font-size: 20px;
font-weight: 700;
- line-height: 1.35;
- color: #a61d24;
+ line-height: 1.4;
color: var(--color-heritage-red, #a61d24);
text-align: right;
+ white-space: nowrap;
}
.store-package-detail-store {
diff --git a/packages/shared-types/src/hq-permissions.ts b/packages/shared-types/src/hq-permissions.ts
index 3acd4fc..c3f6953 100644
--- a/packages/shared-types/src/hq-permissions.ts
+++ b/packages/shared-types/src/hq-permissions.ts
@@ -1,4 +1,4 @@
-/** HQ 权限目录(权限分配页勾选源) */
+/** HQ 权限目录(权限分配页勾选源) */
export const HQ_PERMISSION_CATALOG = [
{ key: 'dashboard', label: '概览', group: '业务' },
{ key: 'users', label: '用户管理', group: '业务' },
@@ -30,6 +30,7 @@ export const HQ_PERMISSION_CATALOG = [
{ key: 'system_settings_sms', label: '短信', group: '系统设置' },
{ key: 'system_settings_wechat', label: '微信', group: '系统设置' },
{ key: 'system_settings_wechat_mini', label: '微信小程序', group: '系统设置' },
+ { key: 'system_settings_wechat_mini_share', label: '小程序分享配置', group: '系统设置' },
{ key: 'system_settings_oss', label: '对象存储 OSS', group: '系统设置' },
{ key: 'system_settings_app', label: '应用链接', group: '系统设置' },
{ key: 'system_settings_deploy', label: '发布部署', group: '系统设置' },
@@ -60,6 +61,7 @@ export const SYSTEM_CONFIG_GROUP_PERMISSION: Record
= {
sms: 'system_settings_sms',
wechat: 'system_settings_wechat',
wechat_mini: 'system_settings_wechat_mini',
+ wechat_mini_share: 'system_settings_wechat_mini_share',
oss: 'system_settings_oss',
app: 'system_settings_app',
deploy: 'system_settings_deploy',
@@ -121,6 +123,7 @@ export const HQ_ROLE_DEFAULT_PERMISSIONS: Record = {
'resources',
'logs',
'system_settings_wechat_mini',
+ 'system_settings_wechat_mini_share',
],
FINANCE: [
'dashboard',
diff --git a/packages/shared-types/src/wechat.ts b/packages/shared-types/src/wechat.ts
index 886acff..1f21e61 100644
--- a/packages/shared-types/src/wechat.ts
+++ b/packages/shared-types/src/wechat.ts
@@ -64,6 +64,10 @@ export type ClientRuntimeConfig = {
qualificationDisclosureUrl?: string;
/** 总部客服电话 */
customerServicePhone?: string;
+ /** 合伙人入驻:企微客服二维码图片 URL */
+ partnerOnboardCsQrUrl?: string | null;
+ /** 合伙人入驻:企微客服提示文案 */
+ partnerOnboardCsHint?: string | null;
/** 小程序各场景分享文案/图 */
share?: MiniShareRuntime;
};
diff --git a/server/dukang-api/src/common/system-config/system-config.registry.ts b/server/dukang-api/src/common/system-config/system-config.registry.ts
index 6bd424b..6cb68a7 100644
--- a/server/dukang-api/src/common/system-config/system-config.registry.ts
+++ b/server/dukang-api/src/common/system-config/system-config.registry.ts
@@ -213,6 +213,23 @@ export const SYSTEM_CONFIG_FIELDS: SystemConfigFieldMeta[] = [
placeholder: '13203801799',
description: 'C 端联系客服拨号号码',
},
+ {
+ key: 'PARTNER_ONBOARD_CS_QR_URL',
+ label: '合伙人入驻 · 企微客服二维码',
+ group: G.wechat_mini,
+ type: 'image',
+ requiresRestart: false,
+ description: '合伙人 H5 录入门店提交前展示;未配置时禁止提交入驻',
+ },
+ {
+ key: 'PARTNER_ONBOARD_CS_HINT',
+ label: '合伙人入驻 · 客服提示文案',
+ group: G.wechat_mini,
+ type: 'string',
+ requiresRestart: false,
+ placeholder: '使用问题、提现问题等随时可联系【杜康好客】客服',
+ description: '二维码下方说明;留空用默认文案',
+ },
{
key: 'MOCK_SMS_FIXED_CODE',
label: 'Mock 短信固定验证码',
diff --git a/server/dukang-api/src/main.ts b/server/dukang-api/src/main.ts
index 3828a4b..5970bbb 100644
--- a/server/dukang-api/src/main.ts
+++ b/server/dukang-api/src/main.ts
@@ -86,12 +86,13 @@ async function bootstrap() {
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
app.useGlobalFilters(new HttpExceptionFilter(app.get(AlertService)));
app.useGlobalInterceptors(new ResponseInterceptor(), app.get(LoggingInterceptor));
- const port = process.env.PORT || 3010;
+ const port = Number(process.env.PORT || 3010);
+ const host = process.env.HOST || '0.0.0.0';
const cfg = loadAppConfig();
const smsMode = cfg.mockSms ? 'MOCK' : 'ALIYUN';
console.log(`[config] NODE_ENV=${process.env.NODE_ENV} MOCK_SMS=${cfg.mockSms} SMS=${smsMode}`);
- await app.listen(port);
- console.log(`dukang-api listening on http://localhost:${port}/api/v1`);
+ await app.listen(port, host);
+ console.log(`dukang-api listening on http://${host}:${port}/api/v1`);
}
bootstrap();
diff --git a/server/dukang-api/src/modules/common/client-config.controller.ts b/server/dukang-api/src/modules/common/client-config.controller.ts
index 84c56b5..a51c5d1 100644
--- a/server/dukang-api/src/modules/common/client-config.controller.ts
+++ b/server/dukang-api/src/modules/common/client-config.controller.ts
@@ -37,6 +37,10 @@ export class ClientConfigController {
brandLogoMarkUrl: brand.brandLogoMarkUrl,
qualificationDisclosureUrl: brand.qualificationDisclosureUrl,
customerServicePhone: brand.customerServicePhone,
+ partnerOnboardCsQrUrl: (env.PARTNER_ONBOARD_CS_QR_URL ?? '').trim() || null,
+ partnerOnboardCsHint:
+ (env.PARTNER_ONBOARD_CS_HINT ?? '').trim() ||
+ '使用问题、提现问题等随时可联系【杜康好客】客服',
share,
};
}
diff --git a/server/dukang-api/src/modules/common/client-error.service.ts b/server/dukang-api/src/modules/common/client-error.service.ts
index 9e0a31f..2e7a814 100644
--- a/server/dukang-api/src/modules/common/client-error.service.ts
+++ b/server/dukang-api/src/modules/common/client-error.service.ts
@@ -5,7 +5,6 @@ import { AlertService } from '../../common/alert/alert.service';
import type { AlertLevel } from '../../common/alert/alert.constants';
import type { AuthUser } from '../../common/guards/jwt-auth.guard';
import type { ReportClientErrorDto } from './dto/client-error.dto';
-import { SupportTicketService } from './support-ticket.service';
const WECOM_LEVELS = new Set(['fatal', 'error']);
@@ -16,7 +15,6 @@ export class ClientErrorService {
constructor(
private readonly prisma: PrismaService,
private readonly alert: AlertService,
- private readonly supportTicket: SupportTicketService,
) {}
async report(dto: ReportClientErrorDto, user?: AuthUser, headerClientApp?: string) {
@@ -109,25 +107,6 @@ export class ClientErrorService {
);
}
- if (dto.category === 'validation_error') {
- const apiPath =
- dto.extra && typeof dto.extra.url === 'string' ? dto.extra.url.slice(0, 256) : undefined;
- void this.supportTicket
- .createFromClientValidation({
- clientApp,
- message,
- pagePath,
- apiPath,
- actorLabel:
- user?.actorId != null ? `${user.actorType}:${String(user.actorId)}` : undefined,
- })
- .catch((e) => {
- this.logger.warn(
- `auto support ticket failed: ${e instanceof Error ? e.message : String(e)}`,
- );
- });
- }
-
return { ok: true };
}
}
diff --git a/server/dukang-api/src/modules/common/support-ticket.service.ts b/server/dukang-api/src/modules/common/support-ticket.service.ts
index fce12a4..dcd735e 100644
--- a/server/dukang-api/src/modules/common/support-ticket.service.ts
+++ b/server/dukang-api/src/modules/common/support-ticket.service.ts
@@ -38,17 +38,8 @@ function generateSupportTicketNo() {
return `ST${Date.now()}${Math.floor(Math.random() * 900 + 100)}`;
}
-const AUTO_VALIDATION_TICKET_APPS = new Set([
- 'USER_H5',
- 'USER_MINI',
- 'SHOP_H5',
- 'PARTNER_H5',
-]);
-
@Injectable()
export class SupportTicketService {
- private systemCreatorCache: { id: bigint; name: string } | null = null;
-
constructor(
private readonly prisma: PrismaService,
private readonly alert: AlertService,
@@ -105,66 +96,6 @@ export class SupportTicketService {
return serializeBigInt(mapSupportTicketRow(ticket));
}
- /** 客户端 400 验证错误自动建单(1 小时内同标题去重) */
- async createFromClientValidation(input: {
- clientApp: string;
- message: string;
- pagePath?: string;
- apiPath?: string;
- actorLabel?: string;
- }) {
- if (!AUTO_VALIDATION_TICKET_APPS.has(input.clientApp)) {
- return { skipped: true as const, reason: 'unsupported_app' as const };
- }
-
- const title = `[客户端验证] ${input.clientApp}${input.pagePath ? ` · ${input.pagePath}` : ''} · ${input.message.slice(0, 60)}`;
- const oneHourAgo = new Date(Date.now() - 3600_000);
- const existing = await this.prisma.commonSupportTicket.findFirst({
- where: { title, createdAt: { gte: oneHourAgo } },
- select: { id: true },
- });
- if (existing) {
- return { skipped: true as const, ticketId: existing.id.toString() };
- }
-
- const creator = await this.resolveSystemCreator();
- const content = [
- `端:${input.clientApp}`,
- input.pagePath ? `页面:${input.pagePath}` : null,
- input.apiPath ? `接口:${input.apiPath}` : null,
- input.actorLabel ? `用户:${input.actorLabel}` : null,
- '',
- input.message,
- ]
- .filter(Boolean)
- .join('\n');
-
- const ticket = await this.create(
- {
- ticketType: 'BUG',
- title,
- content,
- remark: '客户端验证错误自动上报',
- },
- creator,
- );
- return { skipped: false as const, ticketId: String(ticket.id) };
- }
-
- private async resolveSystemCreator() {
- if (this.systemCreatorCache) return this.systemCreatorCache;
- const account = await this.prisma.hqAccount.findFirst({
- where: { adminRole: 'SUPER_ADMIN', status: 'ACTIVE' },
- orderBy: { id: 'asc' },
- select: { id: true },
- });
- if (!account) {
- throw new BadRequestException('未找到系统管理员账号,无法自动创建工单');
- }
- this.systemCreatorCache = { id: account.id, name: '系统自动' };
- return this.systemCreatorCache;
- }
-
async update(id: bigint, dto: UpdateSupportTicketDto) {
const ticket = await this.getOrThrow(id);
if (ticket.status !== 'PENDING_REVIEW') {
diff --git a/杜康好客-v3-PRD.md b/杜康好客-v3-PRD.md
index 2f72baa..48b45fd 100644
--- a/杜康好客-v3-PRD.md
+++ b/杜康好客-v3-PRD.md
@@ -21,7 +21,7 @@
| 3.4.13 | 08-05 | metrics/核销详情/版本联动 PUBLISHED/mini 体验/H5 OAuth | [`v3.4.13`](./杜康好客-v3.4.13-体验优化开发文档.md) |
| 3.4.14 | 08-06 | mini-user 门头/套餐详情;小程序可配置;**统一测试白名单(不计账+限测可见+Mock旁路)** | [`v3.4.14`](./杜康好客-v3.4.14-mini-user门店体验开发文档.md) |
| 3.4.15 | 08-07 | mini-user 门店列表卡片;HQ 门店照片替换/删除;套餐多图上限 20 | [`v3.4.15`](./杜康好客-v3.4.15-mini-user门店列表优化.md) |
-| 3.4.16 | 08-09 | 门店登录手机号与对外联系电话分离 | 见 §3.12 |
+| 3.4.16 | 08-09 | 联系电话分离;取消自动 ST;套餐 UI;合伙人列表/入驻客服门槛;HQ 门店列表表格 | [`v3.4.16`](./杜康好客-v3.4.16-门店联系电话与体验优化.md) |
---
@@ -94,9 +94,9 @@
| 3.9 | 门店套餐 ≤10 条、独立审核 | v3.4.10 |
| 3.10 | 开发计划/任务/版本/技术支持联动 | v3.4.11 |
| 3.11 | 企微智能机器人 + 消息推送 Webhook | v3.4.11 |
-| 3.12 | **登录手机号 ≠ 对外联系电话** | 见下 |
+| 3.12 | **登录手机号 ≠ 对外联系电话**;体验优化 | v3.4.16 |
-### 3.12 门店登录号与对外联系电话
+### 3.12 门店登录号与体验增量(v3.4.16)
| 字段 | 含义 | 用途 |
|------|------|------|
@@ -105,6 +105,10 @@
- 合伙人拓店入驻、门店资料修改均须采集 **联系电话**(可与登录号不同)。
- 未填联系电话时,对外展示回退登录手机号(兼容旧数据)。
+- **不**再因客户端 `validation_error` 自动创建技术支持工单。
+- 合伙人入驻提交前须展示 HQ 可配企微客服二维码,并确认已添加客服。
+- HQ 门店列表:长店名截断、操作列右固定、表过宽可横向滚动。
+- 详 [`v3.4.16`](./杜康好客-v3.4.16-门店联系电话与体验优化.md)。
---
diff --git a/杜康好客-v3-现状对照.md b/杜康好客-v3-现状对照.md
index 0708380..44f9fcf 100644
--- a/杜康好客-v3-现状对照.md
+++ b/杜康好客-v3-现状对照.md
@@ -9,7 +9,7 @@
|------|------|
| 主链路 | 登录→下单支付→权益→扫码核销→payout→HQ 打款 **可跑通** |
| C 端 | `mini-user` 小程序 + h5-user;v3.4.13 版本门控/门店/物流已上 |
-| 近期版本 | … · v3.4.15 门店列表/套餐多图 · **v3.4.16 登录号/联系电话分离(开发中)** |
+| 近期版本 | … · v3.4.15 · **v3.4.16 联系电话/套餐 UI/合伙人列表与入驻客服门槛/HQ 门店列表表格** |
| 冒烟 | `scripts/smoke-v3.mjs` 窄路径 ≠ 全量 ACC |
| REQ 明细 | PRD §4 + `.cursor/skills/dukang-v3/reference-req-index.md` |
diff --git a/杜康好客-v3.4.16-门店联系电话与体验优化.md b/杜康好客-v3.4.16-门店联系电话与体验优化.md
new file mode 100644
index 0000000..b8afbab
--- /dev/null
+++ b/杜康好客-v3.4.16-门店联系电话与体验优化.md
@@ -0,0 +1,83 @@
+# 杜康好客 · v3.4.16 门店联系电话与体验优化
+
+> **2026-08-09** · PRD §3.12 · mini-user / h5-partner / admin-web / API
+
+## 范围
+
+| 项 | 交付 |
+|----|------|
+| A. 登录号 / 联系电话分离 | `Store.contactPhone`;合伙人入驻/编辑;HQ;C 端 `publicDial` |
+| B. 取消自动技术支持工单 | `validation_error` 不再自动建 ST;上报与人工 ST 保留 |
+| C. 套餐详情排版 | 套餐名 + 价格:一行能放下则同行,否则名称换行、价格次行右对齐 |
+| D. 门店详情套餐列表 | 套餐名称后展示价格 |
+| E. 合伙人门店列表 | 右上角状态;开关营业/临时闭店;永久闭店按钮;下拉筛选 |
+| F. 入驻企微客服门槛 | HQ 可配二维码;勾选「已添加客服」后才可提交 |
+| G. HQ 门店列表表格 | 店名过长截断;操作列右侧固定;横向滚动 |
+
+## A. 登录号 ≠ 对外联系电话
+
+| 字段 | 含义 | 用途 |
+|------|------|------|
+| `phone` | 老板主账号 | 门店端登录;`StoreAccount.phone` |
+| `contactPhone` | 店长/对外 | C 端拨号与展示 |
+
+- C 端 `GET /stores`、`GET /stores/:id`:`phone` = `contactPhone ?? phone`(兼容旧小程序)
+- 未填联系电话时对外回退登录号
+
+## B. 取消 validation_error 自动 ST
+
+- 原路径:`POST /common/client-errors` + `category=validation_error` → `createFromClientValidation`
+- **删除**自动建单;保留 client-error 落库 / WeCom 告警;HQ 手动建单与企微「创建」不变
+
+## C / D. mini-user 套餐 UI
+
+- 套餐详情标题与门店详情「门店套餐」列表一致:名称在左可换行,价格始终居右;放不下时价格落在次行右侧
+
+## E. 合伙人门店列表
+
+| UI | 行为 |
+|----|------|
+| 右上角徽标 | 待审核 / 审核驳回 / 营业中 / 临时闭店 / 永久闭店 |
+| 开关 | `OPEN` ↔ `PAUSED`;CLOSED / PENDING 禁用 |
+| 永久闭店 | 独立按钮 + 确认 → `CLOSED` |
+| 筛选 | 下拉:全部 / 营业中 / 临时闭店 / 待审核 / 已驳回 / 永久闭店 |
+
+API:`PUT /partner/stores/:id/status`(不变)
+
+## F. 入驻企微客服二维码门槛
+
+| 配置键 | 说明 |
+|--------|------|
+| `PARTNER_ONBOARD_CS_QR_URL` | 企微客服二维码图片 URL(OSS) |
+| `PARTNER_ONBOARD_CS_HINT` | 提示文案(默认见下) |
+
+默认提示:`使用问题、提现问题等随时可联系【杜康好客】客服`
+
+- `GET /common/client-config` 对 PARTNER 下发 `partnerOnboardCsQrUrl` / `partnerOnboardCsHint`
+- HQ **系统设置 → 微信小程序配置**(与「总部客服电话」同组):可上传二维码、编辑提示文案
+- 入驻信息填完后展示二维码;须勾选「我已添加【杜康好客】客服」才可提交/跳过
+- **未配置二维码**:禁止提交,提示联系总部
+
+## G. HQ 门店列表页表格体验
+
+[`apps/admin-web` 门店列表](apps/admin-web/src/pages/StoresPage.tsx):
+
+| 点 | 行为 |
+|----|------|
+| 门店名 | 过长省略号截断,悬停可看全名;「测试」标签保留 |
+| 操作列 | `fixed: 'right'`,横向滚动时详情/评价始终可见 |
+| 整表过宽 | `scroll.x` 开启横向滚动条(列宽合计约 1720) |
+
+合伙人名、介绍、分类等长文本列同样启用 ellipsis,避免撑破布局。
+
+## ACC
+
+- [ ] 旧门店对外拨号仍可用(回退登录号或已回填 contactPhone)
+- [ ] 合伙人可分别维护登录号与联系电话
+- [ ] API 400 不再产生「客户端验证错误自动上报」ST
+- [ ] 套餐详情短/长标题排版符合定案
+- [ ] 门店详情套餐列表可见价格
+- [ ] 合伙人列表状态/开关/永久闭店/下拉筛选可用
+- [ ] 无二维码不可提交;有二维码须勾选后提交
+- [ ] HQ 可上传/更换入驻客服二维码
+- [ ] HQ 门店列表:长店名截断、操作栏右固定、可横向滚动