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:
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user