@@ -238,13 +213,45 @@ const ActivityDetail: React.FC = () => {
pagination={{ current: wdPage, total: wdTotal, pageSize: 10, onChange: setWdPage }} />
),
},
+ {
+ key: 'config',
+ label: '活动配置',
+ children: (
+
-
-
-
+
邀请返现管理
@@ -255,4 +262,4 @@ const ActivityDetail: React.FC = () => {
);
};
-export default ActivityDetail;
+export default InviteManage;
\ No newline at end of file
diff --git a/apps/platform-admin/src/pages/SystemSettings.tsx b/apps/platform-admin/src/pages/SystemSettings.tsx
index 72ad8de..036575c 100644
--- a/apps/platform-admin/src/pages/SystemSettings.tsx
+++ b/apps/platform-admin/src/pages/SystemSettings.tsx
@@ -1,11 +1,88 @@
-import React from 'react';
-import { Empty } from 'antd';
+import React, { useEffect, useState } from 'react';
+import { Card, Form, InputNumber, Button, message, Spin, Divider } from 'antd';
+import { getServiceFeeConfig, updateServiceFeeConfig } from '@/api/config';
-const SystemSettings: React.FC = () => (
-
-
系统设置
-
-
-);
+const SystemSettings: React.FC = () => {
+ const [loading, setLoading] = useState(false);
+ const [saving, setSaving] = useState(false);
+ const [form] = Form.useForm();
-export default SystemSettings;
+ const fetchConfig = async () => {
+ setLoading(true);
+ try {
+ const res: any = await getServiceFeeConfig();
+ form.setFieldsValue({ serviceFeeRate: res.data?.rate || 0.05 });
+ } catch (e) {
+ form.setFieldsValue({ serviceFeeRate: 0.05 });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => { fetchConfig(); }, []);
+
+ const handleSave = async () => {
+ try {
+ const values = await form.validateFields();
+ setSaving(true);
+ await updateServiceFeeConfig(values.serviceFeeRate);
+ message.success('服务费配置已保存');
+ fetchConfig();
+ } catch (e: any) {
+ if (e?.message) message.error(e.message);
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ if (loading) return
;
+
+ return (
+
+
系统设置
+
+
+
+ `${(Number(v) * 100).toFixed(2)}%`}
+ parser={(v) => Number(v?.replace('%', '')) / 100}
+ />
+
+
+
+
+
+
+
+
+
+
计算公式:
+
软件服务费 = 实付金额 × 服务费比例
+
商家预计收入 = 实付金额 - 软件服务费
+
+ 示例:实付金额 ¥100.00,服务费比例 5%
+
+
软件服务费 = ¥5.00,商家预计收入 = ¥95.00
+
+
+
+ );
+};
+
+export default SystemSettings;
\ No newline at end of file
diff --git a/apps/platform-admin/src/pages/activity/ActivityList.tsx b/apps/platform-admin/src/pages/activity/ActivityList.tsx
deleted file mode 100644
index 27ba543..0000000
--- a/apps/platform-admin/src/pages/activity/ActivityList.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { Card, Table, Button, Tag, Space, message } from 'antd';
-import { useNavigate } from 'react-router-dom';
-import { getActivities, toggleActivity } from '@/api/activity';
-import dayjs from 'dayjs';
-
-const ACTIVITY_TYPES: Record
= {
- invite_cashback: '邀请返现',
-};
-
-const ActivityList: React.FC = () => {
- const navigate = useNavigate();
- const [list, setList] = useState([]);
- const [total, setTotal] = useState(0);
- const [page, setPage] = useState(1);
- const [loading, setLoading] = useState(false);
-
- const fetchList = async () => {
- setLoading(true);
- try {
- const res = await getActivities({ page, pageSize: 10 });
- setList(res.data?.list || []);
- setTotal(res.data?.total || 0);
- } finally {
- setLoading(false);
- }
- };
-
- useEffect(() => { fetchList(); }, [page]);
-
- const handleToggle = async (record: any) => {
- try {
- await toggleActivity(record.id);
- message.success(record.enabled ? '已停用' : '已启用');
- fetchList();
- } catch {}
- };
-
- const columns = [
- { title: 'ID', dataIndex: 'id', width: 60 },
- { title: '活动名称', dataIndex: 'name' },
- { title: '活动类型', dataIndex: 'type', render: (v: string) => ACTIVITY_TYPES[v] || v },
- { title: '状态', dataIndex: 'enabled', render: (v: boolean) => {v ? '启用' : '停用'} },
- {
- title: '活动时间',
- render: (_: any, r: any) => {
- if (!r.startTime) return '不限';
- return `${dayjs(r.startTime).format('YYYY-MM-DD')} ~ ${dayjs(r.endTime).format('YYYY-MM-DD')}`;
- },
- },
- { title: '创建时间', dataIndex: 'createdAt', render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' },
- {
- title: '操作',
- render: (_: any, r: any) => (
-
-
-
-
- ),
- },
- ];
-
- return (
- navigate('/activity/detail/create')}>创建活动}>
-
-
- );
-};
-
-export default ActivityList;
diff --git a/apps/platform-admin/src/pages/finance/Earnings.tsx b/apps/platform-admin/src/pages/finance/Earnings.tsx
index a33303f..46c3035 100644
--- a/apps/platform-admin/src/pages/finance/Earnings.tsx
+++ b/apps/platform-admin/src/pages/finance/Earnings.tsx
@@ -100,19 +100,8 @@ const Earnings: React.FC = () => {
-
-
-
-
- {
const columns: ColumnsType = [
{ title: '商家', dataIndex: ['merchant', 'shopName'], width: 120, ellipsis: true },
{ title: '提现金额', dataIndex: 'amount', width: 120, render: (v) => `¥${Number(v).toFixed(2)}` },
- { title: '手续费', dataIndex: 'fee', width: 90, render: (v) => `¥${Number(v).toFixed(2)}` },
- { title: '佣金', dataIndex: 'commissionAmount', width: 90, render: (v) => `¥${Number(v).toFixed(2)}` },
{ title: '实际到账', dataIndex: 'actualAmount', width: 120, render: (v) => ¥{Number(v).toFixed(2)} },
{ title: '开户银行', dataIndex: 'bankName', width: 120 },
{ title: '银行账号', dataIndex: 'bankAccount', width: 150 },
diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts
index 5a4992f..3ec8f7d 100644
--- a/apps/server/src/app.module.ts
+++ b/apps/server/src/app.module.ts
@@ -16,6 +16,7 @@ import { AdminAuthModule } from './modules/admin-auth/admin-auth.module';
import { ReviewModule } from './modules/review/review.module';
import { FinanceModule } from './modules/finance/finance.module';
import { ActivityModule } from './modules/activity/activity.module';
+import { PlatformConfigModule } from './modules/config/config.module';
import { ScheduleModule as TaskScheduleModule } from './schedule/schedule.module';
@Module({
@@ -53,6 +54,7 @@ import { ScheduleModule as TaskScheduleModule } from './schedule/schedule.module
RoomCalendarModule,
OrderModule,
AdminAuthModule,
+ PlatformConfigModule,
],
})
export class AppModule {}
diff --git a/apps/server/src/entities/order.entity.ts b/apps/server/src/entities/order.entity.ts
index e474419..92463f1 100644
--- a/apps/server/src/entities/order.entity.ts
+++ b/apps/server/src/entities/order.entity.ts
@@ -44,6 +44,12 @@ export class Order {
@Column({ name: 'room_amount', type: 'decimal', precision: 10, scale: 2, unsigned: true, comment: '房费总额' })
roomAmount: number;
+ @Column({ name: 'service_fee', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0, comment: '软件服务费' })
+ serviceFee: number;
+
+ @Column({ name: 'merchant_income', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0, comment: '商家预计收入' })
+ merchantIncome: number;
+
@Column({ name: 'coupon_discount', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0, comment: '优惠券抵扣' })
couponDiscount: number;
diff --git a/apps/server/src/entities/platform-config.entity.ts b/apps/server/src/entities/platform-config.entity.ts
new file mode 100644
index 0000000..41d08bd
--- /dev/null
+++ b/apps/server/src/entities/platform-config.entity.ts
@@ -0,0 +1,22 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('platform_configs')
+export class PlatformConfig {
+ @PrimaryGeneratedColumn({ type: 'bigint', unsigned: true })
+ id: number;
+
+ @Column({ name: 'config_key', length: 50, unique: true, comment: '配置键' })
+ configKey: string;
+
+ @Column({ name: 'config_value', length: 500, comment: '配置值' })
+ configValue: string;
+
+ @Column({ length: 200, nullable: true, comment: '配置说明' })
+ description: string;
+
+ @CreateDateColumn({ comment: '创建时间' })
+ createdAt: Date;
+
+ @UpdateDateColumn({ comment: '更新时间' })
+ updatedAt: Date;
+}
\ No newline at end of file
diff --git a/apps/server/src/modules/activity/activity.controller.ts b/apps/server/src/modules/activity/activity.controller.ts
index 296d7b6..52a8b51 100644
--- a/apps/server/src/modules/activity/activity.controller.ts
+++ b/apps/server/src/modules/activity/activity.controller.ts
@@ -124,6 +124,18 @@ export class AdminActivityController {
return this.activityService.getAdminStats();
}
+ @Get('invite/config')
+ @ApiOperation({ summary: '获取邀请活动配置' })
+ async getInviteConfig() {
+ return this.activityService.getInviteCashbackConfig();
+ }
+
+ @Put('invite/config')
+ @ApiOperation({ summary: '更新邀请活动配置' })
+ async updateInviteConfig(@Body() dto: UpdateActivityDto) {
+ return this.activityService.updateInviteCashbackConfig(dto);
+ }
+
@Get('invite/records')
@ApiOperation({ summary: '邀请记录列表' })
async getRecords(@Query() dto: QueryInviteRecordsDto) {
diff --git a/apps/server/src/modules/activity/activity.service.ts b/apps/server/src/modules/activity/activity.service.ts
index baff6c6..f535b1a 100644
--- a/apps/server/src/modules/activity/activity.service.ts
+++ b/apps/server/src/modules/activity/activity.service.ts
@@ -435,6 +435,36 @@ export class ActivityService {
// ===== 管理端功能 =====
+ async getInviteCashbackConfig() {
+ const activity = await this.activityRepo.findOne({
+ where: { type: 'invite_cashback' },
+ });
+ if (!activity) {
+ // 创建默认配置
+ activity = this.activityRepo.create({
+ name: '邀请返现',
+ type: 'invite_cashback',
+ enabled: true,
+ config: {
+ firstOrderRate: 0.05,
+ secondOrderRate: 0.005,
+ minCashback: 0.01,
+ maxCashback: 50,
+ withdrawThreshold: 10,
+ },
+ });
+ await this.activityRepo.save(activity);
+ }
+ return activity;
+ }
+
+ async updateInviteCashbackConfig(dto: UpdateActivityDto) {
+ const activity = await this.getInviteCashbackConfig();
+ if (dto.enabled !== undefined) activity.enabled = dto.enabled;
+ if (dto.config) activity.config = { ...activity.config, ...dto.config };
+ return this.activityRepo.save(activity);
+ }
+
async getAdminStats(activityId?: number) {
const where: any = {};
if (activityId) where.activityId = activityId;
diff --git a/apps/server/src/modules/config/config.controller.ts b/apps/server/src/modules/config/config.controller.ts
new file mode 100644
index 0000000..c369fa9
--- /dev/null
+++ b/apps/server/src/modules/config/config.controller.ts
@@ -0,0 +1,26 @@
+import { Controller, Get, Put, Body, UseGuards } from '@nestjs/common';
+import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
+import { ConfigService } from './config.service';
+import { JwtAuthGuard, RolesGuard, Roles } from '@/common';
+
+@ApiTags('管理端-系统配置')
+@ApiBearerAuth()
+@UseGuards(JwtAuthGuard, RolesGuard)
+@Roles('admin')
+@Controller('admin/config')
+export class ConfigController {
+ constructor(private readonly configService: ConfigService) {}
+
+ @Get('service-fee')
+ @ApiOperation({ summary: '获取服务费配置' })
+ async getServiceFeeConfig() {
+ return this.configService.getServiceFeeConfig();
+ }
+
+ @Put('service-fee')
+ @ApiOperation({ summary: '更新服务费配置' })
+ async updateServiceFeeConfig(@Body() body: { rate: number }) {
+ await this.configService.updateServiceFeeRate(body.rate);
+ return this.configService.getServiceFeeConfig();
+ }
+}
\ No newline at end of file
diff --git a/apps/server/src/modules/config/config.module.ts b/apps/server/src/modules/config/config.module.ts
new file mode 100644
index 0000000..54c90a9
--- /dev/null
+++ b/apps/server/src/modules/config/config.module.ts
@@ -0,0 +1,13 @@
+import { Module } from '@nestjs/common';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import { PlatformConfig } from '@/entities/platform-config.entity';
+import { ConfigService } from './config.service';
+import { ConfigController } from './config.controller';
+
+@Module({
+ imports: [TypeOrmModule.forFeature([PlatformConfig])],
+ controllers: [ConfigController],
+ providers: [ConfigService],
+ exports: [ConfigService],
+})
+export class ConfigModule {}
\ No newline at end of file
diff --git a/apps/server/src/modules/config/config.service.ts b/apps/server/src/modules/config/config.service.ts
new file mode 100644
index 0000000..0f7d9cb
--- /dev/null
+++ b/apps/server/src/modules/config/config.service.ts
@@ -0,0 +1,57 @@
+import { Injectable } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository } from 'typeorm';
+import { PlatformConfig } from '@/entities/platform-config.entity';
+
+@Injectable()
+export class ConfigService {
+ constructor(
+ @InjectRepository(PlatformConfig)
+ private configRepo: Repository,
+ ) {}
+
+ async getConfig(key: string): Promise {
+ const config = await this.configRepo.findOne({ where: { configKey: key } });
+ return config?.configValue || null;
+ }
+
+ async setConfig(key: string, value: string, description?: string): Promise {
+ let config = await this.configRepo.findOne({ where: { configKey: key } });
+ if (config) {
+ config.configValue = value;
+ if (description) config.description = description;
+ } else {
+ config = this.configRepo.create({ configKey: key, configValue: value, description });
+ }
+ await this.configRepo.save(config);
+ }
+
+ async getServiceFeeRate(): Promise {
+ const value = await this.getConfig('service_fee_rate');
+ const rate = parseFloat(value || '0.05');
+ return isNaN(rate) ? 0.05 : rate;
+ }
+
+ async getCommissionRate(): Promise {
+ const value = await this.getConfig('commission_rate');
+ const rate = parseFloat(value || '0.10');
+ return isNaN(rate) ? 0.10 : rate;
+ }
+
+ async getServiceFeeConfig(): Promise<{ rate: number; description: string }> {
+ const value = await this.getConfig('service_fee_rate');
+ const config = await this.configRepo.findOne({ where: { configKey: 'service_fee_rate' } });
+ const rate = parseFloat(value || '0.05');
+ return {
+ rate: isNaN(rate) ? 0.05 : rate,
+ description: config?.description || '软件服务费比例',
+ };
+ }
+
+ async updateServiceFeeRate(rate: number): Promise {
+ if (rate < 0 || rate > 1) {
+ throw new Error('服务费比例必须在0-1之间');
+ }
+ await this.setConfig('service_fee_rate', rate.toString(), '软件服务费比例');
+ }
+}
\ No newline at end of file
diff --git a/apps/server/src/modules/finance/finance.service.ts b/apps/server/src/modules/finance/finance.service.ts
index 99dc31e..c0ccb14 100644
--- a/apps/server/src/modules/finance/finance.service.ts
+++ b/apps/server/src/modules/finance/finance.service.ts
@@ -119,13 +119,6 @@ export class FinanceService {
order: { createdAt: 'ASC' },
});
- // 计算手续费率
- const feeRate = 0.006;
- const fee = Math.round(dto.amount * feeRate * 100) / 100;
- const commissionRate = Number(merchant.walletBalance) > 0 ? 0.10 : 0;
- const commissionAmount = Math.round(dto.amount * commissionRate * 100) / 100;
- const actualAmount = Math.round((dto.amount - fee - commissionAmount) * 100) / 100;
-
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
@@ -138,9 +131,9 @@ export class FinanceService {
merchantId,
settlementIds: approvedSettlements.map(s => s.id),
amount: dto.amount,
- fee,
- commissionAmount,
- actualAmount,
+ fee: 0,
+ commissionAmount: 0,
+ actualAmount: dto.amount,
bankName: dto.bankName,
bankAccount: dto.bankAccount,
accountName: dto.accountName,
@@ -361,8 +354,13 @@ export class FinanceService {
const withdrawalStats = await wQb
.select('COUNT(*)', 'totalWithdrawals')
.addSelect('COALESCE(SUM(CASE WHEN w.status = \'paid\' THEN w.amount ELSE 0 END), 0)', 'paidAmount')
- .addSelect('COALESCE(SUM(w.fee), 0)', 'totalFee')
- .addSelect('COALESCE(SUM(w.commission_amount), 0)', 'totalWithdrawCommission')
+ .getRawOne();
+
+ // 服务费统计
+ const serviceFeeStats = await this.orderRepo
+ .createQueryBuilder('o')
+ .where('o.status IN (:...statuses)', { statuses: ['completed', 'checked_in'] })
+ .select('COALESCE(SUM(o.service_fee), 0)', 'totalServiceFee')
.getRawOne();
return {
@@ -371,9 +369,8 @@ export class FinanceService {
settlementAmount: Number(settlementStats?.totalSettlementAmount || 0),
totalSettlements: Number(settlementStats?.totalSettlements || 0),
paidAmount: Number(withdrawalStats?.paidAmount || 0),
- totalFee: Number(withdrawalStats?.totalFee || 0),
- totalWithdrawCommission: Number(withdrawalStats?.totalWithdrawCommission || 0),
totalWithdrawals: Number(withdrawalStats?.totalWithdrawals || 0),
+ totalServiceFee: Number(serviceFeeStats?.totalServiceFee || 0),
};
}
diff --git a/apps/server/src/modules/order/order.module.ts b/apps/server/src/modules/order/order.module.ts
index 4b3b535..9ae4789 100644
--- a/apps/server/src/modules/order/order.module.ts
+++ b/apps/server/src/modules/order/order.module.ts
@@ -7,12 +7,14 @@ import { OrderService } from './order.service';
import { UserOrderController, MerchantOrderController, AdminOrderController } from './order.controller';
import { MerchantModule } from '../merchant/merchant.module';
import { ActivityModule } from '../activity/activity.module';
+import { PlatformConfigModule } from '../config/config.module';
@Module({
imports: [
TypeOrmModule.forFeature([Order, Room, RoomCalendar]),
MerchantModule,
forwardRef(() => ActivityModule),
+ PlatformConfigModule,
],
controllers: [UserOrderController, MerchantOrderController, AdminOrderController],
providers: [OrderService],
diff --git a/apps/server/src/modules/order/order.service.ts b/apps/server/src/modules/order/order.service.ts
index c79c335..ad8ace5 100644
--- a/apps/server/src/modules/order/order.service.ts
+++ b/apps/server/src/modules/order/order.service.ts
@@ -6,6 +6,7 @@ import { Room } from '@/entities/room.entity';
import { RoomCalendar } from '@/entities/room-calendar.entity';
import { CreateOrderDto, QueryOrderDto, ConfirmOrderDto } from './dto/order.dto';
import { ActivityService } from '@/modules/activity/activity.service';
+import { ConfigService } from '@/modules/config/config.service';
@Injectable()
export class OrderService {
@@ -17,6 +18,7 @@ export class OrderService {
@InjectRepository(RoomCalendar)
private calendarRepo: Repository,
private readonly activityService: ActivityService,
+ private readonly configService: ConfigService,
) {}
async create(userId: number, dto: CreateOrderDto) {
@@ -36,6 +38,11 @@ export class OrderService {
const totalAmount = roomAmount;
const payAmount = totalAmount - couponDiscount;
+ // 计算软件服务费和商家收入
+ const serviceFeeRate = await this.configService.getServiceFeeRate();
+ const serviceFee = Math.round(payAmount * serviceFeeRate * 100) / 100;
+ const merchantIncome = Math.round((payAmount - serviceFee) * 100) / 100;
+
const now = new Date();
const orderNo = [
now.getFullYear(),
@@ -59,6 +66,8 @@ export class OrderService {
guestCount: dto.guestCount || 1,
roomPrice: room.price,
roomAmount,
+ serviceFee,
+ merchantIncome,
couponDiscount,
totalAmount,
payAmount,
diff --git a/database/migrations/001_init_schema.sql b/database/migrations/001_init_schema.sql
index 7ef101d..1596ed7 100644
--- a/database/migrations/001_init_schema.sql
+++ b/database/migrations/001_init_schema.sql
@@ -209,7 +209,8 @@ CREATE TABLE `orders` (
`room_count` TINYINT UNSIGNED DEFAULT 1 COMMENT '房间数',
`room_price` DECIMAL(10,2) UNSIGNED NOT NULL COMMENT '房费单价',
`room_amount` DECIMAL(10,2) UNSIGNED NOT NULL COMMENT '房费总额',
- `service_fee` DECIMAL(10,2) UNSIGNED DEFAULT 0.00 COMMENT '服务费',
+ `service_fee` DECIMAL(10,2) UNSIGNED DEFAULT 0.00 COMMENT '软件服务费',
+ `merchant_income` DECIMAL(10,2) UNSIGNED DEFAULT 0.00 COMMENT '商家预计收入',
`coupon_discount` DECIMAL(10,2) UNSIGNED DEFAULT 0.00 COMMENT '优惠券抵扣',
`total_amount` DECIMAL(10,2) UNSIGNED NOT NULL COMMENT '订单总金额',
`pay_amount` DECIMAL(10,2) UNSIGNED NOT NULL COMMENT '实付金额',
diff --git a/database/modules.md b/database/modules.md
index acc2994..1eab957 100644
--- a/database/modules.md
+++ b/database/modules.md
@@ -136,12 +136,32 @@
```
待支付 ──支付成功──> 待确认 ──商家确认──> 待入住 ──入住──> 已入住 ──离店──> 已完成
- │ │
- └──超时/取消──> 已取消 └──商家拒绝──> 已取消
+ │ │ │
+ └──超时/取消──> 已取消 └──商家拒绝──> 已取消 └──用户退款──> 退款中
+ │
+ ├──商家通过──> 已退款(恢复房量)
+ └──商家拒绝──> 已取消
-已完成 ──申请退款──> 退款中 ──平台处理──> 已退款
+待确认 ──用户退款──> 退款中 ──商家通过──> 已退款(恢复房量)
```
+### 用户订单操作
+
+| 操作 | 接口 | 适用状态 | 说明 |
+| -------- | ------------------------- | ---------------------------- | ------------------------ |
+| 取消订单 | PUT /orders/:id/cancel | pending_pay, pending_confirm | 取消订单,已支付则退款 |
+| 申请退款 | PUT /orders/:id/refund | pending_confirm, pending_checkin | 提交退款申请 |
+
+### 商家订单操作
+
+| 操作 | 接口 | 适用状态 | 说明 |
+| ---------- | ----------------------------------------- | ----------------- | ----------------------------- |
+| 确认订单 | PUT /seller/orders/:id/confirm | pending_confirm | 确认后等待用户入住 |
+| 拒绝订单 | PUT /seller/orders/:id/reject | pending_confirm | 拒绝并退款 |
+| 办理入住 | PUT /seller/orders/:id/checkin | pending_checkin | 标记已入住 |
+| 通过退款 | PUT /seller/orders/:id/approve-refund | refunding | 同意退款,恢复房量日历库存 |
+| 拒绝退款 | PUT /seller/orders/:id/reject-refund | refunding | 拒绝退款,订单变为已取消 |
+
### 自动化规则
- 待支付订单: 30分钟未支付自动取消
@@ -152,11 +172,17 @@
```
房费总额 = 房价/晚 × 入住晚数 × 房间数
-服务费 = 房费总额 × 服务费比例(默认5%)
-订单总额 = 房费总额 + 服务费
+订单总额 = 房费总额
实付金额 = 订单总额 - 优惠券抵扣
+软件服务费 = 实付金额 × 服务费比例(默认5%,平台后台可配置)
+商家预计收入 = 实付金额 - 软件服务费
```
+**说明**:
+- 软件服务费归平台所有,在订单创建时计算
+- 商家预计收入 = 实付金额 - 软件服务费
+- 费用明细仅在商家订单详情中展示,用户端不显示
+
### 佣金计算
```
@@ -242,9 +268,9 @@
### 提现规则
- 最低提现金额: 100元
-- 提现手续费: 默认0.6%
+- 提现手续费: 无
- 到账时间: T+1工作日
-- 需绑定银行卡且实名认证
+- 需绑定银行卡
### 对账维度
@@ -330,6 +356,10 @@
## 11. 邀请返现活动模块
+### 模块概述
+
+邀请返现是平台级营销活动,独立于其他促销活动管理。平台管理后台有专门的「邀请返现」菜单入口,不与其他活动混在一起。
+
### 活动概述
平台级营销活动,用户(邀请人)通过分享邀请链接/海报邀请新用户注册并完成订单,邀请人获得返现奖励。被邀请人无额外收益。
@@ -517,19 +547,19 @@
**平台管理端(需管理员Token):**
-| 方法 | 路径 | 说明 |
-| ---- | ---------------------------------------------- | ------------- |
-| GET | /admin/activity/list | 活动列表 |
-| POST | /admin/activity/create | 创建活动 |
-| PUT | /admin/activity/:id/update | 编辑活动 |
-| PUT | /admin/activity/:id/toggle | 启用/停用活动 |
-| GET | /admin/activity/invite/stats | 邀请数据总览 |
-| GET | /admin/activity/invite/records | 邀请记录列表 |
-| GET | /admin/activity/invite/cashbacks | 返现记录列表 |
-| GET | /admin/activity/invite/withdrawals | 提现审核列表 |
-| PUT | /admin/activity/invite/withdrawals/:id/approve | 审核通过 |
-| PUT | /admin/activity/invite/withdrawals/:id/reject | 审核拒绝 |
-| PUT | /admin/activity/invite/withdrawals/:id/pay | 确认打款 |
+| 方法 | 路径 | 说明 |
+| ---- | ---------------------------------------------- | --------------- |
+| GET | /admin/activity/invite/stats | 邀请数据总览 |
+| GET | /admin/activity/invite/config | 获取活动配置 |
+| PUT | /admin/activity/invite/config | 更新活动配置 |
+| GET | /admin/activity/invite/records | 邀请记录列表 |
+| GET | /admin/activity/invite/cashbacks | 返现记录列表 |
+| GET | /admin/activity/invite/withdrawals | 提现审核列表 |
+| PUT | /admin/activity/invite/withdrawals/:id/approve | 审核通过 |
+| PUT | /admin/activity/invite/withdrawals/:id/reject | 审核拒绝 |
+| PUT | /admin/activity/invite/withdrawals/:id/pay | 确认打款 |
+
+**注意**: 平台管理后台不再提供通用的活动列表CRUD,邀请返现活动有独立的配置管理页面。
### 小程序页面路由
@@ -538,4 +568,17 @@
| 邀请首页 | /pages/invite/index | 邀请统计、海报生成、分享 |
| 邀请记录 | /pages/invite/records | 邀请人数、被邀请人列表 |
| 返现记录 | /pages/invite/cashbacks | 返现明细列表 |
-| 提现页面 | /pages/invite/withdraw | 余额展示、提现操作、提现记录 |
+| 提现页面 | /pages/invite/withdraw | 余额展示、提现操作 |
+| 提现记录 | /pages/invite/withdrawals | 提现历史列表 |
+| 邀请海报 | /pages/invite/poster | 生成分享海报 |
+
+### 平台管理后台页面
+
+| 页面 | 路径 | 说明 |
+| ------------ | -------- | ---------------------------------------------- |
+| 邀请返现管理 | /invite | 数据概览、提现审核、活动配置三个Tab |
+
+**页面结构**:
+- 数据概览Tab: 统计卡片 + 邀请记录列表 + 返现记录列表
+- 提现审核Tab: 提现申请列表,支持审核通过/拒绝/确认打款
+- 活动配置Tab: 启用/停用开关 + 返现比例/金额限制/提现门槛配置表单
diff --git a/database/seeds/001_init_data.sql b/database/seeds/001_init_data.sql
index 14fe333..76af248 100644
--- a/database/seeds/001_init_data.sql
+++ b/database/seeds/001_init_data.sql
@@ -28,7 +28,7 @@ INSERT INTO `member_levels` (`name`, `level`, `min_points`, `discount`, `benefit
-- 平台配置
INSERT INTO `platform_configs` (`config_key`, `config_value`, `description`) VALUES
('commission_rate', '0.10', '默认平台佣金比例'),
-('withdraw_fee_rate', '0.006', '提现手续费率'),
+('service_fee_rate', '0.05', '软件服务费比例'),
('min_deposit', '5000', '最低保证金金额'),
('auto_cancel_minutes', '30', '未支付订单自动取消时间(分钟)'),
('auto_complete_hours', '24', '入住后自动完成订单时间(小时)'),
diff --git a/database/skills.md b/database/skills.md
index 3dcbe9a..9da9a8d 100644
--- a/database/skills.md
+++ b/database/skills.md
@@ -19,7 +19,7 @@
| rooms | 房源表 | merchant_id, name, type, price, status, audit_status |
| room_calendar | 房量房价日历 | room_id, date, price, stock, status |
| room_calendar_logs | 房态变更日志 | room_id, operator_id, change_type |
-| orders | 订单表 | order_no, user_id, merchant_id, room_id, status |
+| orders | 订单表 | order_no, user_id, merchant_id, room_id, pay_amount, service_fee, merchant_income, status |
| reviews | 评价表 | order_id, user_id, rating, content |
| favorites | 收藏表 | user_id, room_id |
| coupons | 优惠券模板 | name, type, value, scope |
@@ -100,6 +100,29 @@ orders (1) ──── (1) settlements
- `refunding` - 退款中
- `refunded` - 已退款
+### 订单状态流转
+
+```
+待支付 ──支付成功──> 待确认 ──商家确认──> 待入住 ──入住──> 已入住 ──离店──> 已完成
+ │ │ │
+ └──超时/取消──> 已取消 └──商家拒绝──> 已取消 └──用户退款──> 退款中
+ │
+ ├──商家通过──> 已退款(恢复房量)
+ └──商家拒绝──> 已取消
+
+待确认 ──用户退款──> 退款中 ──商家通过──> 已退款(恢复房量)
+```
+
+### 订单取消/退款规则
+
+| 操作 | 适用状态 | 说明 |
+| ------------ | ---------------------------- | --------------------------------------- |
+| 用户取消 | pending_pay | 直接取消,无退款 |
+| 用户取消 | pending_confirm | 取消并退款,恢复房量 |
+| 用户退款申请 | pending_confirm, pending_checkin | 状态变为 refunding,等待商家审核 |
+| 商家通过退款 | refunding | 状态变为 refunded,恢复房量日历库存 |
+| 商家拒绝退款 | refunding | 状态变为 cancelled |
+
### 房源状态 (rooms.status)
- `on_sale` - 在售
@@ -210,6 +233,26 @@ mysql -u root -p rent_platform < database/seeds/001_init_data.sql
---
+## 平台配置表 (platform_configs)
+
+| 配置键 | 默认值 | 说明 |
+| ----------------- | ------ | ------------------------ |
+| commission_rate | 0.10 | 默认平台佣金比例 |
+| service_fee_rate | 0.05 | 软件服务费比例(可配置) |
+| min_deposit | 5000 | 最低保证金金额 |
+| auto_cancel_minutes | 30 | 未支付订单自动取消时间 |
+| auto_complete_hours | 24 | 入住后自动完成订单时间 |
+| sms_enabled | true | 是否启用短信通知 |
+| max_images_per_room | 20 | 每个房源最大图片数 |
+| max_images_per_review | 9 | 每条评价最大图片数 |
+
+**服务费计算**:
+- 软件服务费 = 实付金额 × service_fee_rate
+- 商家预计收入 = 实付金额 - 软件服务费
+- 配置可通过平台管理后台「系统设置」页面调整
+
+---
+
## 商家入驻流程
### 流程概述
@@ -295,7 +338,9 @@ interface SellerLoginResult {
| ------------- | ---------------------------- | ---------------------------- |
| 个人中心 | /pages/mine/index | 入口:商家中心、商家入驻按钮 |
| 商家注册/登录 | /pages/seller-register/index | 商家账号注册登录 |
-| 商家中心 | /pages/merchant/home | 商家管理入口 |
+| 商家中心 | /pages/seller/home | 商家管理入口 |
+| 商家订单列表 | /pages/seller/orders | 商家订单管理 |
+| 商家订单详情 | /pages/seller/order-detail | 商家订单详情 |
| 创建店铺 | /pages/shop-create/index | 填写店铺信息提交审核 |
| 修改店铺 | /pages/shop-edit/index | 修改店铺信息重新审核 |
@@ -437,39 +482,39 @@ async update(id, dto) {
2. **商家实名认证**: 身份证上传、银行卡绑定
3. **图片上传接口**: 营业执照真实上传功能
-- 显示状态标签"已冻结"
-- 显示提示"店铺已被冻结,请联系平台客服"
-- 不显示修改按钮
-- 不显示数据概览和功能菜单
+---
-### 修改店铺重新审核逻辑
+## 商家订单管理
-**后端逻辑** (`merchant.service.ts`):
+### 商家订单接口
-```typescript
-async update(id, dto) {
- const merchant = await this.findById(id);
- // 审核通过或拒绝后修改,重置为pending
- if (merchant.status === 'approved' || merchant.status === 'rejected') {
- await this.merchantRepo.update(id, { ...dto, status: 'pending', rejectReason: null });
- } else {
- await this.merchantRepo.update(id, dto);
- }
- return this.findById(id);
-}
-```
+| 接口 | 路径 | 说明 |
+| -------------- | --------------------------------- | ---------------------------- |
+| 订单列表 | GET /api/seller/orders | 支持状态筛选、订单号搜索 |
+| 订单详情 | GET /api/seller/orders/:id | 获取订单详情 |
+| 确认订单 | PUT /api/seller/orders/:id/confirm| pending_confirm → pending_checkin |
+| 拒绝订单 | PUT /api/seller/orders/:id/reject | pending_confirm → cancelled |
+| 办理入住 | PUT /api/seller/orders/:id/checkin| pending_checkin → checked_in |
+| 通过退款 | PUT /api/seller/orders/:id/approve-refund | refunding → refunded,恢复房量 |
+| 拒绝退款 | PUT /api/seller/orders/:id/reject-refund | refunding → cancelled |
-**修改流程**:
+### 商家订单状态Tab
-```
-点击"修改店铺信息" → 填写表单 → 提交修改
- → 调用 PUT /merchant/update
- → 后端自动将 status 重置为 pending,清空 rejectReason
- → 跳转商家中心显示"审核中"状态
-```
+| Tab名称 | 对应状态 |
+| --------- | --------------------------------------------- |
+| 全部 | 所有状态 |
+| 待确认 | pending_confirm |
+| 待入住 | pending_checkin |
+| 已入住 | checked_in |
+| 已完成 | completed |
+| 已取消 | cancelled |
+| 已退款 | refunded, refunding |
-### 后续迭代事项
+### 商家订单操作权限
-1. **商家管理后台集成**: 商家后台跳转入口
-2. **商家实名认证**: 身份证上传、银行卡绑定
-3. **图片上传接口**: 营业执照真实上传功能
\ No newline at end of file
+| 状态 | 可操作 |
+| ---------------- | -------------------------- |
+| pending_confirm | 确认、拒绝 |
+| pending_checkin | 办理入住 |
+| refunding | 通过退款、拒绝退款 |
+| 其他状态 | 无操作 |
\ No newline at end of file