代下单功能
This commit is contained in:
@@ -18,12 +18,19 @@ export interface AppConfig {
|
||||
aliyunSmsTemplateCode: string;
|
||||
/** 核销确认短信模板(REDEEM_PHONE_CONFIRM);未配置时回退 aliyunSmsTemplateCode */
|
||||
aliyunSmsRedeemConfirmTemplateCode: string;
|
||||
/** 合伙人代下单短信模板(PARTNER_PROXY_ORDER / 线下代发货);未配置时回退 aliyunSmsTemplateCode */
|
||||
aliyunSmsProxyOrderTemplateCode: string;
|
||||
aliyunSmsAccessKeyId: string;
|
||||
aliyunSmsAccessKeySecret: string;
|
||||
/** 腾讯位置服务 Key(逆地理编码) */
|
||||
tencentLbsKey: string;
|
||||
/** C 端 H5 落地页(推广码二维码链接前缀) */
|
||||
userH5Url: string;
|
||||
}
|
||||
|
||||
/** 推广码 / C 端 H5 默认落地页(未配置 USER_H5_URL 时使用) */
|
||||
export const DEFAULT_USER_H5_URL = 'https://user.runxian.top/user';
|
||||
|
||||
/** 总部客服电话(C 端联系客服) */
|
||||
export const CUSTOMER_SERVICE_PHONE = '400-888-1234';
|
||||
|
||||
@@ -48,8 +55,10 @@ export function loadAppConfig(env?: Record<string, string | undefined>): AppConf
|
||||
aliyunSmsSignName: e.ALIYUN_SMS_SIGN_NAME ?? '',
|
||||
aliyunSmsTemplateCode: e.ALIYUN_SMS_TEMPLATE_CODE ?? '',
|
||||
aliyunSmsRedeemConfirmTemplateCode: e.ALIYUN_SMS_REDEEM_CONFIRM_TEMPLATE_CODE ?? '',
|
||||
aliyunSmsProxyOrderTemplateCode: e.ALIYUN_SMS_PROXY_ORDER_TEMPLATE_CODE ?? '',
|
||||
aliyunSmsAccessKeyId: e.ALIYUN_SMS_ACCESS_KEY_ID ?? e.OSS_ACCESS_KEY_ID ?? '',
|
||||
aliyunSmsAccessKeySecret: e.ALIYUN_SMS_ACCESS_KEY_SECRET ?? e.OSS_ACCESS_KEY_SECRET ?? '',
|
||||
tencentLbsKey: e.TENCENT_LBS_KEY ?? '',
|
||||
userH5Url: (e.USER_H5_URL || DEFAULT_USER_H5_URL).replace(/\/$/, ''),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,6 +15,25 @@ export enum ActorType {
|
||||
HQ = 'HQ',
|
||||
}
|
||||
|
||||
/** 用户注册/首次归因来源(对应 user_user.source_type) */
|
||||
export enum UserSourceType {
|
||||
ORGANIC = 'ORGANIC',
|
||||
PROMO_CODE = 'PROMO_CODE',
|
||||
SHARE_LINK = 'SHARE_LINK',
|
||||
FRIEND_REFERRAL = 'FRIEND_REFERRAL',
|
||||
OFFLINE_EVENT = 'OFFLINE_EVENT',
|
||||
OTHER = 'OTHER',
|
||||
}
|
||||
|
||||
export const USER_SOURCE_TYPE_LABELS: Record<UserSourceType, string> = {
|
||||
[UserSourceType.ORGANIC]: '自然流量',
|
||||
[UserSourceType.PROMO_CODE]: '推广码',
|
||||
[UserSourceType.SHARE_LINK]: '分享链接',
|
||||
[UserSourceType.FRIEND_REFERRAL]: '好友推荐',
|
||||
[UserSourceType.OFFLINE_EVENT]: '线下活动',
|
||||
[UserSourceType.OTHER]: '其他',
|
||||
};
|
||||
|
||||
export enum SmsScene {
|
||||
USER_LOGIN = 'USER_LOGIN',
|
||||
STORE_LOGIN = 'STORE_LOGIN',
|
||||
@@ -27,8 +46,22 @@ export enum SmsScene {
|
||||
REDEEM_PHONE_LOOKUP = 'REDEEM_PHONE_LOOKUP',
|
||||
/** 门店手机号核销:核销确认验证码(阿里云模板「核销确认」) */
|
||||
REDEEM_PHONE_CONFIRM = 'REDEEM_PHONE_CONFIRM',
|
||||
/** 合伙人代下单:线下代发货确认验证码(发至用户手机) */
|
||||
PARTNER_PROXY_ORDER = 'PARTNER_PROXY_ORDER',
|
||||
}
|
||||
|
||||
export enum OrderType {
|
||||
NORMAL = 'NORMAL',
|
||||
RESHIPMENT = 'RESHIPMENT',
|
||||
PROXY = 'PROXY',
|
||||
}
|
||||
|
||||
export const ORDER_TYPE_LABELS: Record<OrderType, string> = {
|
||||
[OrderType.NORMAL]: '普通订单',
|
||||
[OrderType.RESHIPMENT]: '补发订单',
|
||||
[OrderType.PROXY]: '线下代下单',
|
||||
};
|
||||
|
||||
export enum OrderStatus {
|
||||
PENDING_PAY = 'PENDING_PAY',
|
||||
PENDING_SHIP = 'PENDING_SHIP',
|
||||
|
||||
@@ -1,24 +1,87 @@
|
||||
export type PromoCodeStatus = 'ACTIVE' | 'DISABLED';
|
||||
|
||||
export type PromoCodeScene =
|
||||
| 'ONLINE_LINK'
|
||||
| 'OFFLINE_PICKUP'
|
||||
| 'PARTNER_CHANNEL'
|
||||
| 'EVENT'
|
||||
| 'OTHER';
|
||||
|
||||
export const PROMO_CODE_SCENE_LABELS: Record<PromoCodeScene, string> = {
|
||||
ONLINE_LINK: '线上链接',
|
||||
OFFLINE_PICKUP: '现场提货',
|
||||
PARTNER_CHANNEL: '合伙人渠道',
|
||||
EVENT: '活动品鉴',
|
||||
OTHER: '其他',
|
||||
};
|
||||
|
||||
export const PROMO_CODE_STATUS_LABELS: Record<PromoCodeStatus, string> = {
|
||||
ACTIVE: '启用',
|
||||
DISABLED: '已关闭',
|
||||
};
|
||||
|
||||
export type PromoCodeOwnerUser = {
|
||||
id: string;
|
||||
userNo?: string | null;
|
||||
nickname?: string | null;
|
||||
phone?: string | null;
|
||||
};
|
||||
|
||||
export type PromoCodeItem = {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
scene: PromoCodeScene;
|
||||
qrcodeId: string;
|
||||
status: PromoCodeStatus;
|
||||
scanCount: number;
|
||||
orderCount: number;
|
||||
landingUrl: string;
|
||||
qrcodeUrl?: string | null;
|
||||
remark?: string | null;
|
||||
ownerUser?: PromoCodeOwnerUser | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type PromoCodeStats = {
|
||||
scanCount: number;
|
||||
orderCount: number;
|
||||
conversionRate: number;
|
||||
attributionCount?: number;
|
||||
/** 用户表 source_ref_id 指向本推广码的用户数 */
|
||||
sourceMarkedCount?: number;
|
||||
};
|
||||
|
||||
export type PromoCodeAttributedUser = {
|
||||
id: string;
|
||||
userNo: string;
|
||||
nickname: string | null;
|
||||
phone: string | null;
|
||||
phoneVerifiedAt: string | null;
|
||||
sourceType: string;
|
||||
sourceRefId: string | null;
|
||||
firstTouchAt: string | null;
|
||||
orderCount: number;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type PromoTouchResult = {
|
||||
promoCode: string;
|
||||
promoCodeId: string;
|
||||
channelName: string;
|
||||
/** 是否首次写入 user_promo_attribution */
|
||||
attributed: boolean;
|
||||
/** 是否已将用户来源标记为 PROMO_CODE */
|
||||
sourceApplied: boolean;
|
||||
};
|
||||
|
||||
export function promoConversion(scan: number, orders: number): string {
|
||||
if (scan <= 0) return '0%';
|
||||
return `${Math.round((orders / scan) * 1000) / 10}%`;
|
||||
}
|
||||
|
||||
export function buildPromoLandingUrl(baseUrl: string, code: string, qrcodeId: string): string {
|
||||
const base = baseUrl.replace(/\/$/, '');
|
||||
return `${base}/?promo=${encodeURIComponent(code)}&pid=${encodeURIComponent(qrcodeId)}`;
|
||||
}
|
||||
|
||||
@@ -33,3 +33,50 @@ export interface DeliveryDto {
|
||||
shippingAt?: string;
|
||||
deliveredAt?: string;
|
||||
}
|
||||
|
||||
export type PartnerProxyOrderProductOption = {
|
||||
id: string;
|
||||
name: string;
|
||||
spec: string;
|
||||
price: number;
|
||||
benefitAmount: number | null;
|
||||
};
|
||||
|
||||
export type PartnerProxyOrderPromoOption = {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type PartnerProxyOrderOptions = {
|
||||
products: PartnerProxyOrderProductOption[];
|
||||
promoCodes: PartnerProxyOrderPromoOption[];
|
||||
};
|
||||
|
||||
export type PartnerProxyOrderPreviewRequest = {
|
||||
productId: string;
|
||||
quantity: number;
|
||||
receiverCity?: string;
|
||||
receiverDistrict?: string;
|
||||
};
|
||||
|
||||
export type PartnerProxyOrderPreviewResult = {
|
||||
productAmount: number;
|
||||
payAmount: number;
|
||||
benefitAmount: number;
|
||||
deliveryType: 'LOCAL' | 'CROSS_CITY';
|
||||
unitPrice: number;
|
||||
};
|
||||
|
||||
export type PartnerProxyOrderCreateRequest = {
|
||||
phone: string;
|
||||
smsCode: string;
|
||||
receiverName?: string;
|
||||
province: string;
|
||||
city: string;
|
||||
district: string;
|
||||
addressDetail: string;
|
||||
productId: string;
|
||||
quantity: number;
|
||||
promoCodeId?: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user