From ed1845c0ba98c2e1d6d80d5bf4eec2b46cdf0952 Mon Sep 17 00:00:00 2001 From: Jacy <18049821889@163.com> Date: Tue, 30 Jun 2026 23:44:31 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E5=B0=8F=E9=A3=9E=E4=BE=A0=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E5=AF=B9=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/dukang-api/.env.example | 10 ++ .../integrations/courier/courier.config.ts | 61 +++++++ .../integrations/courier/courier.constants.ts | 7 + .../src/integrations/courier/courier.error.ts | 11 ++ .../integrations/courier/courier.module.ts | 34 ++++ .../integrations/courier/courier.service.ts | 58 ++++++ .../src/integrations/courier/courier.types.ts | 117 ++++++++++++ .../src/integrations/courier/index.ts | 5 + .../courier/xiaofeixia/xiaofeixia.client.ts | 82 +++++++++ .../xiaofeixia/xiaofeixia.constants.ts | 12 ++ .../courier/xiaofeixia/xiaofeixia.provider.ts | 168 ++++++++++++++++++ .../courier/xiaofeixia/xiaofeixia.sign.ts | 28 +++ .../courier/xiaofeixia/xiaofeixia.types.ts | 57 ++++++ .../src/integrations/integrations.module.ts | 5 +- 14 files changed, 653 insertions(+), 2 deletions(-) create mode 100644 server/dukang-api/src/integrations/courier/courier.config.ts create mode 100644 server/dukang-api/src/integrations/courier/courier.constants.ts create mode 100644 server/dukang-api/src/integrations/courier/courier.error.ts create mode 100644 server/dukang-api/src/integrations/courier/courier.module.ts create mode 100644 server/dukang-api/src/integrations/courier/courier.service.ts create mode 100644 server/dukang-api/src/integrations/courier/courier.types.ts create mode 100644 server/dukang-api/src/integrations/courier/index.ts create mode 100644 server/dukang-api/src/integrations/courier/xiaofeixia/xiaofeixia.client.ts create mode 100644 server/dukang-api/src/integrations/courier/xiaofeixia/xiaofeixia.constants.ts create mode 100644 server/dukang-api/src/integrations/courier/xiaofeixia/xiaofeixia.provider.ts create mode 100644 server/dukang-api/src/integrations/courier/xiaofeixia/xiaofeixia.sign.ts create mode 100644 server/dukang-api/src/integrations/courier/xiaofeixia/xiaofeixia.types.ts diff --git a/server/dukang-api/.env.example b/server/dukang-api/.env.example index abeb435..b96e32d 100644 --- a/server/dukang-api/.env.example +++ b/server/dukang-api/.env.example @@ -8,3 +8,13 @@ MOCK_SMS_CODE=123456 MOCK_PAY=true MOCK_DELIVERY_AUTO=true AUTO_APPROVE_STORE=true + +# 快递中间层(xiaofeixia | sf | jd,目前仅实现 xiaofeixia) +COURIER_PROVIDER=xiaofeixia +# 小飞侠同城快递 +XIAOFEIXIA_API_URL=https://beta.51xiaoju.cn/app/api/interface.do +XIAOFEIXIA_APP_ID= +XIAOFEIXIA_MCH_ID= +XIAOFEIXIA_API_KEY= +# 签名类型:MD5(默认)或 HMAC-SHA256 +XIAOFEIXIA_SIGN_TYPE=MD5 diff --git a/server/dukang-api/src/integrations/courier/courier.config.ts b/server/dukang-api/src/integrations/courier/courier.config.ts new file mode 100644 index 0000000..eab15d3 --- /dev/null +++ b/server/dukang-api/src/integrations/courier/courier.config.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { CourierProviderCode } from './courier.constants'; + +export type XiaofeixiaSignType = 'MD5' | 'HMAC-SHA256'; + +export interface XiaofeixiaConfig { + apiUrl: string; + appId?: string; + mchId: string; + apiKey: string; + signType: XiaofeixiaSignType; +} + +export interface CourierIntegrationConfig { + provider: CourierProviderCode; + xiaofeixia: XiaofeixiaConfig; +} + +@Injectable() +export class CourierConfigService { + constructor(private readonly config: ConfigService) {} + + load(): CourierIntegrationConfig { + const providerRaw = (this.config.get('COURIER_PROVIDER') ?? 'xiaofeixia').toLowerCase(); + const provider = this.resolveProvider(providerRaw); + + return { + provider, + xiaofeixia: { + apiUrl: + this.config.get('XIAOFEIXIA_API_URL') ?? + 'https://beta.51xiaoju.cn/app/api/interface.do', + appId: this.config.get('XIAOFEIXIA_APP_ID') || undefined, + mchId: this.config.get('XIAOFEIXIA_MCH_ID') ?? '', + apiKey: this.config.get('XIAOFEIXIA_API_KEY') ?? '', + signType: this.resolveSignType(this.config.get('XIAOFEIXIA_SIGN_TYPE')), + }, + }; + } + + private resolveProvider(raw: string): CourierProviderCode { + switch (raw) { + case 'xiaofeixia': + return CourierProviderCode.XIAOFEIXIA; + case 'sf': + return CourierProviderCode.SF; + case 'jd': + return CourierProviderCode.JD; + default: + throw new Error(`Unsupported COURIER_PROVIDER: ${raw}`); + } + } + + private resolveSignType(raw?: string): XiaofeixiaSignType { + if (raw?.toUpperCase() === 'HMAC-SHA256') { + return 'HMAC-SHA256'; + } + return 'MD5'; + } +} diff --git a/server/dukang-api/src/integrations/courier/courier.constants.ts b/server/dukang-api/src/integrations/courier/courier.constants.ts new file mode 100644 index 0000000..79d3ecc --- /dev/null +++ b/server/dukang-api/src/integrations/courier/courier.constants.ts @@ -0,0 +1,7 @@ +export const COURIER_PROVIDER = 'COURIER_PROVIDER'; + +export enum CourierProviderCode { + XIAOFEIXIA = 'XIAOFEIXIA', + SF = 'SF', + JD = 'JD', +} diff --git a/server/dukang-api/src/integrations/courier/courier.error.ts b/server/dukang-api/src/integrations/courier/courier.error.ts new file mode 100644 index 0000000..a37aad0 --- /dev/null +++ b/server/dukang-api/src/integrations/courier/courier.error.ts @@ -0,0 +1,11 @@ +export class CourierApiError extends Error { + constructor( + message: string, + readonly code: string, + readonly providerCode?: string, + readonly raw?: unknown, + ) { + super(message); + this.name = 'CourierApiError'; + } +} diff --git a/server/dukang-api/src/integrations/courier/courier.module.ts b/server/dukang-api/src/integrations/courier/courier.module.ts new file mode 100644 index 0000000..c512c02 --- /dev/null +++ b/server/dukang-api/src/integrations/courier/courier.module.ts @@ -0,0 +1,34 @@ +import { Module } from '@nestjs/common'; +import { COURIER_PROVIDER, CourierProviderCode } from './courier.constants'; +import { CourierConfigService } from './courier.config'; +import { CourierService } from './courier.service'; +import { XiaofeixiaClient } from './xiaofeixia/xiaofeixia.client'; +import { XiaofeixiaProvider } from './xiaofeixia/xiaofeixia.provider'; +import type { ICourierProvider } from './courier.types'; + +@Module({ + providers: [ + CourierConfigService, + XiaofeixiaClient, + XiaofeixiaProvider, + { + provide: COURIER_PROVIDER, + useFactory: ( + configService: CourierConfigService, + xiaofeixia: XiaofeixiaProvider, + ): ICourierProvider => { + const { provider } = configService.load(); + switch (provider) { + case CourierProviderCode.XIAOFEIXIA: + return xiaofeixia; + default: + throw new Error(`Courier provider not implemented: ${provider}`); + } + }, + inject: [CourierConfigService, XiaofeixiaProvider], + }, + CourierService, + ], + exports: [CourierService, COURIER_PROVIDER], +}) +export class CourierModule {} diff --git a/server/dukang-api/src/integrations/courier/courier.service.ts b/server/dukang-api/src/integrations/courier/courier.service.ts new file mode 100644 index 0000000..e01acb3 --- /dev/null +++ b/server/dukang-api/src/integrations/courier/courier.service.ts @@ -0,0 +1,58 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { COURIER_PROVIDER } from './courier.constants'; +import type { + BatchShipmentQuery, + CreateShipmentInput, + CreateShipmentResult, + DeliveryCoverageResult, + FreightEstimateResult, + ICourierProvider, + ShipmentDetail, + ShipmentQuery, + TrackCallbackResponse, + TrackNode, +} from './courier.types'; + +/** + * 快递统一门面:业务层只依赖本 Service,不感知具体快递商实现。 + */ +@Injectable() +export class CourierService { + constructor(@Inject(COURIER_PROVIDER) private readonly provider: ICourierProvider) {} + + get activeProvider() { + return this.provider.code; + } + + createShipment(input: CreateShipmentInput): Promise { + return this.provider.createShipment(input); + } + + cancelShipment(query: ShipmentQuery): Promise { + return this.provider.cancelShipment(query); + } + + getShipment(query: ShipmentQuery): Promise { + return this.provider.getShipment(query); + } + + batchGetShipments(query: BatchShipmentQuery): Promise { + return this.provider.batchGetShipments(query); + } + + getTrack(query: ShipmentQuery): Promise { + return this.provider.getTrack(query); + } + + checkDeliveryCoverage(toAddress: string): Promise { + return this.provider.checkDeliveryCoverage(toAddress); + } + + estimateFreight(weight: number): Promise { + return this.provider.estimateFreight(weight); + } + + buildTrackCallbackResponse(success?: boolean): TrackCallbackResponse { + return this.provider.buildTrackCallbackResponse(success); + } +} diff --git a/server/dukang-api/src/integrations/courier/courier.types.ts b/server/dukang-api/src/integrations/courier/courier.types.ts new file mode 100644 index 0000000..4d52d15 --- /dev/null +++ b/server/dukang-api/src/integrations/courier/courier.types.ts @@ -0,0 +1,117 @@ +import { CourierProviderCode } from './courier.constants'; + +export interface CourierCoordinate { + lng: number; + lat: number; +} + +export interface CourierContact { + name: string; + mobile: string; + address: string; + addressDetail: string; + coordinate?: CourierCoordinate; +} + +/** 付费对象:寄付 / 到付 */ +export enum CourierPayMode { + SENDER = '1', + RECEIVER = '2', +} + +export interface CreateShipmentInput { + outNumber: string; + customerId?: string; + from: CourierContact; + to: CourierContact; + goodsName?: string; + goodsNum?: number; + weight?: number; + insuredSumPrice?: number; + collectionPrice?: number; + payMode: CourierPayMode; + remark?: string; +} + +export interface CreateShipmentResult { + providerShipmentId: number; + trackingNumber: string; +} + +export interface ShipmentQuery { + trackingNumber?: string; + outNumber?: string; +} + +export interface BatchShipmentQuery { + trackingNumbers?: string[]; + outNumbers?: string[]; +} + +export interface TrackNode { + trackInfo: string; + createTime: string; + statusName: string; +} + +export interface ShipmentDetail { + trackingNumber: string; + toCarrierName?: string; + toSiteName?: string; + toName: string; + toMobile: string; + toAddress: string; + toAddressDetail?: string; + fromCarrierName?: string; + fromSiteName?: string; + fromName: string; + fromMobile: string; + fromAddress: string; + fromAddressDetail?: string; + weight?: number; + payModeName?: string; + freightPrice?: string; + insuredPrice?: number; + collectionPrice?: number; + sumPrice?: number; + goodsName?: string; + remark?: string; +} + +export interface DeliveryCoverageResult { + arriveTime: string; + siteName: string; + siteId: string; +} + +export interface FreightEstimateResult { + freightPrice: number; +} + +/** 路由变化回调(各快递商 POST 到业务方) */ +export interface TrackCallbackPayload { + outNumber: string; + trackingNumber: string; + status: string; + statusName: string; + trackInfo: string; + createTime: string; +} + +export interface TrackCallbackResponse { + code: string; + message: string; +} + +export interface ICourierProvider { + readonly code: CourierProviderCode; + + createShipment(input: CreateShipmentInput): Promise; + cancelShipment(query: ShipmentQuery): Promise; + getShipment(query: ShipmentQuery): Promise; + batchGetShipments(query: BatchShipmentQuery): Promise; + getTrack(query: ShipmentQuery): Promise; + checkDeliveryCoverage(toAddress: string): Promise; + estimateFreight(weight: number): Promise; + buildTrackCallbackResponse(success?: boolean): TrackCallbackResponse; +} diff --git a/server/dukang-api/src/integrations/courier/index.ts b/server/dukang-api/src/integrations/courier/index.ts new file mode 100644 index 0000000..5aa4130 --- /dev/null +++ b/server/dukang-api/src/integrations/courier/index.ts @@ -0,0 +1,5 @@ +export * from './courier.constants'; +export * from './courier.types'; +export * from './courier.error'; +export * from './courier.service'; +export * from './courier.module'; diff --git a/server/dukang-api/src/integrations/courier/xiaofeixia/xiaofeixia.client.ts b/server/dukang-api/src/integrations/courier/xiaofeixia/xiaofeixia.client.ts new file mode 100644 index 0000000..d3c7417 --- /dev/null +++ b/server/dukang-api/src/integrations/courier/xiaofeixia/xiaofeixia.client.ts @@ -0,0 +1,82 @@ +import { Injectable } from '@nestjs/common'; +import { CourierApiError } from '../courier.error'; +import { CourierConfigService } from '../courier.config'; +import { buildXiaofeixiaSign } from './xiaofeixia.sign'; +import { XIAOFEIXIA_SUCCESS_CODE } from './xiaofeixia.constants'; +import type { XiaofeixiaApiResponse } from './xiaofeixia.types'; + +type RequestParams = Record; + +@Injectable() +export class XiaofeixiaClient { + constructor(private readonly courierConfig: CourierConfigService) {} + + async request(cmd: string, bizParams: RequestParams): Promise { + const cfg = this.courierConfig.load().xiaofeixia; + + if (!cfg.mchId || !cfg.apiKey) { + throw new CourierApiError( + '小飞侠商户配置不完整,请设置 XIAOFEIXIA_MCH_ID 与 XIAOFEIXIA_API_KEY', + 'CONFIG_ERROR', + 'XIAOFEIXIA', + ); + } + + const baseParams: RequestParams = { + mchId: cfg.mchId, + cmd, + signType: cfg.signType, + ...bizParams, + }; + + if (cfg.appId) { + baseParams.appId = cfg.appId; + } + + const sign = buildXiaofeixiaSign(baseParams, cfg.apiKey, cfg.signType); + const body = new URLSearchParams(); + + for (const [key, value] of Object.entries({ ...baseParams, sign })) { + if (value !== undefined && value !== null && value !== '') { + body.append(key, String(value)); + } + } + + let response: Response; + try { + response = await fetch(cfg.apiUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: body.toString(), + }); + } catch (error) { + throw new CourierApiError( + '小飞侠接口网络异常', + '200000', + 'XIAOFEIXIA', + error, + ); + } + + if (!response.ok) { + throw new CourierApiError( + `小飞侠 HTTP 请求失败: ${response.status}`, + '200000', + 'XIAOFEIXIA', + ); + } + + const payload = (await response.json()) as XiaofeixiaApiResponse; + + if (payload.code !== XIAOFEIXIA_SUCCESS_CODE) { + throw new CourierApiError( + payload.message || '小飞侠接口业务失败', + payload.code, + 'XIAOFEIXIA', + payload, + ); + } + + return payload.data as T; + } +} diff --git a/server/dukang-api/src/integrations/courier/xiaofeixia/xiaofeixia.constants.ts b/server/dukang-api/src/integrations/courier/xiaofeixia/xiaofeixia.constants.ts new file mode 100644 index 0000000..0687343 --- /dev/null +++ b/server/dukang-api/src/integrations/courier/xiaofeixia/xiaofeixia.constants.ts @@ -0,0 +1,12 @@ +/** 小飞侠接口 cmd 编号 */ +export const XIAOFEIXIA_CMD = { + CREATE_ORDER: '100101', + TRACK_ROUTE: '100102', + CANCEL_ORDER: '100103', + GET_ORDER: '100104', + ESTIMATE_FREIGHT: '100105', + BATCH_GET_ORDER: '100106', + DELIVERY_COVERAGE: '100301', +} as const; + +export const XIAOFEIXIA_SUCCESS_CODE = '100000'; diff --git a/server/dukang-api/src/integrations/courier/xiaofeixia/xiaofeixia.provider.ts b/server/dukang-api/src/integrations/courier/xiaofeixia/xiaofeixia.provider.ts new file mode 100644 index 0000000..ea67094 --- /dev/null +++ b/server/dukang-api/src/integrations/courier/xiaofeixia/xiaofeixia.provider.ts @@ -0,0 +1,168 @@ +import { Injectable } from '@nestjs/common'; +import { CourierProviderCode } from '../courier.constants'; +import { CourierApiError } from '../courier.error'; +import type { + BatchShipmentQuery, + CreateShipmentInput, + CreateShipmentResult, + DeliveryCoverageResult, + FreightEstimateResult, + ICourierProvider, + ShipmentDetail, + ShipmentQuery, + TrackCallbackResponse, + TrackNode, +} from '../courier.types'; +import { XiaofeixiaClient } from './xiaofeixia.client'; +import { XIAOFEIXIA_CMD } from './xiaofeixia.constants'; +import type { + XiaofeixiaCreateOrderData, + XiaofeixiaDeliveryCoverageData, + XiaofeixiaFreightEstimateData, + XiaofeixiaOrderDetail, + XiaofeixiaTrackNode, +} from './xiaofeixia.types'; + +@Injectable() +export class XiaofeixiaProvider implements ICourierProvider { + readonly code = CourierProviderCode.XIAOFEIXIA; + + constructor(private readonly client: XiaofeixiaClient) {} + + async createShipment(input: CreateShipmentInput): Promise { + const data = await this.client.request(XIAOFEIXIA_CMD.CREATE_ORDER, { + customerId: input.customerId, + outNumber: input.outNumber, + fromAddress: input.from.address, + fromAddressDetail: input.from.addressDetail, + fromCoordinate: this.formatCoordinate(input.from.coordinate), + fromMobile: input.from.mobile, + fromName: input.from.name, + toAddress: input.to.address, + toAddressDetail: input.to.addressDetail, + toCoordinate: this.formatCoordinate(input.to.coordinate), + toMobile: input.to.mobile, + toName: input.to.name, + goodsName: input.goodsName, + goodsNum: input.goodsNum, + weight: input.weight, + insuredSumPrice: input.insuredSumPrice, + collectionPrice: input.collectionPrice, + payMode: input.payMode, + remark: input.remark, + }); + + return { + providerShipmentId: data.id, + trackingNumber: data.number, + }; + } + + async cancelShipment(query: ShipmentQuery): Promise { + this.assertShipmentQuery(query); + await this.client.request(XIAOFEIXIA_CMD.CANCEL_ORDER, { + number: query.trackingNumber, + outNumber: query.outNumber, + }); + } + + async getShipment(query: ShipmentQuery): Promise { + this.assertShipmentQuery(query); + const data = await this.client.request(XIAOFEIXIA_CMD.GET_ORDER, { + number: query.trackingNumber, + outNumber: query.outNumber, + }); + return this.mapOrderDetail(data); + } + + async batchGetShipments(query: BatchShipmentQuery): Promise { + const number = query.trackingNumbers?.join(','); + const outNumber = query.outNumbers?.join(','); + + if (!number && !outNumber) { + throw new CourierApiError('运单号与商家单号至少传一个', '300000', this.code); + } + + const data = await this.client.request(XIAOFEIXIA_CMD.BATCH_GET_ORDER, { + number, + outNumber, + }); + + return (data ?? []).map((item) => this.mapOrderDetail(item)); + } + + async getTrack(query: ShipmentQuery): Promise { + this.assertShipmentQuery(query); + const data = await this.client.request(XIAOFEIXIA_CMD.TRACK_ROUTE, { + number: query.trackingNumber, + outNumber: query.outNumber, + }); + return data ?? []; + } + + async checkDeliveryCoverage(toAddress: string): Promise { + const data = await this.client.request( + XIAOFEIXIA_CMD.DELIVERY_COVERAGE, + { toAddress }, + ); + + return { + arriveTime: data.arriveTime, + siteName: data.name, + siteId: data.id, + }; + } + + async estimateFreight(weight: number): Promise { + const data = await this.client.request( + XIAOFEIXIA_CMD.ESTIMATE_FREIGHT, + { weight }, + ); + + return { freightPrice: data.freightPrice }; + } + + buildTrackCallbackResponse(success = true): TrackCallbackResponse { + return { + code: success ? '100000' : '300000', + message: success ? 'success' : 'fail', + }; + } + + private assertShipmentQuery(query: ShipmentQuery): void { + if (!query.trackingNumber && !query.outNumber) { + throw new CourierApiError('运单号与商家单号至少传一个', '300000', this.code); + } + } + + private formatCoordinate(coordinate?: { lng: number; lat: number }): string | undefined { + if (!coordinate) return undefined; + return JSON.stringify({ lng: coordinate.lng, lat: coordinate.lat }); + } + + private mapOrderDetail(data: XiaofeixiaOrderDetail): ShipmentDetail { + return { + trackingNumber: data.number, + toCarrierName: data.toCarrierName, + toSiteName: data.toSiteName, + toName: data.toName, + toMobile: data.toMobile, + toAddress: data.toAddress, + toAddressDetail: data.toAddressDetail, + fromCarrierName: data.fromCarrierName, + fromSiteName: data.fromSiteName, + fromName: data.fromName, + fromMobile: data.fromMobile, + fromAddress: data.fromAddress, + fromAddressDetail: data.fromAddressDetail, + weight: data.weight, + payModeName: data.payModeName, + freightPrice: data.freightPrice, + insuredPrice: data.insuredPrice, + collectionPrice: data.collectionPrice, + sumPrice: data.sumPrice, + goodsName: data.goodsName, + remark: data.remark, + }; + } +} diff --git a/server/dukang-api/src/integrations/courier/xiaofeixia/xiaofeixia.sign.ts b/server/dukang-api/src/integrations/courier/xiaofeixia/xiaofeixia.sign.ts new file mode 100644 index 0000000..2166378 --- /dev/null +++ b/server/dukang-api/src/integrations/courier/xiaofeixia/xiaofeixia.sign.ts @@ -0,0 +1,28 @@ +import { createHash, createHmac } from 'crypto'; +import type { XiaofeixiaSignType } from '../courier.config'; + +type SignParams = Record; + +function isEmpty(value: unknown): boolean { + return value === undefined || value === null || value === ''; +} + +/** 按 ASCII 字典序拼接并生成签名 */ +export function buildXiaofeixiaSign( + params: SignParams, + apiKey: string, + signType: XiaofeixiaSignType = 'MD5', +): string { + const sortedKeys = Object.keys(params) + .filter((key) => key !== 'sign' && !isEmpty(params[key])) + .sort(); + + const stringA = sortedKeys.map((key) => `${key}=${params[key]}`).join('&'); + const stringSignTemp = `${stringA}&key=${apiKey}`; + + if (signType === 'HMAC-SHA256') { + return createHmac('sha256', apiKey).update(stringSignTemp).digest('hex').toUpperCase(); + } + + return createHash('md5').update(stringSignTemp).digest('hex').toUpperCase(); +} diff --git a/server/dukang-api/src/integrations/courier/xiaofeixia/xiaofeixia.types.ts b/server/dukang-api/src/integrations/courier/xiaofeixia/xiaofeixia.types.ts new file mode 100644 index 0000000..cae088e --- /dev/null +++ b/server/dukang-api/src/integrations/courier/xiaofeixia/xiaofeixia.types.ts @@ -0,0 +1,57 @@ +export interface XiaofeixiaApiResponse { + cmd: string; + code: string; + message: string; + bizCode?: string; + data?: T; +} + +export interface XiaofeixiaCreateOrderData { + id: number; + number: string; +} + +export interface XiaofeixiaTrackNode { + trackInfo: string; + createTime: string; + statusName: string; +} + +export interface XiaofeixiaOrderDetail { + number: string; + toCarrierName?: string; + toSiteName?: string; + toName: string; + toMobile: string; + toAddress: string; + toAddressDetail?: string; + fromCarrierName?: string; + fromSiteName?: string; + fromName: string; + fromMobile: string; + fromAddress: string; + fromAddressDetail?: string; + weight?: number; + payModeName?: string; + freightPrice?: string; + insuredPrice?: number; + collectionPrice?: number; + sumPrice?: number; + goodsName?: string; + remark?: string; +} + +export interface XiaofeixiaDeliveryCoverageData { + arriveTime: string; + name: string; + id: string; +} + +export interface XiaofeixiaFreightEstimateData { + freightPrice: number; +} + +export interface XiaofeixiaCancelOrderData { + id: number; + number: string; +} diff --git a/server/dukang-api/src/integrations/integrations.module.ts b/server/dukang-api/src/integrations/integrations.module.ts index 0e3e27c..481f73f 100644 --- a/server/dukang-api/src/integrations/integrations.module.ts +++ b/server/dukang-api/src/integrations/integrations.module.ts @@ -3,11 +3,12 @@ import { BullModule } from '@nestjs/bullmq'; import { SmsMockProvider } from './sms/sms.mock.provider'; import { PayMockProvider } from './pay/pay.mock.provider'; import { DeliveryMockProvider } from './delivery/delivery.mock.provider'; +import { CourierModule } from './courier/courier.module'; import { SMS_PROVIDER, PAY_PROVIDER, DELIVERY_PROVIDER } from './integrations.constants'; import { DELIVERY_QUEUE } from '../jobs/jobs.constants'; @Module({ - imports: [BullModule.registerQueue({ name: DELIVERY_QUEUE })], + imports: [BullModule.registerQueue({ name: DELIVERY_QUEUE }), CourierModule], providers: [ { provide: SMS_PROVIDER, useClass: SmsMockProvider }, { provide: PAY_PROVIDER, useClass: PayMockProvider }, @@ -16,6 +17,6 @@ import { DELIVERY_QUEUE } from '../jobs/jobs.constants'; PayMockProvider, DeliveryMockProvider, ], - exports: [SMS_PROVIDER, PAY_PROVIDER, DELIVERY_PROVIDER], + exports: [SMS_PROVIDER, PAY_PROVIDER, DELIVERY_PROVIDER, CourierModule], }) export class IntegrationsModule {} From 743d242d52eda18b463bebe7d3cb3c0eb819c735 Mon Sep 17 00:00:00 2001 From: jacy-dukang Date: Sat, 4 Jul 2026 20:36:26 +0800 Subject: [PATCH 2/3] =?UTF-8?q?v3=E8=AE=A1=E5=88=92=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/agents/backend-lead.md | 2 +- .cursor/agents/boundary-reviewer.md | 4 +- .cursor/agents/owner-d-shop-redeem.md | 2 +- .cursor/rules/dukang-core.mdc | 2 +- .cursor/rules/frontend-h5.mdc | 2 +- .cursor/rules/packages-shared.mdc | 4 +- .cursor/skills/dukang-coding/SKILL.md | 4 +- .../skills/dukang-coding/reference-backend.md | 8 +- .../dukang-coding/reference-frontend.md | 8 +- .cursor/skills/dukang-task-card/SKILL.md | 6 +- AGENTS.md | 14 +- agent.md | 4 +- apps/AGENTS.md | 8 +- apps/h5-partner/src/App.tsx | 1 + apps/h5-partner/src/pages/LoginPage.tsx | 48 ++++- conventions.md | 14 +- packages/domain/src/index.test.ts | 22 ++- packages/domain/src/index.ts | 7 +- packages/shared-types/src/enums.ts | 3 +- server/dukang-api/AGENTS.md | 4 +- .../src/modules/redeem/redeem.service.ts | 4 +- skills.md | 4 +- 杜康好客-v3编码手册.md | 169 ++++++++++++++++++ 23 files changed, 291 insertions(+), 53 deletions(-) create mode 100644 杜康好客-v3编码手册.md diff --git a/.cursor/agents/backend-lead.md b/.cursor/agents/backend-lead.md index 5166632..3d401ce 100644 --- a/.cursor/agents/backend-lead.md +++ b/.cursor/agents/backend-lead.md @@ -50,7 +50,7 @@ Never scatter `if (process.env.MOCK_PAY)` in trade/benefit services. 1. Align with V2 manual §五 2. Notify table owner: jacy-dukang 或 刘京尧(store/redeem 相关表) 3. `pnpm db:validate` + seed still works -4. PR: jacy-dukang Review;涉及 store/redeem 表时 @刘京尧 +4. PR: jacy-dukang Review;涉及 store/redeem 表时通知刘京尧 ## Forbidden diff --git a/.cursor/agents/boundary-reviewer.md b/.cursor/agents/boundary-reviewer.md index a3d5f75..65a4d81 100644 --- a/.cursor/agents/boundary-reviewer.md +++ b/.cursor/agents/boundary-reviewer.md @@ -25,8 +25,8 @@ Inspect the diff (or named files) for: | 逻辑域 | Git 账号 | Apps | Modules | |--------|----------|------|---------| -| 主责 | jacy-dukang | h5-user, admin-web | iam, trade, benefit, analytics, catalog, settlement, ops | -| 合伙人+门店 | 刘京尧 | h5-partner, h5-shop | store, redeem | +| 主责 | jacy-dukang | h5-user, admin-web, 门店 | iam, trade, benefit, analytics, catalog, settlement, ops | +| 合伙人 | 刘京尧 | h5-partner, h5-shop | store, redeem | | 横切 | jacy-dukang | — | packages, callbacks, jobs, common, integrations | 逻辑 A/B/C/D 边界仍有效;刘京尧 同时负责 B+D,但 **store 与 redeem 模块仍不可互写表**。 diff --git a/.cursor/agents/owner-d-shop-redeem.md b/.cursor/agents/owner-d-shop-redeem.md index 6a697ee..dbef3c1 100644 --- a/.cursor/agents/owner-d-shop-redeem.md +++ b/.cursor/agents/owner-d-shop-redeem.md @@ -36,7 +36,7 @@ User (Owner A) generates token → Shop scans/confirms (you) ## Hard rules - Redis token: `redeem:token:{token}` TTL 300s -- Amount: `0 < amount ≤ min(balance, 500)` +- Amount: direct redeem `0 < amount ≤ total ACTIVE benefit balance`; document redeem `0 < amount ≤ document balance` - Transaction + coupon `version` optimistic lock - **Never** `prisma.order.update` in redeem module diff --git a/.cursor/rules/dukang-core.mdc b/.cursor/rules/dukang-core.mdc index 070ea54..9f44df1 100644 --- a/.cursor/rules/dukang-core.mdc +++ b/.cursor/rules/dukang-core.mdc @@ -41,7 +41,7 @@ alwaysApply: true ## 核心业务常量 - 权益额 = `benefit_amount ?? price` -- 核销:0 < amount ≤ min(balance, **500**) +- 核销:直接核销 `0 < amount ≤ 全部 ACTIVE 权益总余额`;带单据核销 `0 < amount ≤ 该单据可用金额` - 订单 Tab:`all | pending_pay | pending_ship | pending_receive | completed` - API:`/api/v1`,响应 `{ code, message, data }` diff --git a/.cursor/rules/frontend-h5.mdc b/.cursor/rules/frontend-h5.mdc index 0415a19..5ddef82 100644 --- a/.cursor/rules/frontend-h5.mdc +++ b/.cursor/rules/frontend-h5.mdc @@ -24,7 +24,7 @@ src/ | App | 端口 | 负责人 | 样式 | X-Client-App | |-----|------|--------|------|--------------| | h5-user | 5173 | jacy-dukang | shared-ui tokens | USER_H5 | -| h5-shop | 5174 | 刘京尧 | shared-ui tokens | SHOP_H5 | +| h5-shop | 5174 | jacy-dukang | shared-ui tokens | SHOP_H5 | | h5-partner | 5175 | 刘京尧 | shared-ui tokens | PARTNER_H5 | | admin-web | 5175 | jacy-dukang | Ant Design 5 | HQ_WEB | diff --git a/.cursor/rules/packages-shared.mdc b/.cursor/rules/packages-shared.mdc index ab73ae1..b3f2557 100644 --- a/.cursor/rules/packages-shared.mdc +++ b/.cursor/rules/packages-shared.mdc @@ -24,13 +24,13 @@ alwaysApply: false ## domain - 纯函数,无 IO(无 Prisma/Redis/HTTP) -- 起购 2/6 瓶、权益 `benefitAmount ?? price`、核销 ¥500 上限在此实现 +- 起购 2/6 瓶、权益 `benefitAmount ?? price`、V3 两路径核销规则在此实现 - 变更必须有单元测试 ```typescript const benefitAmount = product.benefitAmount ?? product.price; // 同城起购 2 瓶 / 跨城 6 瓶 -// 核销上限 ¥500 +// 直接核销按全部 ACTIVE 权益总余额;带单据核销按该单据可用金额 ``` ## shared-ui diff --git a/.cursor/skills/dukang-coding/SKILL.md b/.cursor/skills/dukang-coding/SKILL.md index 691858a..c34ef10 100644 --- a/.cursor/skills/dukang-coding/SKILL.md +++ b/.cursor/skills/dukang-coding/SKILL.md @@ -81,14 +81,14 @@ await this.prisma.order.update({ ... }); ## 数据库变更 1. 改 `server/dukang-api/prisma/schema.prisma` 对齐手册 §五 -2. 迁移需 OWNER Review(store/redeem 表 → @刘京尧) +2. 迁移需 OWNER Review(store/redeem 表 → 刘景尧) 3. 初始化 SQL:`server/dukang-api/prisma/init_v3.sql` ## 核心业务(packages/domain) ```typescript const benefitAmount = product.benefitAmount ?? product.price; -// 核销:0 < amount ≤ min(balance, 500) +// 核销:直接核销按全部 ACTIVE 权益总余额;带单据核销按该单据可用金额 // 同城 min 2 瓶 / 跨城 min 6 瓶 // 支付成功 → log_third_party + user_order.pay_status=PAID diff --git a/.cursor/skills/dukang-coding/reference-backend.md b/.cursor/skills/dukang-coding/reference-backend.md index c9e5948..58e3836 100644 --- a/.cursor/skills/dukang-coding/reference-backend.md +++ b/.cursor/skills/dukang-coding/reference-backend.md @@ -36,8 +36,8 @@ Admin 路由在 `modules/ops/` 下,前缀 `/admin/*`。 | catalog | `/catalog`, `/admin/cities`, `/admin/products` | jacy-dukang | | trade | `/trade`, `/partner/orders`, `/admin/orders` | jacy-dukang | | benefit | `/benefit` | jacy-dukang | -| store | `/stores`, `/partner/stores`, `/admin/store-audits` | 刘京尧 | -| redeem | `/redeem`, `/shop/redeem` | 刘京尧 | +| store | `/stores`, `/partner/stores`, `/admin/store-audits` | 刘景尧 | +| redeem | `/redeem`, `/shop/redeem` | 刘景尧 | | settlement | `/settlement`, `/partner/settlement`, `/admin/settlement` | jacy-dukang | | ops | `/admin/dashboard`, `/admin/reports` | jacy-dukang | | analytics | `/analytics`, `/promo/touch` | jacy-dukang | @@ -48,8 +48,8 @@ Admin 路由在 `modules/ops/` 下,前缀 `/admin/*`。 | 前缀 | 示例表 | 负责人 | |------|--------|--------| | user_ | user_user, user_order, user_benefit_coupon | jacy-dukang | -| store_ | store_store, store_account, store_payout | 刘京尧 / jacy-dukang(settlement) | -| partner_ | partner_partner, partner_bill | 刘京尧 / jacy-dukang(settlement) | +| store_ | store_store, store_account, store_payout | 刘景尧 / jacy-dukang(settlement) | +| partner_ | partner_partner, partner_bill | 刘景尧 / jacy-dukang(settlement) | | hq_ | hq_account | jacy-dukang | | common_ | common_product_item, common_event, common_city | jacy-dukang | | log_ | log_third_party, log_user_analytics | 写入方 Module | diff --git a/.cursor/skills/dukang-coding/reference-frontend.md b/.cursor/skills/dukang-coding/reference-frontend.md index 4195ee8..4dcb79b 100644 --- a/.cursor/skills/dukang-coding/reference-frontend.md +++ b/.cursor/skills/dukang-coding/reference-frontend.md @@ -7,8 +7,8 @@ | App | 目录 | 端口 | 负责人 | X-Client-App | 原型 | |-----|------|------|--------|--------------|------| | h5-user | apps/h5-user | 5173 | jacy-dukang | USER_H5 | pages/user/ | -| h5-shop | apps/h5-shop | 5174 | 刘京尧 | SHOP_H5 | pages/shop/ | -| h5-partner | apps/h5-partner | 5175 | 刘京尧 | PARTNER_H5 | pages/partner/ | +| h5-shop | apps/h5-shop | 5174 | 刘景尧 | SHOP_H5 | pages/shop/ | +| h5-partner | apps/h5-partner | 5175 | 刘景尧 | PARTNER_H5 | pages/partner/ | | admin-web | apps/admin-web | 5175 | jacy-dukang | HQ_WEB | pages/hq/ | > **h5-partner 与 admin-web 端口同为 5175**,勿同时 `dev:partner` + `dev:admin`。 @@ -51,7 +51,7 @@ H5 三端引用 `@dukang/shared-ui`(`tokens.css`)。admin-web 使用 Ant Des all | pending_pay | pending_ship | pending_receive | completed ``` -## h5-shop 路由(刘京尧) +## h5-shop 路由(刘景尧) | 路由 | Page | 主要 API | |------|------|----------| @@ -63,7 +63,7 @@ all | pending_pay | pending_ship | pending_receive | completed | /status | StatusPage | /shop/store | | /mine | MinePage | /shop/store | -## h5-partner 路由(刘京尧) +## h5-partner 路由(刘景尧) | 路由 | Page | 主要 API | |------|------|----------| diff --git a/.cursor/skills/dukang-task-card/SKILL.md b/.cursor/skills/dukang-task-card/SKILL.md index 6deacb9..7ca7621 100644 --- a/.cursor/skills/dukang-task-card/SKILL.md +++ b/.cursor/skills/dukang-task-card/SKILL.md @@ -31,8 +31,8 @@ description: >- |------|--------|---------| | C 端 FE、h5-user、交易后端 | jacy-dukang | P1-M1-002, M2-* | | admin、catalog、settlement | jacy-dukang | M1-BE-CAT-* | -| 合伙人、store | 刘京尧 | P1-M4-001 | -| 门店核销、h5-shop | 刘京尧 | P1-M3-002 | +| 合伙人、store | 刘景尧 | P1-M4-001 | +| 门店核销、h5-shop | 刘景尧 | P1-M3-002 | | Monorepo/integrations | jacy-dukang | P1-M0-* | ## preV1 任务卡(精简) @@ -47,7 +47,7 @@ description: >- | P1-M2-001 | 起购校验 2/6 瓶 | | P1-M2-002 | Mock 支付发券 | | P1-M2-003 | 5 Tab 含 pending_ship | -| P1-M3-001 | 核销 ¥500 上限 | +| P1-M3-001 | V3 两路径核销金额限制 | | P1-M3-002 | 门店扫码核销 | | P1-M4-001 | 录店 AUTO_APPROVE → C 端可见 | | P1-M5-001 | 配送自动推进到 COMPLETED | diff --git a/AGENTS.md b/AGENTS.md index deddf75..43fb334 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,16 +54,16 @@ Mock 验证码:`123456`。测试账号见 [`README.md`](./README.md)。 | 负责人 | Git 账号 | 职责 | |--------|----------|------| | **Jacy**(管理员) | `jacy-dukang` | C 端、admin-web、后端主模块、packages、Prisma 迁移主 Review | -| **刘京尧** | `刘京尧` | 合伙人 H5、门店 H5、`store` / `redeem` 模块 | +| **刘景尧** | `刘景尧` | 合伙人 H5、门店 H5、`store` / `redeem` 模块 | 逻辑模块边界仍按 A/B/C/D 划分(便于 Agent 隔离),**人员合并**如下: | 逻辑 OWNER | 负责人 | 可改路径 | 后端 Module | |------------|--------|----------|-------------| | **A + C + Lead** | jacy-dukang | `apps/h5-user/`, `apps/admin-web/`, `packages/*`, `callbacks/`, `jobs/`, `common/`, `integrations/` | `iam`, `trade`, `benefit`, `analytics`, `catalog`, `settlement`, `ops` | -| **B + D** | 刘京尧 | `apps/h5-partner/`, `apps/h5-shop/` | `store`, `redeem` | +| **B + D** | 刘景尧 | `apps/h5-partner/`, `apps/h5-shop/` | `store`, `redeem` | -**Prisma 迁移**:jacy-dukang 主 Review;若改 `store_*` / 核销相关表,需 `@刘京尧` 共同 Review。 +**Prisma 迁移**:jacy-dukang 主 Review;若改 `store_*` / 核销相关表,需刘景尧共同 Review。 ### 跨模块规则(R1–R8 摘要) @@ -91,8 +91,8 @@ Mock 验证码:`123456`。测试账号见 [`README.md`](./README.md)。 | `owner-a-user-trade` | jacy-dukang | C 端 H5、订单、支付、权益、埋点 | | `owner-c-catalog-ops` | jacy-dukang | admin-web、开城商品、结算、运营 | | `backend-lead` | jacy-dukang | packages、callbacks、jobs、Prisma 横切 | -| `owner-b-partner-store` | 刘京尧 | 合伙人 H5、门店 CRUD/审核 | -| `owner-d-shop-redeem` | 刘京尧 | 门店 H5、核销 | +| `owner-b-partner-store` | 刘景尧 | 合伙人 H5、门店 CRUD/审核 | +| `owner-d-shop-redeem` | 刘景尧 | 门店 H5、核销 | | `boundary-reviewer` | — | PR 前只读审查跨模块违规 | ## Skills(按需 @) @@ -116,7 +116,7 @@ Mock 验证码:`123456`。测试账号见 [`README.md`](./README.md)。 ``` 权益额 = benefit_amount ?? price 同城起购 2 瓶 / 跨城 6 瓶 -核销:0 < amount ≤ min(balance, 500);Redis 码 5 分钟 +核销:直接核销 0 < amount ≤ 全部 ACTIVE 权益总余额;带单据核销 0 < amount ≤ 该单据可用金额;Redis 码 5 分钟 C 端门店仅 status=OPEN 订单 Tab:all | pending_pay | pending_ship | pending_receive | completed ``` @@ -124,7 +124,7 @@ C 端门店仅 status=OPEN ## 提交与 PR - Conventional Commits:`feat(trade):`、`fix(redeem):`;scope = 端或模块 -- 跨模块 PR → jacy-dukang + 刘京尧 共同 Review(若涉及双方模块) +- 跨模块 PR → jacy-dukang + 刘景尧 共同 Review(若涉及双方模块) - 改 API/表 → 同步 V2 手册 §五/§六 + `shared-types` - 不提交 `.env`、`dist/`、`node_modules/` diff --git a/agent.md b/agent.md index 6e85a58..ff0be89 100644 --- a/agent.md +++ b/agent.md @@ -81,7 +81,7 @@ | 负责人 | Git 账号 | App(preV1 → V2) | Module | |--------|----------|-------------------|--------| | Jacy | jacy-dukang | h5-user、admin-web → mini-user/mini-hq | iam, trade, benefit, analytics, catalog, settlement, ops | -| 刘京尧 | 刘京尧 | h5-partner、h5-shop → mini-partner | store, redeem | +| 刘景尧 | 刘景尧 | h5-partner、h5-shop → mini-partner | store, redeem | | Jacy(横切) | jacy-dukang | packages/*, callbacks/, jobs/, integrations/ | 基础设施 | **禁止**:Module A 直写 Module B 的 Prisma 表;apps import server 源码。 @@ -93,7 +93,7 @@ ```text 权益发放额 = common_product_item.benefit_amount ?? price 同城起购 2 瓶 / 跨城 6 瓶 -核销:0 < amount ≤ min(balance, 500) +核销:直接核销 0 < amount ≤ 全部 ACTIVE 权益总余额;带单据核销 0 < amount ≤ 该单据可用金额 C 端门店列表仅 status=OPEN 订单 Tab:all | pending_pay | pending_ship | pending_receive | completed 支付回调幂等 → 发券 → common_event(BENEFIT_LEDGER, GRANT) diff --git a/apps/AGENTS.md b/apps/AGENTS.md index 59250e4..68ed792 100644 --- a/apps/AGENTS.md +++ b/apps/AGENTS.md @@ -7,8 +7,8 @@ | App | 端口 | X-Client-App | 负责人 | 原型 | |-----|------|--------------|--------|------| | `h5-user` | 5173 | `USER_H5` | jacy-dukang | `pages/user/` | -| `h5-shop` | 5174 | `SHOP_H5` | 刘京尧 | `pages/shop/` | -| `h5-partner` | 5175 | `PARTNER_H5` | 刘京尧 | `pages/partner/` | +| `h5-shop` | 5174 | `SHOP_H5` | 刘景尧 | `pages/shop/` | +| `h5-partner` | 5175 | `PARTNER_H5` | 刘景尧 | `pages/partner/` | | `admin-web` | — | (Admin JWT) | jacy-dukang | preV1 内部 HQ 替代 | V2 目标:`mini-user` / `mini-partner` / `mini-hq` 替换对应 H5(除门店仍 H5)。 @@ -36,13 +36,13 @@ V2 目标:`mini-user` / `mini-partner` / `mini-hq` 替换对应 H5(除门店 **勿改**:门店核销确认 UI(属 h5-shop)、合伙人录店(属 h5-partner) -### h5-shop(刘京尧) +### h5-shop(刘景尧) 主链路:门店登录 → 首页 → 扫码/输入核销 → 确认 → 记录 → 营业状态 **勿改**:C 端出码页面(属 h5-user) -### h5-partner(刘京尧) +### h5-partner(刘景尧) 主链路:登录 → 工作台 → 录店 → 门店列表 → 辖区订单 → Mock 推进配送(preV1) diff --git a/apps/h5-partner/src/App.tsx b/apps/h5-partner/src/App.tsx index 9f4f4c7..a14e454 100644 --- a/apps/h5-partner/src/App.tsx +++ b/apps/h5-partner/src/App.tsx @@ -22,6 +22,7 @@ function WechatOAuthHandler() { useEffect(() => { if (!isWechatEnv() || !location.search.includes('code=')) return; + if (location.pathname === '/login') return; void handlePartnerWechatCallback() .then((result) => { if (!result || !savePartnerWechatAuth(result)) return; diff --git a/apps/h5-partner/src/pages/LoginPage.tsx b/apps/h5-partner/src/pages/LoginPage.tsx index f1641e4..2f306e8 100644 --- a/apps/h5-partner/src/pages/LoginPage.tsx +++ b/apps/h5-partner/src/pages/LoginPage.tsx @@ -1,6 +1,12 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { request, saveAuth } from '../lib/api'; +import { + authorizePartnerWechat, + handlePartnerWechatCallback, + savePartnerWechatAuth, +} from '../lib/wechat-auth'; +import { isWechatEnv } from '../lib/weixin'; export default function LoginPage() { const navigate = useNavigate(); @@ -9,7 +15,21 @@ export default function LoginPage() { const [phone, setPhone] = useState('13700000001'); const [code, setCode] = useState('123456'); const [loading, setLoading] = useState(false); + const [wechatLoading, setWechatLoading] = useState(false); const [codeCooldown, setCodeCooldown] = useState(0); + const [msg, setMsg] = useState(''); + + useEffect(() => { + if (!isWechatEnv()) return; + void handlePartnerWechatCallback() + .then((result) => { + if (!result) return; + if (savePartnerWechatAuth(result)) { + navigate('/'); + } + }) + .catch((e) => setMsg(e instanceof Error ? e.message : '微信登录失败')); + }, [navigate]); async function login() { setLoading(true); @@ -29,6 +49,21 @@ export default function LoginPage() { } } + async function wechatLogin() { + setMsg(''); + if (!isWechatEnv()) { + setMsg('请在微信内打开以使用微信一键登录'); + return; + } + setWechatLoading(true); + try { + await authorizePartnerWechat(); + } catch (e) { + setMsg(e instanceof Error ? e.message : '微信登录失败'); + setWechatLoading(false); + } + } + function sendCode() { if (codeCooldown > 0) return; request('PARTNER_H5', '/partner/auth/sms/send', { @@ -122,10 +157,17 @@ export default function LoginPage() { {!loading && arrow_forward}
其他登录方式
- + {msg &&

{msg}

}