feat(v3.4.14): mini-user store UX, brand config, client settings

Store list/detail package flow, configurable WeChat mini brand assets and customer service, shop H5 home refresh.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-06 15:21:59 +08:00
parent 53a79d1255
commit a2abbb6169
29 changed files with 1408 additions and 513 deletions
@@ -1,4 +1,15 @@
import type { SystemConfigFieldMeta, SystemConfigGroupMeta } from '@dukang/shared-types';
import {
BRAND_LOGO_MARK_URL,
BRAND_LOGO_OSS_BASE,
BRAND_LOGO_URL,
BRAND_LOGO_WIDE_URL,
CUSTOMER_SERVICE_PHONE,
DEFAULT_USER_H5_URL,
MINI_USER_STATIC_OSS_BASE,
MOCK_SMS_FIXED_CODE,
QUALIFICATION_DISCLOSURE_URL,
} from '@dukang/shared-types';
/** 运行环境、安全与鉴权仅保留在 .env,不在 HQ 系统设置中维护 */
export const SYSTEM_CONFIG_GROUPS: SystemConfigGroupMeta[] = [
@@ -122,9 +133,86 @@ export const SYSTEM_CONFIG_FIELDS: SystemConfigFieldMeta[] = [
group: G.wechat_mini,
type: 'string',
requiresRestart: false,
placeholder: '3.4.13',
placeholder: '3.4.14',
description: 'semver 格式;客户端低于此版本时提示更新',
},
{
key: 'USER_H5_URL',
label: 'C 端 H5 落地页',
group: G.wechat_mini,
type: 'string',
requiresRestart: false,
placeholder: 'https://user.example.com/user',
description: '推广码二维码 / 未配置时的默认落地页前缀(无末尾斜杠)',
},
{
key: 'BRAND_LOGO_OSS_BASE',
label: '品牌 Logo OSS 根路径',
group: G.wechat_mini,
type: 'string',
requiresRestart: false,
placeholder: 'https://xxx.oss-cn-beijing.aliyuncs.com/logo/',
description: '仅作说明/备份;下方三张 Logo 请直接配置完整 URL',
},
{
key: 'BRAND_LOGO_URL',
label: '方形 Logo',
group: G.wechat_mini,
type: 'image',
requiresRestart: false,
description: '分享卡片默认图、首页等品牌展示',
},
{
key: 'BRAND_LOGO_WIDE_URL',
label: '长方形 Logo',
group: G.wechat_mini,
type: 'image',
requiresRestart: false,
description: '含文字,登录页等场景',
},
{
key: 'BRAND_LOGO_MARK_URL',
label: '图标 Logo',
group: G.wechat_mini,
type: 'image',
requiresRestart: false,
description: '默认头像(未微信授权时)',
},
{
key: 'MINI_USER_STATIC_OSS_BASE',
label: '小程序静态资源 OSS 根路径',
group: G.wechat_mini,
type: 'string',
requiresRestart: false,
placeholder: 'https://xxx.oss-cn-beijing.aliyuncs.com/static/mini-user/',
description: '资质公示等静态资源根路径;完整 URL 优先用下方「资质公示图」',
},
{
key: 'QUALIFICATION_DISCLOSURE_URL',
label: '资质公示长图',
group: G.wechat_mini,
type: 'image',
requiresRestart: false,
description: '「我的」页资质公示大图',
},
{
key: 'CUSTOMER_SERVICE_PHONE',
label: '总部客服电话',
group: G.wechat_mini,
type: 'string',
requiresRestart: false,
placeholder: '13203801799',
description: 'C 端联系客服拨号号码',
},
{
key: 'MOCK_SMS_FIXED_CODE',
label: 'Mock 短信固定验证码',
group: G.wechat_mini,
type: 'string',
requiresRestart: false,
placeholder: '999888',
description: '仅 MOCK_SMS 开启时生效;留空则用默认 999888',
},
{ key: 'OSS_ACCESS_KEY_ID', label: 'OSS AccessKey ID', group: G.oss, type: 'password', secret: true, requiresRestart: true },
{ key: 'OSS_ACCESS_KEY_SECRET', label: 'OSS AccessKey Secret', group: G.oss, type: 'password', secret: true, requiresRestart: true },
@@ -137,7 +225,6 @@ export const SYSTEM_CONFIG_FIELDS: SystemConfigFieldMeta[] = [
{ key: 'OSS_UPLOAD_EXPIRE_SECONDS', label: '直传凭证有效期(秒)', group: G.oss, type: 'number', requiresRestart: false },
{ key: 'OSS_MAX_UPLOAD_BYTES', label: '单文件上限(字节)', group: G.oss, type: 'number', requiresRestart: false },
{ key: 'USER_H5_URL', label: 'C 端 H5 落地页', group: G.app, type: 'string', requiresRestart: false },
{
key: 'TENCENT_LBS_KEY',
label: '腾讯位置服务 Key',
@@ -247,6 +334,23 @@ export const SYSTEM_CONFIG_RETIRED_KEYS = [
export const SYSTEM_CONFIG_KEY_SET = new Set(SYSTEM_CONFIG_FIELDS.map((f) => f.key));
/** 表单空值时展示 / 启动补种的默认值(与 shared-types 常量对齐) */
export const SYSTEM_CONFIG_DEFAULTS: Record<string, string> = {
USER_H5_URL: DEFAULT_USER_H5_URL.replace(/\/$/, ''),
BRAND_LOGO_OSS_BASE: BRAND_LOGO_OSS_BASE,
BRAND_LOGO_URL: BRAND_LOGO_URL,
BRAND_LOGO_WIDE_URL: BRAND_LOGO_WIDE_URL,
BRAND_LOGO_MARK_URL: BRAND_LOGO_MARK_URL,
MINI_USER_STATIC_OSS_BASE: MINI_USER_STATIC_OSS_BASE,
QUALIFICATION_DISCLOSURE_URL: QUALIFICATION_DISCLOSURE_URL,
CUSTOMER_SERVICE_PHONE: CUSTOMER_SERVICE_PHONE,
MOCK_SMS_FIXED_CODE: MOCK_SMS_FIXED_CODE,
};
export function getSystemConfigField(key: string): SystemConfigFieldMeta | undefined {
return SYSTEM_CONFIG_FIELDS.find((f) => f.key === key);
}
export function getSystemConfigDefault(key: string): string {
return SYSTEM_CONFIG_DEFAULTS[key] ?? '';
}
@@ -8,6 +8,7 @@ import type {
import { loadAppConfig, parseMiniHomeBanners, serializeMiniHomeBanners } from '@dukang/shared-types';
import { PrismaService } from '../prisma/prisma.module';
import {
SYSTEM_CONFIG_DEFAULTS,
SYSTEM_CONFIG_FIELDS,
SYSTEM_CONFIG_GROUPS,
SYSTEM_CONFIG_KEY_SET,
@@ -42,6 +43,7 @@ export class SystemConfigService implements OnModuleInit {
try {
await this.purgeRetiredKeys();
await this.seedMissingFromProcessEnv();
await this.seedMissingDefaults();
const rows = await this.prisma.systemConfig.findMany();
applyEnvOverlay(Object.fromEntries(rows.map((r) => [r.configKey, r.value])));
this.lastUpdatedAt = rows.reduce<Date | null>(
@@ -82,7 +84,7 @@ export class SystemConfigService implements OnModuleInit {
for (const field of fields) {
const fromDb = dbMap.get(field.key);
const fromEnv = process.env[field.key];
const raw = fromDb ?? fromEnv ?? '';
const raw = (fromDb ?? fromEnv ?? SYSTEM_CONFIG_DEFAULTS[field.key] ?? '').trim();
if (field.secret) {
if (raw) configuredSecrets.push(field.key);
values[field.key] = '';
@@ -227,6 +229,26 @@ export class SystemConfigService implements OnModuleInit {
}
}
/** 将代码默认值写入空缺配置,便于 HQ 表单可见、可改 */
private async seedMissingDefaults() {
const overlay: Record<string, string> = {};
for (const field of SYSTEM_CONFIG_FIELDS) {
const defaultVal = SYSTEM_CONFIG_DEFAULTS[field.key];
if (!defaultVal?.trim()) continue;
const existing = await this.prisma.systemConfig.findUnique({ where: { configKey: field.key } });
if (existing?.value?.trim()) continue;
if (process.env[field.key]?.trim()) continue;
const value = this.normalizeByMeta(field, defaultVal);
await this.prisma.systemConfig.upsert({
where: { configKey: field.key },
create: { configKey: field.key, value },
update: { value },
});
overlay[field.key] = value;
}
if (Object.keys(overlay).length) applyEnvOverlay(overlay);
}
private normalizeByMeta(meta: { key?: string; type: string }, raw: string): string {
if (meta.type === 'boolean') {
return raw === 'true' || raw === '1' ? 'true' : 'false';
@@ -1,8 +1,9 @@
import { Injectable, Logger } from '@nestjs/common';
import { resolveMockSmsFixedCode } from '@dukang/shared-types';
import { PrismaService } from '../../common/prisma/prisma.module';
import { MockSmsCodeService } from '../../common/mock-sms-code/mock-sms-code.service';
import type { ISmsProvider, SmsActorRef, SmsSendResult } from './sms.interface';
import { MOCK_SMS_FIXED_CODE, SmsCodeStore } from './sms-code.store';
import { SmsCodeStore } from './sms-code.store';
function maskPhone(phone: string) {
return `${phone.slice(0, 3)}****${phone.slice(-4)}`;
@@ -19,7 +20,8 @@ export class SmsMockProvider implements ISmsProvider {
) {}
async send(phone: string, scene: string, actorRef?: SmsActorRef): Promise<SmsSendResult> {
const code = await this.smsCodeStore.storeCode(phone, scene, MOCK_SMS_FIXED_CODE);
const fixedCode = resolveMockSmsFixedCode(process.env);
const code = await this.smsCodeStore.storeCode(phone, scene, fixedCode);
await this.mockSmsCodeService.record(phone, scene, code);
const masked = maskPhone(phone);
this.logger.log(`Mock SMS → ${masked} scene=${scene} code=${code}`);
@@ -1,5 +1,5 @@
import { Controller, Get } from '@nestjs/common';
import { parseMiniHomeBanners } from '@dukang/shared-types';
import { parseMiniHomeBanners, resolveClientBrandRuntime } from '@dukang/shared-types';
import { SystemConfigService } from '../../common/system-config/system-config.service';
@Controller('common')
@@ -12,6 +12,7 @@ export class ClientConfigController {
const env = this.systemConfig.getMergedEnv();
const footer = (env.MINI_HOME_FOOTER_URL ?? '').trim();
const minClientVersion = (env.MINI_USER_MIN_VERSION ?? '').trim() || null;
const brand = resolveClientBrandRuntime(env);
return {
mockPay: cfg.mockPay,
wechatPayEnabled: cfg.wechatPayEnabled,
@@ -25,6 +26,12 @@ export class ClientConfigController {
banners: parseMiniHomeBanners(env.MINI_HOME_BANNERS),
footerUrl: footer || null,
},
userH5Url: brand.userH5Url,
brandLogoUrl: brand.brandLogoUrl,
brandLogoWideUrl: brand.brandLogoWideUrl,
brandLogoMarkUrl: brand.brandLogoMarkUrl,
qualificationDisclosureUrl: brand.qualificationDisclosureUrl,
customerServicePhone: brand.customerServicePhone,
};
}
}